Skip to content

Commit 97b24b2

Browse files
committed
internal/encoding/gotypes: generate disjunctions of structs as a map
For a simple and closed CUE struct, we can generate a Go struct. However, for a disjunction of structs we cannot generate a single Go struct in the general case. For now, generate a map[string]any, which is fairly loose but at least will work. Ideas for future enhancements are left under a TODO. Note that we were incorrectly generating pointers to maps, given that the decision to use a Go pointer was made before the decision to use a Go map to represent a CUE struct. This is resolved now as well, along with regression tests. Signed-off-by: Daniel Martí <[email protected]> Change-Id: I2ce4eb40b107b456c20a44d28332df668be33045 Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1207219 Reviewed-by: Roger Peppe <[email protected]> TryBot-Result: CUEcueckoo <[email protected]> Unity-Result: CUE porcuepine <[email protected]>
1 parent d113c59 commit 97b24b2

File tree

2 files changed

+64
-45
lines changed

2 files changed

+64
-45
lines changed

cmd/cue/cmd/testdata/script/exp_gengotypes.txtar

+34-31
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,8 @@ fail: cue: isNotEqual: {mustEqual1: 8, mustEqual2: 99}
7979
pass: isEqual: {mustEqual1: 8, mustEqual2: 8}
8080

8181
fail: cue: discBoth: discriminatorField: {one: 5, two: 5}
82-
// TODO: discriminator field structs seem to generate empty Go structs.
83-
// pass: discOne: discriminatorField: one: 5
84-
// pass: discTwo: discriminatorField: two: 5
82+
pass: discOne: discriminatorField: one: 5
83+
pass: discTwo: discriminatorField: two: 5
8584

