Skip to content

Commit

Permalink
Bootstrap JSON → code conversion in Go
Browse files Browse the repository at this point in the history
  • Loading branch information
K-Phoen committed Sep 27, 2024
1 parent cd35dd6 commit 59f3c6b
Show file tree
Hide file tree
Showing 35 changed files with 1,246 additions and 162 deletions.
7 changes: 7 additions & 0 deletions internal/ast/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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]
}
Expand Down
4 changes: 4 additions & 0 deletions internal/ast/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
5 changes: 3 additions & 2 deletions internal/codegen/output.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`

Expand Down
7 changes: 4 additions & 3 deletions internal/codegen/pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}

Expand Down
26 changes: 3 additions & 23 deletions internal/jennies/golang/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type Builder struct {
Tmpl *template.Template

typeImportMapper func(pkg string) string
pathFormatter func(path ast.Path) string
typeFormatter *typeFormatter
}

Expand Down Expand Up @@ -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")
Expand All @@ -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)
Expand Down Expand Up @@ -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:
Expand Down
77 changes: 77 additions & 0 deletions internal/jennies/golang/converter.go
Original file line number Diff line number Diff line change
@@ -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,
})
}
56 changes: 0 additions & 56 deletions internal/jennies/golang/equality_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}}))

Expand All @@ -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"}}))
}
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 6 additions & 2 deletions internal/jennies/golang/jennies.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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))

Expand All @@ -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{},
Expand Down
24 changes: 21 additions & 3 deletions internal/jennies/golang/jsonmarshalling.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}),
Expand Down Expand Up @@ -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,
})
}
2 changes: 1 addition & 1 deletion internal/jennies/golang/rawtypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
8 changes: 8 additions & 0 deletions internal/jennies/golang/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
`)
}

Expand Down
Loading

0 comments on commit 59f3c6b

Please sign in to comment.