Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions internal/jennies/golang/jsonmarshalling.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,22 @@ func (jenny JSONMarshalling) generateForObject(buffer *strings.Builder, context
buffer.WriteString("\n")
}

if jenny.objectIsOpenStruct(object) {
jsonMarshal, err := jenny.renderOpenStructMarshal(object)
if err != nil {
return err
}
buffer.WriteString(jsonMarshal)
buffer.WriteString("\n")

jsonUnmarshal, err := jenny.renderOpenStructUnmarshal(object)
if err != nil {
return err
}
buffer.WriteString(jsonUnmarshal)
buffer.WriteString("\n")
}

return nil
}

Expand Down Expand Up @@ -229,3 +245,33 @@ func (resource *%[1]s) UnmarshalJSON(raw []byte) error {
}
`, formatObjectName(obj.Name), buffer.String()), nil
}

func (jenny JSONMarshalling) objectIsOpenStruct(obj ast.Object) bool {
return obj.Type.HasHint(ast.HintOpenStruct)
}

func (jenny JSONMarshalling) renderOpenStructMarshal(obj ast.Object) (string, error) {
jenny.apiRefCollector.ObjectMethod(obj, common.MethodReference{
Name: "MarshalJSON",
Comments: []string{
fmt.Sprintf("MarshalJSON implements a custom JSON marshalling logic to encode `%s` as JSON.", formatObjectName(obj.Name)),
},
Return: "([]byte, error)",
})
return jenny.tmpl.Render("types/open_struct.json_marshal.tmpl", map[string]any{
"def": obj,
})
}

func (jenny JSONMarshalling) renderOpenStructUnmarshal(obj ast.Object) (string, error) {
jenny.apiRefCollector.ObjectMethod(obj, common.MethodReference{
Name: "MarshalJSON",
Comments: []string{
fmt.Sprintf("MarshalJSON implements a custom JSON marshalling logic to encode `%s` as JSON.", formatObjectName(obj.Name)),
},
Return: "([]byte, error)",
})
return jenny.tmpl.Render("types/open_struct.json_unmarshal.tmpl", map[string]any{
"def": obj,
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{{- $json := importStdPkg "encoding/json" -}}
// MarshalJSON implements a custom JSON marshalling logic to encode `{{ .def.Name|formatObjectName }}` as JSON.
func (resource *{{ .def.Name|formatObjectName }}) MarshalJSON() ([]byte, error) {
type {{ .def.Name | lowerCamelCase }} {{ .def.Name }}
base, err := json.Marshal((*{{ .def.Name | lowerCamelCase }})(resource))
if err != nil {
return nil, err
}

var baseMap map[string]any
if err := json.Unmarshal(base, &baseMap); err != nil {
return nil, err
}

for k, v := range resource.ExtraFields {
baseMap[k] = v
}

return json.Marshal(baseMap)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{{- $json := importStdPkg "encoding/json" -}}
{{- $errors := importStdPkg "errors" -}}
// UnmarshalJSON implements a custom JSON unmarshalling logic to decode `{{ .def.Name|formatObjectName }}` from JSON.
func (resource *{{ .def.Name|formatObjectName }}) UnmarshalJSON(raw []byte) error {
if raw == nil {
return nil
}

var data map[string]json.RawMessage
if err := json.Unmarshal(raw, &data); err != nil {
return err
}

{{- range .def.Type.Struct.Fields }}
// {{ .Name | upperCamelCase }}
if v, ok := data["{{ .Name }}"]; ok {
if err := json.Unmarshal(v, &resource.{{ .Name | upperCamelCase }}); err != nil {
return err
}
delete(data, "{{ .Name }}")
}
{{- end }}

resource.ExtraFields = make(map[string]any)
for key, value := range data {
var v any
if err := json.Unmarshal(value, &v); err != nil {
return err
}
resource.ExtraFields[key] = v
}

return nil
}
8 changes: 6 additions & 2 deletions internal/jennies/golang/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ func (formatter *typeFormatter) doFormatType(def ast.Type, resolveBuilders bool)

// anonymous struct or struct body
if def.IsStruct() {
output := formatter.formatStructBody(def.AsStruct())
output := formatter.formatStructBody(def.AsStruct(), def.HasHint(ast.HintOpenStruct))
if def.Nullable {
output = "*" + output
}
Expand Down Expand Up @@ -177,7 +177,7 @@ func (formatter *typeFormatter) variantInterface(variant string) string {
return referredPkg + "." + formatObjectName(variant)
}

func (formatter *typeFormatter) formatStructBody(def ast.StructType) string {
func (formatter *typeFormatter) formatStructBody(def ast.StructType, isOpen bool) string {
var buffer strings.Builder

buffer.WriteString("struct {\n")
Expand All @@ -189,6 +189,10 @@ func (formatter *typeFormatter) formatStructBody(def ast.StructType) string {
}
}

if isOpen {
buffer.WriteString("\n\n" + tools.Indent("ExtraFields map[string]any `json:\"-\"`", 4))
}