8685
// To avoid repetition, we template the type test cases.
8786
// Note that some types have multipe test cases.
@@ -163,45 +162,45 @@ _typeTests: [...#typeTest] & [
163162
]
164163
-- cuetest/fail_check.stderr --
165164
fail.both."16_IntList".types.IntList.0: conflicting values "foo" and int (mismatched types string and int):
166-
./cuetest/all.cue:68:40
165+
./cuetest/all.cue:67:40
167166
./cuetest/fail_check.cue:7:12
168167
./cuetest/fail_check.cue:7:17
169168
./root/root.cue:12:10
170169
./root/types.cue:26:20
171170
./root/types.cue:26:23
172171
fail.both."16_IntList".types.IntList.1: conflicting values "bar" and int (mismatched types string and int):
173-
./cuetest/all.cue:68:47
172+
./cuetest/all.cue:67:47
174173
./cuetest/fail_check.cue:7:12
175174
./cuetest/fail_check.cue:7:17
176175
./root/root.cue:12:10
177176
./root/types.cue:26:20
178177
./root/types.cue:26:23
179178
fail.both."20_IntMap".types.IntMap.one: conflicting values "x" and int (mismatched types string and int):
180-
./cuetest/all.cue:72:47
179+
./cuetest/all.cue:71:47
181180
./cuetest/fail_check.cue:7:12
182181
./cuetest/fail_check.cue:7:17
183182
./root/root.cue:12:10
184183
./root/types.cue:28:29
185184
fail.both."20_IntMap".types.IntMap.two: conflicting values "y" and int (mismatched types string and int):
186-
./cuetest/all.cue:72:59
185+
./cuetest/all.cue:71:59
187186
./cuetest/fail_check.cue:7:12
188187
./cuetest/fail_check.cue:7:17
189188
./root/root.cue:12:10
190189
./root/types.cue:28:29
191190
fail.both."37_NonEmptyString".types.NonEmptyString: conflicting values string and 123 (mismatched types string and int):
192-
./cuetest/all.cue:21:1
193-
./cuetest/all.cue:28:2
194-
./cuetest/all.cue:29:56
195-
./cuetest/all.cue:90:43
191+
./cuetest/all.cue:20:1
192+
./cuetest/all.cue:27:2
193+
./cuetest/all.cue:28:56
194+
./cuetest/all.cue:89:43
196195
./root/types.cue:38:23
197196
fail.both."40_NonEmptyString".types.NonEmptyString: conflicting values string and [1,2,3] (mismatched types string and list):
198-
./cuetest/all.cue:21:1
199-
./cuetest/all.cue:28:2
200-
./cuetest/all.cue:29:56
201-
./cuetest/all.cue:93:43
197+
./cuetest/all.cue:20:1
198+
./cuetest/all.cue:27:2
199+
./cuetest/all.cue:28:56
200+
./cuetest/all.cue:92:43
202201
./root/types.cue:38:23
203202
fail.both."42_LinkedList".types.LinkedList.next: conflicting values "x" and {item?:_,next?:#linkedList} (mismatched types string and struct):
204-
./cuetest/all.cue:95:50
203+
./cuetest/all.cue:94:50
205204
./cuetest/fail_check.cue:7:12
206205
./cuetest/fail_check.cue:7:17
207206
./root/root.cue:12:10
@@ -219,41 +218,41 @@ fail.both.notString: conflicting values "not_a_struct" and {doc?:_} (mismatched
219218
./cuetest/fail_check.cue:7:17
220219
./root/root.cue:81:8
221220
fail.cue."11_Int8".types.Int8: invalid value 99999 (out of bound <=127):
222-
./cuetest/all.cue:62:30
221+
./cuetest/all.cue:61:30
223222
fail.cue."12_Int8".types.Int8: invalid value -99999 (out of bound >=-128):
224-
./cuetest/all.cue:63:30
223+
./cuetest/all.cue:62:30
225224
fail.cue."18_IntListClosed2".types.IntListClosed2: incompatible list lengths (2 and 4)
226225
fail.cue."29_NullOrStruct".types.NullOrStruct: 2 errors in empty disjunction:
227226
fail.cue."29_NullOrStruct".types.NullOrStruct: conflicting values "foo" and null (mismatched types string and null):
228-
./cuetest/all.cue:82:43
227+
./cuetest/all.cue:81:43
229228
./cuetest/fail_check.cue:7:12
230229
./cuetest/fail_check.cue:7:17
231230
./root/root.cue:12:10
232231
./root/types.cue:35:23
233232
fail.cue."29_NullOrStruct".types.NullOrStruct: conflicting values "foo" and {foo?:int} (mismatched types string and struct):
234-
./cuetest/all.cue:82:43
233+
./cuetest/all.cue:81:43
235234
./cuetest/fail_check.cue:7:12
236235
./cuetest/fail_check.cue:7:17
237236
./root/root.cue:12:10
238237
./root/types.cue:35:30
239238
fail.cue."32_NullOrString".types.NullOrString: 2 errors in empty disjunction:
240239
fail.cue."32_NullOrString".types.NullOrString: conflicting values 123 and null (mismatched types int and null):
241-
./cuetest/all.cue:85:43
240+
./cuetest/all.cue:84:43
242241
./cuetest/fail_check.cue:7:12
243242
./cuetest/fail_check.cue:7:17
244243
./root/root.cue:12:10
245244
./root/types.cue:36:23
246245
fail.cue."32_NullOrString".types.NullOrString: conflicting values 123 and string (mismatched types int and string):
247-
./cuetest/all.cue:85:43
246+
./cuetest/all.cue:84:43
248247
./cuetest/fail_check.cue:7:12
249248
./cuetest/fail_check.cue:7:17
250249
./root/root.cue:12:10
251250
./root/types.cue:36:30
252251
fail.cue."39_UniqueStrings".types.UniqueStrings: invalid value ["foo","foo"] (does not satisfy list.UniqueItems): equal value ("foo") at position 0 and 1:
253-
./cuetest/all.cue:26:55
252+
./cuetest/all.cue:25:55
254253
./root/types.cue:39:23
255254
fail.cue."9_Uint".types.Uint: invalid value -34 (out of bound >=0):
256-
./cuetest/all.cue:60:30
255+
./cuetest/all.cue:59:30
257256
fail.cue.discBoth.discriminatorField: 2 errors in empty disjunction:
258257
fail.cue.discBoth.discriminatorField.one: field not allowed:
259258
./cuetest/all.cue:14:43
@@ -273,11 +272,11 @@ fail.cue.isNotEqual.mustEqual2: conflicting values 8 and 99:
273272
./root/root.cue:77:15
274273
fail.cue."34_NumericBounds".types.NumericBounds: invalid value 5555 (out of bound <100):
275274
./root/types.cue:37:28
276-
./cuetest/all.cue:87:43
275+
./cuetest/all.cue:86:43
277276
fail.cue."36_NonEmptyString".types.NonEmptyString: invalid value "" (out of bound !=""):
278277
./root/types.cue:38:32
279-
./cuetest/all.cue:26:55
280-
./cuetest/all.cue:89:43
278+
./cuetest/all.cue:25:55
279+
./cuetest/all.cue:88:43
281280
./root/types.cue:38:23
282281
-- go.mod --
283282
module "foo.test/bar"
@@ -315,8 +314,11 @@ func main() {
315314
// It's easier to test the fields with a root value.
316315
// We use reflection to ensure that inline struct pointers are initialized.
317316
var zeroRoot root.Root
318-
fv := reflect.ValueOf(&zeroRoot).Elem().FieldByName("Fields")
317+
rv := reflect.ValueOf(&zeroRoot).Elem()
318+
fv := rv.FieldByName("Fields")
319319
fv.Set(reflect.New(fv.Type().Elem()))
320+
tv := rv.FieldByName("Types")
321+
tv.Set(reflect.New(tv.Type().Elem()))
320322

321323
var _ = zeroRoot.Embedded1
322324
var _ = zeroRoot.Embedded2
@@ -327,8 +329,10 @@ func main() {
327329
zeroRoot.AttrType = constant.Kind(0)
328330
zeroRoot.AttrTypeCompat = token.Token(0)
329331
zeroRoot.AttrTypeNested = make(map[any]any)
332+
zeroRoot.DiscriminatorField = make(map[string]any)
330333
zeroRoot.Fields.OptionalStruct = &root.EmptyStruct{}
331334
zeroRoot.Fields.OptionalStructAttrType = root.EmptyStruct{}
335+
zeroRoot.Types.IntMap = make(map[string]int64)
332336

333337
// Sanity check that Go can JSON decode all the values we expect.
334338
// We also re-encode the ones we expect CUE to be able to validate again.
@@ -628,7 +632,7 @@ type Types struct {
628632

629633
IntListClosed2 []any/* CUE closed list */ `json:"IntListClosed2,omitempty"`
630634

631-
IntMap *map[string]int64 `json:"IntMap,omitempty"`
635+
IntMap map[string]int64 `json:"IntMap,omitempty"`
632636

633637
Time time.Time `json:"Time,omitempty"`
634638

@@ -718,8 +722,7 @@ type Root struct {
718722

719723
UseHiddenStruct *hiddenStruct `json:"useHiddenStruct,omitempty"`
720724

721-
DiscriminatorField *struct {
722-
} `json:"discriminatorField,omitempty"`
725+
DiscriminatorField map[string]any `json:"discriminatorField,omitempty"`
723726

724727
MustEqual1 int64 `json:"mustEqual1,omitempty"`
725728

internal/encoding/gotypes/generate.go

+30-14
Original file line numberDiff line numberDiff line change
@@ -201,23 +201,12 @@ func (g *generator) emitType(val cue.Value, optional bool) error {
201201
g.appendf("%s", attrType)
202202
return nil
203203
}
204-
// TODO: should we ensure that optional fields are always nilable in Go?
205-
// On one hand this allows telling int64(0) apart from a missing field,
206-
// but on the other, it's often unnecessary and leads to clumsy types.
207-
// Perhaps add a @go() attribute parameter to require nullability.
208-
//
209-
// For now, only structs are always pointers when optional.
210-
// This is necessary to allow recursive Go types such as linked lists.
211-
// Pointers to structs are still OK in terms of UX, given that
212-
// one can do X.PtrY.Z without needing to do (*X.PtrY).Z.
213-
if optional && cue.Dereference(val).IncompleteKind() == cue.StructKind {
214-
g.appendf("*")
215-
}
216204
// TODO: support nullable types, such as `null | #SomeReference` and
217205
// `null | {foo: int}`.
218-
if g.emitTypeReference(val) {
206+
if g.emitTypeReference(val, optional) {
219207
return nil
220208
}
209+
221210
switch k := val.IncompleteKind(); k {
222211
case cue.StructKind:
223212
if elem := val.LookupPath(cue.MakePath(cue.AnyString)); elem.Err() == nil {
@@ -227,7 +216,22 @@ func (g *generator) emitType(val cue.Value, optional bool) error {
227216
}
228217
break
229218
}
219+
// A disjunction of structs cannot be represented in Go, as it does not have sum types.
220+
// Fall back to a map of string to any, which is not ideal, but will work for any field.
221+
//
222+
// TODO: consider alternatives, such as:
223+
// * For `#StructFoo | #StructBar`, generate named types for each disjunct,
224+
// and use `any` here as a sum type between them.
225+
// * For a disjunction of closed structs, generate a flat struct with the superset
226+
// of all fields, akin to a C union.
227+
if op, _ := val.Expr(); op == cue.OrOp {
228+
g.appendf("map[string]any")
229+
break
230+
}
230231
// TODO: treat a single embedding like `{[string]: int}` like we would `[string]: int`
232+
if optional {
233+
g.appendf("*")
234+
}
231235
g.appendf("struct {\n")
232236
iter, err := val.Fields(cue.Definitions(true), cue.Optional(true))
233237
if err != nil {
@@ -243,6 +247,15 @@ func (g *generator) emitType(val cue.Value, optional bool) error {
243247
cueName := sel.String()
244248
cueName = strings.TrimRight(cueName, "?!")
245249
g.emitDocs(cueName, val.Doc())
250+
// TODO: should we ensure that optional fields are always nilable in Go?
251+
// On one hand this allows telling int64(0) apart from a missing field,
252+
// but on the other, it's often unnecessary and leads to clumsy types.
253+
// Perhaps add a @go() attribute parameter to require nullability.
254+
//
255+
// For now, only structs are always pointers when optional.
256+
// This is necessary to allow recursive Go types such as linked lists.
257+
// Pointers to structs are still OK in terms of UX, given that
258+
// one can do X.PtrY.Z without needing to do (*X.PtrY).Z.
246259
optional := sel.ConstraintType()&cue.OptionalConstraint != 0
247260

248261
// We want the Go name from just this selector, even when it's not a definition.
@@ -352,7 +365,7 @@ func goNameFromPath(path cue.Path, defsOnly bool) string {
352365

353366
// emitTypeReference attempts to generate a CUE value as a Go type via a reference,
354367
// either to a type in the same Go package, or to a type in an imported package.
355-
func (g *generator) emitTypeReference(val cue.Value) bool {
368+
func (g *generator) emitTypeReference(val cue.Value, optional bool) bool {
356369
// References to existing names, either from the same package or an imported package.
357370
root, path := val.ReferencePath()
358371
// TODO: surely there is a better way to check whether ReferencePath returned "no path",
@@ -367,6 +380,9 @@ func (g *generator) emitTypeReference(val cue.Value) bool {
367380
unqualifiedPath := module.ParseImportPath(inst.ImportPath).Unqualified().String()
368381

369382
var sb strings.Builder
383+
if optional && cue.Dereference(val).IncompleteKind() == cue.StructKind {
384+
sb.WriteString("*")
385+
}
370386
if root != g.pkgRoot {
371387
sb.WriteString(inst.PkgName)
372388
sb.WriteString(".")

0 commit comments

Comments
 (0)