Migrate codegen for locale to CUE

This commit is contained in:
Denis Arh
2022-01-26 16:25:53 +01:00
parent c19ee84f5d
commit 3568d0841e
35 changed files with 1284 additions and 1212 deletions
+10 -4
View File
@@ -1,10 +1,16 @@
package codegen
import (
"github.com/cortezaproject/corteza-server/codegen/schema"
"github.com/cortezaproject/corteza-server/codegen/schema"
)
all: [...schema.#codegen] &
rbacAccessControl +
rbacTypes +
// List of all codegen jobs for the entire platform
//
// How to run it?
// @todo when this gets into
// cue eval codegen/*.cue --out json -e platform | go run codegen/tool/*.go -v
platform: [...schema.#codegen] &
rbacAccessControl+
rbacTypes+
localeTypes+
[] // placeholder
@@ -0,0 +1,206 @@
package {{ .package }}
{{ template "gocode/header-gentext.tpl" }}
import (
"context"
{{- range .imports }}
{{ . }}
{{- end }}
"github.com/cortezaproject/corteza-server/pkg/actionlog"
intAuth "github.com/cortezaproject/corteza-server/pkg/auth"
"github.com/cortezaproject/corteza-server/pkg/errors"
"github.com/cortezaproject/corteza-server/pkg/locale"
"github.com/cortezaproject/corteza-server/pkg/options"
"github.com/cortezaproject/corteza-server/store"
systemTypes "github.com/cortezaproject/corteza-server/system/types"
"golang.org/x/text/language"
)
type (
localeAccessControl interface {
CanManageResourceTranslations(ctx context.Context) bool
}
resourceTranslationsManager struct {
actionlog actionlog.Recorder
locale locale.Resource
store store.Storer
ac localeAccessController
}
localeAccessController interface {
CanManageResourceTranslations(context.Context) bool
}
ResourceTranslationsManagerService interface {
{{- range .resources }}
{{ .expIdent }}(ctx context.Context, {{ range .references }}{{ . }} uint64, {{ end }}) (locale.ResourceTranslationSet, error)
{{- end }}
Upsert(context.Context, locale.ResourceTranslationSet) error
Locale() locale.Resource
}
)
var ErrNotAllowedToManageResourceTranslations = errors.Unauthorized("not allowed to manage resource translations")
func ResourceTranslationsManager(ls locale.Resource) *resourceTranslationsManager {
return &resourceTranslationsManager{
actionlog: DefaultActionlog,
store: DefaultStore,
ac: DefaultAccessControl,
locale: ls,
}
}
func (svc resourceTranslationsManager) Upsert(ctx context.Context, rr locale.ResourceTranslationSet) (err error) {
// User is allowed to manage resource translations when:
// - managed resource translation strings are all for default language
// or
// - user is allowed to manage resource translations
if rr.ContainsForeign(svc.Locale().Default().Tag) {
if !svc.ac.CanManageResourceTranslations(ctx) {
return ErrNotAllowedToManageResourceTranslations
}
}
for _, r := range rr {
r.Msg = locale.SanitizeMessage(r.Msg)
}
// @todo validation
me := intAuth.GetIdentityFromContext(ctx)
// - group by resource
localeByRes := make(map[string]locale.ResourceTranslationSet)
for _, r := range rr {
localeByRes[r.Resource] = append(localeByRes[r.Resource], r)
}
// - for each resource, fetch the current state
sysLocale := make(systemTypes.ResourceTranslationSet, 0, len(rr))
for res, rr := range localeByRes {
current, _, err := store.SearchResourceTranslations(ctx, svc.store, systemTypes.ResourceTranslationFilter{
Resource: res,
})
if err != nil {
return err
}
// get deltas and prepare upsert accordingly
aux := current.New(rr)
aux.Walk(func(cc *systemTypes.ResourceTranslation) error {
cc.ID = nextID()
cc.CreatedAt = *now()
cc.CreatedBy = me.Identity()
return nil
})
sysLocale = append(sysLocale, aux...)
aux = current.Old(rr)
_ = aux.Walk(func(cc *systemTypes.ResourceTranslation) error {
cc.UpdatedAt = now()
cc.UpdatedBy = me.Identity()
return nil
})
sysLocale = append(sysLocale, aux...)
}
err = store.UpsertResourceTranslation(ctx, svc.store, sysLocale...)
if err != nil {
return err
}
// Reload ALL resource translations
// @todo we could probably do this more selectively and refresh only updated resources?
_ = locale.Global().ReloadResourceTranslations(ctx)
return nil
}
func (svc resourceTranslationsManager) Locale() locale.Resource {
return svc.locale
}
{{- range .resources }}
func (svc resourceTranslationsManager) {{ .expIdent }}(ctx context.Context, {{ range .references }}{{ . }} uint64, {{ end }}) (locale.ResourceTranslationSet, error) {
var (
err error
out locale.ResourceTranslationSet
res *types.{{ .expIdent }}
k types.LocaleKey
)
res, err = svc.load{{ .expIdent }}(ctx, svc.store, {{ range .references }}{{ . }}, {{ end }})
if err != nil {
return nil, err
}
for _, tag := range svc.locale.Tags() {
{{- range .keys}}
{{- if not .customHandler }}
k = types.{{ .struct }}
out = append(out, &locale.ResourceTranslation{
Resource: res.ResourceTranslation(),
Lang: tag.String(),
Key: k.Path,
Msg: svc.locale.TResourceFor(tag, res.ResourceTranslation(), k.Path),
})
{{ end }}
{{- end}}
}
{{ if .extended }}
tmp, err := svc.{{ .ident }}Extended(ctx, res)
return append(out, tmp...), err
{{- else }}
return out, nil
{{- end }}
}
{{- end }}
func updateTranslations(ctx context.Context, ac localeAccessControl, lsvc ResourceTranslationsManagerService, tt ...*locale.ResourceTranslation) error {
if lsvc == nil || lsvc.Locale() == nil || lsvc.Locale().Default() == nil {
// gracefully handle partial initializations
return nil
}
var (
// assuming options will not change after start
contentLang = lsvc.Locale().Default().Tag
)
if options.Locale().ResourceTranslationsEnabled {
contentLang = locale.GetContentLanguageFromContext(ctx)
// Resource translations enabled
if contentLang == language.Und {
// If no content-language meta (HTTP header) info was
// used, do not run update translations - we do not know
// what is the language that we're sending in
return nil
}
if !lsvc.Locale().SupportedLang(contentLang) {
// unsupported language
return errors.InvalidData("unsupported language")
}
if !ac.CanManageResourceTranslations(ctx) {
return errors.Unauthorized("not allowed to manage resource translations")
}
}
locale.ResourceTranslationSet(tt).SetLanguage(contentLang)
if err := lsvc.Upsert(ctx, tt); err != nil {
return err
}
return nil
}
@@ -0,0 +1,120 @@
package {{ .package }}
{{ template "gocode/header-gentext.tpl" }}
import (
"fmt"
"strconv"
"github.com/cortezaproject/corteza-server/pkg/locale"
)
type (
LocaleKey struct {
Name string
Resource string
Path string
CustomHandler string
}
)
// Types and stuff
const (
{{- range .resources }}
{{ .const }} = "{{ .type }}"
{{- end }}
)
var (
// @todo can we remove LocaleKey struct for string constant?
{{- range .resources }}
{{- range .keys }}
{{ .struct }} = LocaleKey{ Path: {{ printf "%q" .path }} }
{{- end }}
{{- end }}
)
{{- range .resources }}
// ResourceTranslation returns string representation of Locale resource for {{ .expIdent }} by calling {{ .expIdent }}ResourceTranslation fn
//
// Locale resource is in "{{ .type }}/..." format
//
// This function is auto-generated
func (r {{ .expIdent }}) ResourceTranslation() string {
return {{ .expIdent }}ResourceTranslation({{ if .references }}{{ range .references }}r.{{ . }},{{ end }}{{ end }})
}
// {{ .expIdent }}ResourceTranslation returns string representation of Locale resource for {{ .expIdent }}
//
// Locale resource is in the {{ .type }}/{{- if .references }}...{{ end }} format
//
// This function is auto-generated
func {{ .expIdent }}ResourceTranslation({{ if .references }}{{ range .references }}{{ . }} uint64,{{ end }}{{ end }}) string {
{{- if .references }}
cpts := []interface{{"{}"}}{
{{ .expIdent }}ResourceTranslationType,
{{- range .references }}
strconv.FormatUint({{ . }}, 10),
{{- end }}
}
return fmt.Sprintf({{ .expIdent }}ResourceTranslationTpl(), cpts...)
{{- end }}
}
func {{ .expIdent }}ResourceTranslationTpl() string {
{{- if .references }}
return "%s
{{- range .references }}/%s{{- end }}"
{{- else }}
return "%s"
{{- end }}
}
func (r *{{ .expIdent }}) DecodeTranslations(tt locale.ResourceTranslationIndex) {
var aux *locale.ResourceTranslation
{{- range .keys }}
{{ if .decodeFunc }}
{{ if not .extended }}
r.{{ .decodeFunc }}(tt)
{{- end}}
{{ else }}
if aux = tt.FindByKey({{ .struct }}.Path); aux != nil {
r.{{ .fieldPath }} = aux.Msg
}
{{- end}}
{{- end}}
{{- if .extended }}
r.decodeTranslations(tt)
{{- end }}
}
func (r *{{ .expIdent }}) EncodeTranslations() (out locale.ResourceTranslationSet) {
out = locale.ResourceTranslationSet{}
{{- range .keys }}
{{ if .encodeFunc }}
{{ if not .extended }}
out = append(out, r.{{ .encodeFunc }}()...)
{{- end}}
{{ else }}
if r.{{ .fieldPath }} != "" {
out = append(out, &locale.ResourceTranslation{
Resource: r.ResourceTranslation(),
Key: {{ .struct }}.Path,
Msg: locale.SanitizeMessage(r.{{ .fieldPath }}),
})
}
{{- end}}
{{- end}}
{{- if .extended }}
out = append(out, r.encodeTranslations()...)
{{- end }}
return out
}
{{- end }}
@@ -10,7 +10,7 @@ import (
"github.com/cortezaproject/corteza-server/pkg/rbac"
"github.com/cortezaproject/corteza-server/pkg/actionlog"
{{- range .imports }}
"{{ . }}"
{{ . }}
{{- end }}
)
@@ -57,7 +57,7 @@ func (svc accessControl) List() (out []map[string]string) {
{{- range .operations }}
{
"type": {{ .const }},
"any": {{ .ctor }},
"any": {{ .resFunc }}({{ range .references }}0,{{ end }}),
"op": {{ printf "%q" .op }},
},
{{- end }}
@@ -38,7 +38,7 @@ const (
//
// This function is auto-generated
func (r {{ .goType }}) RbacResource() string {
return {{ .resFunc }}({{ if not .component }}{{ range .references }}r.{{ . }},{{ end }}{{ end }})
return {{ .resFunc }}({{ if not .component }}{{ range .references }}r.{{ .refField }},{{ end }}{{ end }})
}
// {{ .resFunc }} returns string representation of RBAC resource for {{ .goType }}
@@ -46,12 +46,12 @@ func (r {{ .goType }}) RbacResource() string {
// RBAC resource is in the {{ .type }}/{{- if .references }}...{{ end }} format
//
// This function is auto-generated
func {{ .resFunc }}({{ if not .component }}{{ range .references }}{{ . }} uint64,{{ end }}{{ end }}) string {
func {{ .resFunc }}({{ if not .component }}{{ range .references }}{{ .param }} uint64,{{ end }}{{ end }}) string {
{{- if .references }}
cpts := []interface{{"{}"}}{{"{"}}{{ .goType }}ResourceType{{"}"}}
{{- range .references }}
if {{ . }} != 0 {
cpts = append(cpts, strconv.FormatUint({{ . }}, 10))
if {{ .param }} != 0 {
cpts = append(cpts, strconv.FormatUint({{ .param }}, 10))
} else {
cpts = append(cpts, "*")
}
+44
View File
@@ -0,0 +1,44 @@
package codegen
import (
"github.com/cortezaproject/corteza-server/app"
"github.com/cortezaproject/corteza-server/codegen/schema"
"strings"
)
localeService:
[...schema.#codegen] &
[
// wrapped with additional for loop to trim out templates with empty types list
for tpl in [
for cmp in app.corteza.components {
template: "gocode/locale/service.go.tpl"
output: "\(cmp.ident)/service/locale.gen.go"
payload: {
package: "service"
imports: [
"\"github.com/cortezaproject/corteza-server/\(cmp.ident)/types\"",
]
resources: [
for res in cmp.resources if (res.locale != _|_) if (!res.locale.skipSvc) {
expIdent: res.expIdent
ident: res.ident
references: [ for field in res.locale.resource.references {strings.ToTitle(field)}]
extended: res.locale.extended
keys: [ for key in res.locale.keys if key.handlerFunc == _|_ {
struct: key.struct
extended: extended
customHandler: key.customHandler
if key.serviceFunc != _|_ {serviceFunc: key.serviceFunc}
}]
},
]
}
},
// skip empty type lists
] if len(tpl.payload.resources) > 0 {tpl}]
+55
View File
@@ -0,0 +1,55 @@
package codegen
import (
"github.com/cortezaproject/corteza-server/app"
"github.com/cortezaproject/corteza-server/codegen/schema"
"strings"
)
localeTypes:
[...schema.#codegen] &
[
// wrapped with additional for loop to trim out templates with empty types list
for tpl in [
for cmp in app.corteza.components {
template: "gocode/locale/types.go.tpl"
output: "\(cmp.ident)/types/locale.gen.go"
payload: {
package: "types"
resources: [
for res in cmp.resources if res.locale != _|_ {
expIdent: res.expIdent
const: res.locale.resource.const
type: res.locale.resource.type
references: [ for field in res.locale.resource.references {strings.ToTitle(field)}]
extended: res.locale.extended
keys: [ for key in res.locale.keys if key.handlerFunc == _|_ {
struct: key.struct
field: strings.ToTitle(key.name)
path: strings.Join([ for p in key.expandedPath {
if p.var {"{{\(p.part)}}"}
if !p.var {p.part}
}], ".")
if !key.customHandler {
fieldPath: strings.Join([ for p in key.expandedPath {
strings.ToTitle(p.part)
}], ".")
}
"extended": extended
if key.decodeFunc != _|_ {decodeFunc: key.decodeFunc}
if key.encodeFunc != _|_ {encodeFunc: key.encodeFunc}
}]
},
]
}
},
// skip empty type lists
] if len(tpl.payload.resources) > 0 {tpl}]
+33 -27
View File
@@ -1,8 +1,8 @@
package codegen
import (
"github.com/cortezaproject/corteza-server/codegen/schema"
"github.com/cortezaproject/corteza-server/app"
"github.com/cortezaproject/corteza-server/codegen/schema"
"github.com/cortezaproject/corteza-server/app"
)
rbacAccessControl:
@@ -12,47 +12,53 @@ rbacAccessControl:
template: "gocode/rbac/access_control.go.tpl"
output: "\(cmp.ident)/service/access_control.gen.go"
payload: {
imports: [
"github.com/cortezaproject/corteza-server/\(cmp.ident)/types",
]
package: "service"
imports: [
"\"github.com/cortezaproject/corteza-server/\(cmp.ident)/types\"",
]
// All possible RBAC operations on component and resources
// flattened
operations: [
for res in cmp.resources for op in res.rbac.operations {
"op": op.handle
"const": "types.\(res.expIdent)ResourceType"
"ctor": "types.\(res.expIdent)RbacResource(\(len(res.rbac.resource.references)*"0,"))"
"goType": res.goType
"description": op.description
"checkFuncName": op.checkFuncName
"op": op.handle
const: "types.\(res.expIdent)ResourceType"
resFunc: "types.\(res.expIdent)RbacResource"
goType: "types.\(res.expIdent)"
description: op.description
checkFuncName: op.checkFuncName
if len(res.parents) > 0 {
references: [ for p in res.parents {p}, {param: "id", refField: "ID"}]
}
},
for op in cmp.rbac.operations {
"op": op.handle
"const": "types.ComponentResourceType"
"ctor": "types.ComponentRbacResource()"
"goType": "types.Component"
"description": op.description
"checkFuncName": op.checkFuncName
"component": true
"op": op.handle
const: "types.ComponentResourceType"
resFunc: "types.ComponentRbacResource"
goType: "types.Component"
description: op.description
checkFuncName: op.checkFuncName
component: true
},
]
// Operation/resource validators, grouped by resource
validation: [
for res in cmp.resources {
"label": res.ident
"const": "types.\(res.expIdent)ResourceType"
"funcName": "rbac\(res.expIdent)ResourceValidator"
"references": res.rbac.resource.references
"operations": [ for op in res.rbac.operations {op.handle}]
label: res.ident
const: "types.\(res.expIdent)ResourceType"
funcName: "rbac\(res.expIdent)ResourceValidator"
if len(res.parents) > 0 {
references: [ for p in res.parents {p.refField}, "ID"]
}
operations: [ for op in res.rbac.operations {op.handle}]
},
{
"label": "\(cmp.ident) component"
"const": "types.ComponentResourceType"
"funcName": "rbacComponentResourceValidator"
"operations": [ for op in cmp.rbac.operations {op.handle}]
label: "\(cmp.ident) component"
const: "types.ComponentResourceType"
funcName: "rbacComponentResourceValidator"
operations: [ for op in cmp.rbac.operations {op.handle}]
},
]
}
+16 -15
View File
@@ -3,7 +3,6 @@ package codegen
import (
"github.com/cortezaproject/corteza-server/app"
"github.com/cortezaproject/corteza-server/codegen/schema"
"strings"
)
rbacTypes:
@@ -19,23 +18,25 @@ rbacTypes:
// Operation/resource validators, grouped by resource
types: [
for res in cmp.resources {
"const": "\(res.expIdent)ResourceType"
"type": res.rbac.resource.type
"resFunc": "\(res.expIdent)RbacResource"
"tplFunc": "\(res.expIdent)RbacResourceTpl"
"attFunc": "\(res.expIdent)RbacAttributes"
"goType": res.expIdent
const: "\(res.expIdent)ResourceType"
type: res.fqrn
resFunc: "\(res.expIdent)RbacResource"
tplFunc: "\(res.expIdent)RbacResourceTpl"
attFunc: "\(res.expIdent)RbacAttributes"
goType: res.expIdent
"references": [ for field in res.rbac.resource.references { strings.ToTitle(field) } ]
if len(res.parents) > 0 {
references: [ for p in res.parents {p}, {param: "id", refField: "ID"}]
}
},
{
"const": "ComponentResourceType"
"type": cmp.rbac.resource.type
"resFunc": "ComponentRbacResource"
"tplFunc": "ComponentRbacResourceTpl"
"attFunc": "ComponentRbacAttributes"
"goType": "Component"
"component": true
const: "ComponentResourceType"
type: cmp.fqrn
resFunc: "ComponentRbacResource"
tplFunc: "ComponentRbacResourceTpl"
attFunc: "ComponentRbacAttributes"
goType: "Component"
component: true
},
]
}