buffer.WriteString("\n}")

return buffer.String()
Expand Down
3 changes: 3 additions & 0 deletions internal/jennies/java/rawtypes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ func TestRawTypes_Generate(t *testing.T) {
test := testutils.GoldenFilesTestSuite[ast.Schema]{
TestDataRoot: "../../../testdata/jennies/rawtypes",
Name: "JavaRawTypes",
Skip: map[string]string{
"open_struct": "TODO",
},
}

cfg := Config{
Expand Down
3 changes: 3 additions & 0 deletions internal/jennies/jsonschema/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ func TestSchema_Generate(t *testing.T) {
test := testutils.GoldenFilesTestSuite[ast.Schema]{
TestDataRoot: "../../../testdata/jennies/rawtypes",
Name: "JSONSchema",
Skip: map[string]string{
"open_struct": "TODO",
},
}

config := Config{Debug: true}
Expand Down
3 changes: 3 additions & 0 deletions internal/jennies/openapi/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ func TestSchema_Generate(t *testing.T) {
test := testutils.GoldenFilesTestSuite[ast.Schema]{
TestDataRoot: "../../../testdata/jennies/rawtypes",
Name: "OpenAPI",
Skip: map[string]string{
"open_struct": "TODO",
},
}

config := Config{debug: true}
Expand Down
1 change: 1 addition & 0 deletions internal/jennies/php/rawtypes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ func TestRawTypes_Generate(t *testing.T) {
Name: "PHPRawTypes",
Skip: map[string]string{
"intersections": "Intersections are not implemented",
"open_struct": "TODO",
},
}

Expand Down
1 change: 1 addition & 0 deletions internal/jennies/python/rawtypes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ func TestRawTypes_Generate(t *testing.T) {
Skip: map[string]string{
"intersections": "Intersections are not implemented",
"dashboard": "the dashboard test schema includes a composable slot, which rely on external input to be properly supported",
"open_struct": "TODO",
},
}

Expand Down
3 changes: 3 additions & 0 deletions internal/jennies/typescript/rawtypes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ func TestRawTypes_Generate(t *testing.T) {
test := testutils.GoldenFilesTestSuite[ast.Schema]{
TestDataRoot: "../../../testdata/jennies/rawtypes",
Name: "TypescriptRawTypes",
Skip: map[string]string{
"open_struct": "TODO",
},
}

config := Config{}
Expand Down
1 change: 1 addition & 0 deletions internal/simplecue/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const cogAnnotationName = "cog"
const cuetsyAnnotationName = "cuetsy"
const hintKindEnum = "enum"
const annotationKindFieldName = "kind"
const annotationOpen = "open"
const enumMembersAttr = "memberNames"

type LibraryInclude struct {
Expand Down
8 changes: 6 additions & 2 deletions internal/simplecue/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@ func hintsFromCueValue(v cue.Value) ast.JenniesHints {
for i < a.NumArgs() {
key, value := a.Arg(i)
hints[key] = value

i++
}
}
Expand Down Expand Up @@ -131,7 +130,12 @@ func getTypeHint(v cue.Value) (string, error) {
return "", err
}

if !found {
_, isOpen, err := attr.Lookup(0, annotationOpen)
if err != nil {
return "", err
}

if !found && !isOpen {
return "", errorWithCueRef(v, "no value for the %q key in @%s attribute", annotationKindFieldName, cogAnnotationName)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package open_struct

import (
"encoding/json"
"errors"
cog "github.com/grafana/cog/generated/cog"
"fmt"
)

type OpenStruct struct {
A string `json:"a"`
B int64 `json:"b"`

ExtraFields map[string]any `json:"-"`
}

// NewOpenStruct creates a new OpenStruct object.
func NewOpenStruct() *OpenStruct {
return &OpenStruct{
}
}
// MarshalJSON implements a custom JSON marshalling logic to encode `OpenStruct` as JSON.
func (resource *OpenStruct) MarshalJSON() ([]byte, error) {
type openStruct OpenStruct
base, err := json.Marshal((*openStruct)(resource))
if err != nil {
return nil, err
}

var baseMap map[string]any
if err := json.Unmarshal(base, &baseMap); err != nil {
return nil, err
}

for k, v := range resource.ExtraFields {
baseMap[k] = v
}

return json.Marshal(baseMap)
}

// UnmarshalJSON implements a custom JSON unmarshalling logic to decode `OpenStruct` from JSON.
func (resource *OpenStruct) UnmarshalJSON(raw []byte) error {
if raw == nil {
return nil
}

var data map[string]json.RawMessage
if err := json.Unmarshal(raw, &data); err != nil {
return err
}
// A
if v, ok := data["a"]; ok {
if err := json.Unmarshal(v, &resource.A); err != nil {
return err
}
delete(data, "a")
}
// B
if v, ok := data["b"]; ok {
if err := json.Unmarshal(v, &resource.B); err != nil {
return err
}
delete(data, "b")
}

resource.ExtraFields = make(map[string]any)
for key, value := range data {
var v any
if err := json.Unmarshal(value, &v); err != nil {
return err
}
resource.ExtraFields[key] = v
}

return nil
}

// UnmarshalJSONStrict implements a custom JSON unmarshalling logic to decode `OpenStruct` from JSON.
// Note: the unmarshalling done by this function is strict. It will fail over required fields being absent from the input, fields having an incorrect type, unexpected fields being present, …
func (resource *OpenStruct) UnmarshalJSONStrict(raw []byte) error {
if raw == nil {
return nil
}
var errs cog.BuildErrors

fields := make(map[string]json.RawMessage)
if err := json.Unmarshal(raw, &fields); err != nil {
return err
}
// Field "a"
if fields["a"] != nil {
if string(fields["a"]) != "null" {
if err := json.Unmarshal(fields["a"], &resource.A); err != nil {
errs = append(errs, cog.MakeBuildErrors("a", err)...)
}
} else {errs = append(errs, cog.MakeBuildErrors("a", errors.New("required field is null"))...)

}
delete(fields, "a")
} else {errs = append(errs, cog.MakeBuildErrors("a", errors.New("required field is missing from input"))...)
}
// Field "b"
if fields["b"] != nil {
if string(fields["b"]) != "null" {
if err := json.Unmarshal(fields["b"], &resource.B); err != nil {
errs = append(errs, cog.MakeBuildErrors("b", err)...)
}
} else {errs = append(errs, cog.MakeBuildErrors("b", errors.New("required field is null"))...)

}
delete(fields, "b")
} else {errs = append(errs, cog.MakeBuildErrors("b", errors.New("required field is missing from input"))...)
}

for field := range fields {
errs = append(errs, cog.MakeBuildErrors("OpenStruct", fmt.Errorf("unexpected field '%s'", field))...)
}

if len(errs) == 0 {
return nil
}

return errs
}


// Equals tests the equality of two `OpenStruct` objects.
func (resource OpenStruct) Equals(other OpenStruct) bool {
if resource.A != other.A {
return false
}
if resource.B != other.B {
return false
}

return true
}


// Validate checks all the validation constraints that may be defined on `OpenStruct` fields for violations and returns them.
func (resource OpenStruct) Validate() error {
return nil
}
Loading