From 834d70f52b6e1046b47933e7247b2e63c3d71263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Fri, 27 Sep 2024 22:44:27 +0200 Subject: [PATCH 1/5] Use the datasource type within queries to unmarshal them --- .../golang/templates/runtime/runtime.tmpl | 21 +++++++++++++++++++ testdata/generated/cog/runtime.go | 21 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/internal/jennies/golang/templates/runtime/runtime.tmpl b/internal/jennies/golang/templates/runtime/runtime.tmpl index b3520cc2..1bfa3f92 100644 --- a/internal/jennies/golang/templates/runtime/runtime.tmpl +++ b/internal/jennies/golang/templates/runtime/runtime.tmpl @@ -69,6 +69,27 @@ func (runtime *Runtime) UnmarshalDataquery(raw []byte, dataqueryTypeHint string) } } + // Dataqueries might reference the datasource to use, and its type. Let's use that. + partialDataquery := struct { + Datasource struct { + Type string `json:"type"` + } `json:"datasource"` + }{} + if err := json.Unmarshal(raw, &partialDataquery); err != nil { + return nil, err + } + if partialDataquery.Datasource.Type != "" { + config, found := runtime.dataqueryVariants[partialDataquery.Datasource.Type] + if found { + dataquery, err := config.DataqueryUnmarshaler(raw) + if err != nil { + return nil, err + } + + return dataquery.(variants.Dataquery), nil + } + } + // We have no idea what type the dataquery is: use our `UnknownDataquery` bag to not lose data. dataquery := variants.UnknownDataquery{} if err := json.Unmarshal(raw, &dataquery); err != nil { diff --git a/testdata/generated/cog/runtime.go b/testdata/generated/cog/runtime.go index 0777ccf2..21389160 100644 --- a/testdata/generated/cog/runtime.go +++ b/testdata/generated/cog/runtime.go @@ -78,6 +78,27 @@ func (runtime *Runtime) UnmarshalDataquery(raw []byte, dataqueryTypeHint string) } } + // Dataqueries might reference the datasource to use, and its type. Let's use that. + partialDataquery := struct { + Datasource struct { + Type string `json:"type"` + } `json:"datasource"` + }{} + if err := json.Unmarshal(raw, &partialDataquery); err != nil { + return nil, err + } + if partialDataquery.Datasource.Type != "" { + config, found := runtime.dataqueryVariants[partialDataquery.Datasource.Type] + if found { + dataquery, err := config.DataqueryUnmarshaler(raw) + if err != nil { + return nil, err + } + + return dataquery.(variants.Dataquery), nil + } + } + // We have no idea what type the dataquery is: use our `UnknownDataquery` bag to not lose data. dataquery := variants.UnknownDataquery{} if err := json.Unmarshal(raw, &dataquery); err != nil { From 4363d52308eee771484be6eb770a779a9fcd3da5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Fri, 27 Sep 2024 16:41:56 +0200 Subject: [PATCH 2/5] =?UTF-8?q?Bootstrap=20JSON=20=E2=86=92=20code=20conve?= =?UTF-8?q?rsion=20in=20Go?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/ast/builder.go | 7 + internal/ast/types.go | 4 + internal/codegen/output.go | 5 +- internal/codegen/pipeline.go | 7 +- internal/jennies/golang/builder.go | 26 +- internal/jennies/golang/converter.go | 77 +++ internal/jennies/golang/equality_test.go | 56 -- internal/jennies/golang/jennies.go | 8 +- internal/jennies/golang/jsonmarshalling.go | 24 +- internal/jennies/golang/rawtypes.go | 2 +- internal/jennies/golang/runtime.go | 8 + .../templates/converters/converter.tmpl | 107 ++++ .../golang/templates/runtime/runtime.tmpl | 125 +++++ .../templates/runtime/variant_models.tmpl | 2 + .../variant_dataquery.json_unmarshal.tmpl | 20 +- .../variant_panelcfg.json_unmarshal.tmpl | 13 +- internal/jennies/golang/tmpl.go | 19 +- internal/jennies/golang/types.go | 27 + internal/jennies/template/template.go | 11 + internal/languages/config.go | 3 + internal/languages/context.go | 20 +- internal/languages/converter.go | 507 ++++++++++++++++++ internal/languages/language.go | 6 + internal/languages/nilchecks.go | 5 +- schemas/pipeline.json | 3 + schemas/veneers.json | 4 + testdata/generated/cog/runtime.go | 127 +++++ testdata/generated/cog/variants/variants.go | 2 + testdata/generated/equality/types_gen.go | 56 +- .../GoRawTypes/defaults/types_gen.go | 69 ++- .../struct_complex_fields/types_gen.go | 21 +- .../struct_optional_fields/types_gen.go | 21 +- .../GoRawTypes/variant_dataquery/types_gen.go | 4 +- .../variant_panelcfg_full/types_gen.go | 8 +- .../types_gen.go | 4 +- 35 files changed, 1246 insertions(+), 162 deletions(-) create mode 100644 internal/jennies/golang/converter.go create mode 100644 internal/jennies/golang/templates/converters/converter.tmpl create mode 100644 internal/languages/converter.go diff --git a/internal/ast/builder.go b/internal/ast/builder.go index ed4d9a00..f9315f0f 100644 --- a/internal/ast/builder.go +++ b/internal/ast/builder.go @@ -196,12 +196,15 @@ type PathItem struct { // useful mostly for composability purposes, when a field Type is "any" // and we're trying to "compose in" something of a known type. TypeHint *Type `json:",omitempty"` + // Is this element of the path the root? (ie: a variable, not a member of a struct) + Root bool } func (item PathItem) DeepCopy() PathItem { clone := PathItem{ Identifier: item.Identifier, Type: item.Type.DeepCopy(), + Root: item.Root, } if item.TypeHint != nil { @@ -241,6 +244,10 @@ func (path Path) Append(suffix Path) Path { return newPath } +func (path Path) AppendStructField(field StructField) Path { + return path.Append(PathFromStructField(field)) +} + func (path Path) Last() PathItem { return path[len(path)-1] } diff --git a/internal/ast/types.go b/internal/ast/types.go index 712fb9ad..a99409cd 100644 --- a/internal/ast/types.go +++ b/internal/ast/types.go @@ -514,6 +514,10 @@ func NewObject(pkg string, name string, objectType Type, passesTrail ...string) } } +func (object *Object) AsRef() Type { + return object.SelfRef.AsType() +} + func (object *Object) AddToPassesTrail(trail string) { object.PassesTrail = append(object.PassesTrail, trail) } diff --git a/internal/codegen/output.go b/internal/codegen/output.go index 84f5c0c9..c7f3fee6 100644 --- a/internal/codegen/output.go +++ b/internal/codegen/output.go @@ -14,8 +14,9 @@ import ( type Output struct { Directory string `yaml:"directory"` - Types bool `yaml:"types"` - Builders bool `yaml:"builders"` + Types bool `yaml:"types"` + Builders bool `yaml:"builders"` + Converters bool `yaml:"converters"` Languages []*OutputLanguage `yaml:"languages"` diff --git a/internal/codegen/pipeline.go b/internal/codegen/pipeline.go index cdc024df..4b65bb21 100644 --- a/internal/codegen/pipeline.go +++ b/internal/codegen/pipeline.go @@ -124,9 +124,10 @@ func (pipeline *Pipeline) interpolate(input string) string { func (pipeline *Pipeline) jenniesConfig() languages.Config { return languages.Config{ - Debug: pipeline.Debug, - Types: pipeline.Output.Types, - Builders: pipeline.Output.Builders, + Debug: pipeline.Debug, + Types: pipeline.Output.Types, + Builders: pipeline.Output.Builders, + Converters: pipeline.Output.Converters, } } diff --git a/internal/jennies/golang/builder.go b/internal/jennies/golang/builder.go index 2f3dd463..873ea806 100644 --- a/internal/jennies/golang/builder.go +++ b/internal/jennies/golang/builder.go @@ -18,6 +18,7 @@ type Builder struct { Tmpl *template.Template typeImportMapper func(pkg string) string + pathFormatter func(path ast.Path) string typeFormatter *typeFormatter } @@ -55,6 +56,7 @@ func (jenny *Builder) generateBuilder(context languages.Context, builder ast.Bui return imports.Add(pkg, jenny.Config.importPath(pkg)) } jenny.typeFormatter = builderTypeFormatter(jenny.Config, context, jenny.typeImportMapper) + jenny.pathFormatter = makePathFormatter(jenny.typeFormatter) // every builder has a dependency on cog's runtime, so let's make sure it's declared. jenny.typeImportMapper("cog") @@ -67,7 +69,7 @@ func (jenny *Builder) generateBuilder(context languages.Context, builder ast.Bui return jenny.Tmpl. Funcs(map[string]any{ - "formatPath": jenny.formatFieldPath, + "formatPath": jenny.pathFormatter, "formatType": jenny.typeFormatter.formatType, "formatTypeNoBuilder": func(typeDef ast.Type) string { return jenny.typeFormatter.doFormatType(typeDef, false) @@ -171,28 +173,6 @@ func (jenny *Builder) formatDefaultTypedArgs(context languages.Context, opt ast. return args } -func (jenny *Builder) formatFieldPath(fieldPath ast.Path) string { - parts := make([]string, len(fieldPath)) - - for i := range fieldPath { - output := tools.UpperCamelCase(fieldPath[i].Identifier) - - // don't generate type hints if: - // * there isn't one defined - // * the type isn't "any" - // * as a trailing element in the path - if !fieldPath[i].Type.IsAny() || fieldPath[i].TypeHint == nil || i == len(fieldPath)-1 { - parts[i] = output - continue - } - - formattedTypeHint := jenny.typeFormatter.formatType(*fieldPath[i].TypeHint) - parts[i] = output + fmt.Sprintf(".(*%s)", formattedTypeHint) - } - - return strings.Join(parts, ".") -} - func (jenny *Builder) emptyValueForType(typeDef ast.Type) string { switch typeDef.Kind { case ast.KindRef, ast.KindStruct, ast.KindArray, ast.KindMap: diff --git a/internal/jennies/golang/converter.go b/internal/jennies/golang/converter.go new file mode 100644 index 00000000..8ac8415d --- /dev/null +++ b/internal/jennies/golang/converter.go @@ -0,0 +1,77 @@ +package golang + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/grafana/codejen" + "github.com/grafana/cog/internal/ast" + "github.com/grafana/cog/internal/jennies/template" + "github.com/grafana/cog/internal/languages" +) + +type Converter struct { + Config Config + NullableConfig languages.NullableConfig + Tmpl *template.Template +} + +func (jenny *Converter) JennyName() string { + return "GoConverter" +} + +func (jenny *Converter) Generate(context languages.Context) (codejen.Files, error) { + files := codejen.Files{} + + for _, builder := range context.Builders { + output, err := jenny.generateConverter(context, builder) + if err != nil { + return nil, err + } + + filename := filepath.Join( + formatPackageName(builder.Package), + fmt.Sprintf("%s_converter_gen.go", strings.ToLower(builder.Name)), + ) + + files = append(files, *codejen.NewFile(filename, output, jenny)) + } + + return files, nil +} + +func (jenny *Converter) generateConverter(context languages.Context, builder ast.Builder) ([]byte, error) { + converter := languages.NewConverterGenerator(jenny.NullableConfig).FromBuilder(context, builder) + + imports := NewImportMap() + typeImportMapper := func(pkg string) string { + if imports.IsIdentical(pkg, builder.Package) { + return "" + } + + return imports.Add(pkg, jenny.Config.importPath(pkg)) + } + typeImportMapper("cog") + formatter := builderTypeFormatter(jenny.Config, context, typeImportMapper) + + dummyImports := NewImportMap() + dummyImportMapper := func(pkg string) string { + return dummyImports.Add(pkg, jenny.Config.importPath(pkg)) + } + + formatRawRef := func(pkg string, ref string) string { + return formatter.formatRef(ast.NewRef(pkg, ref), false) + } + + return jenny.Tmpl. + Funcs(map[string]any{ + "formatType": builderTypeFormatter(jenny.Config, context, dummyImportMapper).formatType, + "formatPath": makePathFormatter(formatter), + "formatRawRef": formatRawRef, + }). + RenderAsBytes("converters/converter.tmpl", map[string]any{ + "Imports": imports, + "Converter": converter, + }) +} diff --git a/internal/jennies/golang/equality_test.go b/internal/jennies/golang/equality_test.go index 8a54d844..f8d29aa4 100644 --- a/internal/jennies/golang/equality_test.go +++ b/internal/jennies/golang/equality_test.go @@ -117,25 +117,6 @@ func TestEquality_Struct_WithArrays(t *testing.T) { req.True(equality.Arrays{Refs: []equality.Variable{}}.Equals(equality.Arrays{Refs: []equality.Variable{}})) req.True(equality.Arrays{Refs: []equality.Variable{{Name: "foo"}}}.Equals(equality.Arrays{Refs: []equality.Variable{{Name: "foo"}}})) - req.True(equality.Arrays{AnonymousStructs: []struct { - Inner string `json:"inner"` - }{}}.Equals(equality.Arrays{ - AnonymousStructs: []struct { - Inner string `json:"inner"` - }{}, - })) - req.True(equality.Arrays{AnonymousStructs: []struct { - Inner string `json:"inner"` - }{ - {Inner: "foo"}, - }}.Equals(equality.Arrays{ - AnonymousStructs: []struct { - Inner string `json:"inner"` - }{ - {Inner: "foo"}, - }, - })) - req.True(equality.Arrays{ArrayOfAny: []any{}}.Equals(equality.Arrays{ArrayOfAny: []any{}})) req.True(equality.Arrays{ArrayOfAny: []any{"foo", 1}}.Equals(equality.Arrays{ArrayOfAny: []any{"foo", 1}})) @@ -146,30 +127,6 @@ func TestEquality_Struct_WithArrays(t *testing.T) { req.False(equality.Arrays{ArrayOfArray: [][]string{{"foo"}, {"bar"}}}.Equals(equality.Arrays{ArrayOfArray: [][]string{{"foo"}}})) req.False(equality.Arrays{ArrayOfArray: [][]string{{"foo"}, {"bar"}}}.Equals(equality.Arrays{ArrayOfArray: [][]string{{"foo"}, {"other"}}})) - req.False(equality.Arrays{AnonymousStructs: []struct { - Inner string `json:"inner"` - }{ - {Inner: "foo"}, - }}.Equals(equality.Arrays{ - AnonymousStructs: []struct { - Inner string `json:"inner"` - }{ - {Inner: "bar"}, - }, - })) - req.False(equality.Arrays{AnonymousStructs: []struct { - Inner string `json:"inner"` - }{ - {Inner: "foo"}, - }}.Equals(equality.Arrays{ - AnonymousStructs: []struct { - Inner string `json:"inner"` - }{ - {Inner: "foo"}, - {Inner: "bar"}, - }, - })) - req.False(equality.Arrays{ArrayOfAny: []any{"foo", 1}}.Equals(equality.Arrays{ArrayOfAny: []any{"foo"}})) req.False(equality.Arrays{ArrayOfAny: []any{"bar"}}.Equals(equality.Arrays{ArrayOfAny: []any{"foo"}})) } @@ -200,19 +157,6 @@ func TestEquality_Struct_WithMaps(t *testing.T) { "foo": {Name: "foo"}, }, })) - req.True(equality.Maps{ - AnonymousStructs: map[string]struct { - Inner string `json:"inner"` - }{ - "foo": {Inner: "foo"}, - }, - }.Equals(equality.Maps{ - AnonymousStructs: map[string]struct { - Inner string `json:"inner"` - }{ - "foo": {Inner: "foo"}, - }, - })) req.True(equality.Maps{ StringToAny: map[string]any{ "foo": 42, diff --git a/internal/jennies/golang/jennies.go b/internal/jennies/golang/jennies.go index 21ac4138..dbd446cc 100644 --- a/internal/jennies/golang/jennies.go +++ b/internal/jennies/golang/jennies.go @@ -15,8 +15,9 @@ import ( const LanguageRef = "go" type Config struct { - debug bool - generateBuilders bool + debug bool + generateBuilders bool + generateConverters bool // GenerateGoMod indicates whether a go.mod file should be generated. // If enabled, PackageRoot is used as module path. @@ -45,6 +46,7 @@ func (config Config) MergeWithGlobal(global languages.Config) Config { newConfig := config newConfig.debug = global.Debug newConfig.generateBuilders = global.Builders + newConfig.generateConverters = global.Converters return newConfig } @@ -85,6 +87,7 @@ func (language *Language) Jennies(globalConfig languages.Config) *codejen.JennyL common.If[languages.Context](globalConfig.Types, RawTypes{Config: config, Tmpl: tmpl}), common.If[languages.Context](!config.SkipRuntime && globalConfig.Builders, &Builder{Config: config, Tmpl: tmpl}), + common.If[languages.Context](!config.SkipRuntime && globalConfig.Builders && globalConfig.Converters, &Converter{Config: config, Tmpl: tmpl, NullableConfig: language.NullableKinds()}), ) jenny.AddPostprocessors(PostProcessFile, common.GeneratedCommentHeader(globalConfig)) @@ -94,6 +97,7 @@ func (language *Language) Jennies(globalConfig languages.Config) *codejen.JennyL func (language *Language) CompilerPasses() compiler.Passes { return compiler.Passes{ &compiler.AnonymousEnumToExplicitType{}, + &compiler.AnonymousStructsToNamed{}, &compiler.PrefixEnumValues{}, &compiler.NotRequiredFieldAsNullableType{}, &compiler.FlattenDisjunctions{}, diff --git a/internal/jennies/golang/jsonmarshalling.go b/internal/jennies/golang/jsonmarshalling.go index 8eb8bdee..c405880e 100644 --- a/internal/jennies/golang/jsonmarshalling.go +++ b/internal/jennies/golang/jsonmarshalling.go @@ -12,12 +12,14 @@ import ( type JSONMarshalling struct { tmpl *template.Template + config Config packageMapper func(string) string typeFormatter *typeFormatter } -func NewJSONMarshalling(tmpl *template.Template, packageMapper func(string) string, typeFormatter *typeFormatter) JSONMarshalling { +func NewJSONMarshalling(config Config, tmpl *template.Template, packageMapper func(string) string, typeFormatter *typeFormatter) JSONMarshalling { return JSONMarshalling{ + config: config, tmpl: tmpl.Funcs(template.FuncMap{ "formatType": typeFormatter.formatType, }), @@ -301,18 +303,34 @@ func (jenny JSONMarshalling) renderPanelcfgVariantUnmarshal(schema *ast.Schema) _, hasOptions := schema.LocateObject("Options") _, hasFieldConfig := schema.LocateObject("FieldConfig") + if jenny.config.generateConverters { + jenny.packageMapper("dashboard") + } + return jenny.tmpl.Render("types/variant_panelcfg.json_unmarshal.tmpl", map[string]any{ "schema": schema, "hasOptions": hasOptions, "hasFieldConfig": hasFieldConfig, + "hasConverter": jenny.config.generateConverters, }) } func (jenny JSONMarshalling) renderDataqueryVariantUnmarshal(schema *ast.Schema, obj ast.Object) (string, error) { jenny.packageMapper("cog/variants") + var disjunctionStruct *ast.StructType + + if obj.Type.IsRef() { + resolved, _ := schema.Resolve(obj.Type) + if resolved.IsStructGeneratedFromDisjunction() { + disjunctionStruct = resolved.Struct + } + } + return jenny.tmpl.Render("types/variant_dataquery.json_unmarshal.tmpl", map[string]any{ - "schema": schema, - "object": obj, + "schema": schema, + "object": obj, + "hasConverter": jenny.config.generateConverters, + "disjunctionStruct": disjunctionStruct, }) } diff --git a/internal/jennies/golang/rawtypes.go b/internal/jennies/golang/rawtypes.go index 79133b6e..ee199323 100644 --- a/internal/jennies/golang/rawtypes.go +++ b/internal/jennies/golang/rawtypes.go @@ -56,7 +56,7 @@ func (jenny RawTypes) generateSchema(context languages.Context, schema *ast.Sche return imports.Add(pkg, jenny.Config.importPath(pkg)) } jenny.typeFormatter = defaultTypeFormatter(jenny.Config, context, packageMapper) - unmarshallerGenerator := NewJSONMarshalling(jenny.Tmpl, packageMapper, jenny.typeFormatter) + unmarshallerGenerator := NewJSONMarshalling(jenny.Config, jenny.Tmpl, packageMapper, jenny.typeFormatter) equalityMethodsGenerator := newEqualityMethods(jenny.Tmpl) schema.Objects.Iterate(func(_ string, object ast.Object) { diff --git a/internal/jennies/golang/runtime.go b/internal/jennies/golang/runtime.go index 5b636a90..a15e7b42 100644 --- a/internal/jennies/golang/runtime.go +++ b/internal/jennies/golang/runtime.go @@ -106,6 +106,14 @@ func MakeBuildErrors(rootPath string, err error) BuildErrors { }} } +func Unptr[T any](v *T) T { + var val T + if v == nil { + return val + } + return *v +} + `) } diff --git a/internal/jennies/golang/templates/converters/converter.tmpl b/internal/jennies/golang/templates/converters/converter.tmpl new file mode 100644 index 00000000..eda92e78 --- /dev/null +++ b/internal/jennies/golang/templates/converters/converter.tmpl @@ -0,0 +1,107 @@ +package {{ .Converter.Package | formatPackageName }} + +{{ $converter := include "converter" . }} + +{{ .Imports }} + +{{ $converter }} + +{{- define "guard" }} + {{- if and (eq .Op "!=") (eq .Value nil) -}} + {{ .Path | formatPath }} != nil + {{- else -}} + {{- $leftOperand := print (.Path.Last.Type | maybeDereference) (.Path | formatPath) -}} + {{- $operator := .Op -}} + {{- if eq .Op "minLength" -}} + {{- $leftOperand = print "len(" $leftOperand ")" -}} + {{- $operator = ">=" -}} + {{- end -}} + {{- if eq .Op "maxLength" -}} + {{- $leftOperand = print "len(" $leftOperand ")" -}} + {{- $operator = "<=" -}} + {{- end -}} + {{- $leftOperand }} {{ $operator}} {{ .Value | formatScalar -}} + {{- end -}} +{{- end }} + +{{- define "value_formatter" -}} + {{- if .Type.IsAny -}} + cog.Dump({{ .Path | formatPath }}) + {{- else if .Type.IsScalar -}} + fmt.Sprintf("%#v", {{ .Type | maybeDereference }}{{ .Path | formatPath }}) + {{- else -}} + cog.Dump({{ .Type | maybeDereference }}{{ .Path | formatPath }}) + {{- end -}} +{{- end }} + +{{- define "guards" }} + {{- $guardsCount := sub1 (len .) -}} + {{- range $i, $guard := . }}{{- template "guard" $guard }}{{ if ne $i $guardsCount }} && {{ end }}{{ end }} +{{- end }} + +{{- define "prepare_arg" -}} + {{- with .Arg.Builder -}} + {{ $.IntoVar }} := {{ formatRawRef .BuilderPkg (print .BuilderName "Converter") }}({{- if not .ValueType.Nullable}}&{{ end }}{{ .ValuePath | formatPath }}) + {{- end -}} + {{- with .Arg.Array -}} + tmp{{ $.IntoVar }} := []string{} + for _, {{ .ValueAs | formatPath }} := range {{ .For | formatPath }} { + {{- $subIntoVar := print "tmp" .For.Last.Identifier (.ValueAs | formatPath) }} + {{ template "prepare_arg" (dict "IntoVar" $subIntoVar "Arg" .ForArg) }} + tmp{{ $.IntoVar }} = append(tmp{{ $.IntoVar }}, {{ $subIntoVar }}) + } + {{ $.IntoVar }} := "{{ .ForType | formatType }}{" + strings.Join(tmp{{ $.IntoVar }}, ",\n") + "}" + {{- end -}} + {{- with .Arg.Runtime -}} + {{ $.IntoVar }} := cog.{{ .FuncName }}({{ range $i, $runtimeArg := .Args }}{{ if eq $i 0}}{{ $runtimeArg.ValuePath | formatPath }}{{ else }}{{ maybeUnptr ($runtimeArg.ValuePath | formatPath) $runtimeArg.ValueType }}{{ end }}, {{ end }}) + {{- end -}} + {{- with .Arg.Direct -}} + {{ $.IntoVar }} := {{- template "value_formatter" (dict "Type" .ValueType "Path" .ValuePath) -}} + {{- end -}} +{{- end }} + +{{- define "option_mapping" -}} + {{- $argsCount := sub1 (len .Args) -}} + {{- with .ArgumentGuards -}}if {{ template "guards" . }} { {{- end }} + {{- if and (eq (len .Guards) 0) (eq (len .ArgumentGuards) 0) -}} { {{- end }} + buffer.WriteString(`{{ .Option.Name | upperCamelCase }}(`) + {{- range $i, $arg := .Args }} + {{- $intoVar := print "arg" $i }} + {{ template "prepare_arg" (dict "IntoVar" $intoVar "Arg" $arg) }} + buffer.WriteString({{ $intoVar }}) + {{ if ne $i $argsCount }}buffer.WriteString(", "){{- end }} + {{- end }} + buffer.WriteString(")") + + calls = append(calls, buffer.String()) + buffer.Reset() + {{ with .ArgumentGuards -}} } {{- end }} + {{- if and (eq (len .Guards) 0) (eq (len .ArgumentGuards) 0) -}} } {{- end }} +{{- end }} + +{{- define "conversion_mapping" -}} + {{- $firstOpt := .Options | first }} + {{- with $firstOpt.Guards -}}if {{ template "guards" . }} { {{- end }} + {{- if ne .RepeatFor nil -}}for _, {{ .RepeatAs }} := range {{ .RepeatFor | formatPath }} { {{- end }} + {{- range $optMapping := .Options }} + {{ template "option_mapping" $optMapping }} + {{- end }} + {{ if ne .RepeatFor nil -}} } {{- end }} + {{- with $firstOpt.Guards -}} } {{- end }} +{{- end }} + +{{- define "converter" -}} + func {{ .Converter.BuilderName | upperCamelCase }}Converter(input *{{ formatRawRef .Converter.Input.TypeRef.ReferredPkg .Converter.Input.TypeRef.ReferredType }}) string { + {{- $constructorArgsCount := sub1 (len .Converter.ConstructorArgs) }} + calls := []string{ + `{{ .Converter.Package | formatPackageName }}.New{{ .Converter.BuilderName | upperCamelCase }}Builder({{ with .Converter.ConstructorArgs}}`+{{ range $i, $arg := . }}{{- template "value_formatter" (dict "Type" $arg.ValueType "Path" $arg.ValuePath ) -}}{{- if ne $i $constructorArgsCount }} + ", " +{{ end }}{{ end }}+`{{ end }})`, + } + var buffer strings.Builder + + {{- range .Converter.Mappings }} + {{ template "conversion_mapping" . }} + {{- end }} + + return strings.Join(calls, ".\t\n") + } +{{- end }} diff --git a/internal/jennies/golang/templates/runtime/runtime.tmpl b/internal/jennies/golang/templates/runtime/runtime.tmpl index 1bfa3f92..b64659b1 100644 --- a/internal/jennies/golang/templates/runtime/runtime.tmpl +++ b/internal/jennies/golang/templates/runtime/runtime.tmpl @@ -110,3 +110,128 @@ func UnmarshalDataquery(raw []byte, dataqueryTypeHint string) (variants.Dataquer func ConfigForPanelcfgVariant(identifier string) (variants.PanelcfgConfig, bool) { return NewRuntime().ConfigForPanelcfgVariant(identifier) } + + +func (runtime *Runtime) ConvertPanelToGo(inputPanel any, panelType string) string { + config, found := runtime.panelcfgVariants[panelType] + if found && config.GoConverter != nil { + return config.GoConverter(inputPanel) + } + + return "/* could not convert panel to go */" +} + +func (runtime *Runtime) ConvertDataqueryToGo(inputPanel any, dataqueryTypeHints ...string) string { + for _, dataqueryTypeHint := range dataqueryTypeHints { + config, found := runtime.dataqueryVariants[dataqueryTypeHint] + if found && config.GoConverter != nil { + return config.GoConverter(inputPanel) + } + } + + return "/* could not convert dataquery to go */" +} + +func ConvertPanelToCode(inputPanel any, panelType string) string { + return NewRuntime().ConvertPanelToGo(inputPanel, panelType) +} + +func ConvertDataqueryToCode(inputPanel any, dataqueryTypeHints ...string) string { + return NewRuntime().ConvertDataqueryToGo(inputPanel, dataqueryTypeHints...) +} + +func Dump(root any) string { + return dumpValue(reflect.ValueOf(root)) +} + +func dumpValue(value reflect.Value) string { + if !value.IsValid() { + return "" + } + + switch value.Kind() { + case reflect.Bool: + return fmt.Sprintf("%#v", value.Bool()) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return fmt.Sprintf("%d", value.Int()) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return fmt.Sprintf("%d", value.Uint()) + case reflect.Float32, reflect.Float64: + return fmt.Sprintf("%#v", value.Float()) + case reflect.String: + return fmt.Sprintf("%#v", value.String()) + case reflect.Array, reflect.Slice: + return dumpArray(value) + case reflect.Map: + return dumpMap(value) + case reflect.Struct: + return dumpStruct(value) + case reflect.Interface: + if !value.CanInterface() { + return "" + } + + return Dump(value.Interface()) + case reflect.Pointer: + if value.IsNil() { + return "nil" + } + + pointed := value.Elem() + + return fmt.Sprintf("cog.ToPtr[%s](%s)", pointed.Type().String(), dumpValue(pointed)) + default: + return fmt.Sprintf("", value.Type(), value.Kind().String()) + } +} + +func dumpArray(value reflect.Value) string { + if value.IsNil() { + return "nil" + } + + parts := make([]string, 0, value.Len()) + for i := 0; i < value.Len(); i++ { + parts = append(parts, dumpValue(value.Index(i))) + } + + return fmt.Sprintf("%s{%s}", value.Type().String(), strings.Join(parts, ", ")) +} + +func dumpMap(value reflect.Value) string { + if value.IsNil() { + return "nil" + } + + parts := make([]string, 0, value.Len()) + iter := value.MapRange() + for iter.Next() { + line := fmt.Sprintf("%s: %s", dumpValue(iter.Key()), dumpValue(iter.Value())) + parts = append(parts, line) + } + + return fmt.Sprintf("%s{%s}", value.Type().String(), strings.Join(parts, ", ")) +} + +func dumpStruct(value reflect.Value) string { + parts := make([]string, 0, value.NumField()) + structType := value.Type() + + for i := 0; i < value.NumField(); i++ { + field := structType.Field(i) + if !field.IsExported() { + continue + } + + fieldValue := value.Field(i) + fieldValueKind := fieldValue.Kind() + if (fieldValueKind == reflect.Pointer || fieldValueKind == reflect.Interface || fieldValueKind == reflect.Array || fieldValueKind == reflect.Slice || fieldValueKind == reflect.Map) && fieldValue.IsNil() { + continue + } + + line := fmt.Sprintf("%s: %s", field.Name, dumpValue(fieldValue)) + parts = append(parts, line) + } + + return fmt.Sprintf("%s{%s}", value.Type().String(), strings.Join(parts, ", ")) +} diff --git a/internal/jennies/golang/templates/runtime/variant_models.tmpl b/internal/jennies/golang/templates/runtime/variant_models.tmpl index 2539c47d..2fc27e21 100644 --- a/internal/jennies/golang/templates/runtime/variant_models.tmpl +++ b/internal/jennies/golang/templates/runtime/variant_models.tmpl @@ -4,11 +4,13 @@ type PanelcfgConfig struct { Identifier string OptionsUnmarshaler func(raw []byte) (any, error) FieldConfigUnmarshaler func(raw []byte) (any, error) + GoConverter func(inputPanel any) string } type DataqueryConfig struct { Identifier string DataqueryUnmarshaler func(raw []byte) (Dataquery, error) + GoConverter func(inputPanel any) string } type Dataquery interface { diff --git a/internal/jennies/golang/templates/types/variant_dataquery.json_unmarshal.tmpl b/internal/jennies/golang/templates/types/variant_dataquery.json_unmarshal.tmpl index 339b518d..d0b6b918 100644 --- a/internal/jennies/golang/templates/types/variant_dataquery.json_unmarshal.tmpl +++ b/internal/jennies/golang/templates/types/variant_dataquery.json_unmarshal.tmpl @@ -2,14 +2,30 @@ func VariantConfig() variants.DataqueryConfig { return variants.DataqueryConfig{ Identifier: "{{ .schema.Metadata.Identifier|lower }}", DataqueryUnmarshaler: func (raw []byte) (variants.Dataquery, error) { - dataquery := {{ .object.Name|upperCamelCase }}{} + dataquery := &{{ .object.Name|upperCamelCase }}{} - if err := json.Unmarshal(raw, &dataquery); err != nil { + if err := json.Unmarshal(raw, dataquery); err != nil { return nil, err } return dataquery, nil }, + {{- if .hasConverter }} + GoConverter: func(inputPanel any) string { + panel := inputPanel.(*{{ .object.Name | upperCamelCase }}) + {{ if ne .disjunctionStruct nil -}} + {{- range $field := .disjunctionStruct.Fields }} + if panel.{{ $field.Name | upperCamelCase }} != nil { + return {{ $field.Type.Ref.ReferredType | upperCamelCase }}Converter(panel.{{ $field.Name | upperCamelCase }}) + } + {{- end }} + + return "" + {{- else -}} + return {{ .object.Name | upperCamelCase }}Converter(panel) + {{- end }} + }, + {{- end }} } } diff --git a/internal/jennies/golang/templates/types/variant_panelcfg.json_unmarshal.tmpl b/internal/jennies/golang/templates/types/variant_panelcfg.json_unmarshal.tmpl index 4c116f17..b3a6d90f 100644 --- a/internal/jennies/golang/templates/types/variant_panelcfg.json_unmarshal.tmpl +++ b/internal/jennies/golang/templates/types/variant_panelcfg.json_unmarshal.tmpl @@ -3,9 +3,9 @@ func VariantConfig() variants.PanelcfgConfig { Identifier: "{{ .schema.Metadata.Identifier|lower }}", {{- if .hasOptions }} OptionsUnmarshaler: func (raw []byte) (any, error) { - options := Options{} + options := &Options{} - if err := json.Unmarshal(raw, &options); err != nil { + if err := json.Unmarshal(raw, options); err != nil { return nil, err } @@ -14,15 +14,20 @@ func VariantConfig() variants.PanelcfgConfig { {{- end }} {{- if .hasFieldConfig }} FieldConfigUnmarshaler: func (raw []byte) (any, error) { - fieldConfig := FieldConfig{} + fieldConfig := &FieldConfig{} - if err := json.Unmarshal(raw, &fieldConfig); err != nil { + if err := json.Unmarshal(raw, fieldConfig); err != nil { return nil, err } return fieldConfig, nil }, {{- end }} + {{- if .hasConverter }} + GoConverter: func(inputPanel any) string { + return PanelConverter(inputPanel.(*dashboard.Panel)) + }, + {{- end }} } } diff --git a/internal/jennies/golang/tmpl.go b/internal/jennies/golang/tmpl.go index 803d894c..9f2175f7 100644 --- a/internal/jennies/golang/tmpl.go +++ b/internal/jennies/golang/tmpl.go @@ -8,7 +8,7 @@ import ( "github.com/grafana/cog/internal/jennies/template" ) -//go:embed templates/runtime/*.tmpl templates/builders/*.tmpl templates/types/*.tmpl +//go:embed templates/runtime/*.tmpl templates/builders/*.tmpl templates/converters/*.tmpl templates/types/*.tmpl //nolint:gochecknoglobals var templatesFS embed.FS @@ -27,6 +27,9 @@ func initTemplates(extraTemplatesDirectories []string) *template.Template { "formatTypeNoBuilder": func(_ ast.Type) string { panic("formatType() needs to be overridden by a jenny") }, + "formatRawRef": func(_ ast.Type) string { + panic("formatRawRef() needs to be overridden by a jenny") + }, "emptyValueForGuard": func(_ ast.Type) string { panic("emptyValueForGuard() needs to be overridden by a jenny") }, @@ -66,6 +69,20 @@ func initTemplates(extraTemplatesDirectories []string) *template.Template { return variableName }, + "maybeUnptr": func(variableName string, intoType ast.Type) string { + if !intoType.Nullable || intoType.IsArray() || intoType.IsMap() || intoType.IsComposableSlot() { + return variableName + } + + return "cog.Unptr(" + variableName + ")" + }, + "maybeDereference": func(typeDef ast.Type) string { + if typeDef.Nullable && !typeDef.IsAnyOf(ast.KindArray, ast.KindMap) { + return "*" + } + + return "" + }, "isNullableNonArray": func(typeDef ast.Type) bool { return typeDef.Nullable && !typeDef.IsArray() }, diff --git a/internal/jennies/golang/types.go b/internal/jennies/golang/types.go index 5f7c32b9..1cd052e2 100644 --- a/internal/jennies/golang/types.go +++ b/internal/jennies/golang/types.go @@ -351,3 +351,30 @@ func (formatter *typeFormatter) formatIntersection(def ast.IntersectionType) str return buffer.String() } + +func makePathFormatter(typeFormatter *typeFormatter) func(path ast.Path) string { + return func(fieldPath ast.Path) string { + parts := make([]string, len(fieldPath)) + + for i := range fieldPath { + output := fieldPath[i].Identifier + if !fieldPath[i].Root { + output = tools.UpperCamelCase(output) + } + + // don't generate type hints if: + // * there isn't one defined + // * the type isn't "any" + // * as a trailing element in the path + if !fieldPath[i].Type.IsAny() || fieldPath[i].TypeHint == nil || i == len(fieldPath)-1 { + parts[i] = output + continue + } + + formattedTypeHint := typeFormatter.formatType(*fieldPath[i].TypeHint) + parts[i] = output + fmt.Sprintf(".(*%s)", formattedTypeHint) + } + + return strings.Join(parts, ".") + } +} diff --git a/internal/jennies/template/template.go b/internal/jennies/template/template.go index c032b396..357bf89f 100644 --- a/internal/jennies/template/template.go +++ b/internal/jennies/template/template.go @@ -180,6 +180,7 @@ func (template *Template) builtins() FuncMap { return gotemplate.FuncMap{ "add1": func(i int) int { return i + 1 }, + "sub1": func(i int) int { return i - 1 }, // https://github.com/Masterminds/sprig/blob/581758eb7d96ae4d113649668fa96acc74d46e7f/dict.go#L76 "dict": func(v ...any) map[string]any { dict := map[string]any{} @@ -207,6 +208,16 @@ func (template *Template) builtins() FuncMap { } return given[0] }, + "first": func(list any) any { + tp := reflect.TypeOf(list).Kind() + switch tp { + case reflect.Slice, reflect.Array: + l2 := reflect.ValueOf(list) + return l2.Index(0).Interface() // this will *willingly* panic if the list is empty + default: + panic(fmt.Sprintf("Cannot find first on type %s", tp)) + } + }, // ------- \\ // Strings \\ diff --git a/internal/languages/config.go b/internal/languages/config.go index 5e9d27a8..5844ea65 100644 --- a/internal/languages/config.go +++ b/internal/languages/config.go @@ -10,4 +10,7 @@ type Config struct { // Builders indicates whether builders should be generated or not. Builders bool + + // Converters indicates whether converters should be generated or not. + Converters bool } diff --git a/internal/languages/context.go b/internal/languages/context.go index 3838faa6..a35a2855 100644 --- a/internal/languages/context.go +++ b/internal/languages/context.go @@ -19,28 +19,32 @@ func (context *Context) LocateObjectByRef(ref ast.RefType) (ast.Object, bool) { } func (context *Context) ResolveToBuilder(def ast.Type) bool { + _, ok := context.ResolveAsBuilder(def) + + return ok +} + +func (context *Context) ResolveAsBuilder(def ast.Type) (ast.Builder, bool) { if def.IsArray() { - return context.ResolveToBuilder(def.AsArray().ValueType) + return context.ResolveAsBuilder(def.AsArray().ValueType) } if def.IsDisjunction() { for _, branch := range def.AsDisjunction().Branches { - if context.ResolveToBuilder(branch) { - return true + if builder, found := context.ResolveAsBuilder(branch); found { + return builder, true } } - return false + return ast.Builder{}, false } if !def.IsRef() { - return false + return ast.Builder{}, false } ref := def.AsRef() - _, found := context.Builders.LocateByObject(ref.ReferredPkg, ref.ReferredType) - - return found + return context.Builders.LocateByObject(ref.ReferredPkg, ref.ReferredType) } func (context *Context) IsDisjunctionOfBuilders(def ast.Type) bool { diff --git a/internal/languages/converter.go b/internal/languages/converter.go new file mode 100644 index 00000000..abe2c306 --- /dev/null +++ b/internal/languages/converter.go @@ -0,0 +1,507 @@ +package languages + +import ( + "fmt" + "strings" + + "github.com/grafana/cog/internal/ast" + "github.com/grafana/cog/internal/orderedmap" + "github.com/grafana/cog/internal/tools" +) + +type MappingGuard struct { + Path ast.Path + Op ast.Op + Value any +} + +func (guard MappingGuard) String() string { + return fmt.Sprintf("%s %s %v", guard.Path, guard.Op, guard.Value) +} + +type DirectArgMapping struct { + ValuePath ast.Path // direct mapping between a JSON value and an argument + ValueType ast.Type +} + +// mapping between a JSON value and an argument delegated to a builder +type BuilderArgMapping struct { + ValuePath ast.Path + ValueType ast.Type + BuilderPkg string + BuilderName string +} + +type ArrayArgMapping struct { + For ast.Path + ForType ast.Type + ForArg *ArgumentMapping + ValueAs ast.Path + ValueType ast.Type +} + +type RuntimeArgMapping struct { + FuncName string + Args []*DirectArgMapping +} + +type ArgumentMapping struct { + Direct *DirectArgMapping + Runtime *RuntimeArgMapping + Builder *BuilderArgMapping + Array *ArrayArgMapping + + Guards []MappingGuard +} + +type ConversionMapping struct { + // for _, panel := range input.Panels { WithPanel(panel) } + RepeatFor ast.Path // `input.Panels` + RepeatAs string // `panel` + + Options []OptionMapping +} + +type OptionMapping struct { + Option ast.Option // option in the builder + + Guards []MappingGuard + Args []ArgumentMapping +} + +func (optMapping OptionMapping) ArgumentGuards() []MappingGuard { + var guards []MappingGuard + + for _, arg := range optMapping.Args { + guards = append(guards, arg.Guards...) + } + + return guards +} + +type ConverterInput struct { + ArgName string + TypeRef ast.RefType +} + +type Converter struct { + Package string + + BuilderName string + Input ConverterInput + + // FIXME: assuming we only have direct mappings here is... *optimistic*. + ConstructorArgs []DirectArgMapping + + Mappings []ConversionMapping +} + +func (converter Converter) inputRootPath() ast.Path { + return ast.Path{ + { + Identifier: converter.Input.ArgName, + Type: converter.Input.TypeRef.AsType(), + Root: true, + }, + } +} + +type ConverterGenerator struct { + nullableTypes NullableConfig + + // generatedPaths lets us keep track of the paths in the input that we generated option mappings for. + // Since several options can represent with a single path, it allows us to not have "duplicates". + generatedPaths map[string]struct{} + + listOfDisjunctionOptions map[string][]ast.Option +} + +func NewConverterGenerator(nullableTypes NullableConfig) *ConverterGenerator { + return &ConverterGenerator{ + nullableTypes: nullableTypes, + generatedPaths: make(map[string]struct{}), + listOfDisjunctionOptions: make(map[string][]ast.Option), + } +} + +func (generator *ConverterGenerator) FromBuilder(context Context, builder ast.Builder) Converter { + converter := Converter{ + Package: builder.Package, + BuilderName: builder.Name, + + Input: ConverterInput{ + ArgName: "input", + TypeRef: builder.For.SelfRef, + }, + } + + converter.ConstructorArgs = generator.constructorArgs(converter, builder) + + converter.Mappings = tools.Map(builder.Options, func(option ast.Option) ConversionMapping { + return generator.convertOption(context, converter, option) + }) + + for _, opts := range generator.listOfDisjunctionOptions { + converter.Mappings = append(converter.Mappings, generator.convertListOfDisjunctionOptions(context, converter, opts)) + } + + converter.Mappings = tools.Filter(converter.Mappings, func(mapping ConversionMapping) bool { + return len(mapping.Options) != 0 + }) + + return converter +} + +func (generator *ConverterGenerator) constructorArgs(converter Converter, builder ast.Builder) []DirectArgMapping { + // we're only interested in assignments made from a constructor argument + // (as opposed to constant initializations for example) + argAssignments := tools.Filter(builder.Constructor.Assignments, func(assignment ast.Assignment) bool { + return assignment.Value.Argument != nil + }) + + return tools.Map(argAssignments, func(assignment ast.Assignment) DirectArgMapping { + return DirectArgMapping{ + ValuePath: converter.inputRootPath().Append(assignment.Path), + ValueType: assignment.Path.Last().Type, + } + }) +} + +func (generator *ConverterGenerator) convertListOfDisjunctionOptions(context Context, converter Converter, options []ast.Option) ConversionMapping { + mapping := ConversionMapping{ + RepeatFor: converter.inputRootPath().Append(options[0].Assignments[0].Path), + RepeatAs: "item", + } + + mapping.Options = tools.Map(options, func(option ast.Option) OptionMapping { + return generator.mappingForOption(context, converter, mapping, option) + }) + mapping.Options = tools.Filter(mapping.Options, func(optMapping OptionMapping) bool { + return optMapping.Option.Name != "" + }) + + return mapping +} + +func (generator *ConverterGenerator) convertOption(context Context, converter Converter, option ast.Option) ConversionMapping { + assignments := tools.Filter(option.Assignments, func(assignment ast.Assignment) bool { + key := generator.assignmentKey(assignment) + if _, ok := generator.generatedPaths[key]; ok { + return false + } + + return assignment.Value.Constant == nil + }) + if len(assignments) == 0 { + return ConversionMapping{} + } + + mapping := ConversionMapping{} + + if len(assignments) == 1 && assignments[0].Method == ast.AppendAssignment { + mapping.RepeatFor = converter.inputRootPath().Append(assignments[0].Path) + mapping.RepeatAs = "item" + } + + // if the option appends one possible branch of a disjunction to a list, + // we need to treat it differently + if mapping.RepeatFor != nil && generator.isAssignmentFromDisjunctionStruct(context, assignments[0]) { + path := assignments[0].Path.String() + generator.listOfDisjunctionOptions[path] = append(generator.listOfDisjunctionOptions[path], option) + return ConversionMapping{} + } + + optMapping := generator.mappingForOption(context, converter, mapping, option) + if optMapping.Option.Name == "" { + return ConversionMapping{} + } + + mapping.Options = []OptionMapping{optMapping} + + return mapping +} + +func (generator *ConverterGenerator) mappingForOption(context Context, converter Converter, mapping ConversionMapping, option ast.Option) OptionMapping { + optMapping := OptionMapping{ + Option: option, + Guards: generator.optionGuards(converter, option), + } + + assignments := tools.Filter(option.Assignments, func(assignment ast.Assignment) bool { + key := generator.assignmentKey(assignment) + if _, ok := generator.generatedPaths[key]; ok { + return false + } + + return assignment.Value.Constant == nil + }) + if len(assignments) == 0 { + return OptionMapping{} + } + + i := 0 + for _, assignment := range assignments { + i++ + + generator.generatedPaths[generator.assignmentKey(assignment)] = struct{}{} + + argName := fmt.Sprintf("arg%d", i) + valueType := assignment.Path.Last().Type + valuePath := converter.inputRootPath().Append(assignment.Path) + if mapping.RepeatFor != nil { + valueType = valueType.AsArray().ValueType + valuePath = ast.Path{ + {Identifier: mapping.RepeatAs, Type: valueType, Root: true}, + } + } + + if argument, ok := generator.argumentFromDisjunctionStruct(context, converter, argName, valuePath, assignment); ok { + optMapping.Args = append(optMapping.Args, argument) + continue + } + + if assignment.Value.Envelope != nil { + arguments := generator.argumentsForEnvelope(context, converter, argName, valuePath, assignment) + optMapping.Args = append(optMapping.Args, arguments...) + continue + } + + argument := generator.argumentForType(context, converter, argName, valuePath, valueType) + optMapping.Args = append(optMapping.Args, argument) + } + + return optMapping +} + +func (generator *ConverterGenerator) argumentsForEnvelope(context Context, converter Converter, argName string, valuePath ast.Path, assignment ast.Assignment) []ArgumentMapping { + var mappings []ArgumentMapping + for _, envelopeField := range assignment.Value.Envelope.Values { + fieldValuePath := valuePath.Append(envelopeField.Path) + mappings = append(mappings, generator.argumentForType(context, converter, argName, fieldValuePath, fieldValuePath.Last().Type)) + } + return mappings +} + +func (generator *ConverterGenerator) argumentForType(context Context, converter Converter, argName string, valuePath ast.Path, typeDef ast.Type) ArgumentMapping { + if typeDef.IsComposableSlot() { + var slotTypeHintsArgs []*DirectArgMapping + var guards []MappingGuard + converterRootType := context.ResolveRefs(converter.inputRootPath().Last().Type) + + if converterRootType.IsStruct() { + for _, field := range converterRootType.Struct.Fields { + if !field.Type.IsRef() || field.Type.AsRef().ReferredType != "DataSourceRef" { + continue + } + + refPath := ast.PathFromStructField(field) + datasourceRefType := context.ResolveRefs(field.Type) + + typeField, found := datasourceRefType.Struct.FieldByName("type") + if !found { + continue + } + + typePath := refPath.AppendStructField(typeField) + guards = append(guards, generator.pathNotNullGuards(converter, typePath)...) + + slotTypeHintsArgs = append(slotTypeHintsArgs, &DirectArgMapping{ + ValuePath: converter.inputRootPath().Append(typePath), + ValueType: typeField.Type, + }) + } + } + + return ArgumentMapping{ + Runtime: &RuntimeArgMapping{ + FuncName: fmt.Sprintf("Convert%sToCode", tools.UpperCamelCase(string(typeDef.AsComposableSlot().Variant))), + Args: append([]*DirectArgMapping{ + {ValuePath: valuePath, ValueType: typeDef}, + }, slotTypeHintsArgs...), + }, + Guards: guards, + } + } + + if typeDef.IsArray() { + valueAs := ast.Path{ + {Identifier: argName, Type: typeDef.Array.ValueType, Root: true}, + } + + forArg := generator.argumentForType(context, converter, argName+"Value", valueAs, typeDef.Array.ValueType) + + return ArgumentMapping{ + Array: &ArrayArgMapping{ + For: valuePath, + ForType: typeDef, + ForArg: &forArg, + ValueAs: valueAs, + ValueType: typeDef.Array.ValueType, + }, + } + } + + builder, found := context.ResolveAsBuilder(typeDef) + if found && strings.EqualFold(builder.Package, "dashboard") && strings.EqualFold("panel", builder.For.Name) { + typeField, _ := builder.For.Type.Struct.FieldByName("type") + + return ArgumentMapping{ + Runtime: &RuntimeArgMapping{ + FuncName: "ConvertPanelToCode", + Args: []*DirectArgMapping{ + {ValuePath: valuePath, ValueType: typeDef}, + {ValuePath: valuePath.AppendStructField(typeField), ValueType: typeField.Type}, + }, + }, + } + } + if found { + return ArgumentMapping{ + Builder: &BuilderArgMapping{ + ValuePath: valuePath, + ValueType: typeDef, + BuilderPkg: builder.Package, + BuilderName: builder.Name, + }, + } + } + + return ArgumentMapping{ + Direct: &DirectArgMapping{ + ValuePath: valuePath, + ValueType: typeDef, + }, + } +} + +func (generator *ConverterGenerator) isAssignmentFromDisjunctionStruct(context Context, assignment ast.Assignment) bool { + if assignment.Value.Envelope == nil { + return false + } + + envelopedType := assignment.Value.Envelope.Type + if envelopedType.IsRef() { + referredObject, _ := context.LocateObject(envelopedType.Ref.ReferredPkg, envelopedType.Ref.ReferredType) + envelopedType = referredObject.Type + } + + return envelopedType.IsStructGeneratedFromDisjunction() +} + +func (generator *ConverterGenerator) argumentFromDisjunctionStruct(context Context, converter Converter, argName string, valuePath ast.Path, assignment ast.Assignment) (ArgumentMapping, bool) { + if !generator.isAssignmentFromDisjunctionStruct(context, assignment) { + return ArgumentMapping{}, false + } + + envelopeValues := assignment.Value.Envelope.Values + + arg := generator.argumentForType(context, converter, argName, valuePath.Append(envelopeValues[0].Path), envelopeValues[0].Path.Last().Type) + arg.Guards = tools.Map(envelopeValues, func(envelopedField ast.EnvelopeFieldValue) MappingGuard { + return MappingGuard{ + Path: valuePath.Append(envelopedField.Path), + Op: ast.NotEqualOp, + Value: nil, + } + }) + + return arg, true +} + +func (generator *ConverterGenerator) optionGuards(converter Converter, option ast.Option) []MappingGuard { + // conditions safeguarding the conversion of the current option + guards := orderedmap.New[string, MappingGuard]() + + // TODO: define guards other than "not null" checks? (0, "", ...) + // TODO: builders + array of builders (and array of array of builders, ...) + // TODO: envelopes? + for _, assignment := range option.Assignments { + nullPathChunksGuards := generator.pathNotNullGuards(converter, assignment.Path) + for _, guard := range nullPathChunksGuards { + guards.Set(guard.String(), guard) + } + + if assignment.Value.Constant != nil { + guard := MappingGuard{ + Path: converter.inputRootPath().Append(assignment.Path), + Op: ast.EqualOp, + Value: assignment.Value.Constant, + } + guards.Set(guard.String(), guard) + continue + } + + assignmentType := assignment.Path.Last().Type + + // For arrays: ensure they're not empty + if assignmentType.IsArray() { + guard := MappingGuard{ + Path: converter.inputRootPath().Append(assignment.Path), + Op: ast.MinLengthOp, + Value: 1, + } + guards.Set(guard.String(), guard) + } + + // For strings: ensure they're not empty + // TODO: deal with datetime strings + if assignmentType.IsScalar() && assignmentType.AsScalar().ScalarKind == ast.KindString && !assignmentType.HasHint(ast.HintStringFormatDateTime) { + guard := MappingGuard{ + Path: converter.inputRootPath().Append(assignment.Path), + Op: ast.NotEqualOp, + Value: "", + } + guards.Set(guard.String(), guard) + } + + // TODO: is that correct/needed? + if assignment.Method != ast.AppendAssignment && assignment.Value.Envelope != nil { + for _, envelopePath := range assignment.Value.Envelope.Values { + guard := MappingGuard{ + Path: converter.inputRootPath().Append(assignment.Path.Append(envelopePath.Path)), + Op: ast.NotEqualOp, + Value: nil, + } + guards.Set(guard.String(), guard) + } + continue + } + } + + return guards.Values() +} + +func (generator *ConverterGenerator) pathNotNullGuards(converter Converter, path ast.Path) []MappingGuard { + var guards []MappingGuard + + for i, chunk := range path { + chunkType := chunk.Type + if chunk.TypeHint != nil { + chunkType = *chunk.TypeHint + } + + if !generator.nullableTypes.TypeIsNullable(chunkType) { + continue + } + + guards = append(guards, MappingGuard{ + Path: converter.inputRootPath().Append(path[:i+1]), + Op: ast.NotEqualOp, + Value: nil, + }) + } + + return guards +} + +func (generator *ConverterGenerator) assignmentKey(assignment ast.Assignment) string { + path := assignment.Path.String() + + if assignment.Value.Envelope != nil { + // TODO: envelope of envelope? + for _, envelopeAssignment := range assignment.Value.Envelope.Values { + path += "," + envelopeAssignment.Path.String() + } + } + + return path +} diff --git a/internal/languages/language.go b/internal/languages/language.go index f6925b20..742e20cc 100644 --- a/internal/languages/language.go +++ b/internal/languages/language.go @@ -22,6 +22,12 @@ type NullableKindsProvider interface { NullableKinds() NullableConfig } +func (nullableConfig NullableConfig) TypeIsNullable(typeDef ast.Type) bool { + return typeDef.Nullable || + (typeDef.IsAny() && nullableConfig.AnyIsNullable) || + typeDef.IsAnyOf(nullableConfig.Kinds...) +} + type Languages map[string]Language func (languages Languages) AsLanguageRefs() []string { diff --git a/internal/languages/nilchecks.go b/internal/languages/nilchecks.go index 5c0a5073..17e304e1 100644 --- a/internal/languages/nilchecks.go +++ b/internal/languages/nilchecks.go @@ -38,10 +38,7 @@ func GenerateBuilderNilChecks(language Language, context Context) (Context, erro continue } - nullable := chunk.Type.Nullable || - (chunk.Type.IsAny() && nullableKinds.AnyIsNullable) || - chunk.Type.IsAnyOf(nullableKinds.Kinds...) - if nullable { + if nullableKinds.TypeIsNullable(chunk.Type) { subPath := assignment.Path[:i+1] valueType := subPath.Last().Type if subPath.Last().TypeHint != nil { diff --git a/schemas/pipeline.json b/schemas/pipeline.json index 888fbd87..dc1d13c0 100644 --- a/schemas/pipeline.json +++ b/schemas/pipeline.json @@ -205,6 +205,9 @@ "builders": { "type": "boolean" }, + "converters": { + "type": "boolean" + }, "languages": { "items": { "$ref": "#/$defs/CodegenOutputLanguage" diff --git a/schemas/veneers.json b/schemas/veneers.json index 064dc3c1..432348dd 100644 --- a/schemas/veneers.json +++ b/schemas/veneers.json @@ -167,6 +167,10 @@ "typehint": { "$ref": "#/$defs/AstType", "description": "useful mostly for composability purposes, when a field Type is \"any\"\nand we're trying to \"compose in\" something of a known type." + }, + "root": { + "type": "boolean", + "description": "Is this element of the path the root? (ie: a variable, not a member of a struct)" } }, "additionalProperties": false, diff --git a/testdata/generated/cog/runtime.go b/testdata/generated/cog/runtime.go index 21389160..729df4a8 100644 --- a/testdata/generated/cog/runtime.go +++ b/testdata/generated/cog/runtime.go @@ -7,6 +7,9 @@ package cog import ( "encoding/json" + "fmt" + "reflect" + "strings" "github.com/grafana/cog/testdata/generated/cog/variants" ) @@ -119,3 +122,127 @@ func UnmarshalDataquery(raw []byte, dataqueryTypeHint string) (variants.Dataquer func ConfigForPanelcfgVariant(identifier string) (variants.PanelcfgConfig, bool) { return NewRuntime().ConfigForPanelcfgVariant(identifier) } + +func (runtime *Runtime) ConvertPanelToGo(inputPanel any, panelType string) string { + config, found := runtime.panelcfgVariants[panelType] + if found && config.GoConverter != nil { + return config.GoConverter(inputPanel) + } + + return "/* could not convert panel to go */" +} + +func (runtime *Runtime) ConvertDataqueryToGo(inputPanel any, dataqueryTypeHints ...string) string { + for _, dataqueryTypeHint := range dataqueryTypeHints { + config, found := runtime.dataqueryVariants[dataqueryTypeHint] + if found && config.GoConverter != nil { + return config.GoConverter(inputPanel) + } + } + + return "/* could not convert dataquery to go */" +} + +func ConvertPanelToCode(inputPanel any, panelType string) string { + return NewRuntime().ConvertPanelToGo(inputPanel, panelType) +} + +func ConvertDataqueryToCode(inputPanel any, dataqueryTypeHints ...string) string { + return NewRuntime().ConvertDataqueryToGo(inputPanel, dataqueryTypeHints...) +} + +func Dump(root any) string { + return dumpValue(reflect.ValueOf(root)) +} + +func dumpValue(value reflect.Value) string { + if !value.IsValid() { + return "" + } + + switch value.Kind() { + case reflect.Bool: + return fmt.Sprintf("%#v", value.Bool()) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return fmt.Sprintf("%d", value.Int()) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return fmt.Sprintf("%d", value.Uint()) + case reflect.Float32, reflect.Float64: + return fmt.Sprintf("%#v", value.Float()) + case reflect.String: + return fmt.Sprintf("%#v", value.String()) + case reflect.Array, reflect.Slice: + return dumpArray(value) + case reflect.Map: + return dumpMap(value) + case reflect.Struct: + return dumpStruct(value) + case reflect.Interface: + if !value.CanInterface() { + return "" + } + + return Dump(value.Interface()) + case reflect.Pointer: + if value.IsNil() { + return "nil" + } + + pointed := value.Elem() + + return fmt.Sprintf("cog.ToPtr[%s](%s)", pointed.Type().String(), dumpValue(pointed)) + default: + return fmt.Sprintf("", value.Type(), value.Kind().String()) + } +} + +func dumpArray(value reflect.Value) string { + if value.IsNil() { + return "nil" + } + + parts := make([]string, 0, value.Len()) + for i := 0; i < value.Len(); i++ { + parts = append(parts, dumpValue(value.Index(i))) + } + + return fmt.Sprintf("%s{%s}", value.Type().String(), strings.Join(parts, ", ")) +} + +func dumpMap(value reflect.Value) string { + if value.IsNil() { + return "nil" + } + + parts := make([]string, 0, value.Len()) + iter := value.MapRange() + for iter.Next() { + line := fmt.Sprintf("%s: %s", dumpValue(iter.Key()), dumpValue(iter.Value())) + parts = append(parts, line) + } + + return fmt.Sprintf("%s{%s}", value.Type().String(), strings.Join(parts, ", ")) +} + +func dumpStruct(value reflect.Value) string { + parts := make([]string, 0, value.NumField()) + structType := value.Type() + + for i := 0; i < value.NumField(); i++ { + field := structType.Field(i) + if !field.IsExported() { + continue + } + + fieldValue := value.Field(i) + fieldValueKind := fieldValue.Kind() + if (fieldValueKind == reflect.Pointer || fieldValueKind == reflect.Interface || fieldValueKind == reflect.Array || fieldValueKind == reflect.Slice || fieldValueKind == reflect.Map) && fieldValue.IsNil() { + continue + } + + line := fmt.Sprintf("%s: %s", field.Name, dumpValue(fieldValue)) + parts = append(parts, line) + } + + return fmt.Sprintf("%s{%s}", value.Type().String(), strings.Join(parts, ", ")) +} diff --git a/testdata/generated/cog/variants/variants.go b/testdata/generated/cog/variants/variants.go index 68428940..5306437a 100644 --- a/testdata/generated/cog/variants/variants.go +++ b/testdata/generated/cog/variants/variants.go @@ -11,11 +11,13 @@ type PanelcfgConfig struct { Identifier string OptionsUnmarshaler func(raw []byte) (any, error) FieldConfigUnmarshaler func(raw []byte) (any, error) + GoConverter func(inputPanel any) string } type DataqueryConfig struct { Identifier string DataqueryUnmarshaler func(raw []byte) (Dataquery, error) + GoConverter func(inputPanel any) string } type Dataquery interface { diff --git a/testdata/generated/equality/types_gen.go b/testdata/generated/equality/types_gen.go index 8c04db97..65adb56f 100644 --- a/testdata/generated/equality/types_gen.go +++ b/testdata/generated/equality/types_gen.go @@ -95,14 +95,12 @@ func (resource Optionals) Equals(other Optionals) bool { } type Arrays struct { - Ints []int64 `json:"ints"` - Strings []string `json:"strings"` - ArrayOfArray [][]string `json:"arrayOfArray"` - Refs []Variable `json:"refs"` - AnonymousStructs []struct { - Inner string `json:"inner"` - } `json:"anonymousStructs"` - ArrayOfAny []any `json:"arrayOfAny"` + Ints []int64 `json:"ints"` + Strings []string `json:"strings"` + ArrayOfArray [][]string `json:"arrayOfArray"` + Refs []Variable `json:"refs"` + AnonymousStructs []EqualityArraysAnonymousStructs `json:"anonymousStructs"` + ArrayOfAny []any `json:"arrayOfAny"` } func (resource Arrays) Equals(other Arrays) bool { @@ -159,7 +157,7 @@ func (resource Arrays) Equals(other Arrays) bool { } for i1 := range resource.AnonymousStructs { - if resource.AnonymousStructs[i1].Inner != other.AnonymousStructs[i1].Inner { + if !resource.AnonymousStructs[i1].Equals(other.AnonymousStructs[i1]) { return false } } @@ -179,13 +177,11 @@ func (resource Arrays) Equals(other Arrays) bool { } type Maps struct { - Ints map[string]int64 `json:"ints"` - Strings map[string]string `json:"strings"` - Refs map[string]Variable `json:"refs"` - AnonymousStructs map[string]struct { - Inner string `json:"inner"` - } `json:"anonymousStructs"` - StringToAny map[string]any `json:"stringToAny"` + Ints map[string]int64 `json:"ints"` + Strings map[string]string `json:"strings"` + Refs map[string]Variable `json:"refs"` + AnonymousStructs map[string]EqualityMapsAnonymousStructs `json:"anonymousStructs"` + StringToAny map[string]any `json:"stringToAny"` } func (resource Maps) Equals(other Maps) bool { @@ -225,7 +221,7 @@ func (resource Maps) Equals(other Maps) bool { } for key1 := range resource.AnonymousStructs { - if resource.AnonymousStructs[key1].Inner != other.AnonymousStructs[key1].Inner { + if !resource.AnonymousStructs[key1].Equals(other.AnonymousStructs[key1]) { return false } } @@ -243,3 +239,29 @@ func (resource Maps) Equals(other Maps) bool { return true } + +// Modified by compiler pass 'AnonymousStructsToNamed' +type EqualityArraysAnonymousStructs struct { + Inner string `json:"inner"` +} + +func (resource EqualityArraysAnonymousStructs) Equals(other EqualityArraysAnonymousStructs) bool { + if resource.Inner != other.Inner { + return false + } + + return true +} + +// Modified by compiler pass 'AnonymousStructsToNamed' +type EqualityMapsAnonymousStructs struct { + Inner string `json:"inner"` +} + +func (resource EqualityMapsAnonymousStructs) Equals(other EqualityMapsAnonymousStructs) bool { + if resource.Inner != other.Inner { + return false + } + + return true +} diff --git a/testdata/jennies/rawtypes/field_with_struct_with_defaults/GoRawTypes/defaults/types_gen.go b/testdata/jennies/rawtypes/field_with_struct_with_defaults/GoRawTypes/defaults/types_gen.go index ade8f915..ed6a0547 100644 --- a/testdata/jennies/rawtypes/field_with_struct_with_defaults/GoRawTypes/defaults/types_gen.go +++ b/testdata/jennies/rawtypes/field_with_struct_with_defaults/GoRawTypes/defaults/types_gen.go @@ -21,17 +21,8 @@ type Struct struct { AllFields NestedStruct `json:"allFields"` PartialFields NestedStruct `json:"partialFields"` EmptyFields NestedStruct `json:"emptyFields"` - ComplexField struct { - Uid string `json:"uid"` - Nested struct { - NestedVal string `json:"nestedVal"` -} `json:"nested"` - Array []string `json:"array"` -} `json:"complexField"` - PartialComplexField struct { - Uid string `json:"uid"` - IntVal int64 `json:"intVal"` -} `json:"partialComplexField"` + ComplexField DefaultsStructComplexField `json:"complexField"` + PartialComplexField DefaultsStructPartialComplexField `json:"partialComplexField"` } func (resource Struct) Equals(other Struct) bool { @@ -44,26 +35,68 @@ func (resource Struct) Equals(other Struct) bool { if !resource.EmptyFields.Equals(other.EmptyFields) { return false } - if resource.ComplexField.Uid != other.ComplexField.Uid { + if !resource.ComplexField.Equals(other.ComplexField) { + return false + } + if !resource.PartialComplexField.Equals(other.PartialComplexField) { + return false + } + + return true +} + + +type DefaultsStructComplexFieldNested struct { + NestedVal string `json:"nestedVal"` +} + +func (resource DefaultsStructComplexFieldNested) Equals(other DefaultsStructComplexFieldNested) bool { + if resource.NestedVal != other.NestedVal { + return false + } + + return true +} + + +type DefaultsStructComplexField struct { + Uid string `json:"uid"` + Nested DefaultsStructComplexFieldNested `json:"nested"` + Array []string `json:"array"` +} + +func (resource DefaultsStructComplexField) Equals(other DefaultsStructComplexField) bool { + if resource.Uid != other.Uid { return false } - if resource.ComplexField.Nested.NestedVal != other.ComplexField.Nested.NestedVal { + if !resource.Nested.Equals(other.Nested) { return false } - if len(resource.ComplexField.Array) != len(other.ComplexField.Array) { + if len(resource.Array) != len(other.Array) { return false } - for i1 := range resource.ComplexField.Array { - if resource.ComplexField.Array[i1] != other.ComplexField.Array[i1] { + for i1 := range resource.Array { + if resource.Array[i1] != other.Array[i1] { return false } } - if resource.PartialComplexField.Uid != other.PartialComplexField.Uid { + + return true +} + + +type DefaultsStructPartialComplexField struct { + Uid string `json:"uid"` + IntVal int64 `json:"intVal"` +} + +func (resource DefaultsStructPartialComplexField) Equals(other DefaultsStructPartialComplexField) bool { + if resource.Uid != other.Uid { return false } - if resource.PartialComplexField.IntVal != other.PartialComplexField.IntVal { + if resource.IntVal != other.IntVal { return false } diff --git a/testdata/jennies/rawtypes/struct_with_complex_fields/GoRawTypes/struct_complex_fields/types_gen.go b/testdata/jennies/rawtypes/struct_with_complex_fields/GoRawTypes/struct_complex_fields/types_gen.go index fa323bd9..985c7cd2 100644 --- a/testdata/jennies/rawtypes/struct_with_complex_fields/GoRawTypes/struct_complex_fields/types_gen.go +++ b/testdata/jennies/rawtypes/struct_with_complex_fields/GoRawTypes/struct_complex_fields/types_gen.go @@ -9,9 +9,7 @@ type SomeStruct struct { Operator SomeStructOperator `json:"Operator"` FieldArrayOfStrings []string `json:"FieldArrayOfStrings"` FieldMapOfStringToString map[string]string `json:"FieldMapOfStringToString"` - FieldAnonymousStruct struct { - FieldAny any `json:"FieldAny"` -} `json:"FieldAnonymousStruct"` + FieldAnonymousStruct StructComplexFieldsSomeStructFieldAnonymousStruct `json:"FieldAnonymousStruct"` FieldRefToConstant string `json:"fieldRefToConstant"` } @@ -57,8 +55,7 @@ func (resource SomeStruct) Equals(other SomeStruct) bool { return false } } - // is DeepEqual good enough here? - if !reflect.DeepEqual(resource.FieldAnonymousStruct.FieldAny, other.FieldAnonymousStruct.FieldAny) { + if !resource.FieldAnonymousStruct.Equals(other.FieldAnonymousStruct) { return false } if resource.FieldRefToConstant != other.FieldRefToConstant { @@ -92,6 +89,20 @@ const ( ) +type StructComplexFieldsSomeStructFieldAnonymousStruct struct { + FieldAny any `json:"FieldAny"` +} + +func (resource StructComplexFieldsSomeStructFieldAnonymousStruct) Equals(other StructComplexFieldsSomeStructFieldAnonymousStruct) bool { + // is DeepEqual good enough here? + if !reflect.DeepEqual(resource.FieldAny, other.FieldAny) { + return false + } + + return true +} + + type StringOrBool struct { String *string `json:"String,omitempty"` Bool *bool `json:"Bool,omitempty"` diff --git a/testdata/jennies/rawtypes/struct_with_optional_fields/GoRawTypes/struct_optional_fields/types_gen.go b/testdata/jennies/rawtypes/struct_with_optional_fields/GoRawTypes/struct_optional_fields/types_gen.go index cd56b6ef..995c74c0 100644 --- a/testdata/jennies/rawtypes/struct_with_optional_fields/GoRawTypes/struct_optional_fields/types_gen.go +++ b/testdata/jennies/rawtypes/struct_with_optional_fields/GoRawTypes/struct_optional_fields/types_gen.go @@ -5,9 +5,7 @@ type SomeStruct struct { FieldString *string `json:"FieldString,omitempty"` Operator *SomeStructOperator `json:"Operator,omitempty"` FieldArrayOfStrings []string `json:"FieldArrayOfStrings,omitempty"` - FieldAnonymousStruct *struct { - FieldAny any `json:"FieldAny"` -} `json:"FieldAnonymousStruct,omitempty"` + FieldAnonymousStruct *StructOptionalFieldsSomeStructFieldAnonymousStruct `json:"FieldAnonymousStruct,omitempty"` } func (resource SomeStruct) Equals(other SomeStruct) bool { @@ -53,8 +51,7 @@ func (resource SomeStruct) Equals(other SomeStruct) bool { } if resource.FieldAnonymousStruct != nil { - // is DeepEqual good enough here? - if !reflect.DeepEqual(resource.FieldAnonymousStruct.FieldAny, other.FieldAnonymousStruct.FieldAny) { + if !resource.FieldAnonymousStruct.Equals(*other.FieldAnonymousStruct) { return false } } @@ -84,3 +81,17 @@ const ( ) +type StructOptionalFieldsSomeStructFieldAnonymousStruct struct { + FieldAny any `json:"FieldAny"` +} + +func (resource StructOptionalFieldsSomeStructFieldAnonymousStruct) Equals(other StructOptionalFieldsSomeStructFieldAnonymousStruct) bool { + // is DeepEqual good enough here? + if !reflect.DeepEqual(resource.FieldAny, other.FieldAny) { + return false + } + + return true +} + + diff --git a/testdata/jennies/rawtypes/variant_dataquery/GoRawTypes/variant_dataquery/types_gen.go b/testdata/jennies/rawtypes/variant_dataquery/GoRawTypes/variant_dataquery/types_gen.go index 30559b2d..13ccd6aa 100644 --- a/testdata/jennies/rawtypes/variant_dataquery/GoRawTypes/variant_dataquery/types_gen.go +++ b/testdata/jennies/rawtypes/variant_dataquery/GoRawTypes/variant_dataquery/types_gen.go @@ -15,9 +15,9 @@ func VariantConfig() variants.DataqueryConfig { return variants.DataqueryConfig{ Identifier: "prometheus", DataqueryUnmarshaler: func (raw []byte) (variants.Dataquery, error) { - dataquery := Query{} + dataquery := &Query{} - if err := json.Unmarshal(raw, &dataquery); err != nil { + if err := json.Unmarshal(raw, dataquery); err != nil { return nil, err } diff --git a/testdata/jennies/rawtypes/variant_panelcfg_full/GoRawTypes/variant_panelcfg_full/types_gen.go b/testdata/jennies/rawtypes/variant_panelcfg_full/GoRawTypes/variant_panelcfg_full/types_gen.go index 8429535a..fdbd2be7 100644 --- a/testdata/jennies/rawtypes/variant_panelcfg_full/GoRawTypes/variant_panelcfg_full/types_gen.go +++ b/testdata/jennies/rawtypes/variant_panelcfg_full/GoRawTypes/variant_panelcfg_full/types_gen.go @@ -34,18 +34,18 @@ func VariantConfig() variants.PanelcfgConfig { return variants.PanelcfgConfig{ Identifier: "timeseries", OptionsUnmarshaler: func (raw []byte) (any, error) { - options := Options{} + options := &Options{} - if err := json.Unmarshal(raw, &options); err != nil { + if err := json.Unmarshal(raw, options); err != nil { return nil, err } return options, nil }, FieldConfigUnmarshaler: func (raw []byte) (any, error) { - fieldConfig := FieldConfig{} + fieldConfig := &FieldConfig{} - if err := json.Unmarshal(raw, &fieldConfig); err != nil { + if err := json.Unmarshal(raw, fieldConfig); err != nil { return nil, err } diff --git a/testdata/jennies/rawtypes/variant_panelcfg_only_options/GoRawTypes/variant_panelcfg_only_options/types_gen.go b/testdata/jennies/rawtypes/variant_panelcfg_only_options/GoRawTypes/variant_panelcfg_only_options/types_gen.go index d82aab02..ad673514 100644 --- a/testdata/jennies/rawtypes/variant_panelcfg_only_options/GoRawTypes/variant_panelcfg_only_options/types_gen.go +++ b/testdata/jennies/rawtypes/variant_panelcfg_only_options/GoRawTypes/variant_panelcfg_only_options/types_gen.go @@ -21,9 +21,9 @@ func VariantConfig() variants.PanelcfgConfig { return variants.PanelcfgConfig{ Identifier: "text", OptionsUnmarshaler: func (raw []byte) (any, error) { - options := Options{} + options := &Options{} - if err := json.Unmarshal(raw, &options); err != nil { + if err := json.Unmarshal(raw, options); err != nil { return nil, err } From 63a74ea3ddb436e76cc58c80d1fd4bf0fcac3d2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Sat, 28 Sep 2024 00:48:39 +0200 Subject: [PATCH 3/5] Better conversion of queries --- internal/jennies/golang/rawtypes.go | 10 ++++-- .../golang/templates/runtime/runtime.tmpl | 15 ++++----- .../templates/runtime/variant_models.tmpl | 7 ++-- internal/languages/converter.go | 33 ++----------------- testdata/generated/cog/runtime.go | 14 ++++---- testdata/generated/cog/variants/variants.go | 5 +++ .../GoRawTypes/variant_dataquery/types_gen.go | 3 ++ 7 files changed, 35 insertions(+), 52 deletions(-) diff --git a/internal/jennies/golang/rawtypes.go b/internal/jennies/golang/rawtypes.go index ee199323..2c14f830 100644 --- a/internal/jennies/golang/rawtypes.go +++ b/internal/jennies/golang/rawtypes.go @@ -60,7 +60,7 @@ func (jenny RawTypes) generateSchema(context languages.Context, schema *ast.Sche equalityMethodsGenerator := newEqualityMethods(jenny.Tmpl) schema.Objects.Iterate(func(_ string, object ast.Object) { - objectOutput, innerErr := jenny.formatObject(object) + objectOutput, innerErr := jenny.formatObject(schema, object) if innerErr != nil { err = innerErr return @@ -99,7 +99,7 @@ func (jenny RawTypes) generateSchema(context languages.Context, schema *ast.Sche %[2]s%[3]s`, formatPackageName(schema.Package), importStatements, buffer.String())), nil } -func (jenny RawTypes) formatObject(def ast.Object) ([]byte, error) { +func (jenny RawTypes) formatObject(schema *ast.Schema, def ast.Object) ([]byte, error) { var buffer strings.Builder defName := tools.UpperCamelCase(def.Name) @@ -145,6 +145,12 @@ func (jenny RawTypes) formatObject(def ast.Object) ([]byte, error) { buffer.WriteString(fmt.Sprintf("func (resource %s) Implements%sVariant() {}\n", defName, variant)) buffer.WriteString("\n") + + if def.Type.ImplementedVariant() == string(ast.SchemaVariantDataQuery) { + buffer.WriteString(fmt.Sprintf("func (resource %s) DataqueryType() string {\n", defName)) + buffer.WriteString(fmt.Sprintf("\treturn \"%s\"\n", strings.ToLower(schema.Metadata.Identifier))) + buffer.WriteString("}\n") + } } return []byte(buffer.String()), nil diff --git a/internal/jennies/golang/templates/runtime/runtime.tmpl b/internal/jennies/golang/templates/runtime/runtime.tmpl index b64659b1..3d1c6957 100644 --- a/internal/jennies/golang/templates/runtime/runtime.tmpl +++ b/internal/jennies/golang/templates/runtime/runtime.tmpl @@ -111,7 +111,6 @@ func ConfigForPanelcfgVariant(identifier string) (variants.PanelcfgConfig, bool) return NewRuntime().ConfigForPanelcfgVariant(identifier) } - func (runtime *Runtime) ConvertPanelToGo(inputPanel any, panelType string) string { config, found := runtime.panelcfgVariants[panelType] if found && config.GoConverter != nil { @@ -121,12 +120,10 @@ func (runtime *Runtime) ConvertPanelToGo(inputPanel any, panelType string) strin return "/* could not convert panel to go */" } -func (runtime *Runtime) ConvertDataqueryToGo(inputPanel any, dataqueryTypeHints ...string) string { - for _, dataqueryTypeHint := range dataqueryTypeHints { - config, found := runtime.dataqueryVariants[dataqueryTypeHint] - if found && config.GoConverter != nil { - return config.GoConverter(inputPanel) - } +func (runtime *Runtime) ConvertDataqueryToGo(dataquery variants.Dataquery) string { + config, found := runtime.dataqueryVariants[dataquery.DataqueryType()] + if found && config.GoConverter != nil { + return config.GoConverter(dataquery) } return "/* could not convert dataquery to go */" @@ -136,8 +133,8 @@ func ConvertPanelToCode(inputPanel any, panelType string) string { return NewRuntime().ConvertPanelToGo(inputPanel, panelType) } -func ConvertDataqueryToCode(inputPanel any, dataqueryTypeHints ...string) string { - return NewRuntime().ConvertDataqueryToGo(inputPanel, dataqueryTypeHints...) +func ConvertDataqueryToCode(dataquery variants.Dataquery) string { + return NewRuntime().ConvertDataqueryToGo(dataquery) } func Dump(root any) string { diff --git a/internal/jennies/golang/templates/runtime/variant_models.tmpl b/internal/jennies/golang/templates/runtime/variant_models.tmpl index 2fc27e21..5a7b0e37 100644 --- a/internal/jennies/golang/templates/runtime/variant_models.tmpl +++ b/internal/jennies/golang/templates/runtime/variant_models.tmpl @@ -16,6 +16,7 @@ type DataqueryConfig struct { type Dataquery interface { ImplementsDataqueryVariant() Equals(other Dataquery) bool + DataqueryType() string } type Panelcfg interface { @@ -24,10 +25,12 @@ type Panelcfg interface { type UnknownDataquery map[string]any -func (unknown UnknownDataquery) ImplementsDataqueryVariant() { - +func (unknown UnknownDataquery) DataqueryType() string { + return "unknown" } +func (unknown UnknownDataquery) ImplementsDataqueryVariant() { } + func (unknown UnknownDataquery) Equals(otherCandidate Dataquery) bool { if otherCandidate == nil { return false diff --git a/internal/languages/converter.go b/internal/languages/converter.go index abe2c306..90006a61 100644 --- a/internal/languages/converter.go +++ b/internal/languages/converter.go @@ -284,42 +284,13 @@ func (generator *ConverterGenerator) argumentsForEnvelope(context Context, conve func (generator *ConverterGenerator) argumentForType(context Context, converter Converter, argName string, valuePath ast.Path, typeDef ast.Type) ArgumentMapping { if typeDef.IsComposableSlot() { - var slotTypeHintsArgs []*DirectArgMapping - var guards []MappingGuard - converterRootType := context.ResolveRefs(converter.inputRootPath().Last().Type) - - if converterRootType.IsStruct() { - for _, field := range converterRootType.Struct.Fields { - if !field.Type.IsRef() || field.Type.AsRef().ReferredType != "DataSourceRef" { - continue - } - - refPath := ast.PathFromStructField(field) - datasourceRefType := context.ResolveRefs(field.Type) - - typeField, found := datasourceRefType.Struct.FieldByName("type") - if !found { - continue - } - - typePath := refPath.AppendStructField(typeField) - guards = append(guards, generator.pathNotNullGuards(converter, typePath)...) - - slotTypeHintsArgs = append(slotTypeHintsArgs, &DirectArgMapping{ - ValuePath: converter.inputRootPath().Append(typePath), - ValueType: typeField.Type, - }) - } - } - return ArgumentMapping{ Runtime: &RuntimeArgMapping{ FuncName: fmt.Sprintf("Convert%sToCode", tools.UpperCamelCase(string(typeDef.AsComposableSlot().Variant))), - Args: append([]*DirectArgMapping{ + Args: []*DirectArgMapping{ {ValuePath: valuePath, ValueType: typeDef}, - }, slotTypeHintsArgs...), + }, }, - Guards: guards, } } diff --git a/testdata/generated/cog/runtime.go b/testdata/generated/cog/runtime.go index 729df4a8..d1f7ce61 100644 --- a/testdata/generated/cog/runtime.go +++ b/testdata/generated/cog/runtime.go @@ -132,12 +132,10 @@ func (runtime *Runtime) ConvertPanelToGo(inputPanel any, panelType string) strin return "/* could not convert panel to go */" } -func (runtime *Runtime) ConvertDataqueryToGo(inputPanel any, dataqueryTypeHints ...string) string { - for _, dataqueryTypeHint := range dataqueryTypeHints { - config, found := runtime.dataqueryVariants[dataqueryTypeHint] - if found && config.GoConverter != nil { - return config.GoConverter(inputPanel) - } +func (runtime *Runtime) ConvertDataqueryToGo(dataquery variants.Dataquery) string { + config, found := runtime.dataqueryVariants[dataquery.DataqueryType()] + if found && config.GoConverter != nil { + return config.GoConverter(dataquery) } return "/* could not convert dataquery to go */" @@ -147,8 +145,8 @@ func ConvertPanelToCode(inputPanel any, panelType string) string { return NewRuntime().ConvertPanelToGo(inputPanel, panelType) } -func ConvertDataqueryToCode(inputPanel any, dataqueryTypeHints ...string) string { - return NewRuntime().ConvertDataqueryToGo(inputPanel, dataqueryTypeHints...) +func ConvertDataqueryToCode(dataquery variants.Dataquery) string { + return NewRuntime().ConvertDataqueryToGo(dataquery) } func Dump(root any) string { diff --git a/testdata/generated/cog/variants/variants.go b/testdata/generated/cog/variants/variants.go index 5306437a..92ed6633 100644 --- a/testdata/generated/cog/variants/variants.go +++ b/testdata/generated/cog/variants/variants.go @@ -23,6 +23,7 @@ type DataqueryConfig struct { type Dataquery interface { ImplementsDataqueryVariant() Equals(other Dataquery) bool + DataqueryType() string } type Panelcfg interface { @@ -31,6 +32,10 @@ type Panelcfg interface { type UnknownDataquery map[string]any +func (unknown UnknownDataquery) DataqueryType() string { + return "" +} + func (unknown UnknownDataquery) ImplementsDataqueryVariant() { } diff --git a/testdata/jennies/rawtypes/variant_dataquery/GoRawTypes/variant_dataquery/types_gen.go b/testdata/jennies/rawtypes/variant_dataquery/GoRawTypes/variant_dataquery/types_gen.go index 13ccd6aa..ce4179bf 100644 --- a/testdata/jennies/rawtypes/variant_dataquery/GoRawTypes/variant_dataquery/types_gen.go +++ b/testdata/jennies/rawtypes/variant_dataquery/GoRawTypes/variant_dataquery/types_gen.go @@ -10,6 +10,9 @@ type Query struct { } func (resource Query) ImplementsDataqueryVariant() {} +func (resource Query) DataqueryType() string { + return "prometheus" +} func VariantConfig() variants.DataqueryConfig { return variants.DataqueryConfig{ From 412fbfe7f030627ad0370ccf9939e63307aca21c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Sat, 28 Sep 2024 02:54:30 +0200 Subject: [PATCH 4/5] Generate conversion code for argument-less options --- internal/languages/converter.go | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/internal/languages/converter.go b/internal/languages/converter.go index 90006a61..28e4e596 100644 --- a/internal/languages/converter.go +++ b/internal/languages/converter.go @@ -185,12 +185,8 @@ func (generator *ConverterGenerator) convertListOfDisjunctionOptions(context Con func (generator *ConverterGenerator) convertOption(context Context, converter Converter, option ast.Option) ConversionMapping { assignments := tools.Filter(option.Assignments, func(assignment ast.Assignment) bool { - key := generator.assignmentKey(assignment) - if _, ok := generator.generatedPaths[key]; ok { - return false - } - - return assignment.Value.Constant == nil + _, pathAlreadyGenerated := generator.generatedPaths[generator.assignmentKey(assignment)] + return !pathAlreadyGenerated }) if len(assignments) == 0 { return ConversionMapping{} @@ -228,12 +224,8 @@ func (generator *ConverterGenerator) mappingForOption(context Context, converter } assignments := tools.Filter(option.Assignments, func(assignment ast.Assignment) bool { - key := generator.assignmentKey(assignment) - if _, ok := generator.generatedPaths[key]; ok { - return false - } - - return assignment.Value.Constant == nil + _, pathAlreadyGenerated := generator.generatedPaths[generator.assignmentKey(assignment)] + return !pathAlreadyGenerated }) if len(assignments) == 0 { return OptionMapping{} @@ -245,6 +237,11 @@ func (generator *ConverterGenerator) mappingForOption(context Context, converter generator.generatedPaths[generator.assignmentKey(assignment)] = struct{}{} + // no need for an argument if the assignment uses a constant value + if assignment.Value.Constant != nil { + continue + } + argName := fmt.Sprintf("arg%d", i) valueType := assignment.Path.Last().Type valuePath := converter.inputRootPath().Append(assignment.Path) @@ -467,6 +464,10 @@ func (generator *ConverterGenerator) pathNotNullGuards(converter Converter, path func (generator *ConverterGenerator) assignmentKey(assignment ast.Assignment) string { path := assignment.Path.String() + if assignment.Value.Constant != nil { + path += fmt.Sprintf("=%v", assignment.Value.Constant) + } + if assignment.Value.Envelope != nil { // TODO: envelope of envelope? for _, envelopeAssignment := range assignment.Value.Envelope.Values { From 5423051847247412dc0a23b07cd366d354d9af4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Gomez?= Date: Sat, 28 Sep 2024 03:05:08 +0200 Subject: [PATCH 5/5] Do not convert options when the value is obviously the default one (only simple case for now: scalars) --- internal/languages/converter.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/internal/languages/converter.go b/internal/languages/converter.go index 28e4e596..40b50980 100644 --- a/internal/languages/converter.go +++ b/internal/languages/converter.go @@ -421,6 +421,16 @@ func (generator *ConverterGenerator) optionGuards(converter Converter, option as guards.Set(guard.String(), guard) } + // For scalar values, add a guard against assignments equal to the default value for that path + if assignmentType.IsScalar() && assignmentType.Default != nil { + guard := MappingGuard{ + Path: converter.inputRootPath().Append(assignment.Path), + Op: ast.NotEqualOp, + Value: assignmentType.Default, + } + guards.Set(guard.String(), guard) + } + // TODO: is that correct/needed? if assignment.Method != ast.AppendAssignment && assignment.Value.Envelope != nil { for _, envelopePath := range assignment.Value.Envelope.Values {