this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

encoding/jsonschema: list support

This change adds support for arrays/lists to JSON Schema generation,
including support for both open and closed lists and literal list prefixes.

Signed-off-by: Roger Peppe <rogpeppe@gmail.com>
Change-Id: Iee0a0e7a80a9d8db77608a72582f2520fb83452b
Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1224261
TryBot-Result: CUEcueckoo <cueckoo@cuelang.org>
Unity-Result: CUE porcuepine <cue.porcuepine@gmail.com>
Reviewed-by: Daniel Martí <mvdan@mvdan.cc>

+256 -37
+90 -1
encoding/jsonschema/generate.go
··· 214 214 g.err = errors.Append(g.err, errors.Promote(err, "")) 215 215 } 216 216 217 + func (g *generator) addErrorf(pos cue.Value, f string, a ...any) { 218 + g.addError(pos, fmt.Errorf(f, a...)) 219 + } 220 + 217 221 // makeItem returns an item representing the JSON Schema 218 222 // for v in naive form. 219 223 func (g *generator) makeItem(v cue.Value) item { ··· 353 357 case cue.StructKind: 354 358 it = g.makeStructItem(v) 355 359 case cue.ListKind: 356 - // TODO list type support 360 + it = g.makeListItem(v) 357 361 } 358 362 var elems []item 359 363 if kinds := cueKindToJSONSchemaTypes(kind); len(kinds) > 0 { ··· 471 475 &itemFormat{format: format}, 472 476 }, 473 477 } 478 + case "list.MinItems", "list.MaxItems": 479 + if len(args) != 2 { 480 + g.addError(v, fmt.Errorf("%s expects 1 argument, got %d", funcName, len(args)-1)) 481 + return &itemFalse{} 482 + } 483 + n, err := args[1].Int64() 484 + if err != nil { 485 + g.addError(args[1], err) 486 + return &itemFalse{} 487 + } 488 + var constraint cue.Op 489 + if funcName == "list.MinItems" { 490 + constraint = cue.GreaterThanEqualOp 491 + } else { 492 + constraint = cue.LessThanEqualOp 493 + } 494 + return &itemAllOf{ 495 + elems: []item{ 496 + &itemType{kinds: []string{"array"}}, 497 + &itemItemsBounds{constraint: constraint, n: int(n)}, 498 + }, 499 + } 474 500 475 501 default: 476 502 // For unknown functions, accept anything rather than fail. ··· 514 540 return &itemTrue{} 515 541 } 516 542 return &props 543 + } 544 + 545 + func (g *generator) makeListItem(v cue.Value) item { 546 + ellipsis := v.LookupPath(cue.MakePath(cue.AnyIndex)) 547 + lenv := v.Len() 548 + var n int64 549 + if ellipsis.Exists() { 550 + // It's an open list. The length will be in the form int&>=5 551 + op, args := lenv.Expr() 552 + if op != cue.AndOp || len(args) != 2 { 553 + g.addErrorf(v, "list length has unexpected form; got %v want int&>=N", lenv) 554 + return &itemFalse{} 555 + } 556 + op, args = args[1].Expr() 557 + if op != cue.GreaterThanEqualOp || len(args) != 1 { 558 + g.addErrorf(v, "list length has unexpected form (2); got %v want >=N", lenv) 559 + return &itemFalse{} 560 + } 561 + var err error 562 + n, err = args[0].Int64() 563 + if err != nil { 564 + g.addErrorf(v, "cannot extract list length from %v: %v", v, err) 565 + return &itemFalse{} 566 + } 567 + } else { 568 + var err error 569 + n, err = lenv.Int64() 570 + if err != nil { 571 + g.addErrorf(v, "cannot extract concrete list length from %v: %v", v, err) 572 + } 573 + } 574 + prefix := make([]item, n) 575 + for i := range n { 576 + elem := v.LookupPath(cue.MakePath(cue.Index(i))) 577 + if !elem.Exists() { 578 + g.addErrorf(v, "cannot get value at index %d in %v", i, v) 579 + return &itemFalse{} 580 + } 581 + prefix[i] = g.makeItem(elem) 582 + } 583 + a := &itemAllOf{ 584 + elems: []item{&itemType{kinds: []string{"array"}}}, 585 + } 586 + items := &itemItems{} 587 + if len(prefix) > 0 { 588 + a.elems = append(a.elems, &itemLengthBounds{ 589 + constraint: cue.GreaterThanEqualOp, 590 + n: len(prefix), 591 + }) 592 + items.prefix = prefix 593 + } 594 + if ellipsis.Exists() { 595 + items.rest = g.makeItem(ellipsis) 596 + } else { 597 + a.elems = append(a.elems, &itemLengthBounds{ 598 + constraint: cue.LessThanEqualOp, 599 + n: len(prefix), 600 + }) 601 + } 602 + if items.rest != nil || len(items.prefix) > 0 { 603 + a.elems = append(a.elems, items) 604 + } 605 + return a 517 606 } 518 607 519 608 // cueKindToJSONSchemaTypes converts a CUE kind to JSON Schema type strings
+25 -22
encoding/jsonschema/generate_items.go
··· 401 401 return i 402 402 } 403 403 404 - // itemItems represents an items constraint for arrays 404 + // itemItems represents the items and prefixItems constraint for arrays. 405 405 type itemItems struct { 406 - elem item 406 + // known prefix. 407 + prefix []item 408 + // all elements beyond the prefix. 409 + rest item 407 410 } 408 411 409 412 func (i *itemItems) generate(g *generator) ast.Expr { 410 - return singleKeyword("items", i.elem.generate(g)) 413 + fields := make([]ast.Decl, 0, 2) 414 + if len(i.prefix) > 0 { 415 + items := make([]ast.Expr, len(i.prefix)) 416 + for i, e := range i.prefix { 417 + items[i] = e.generate(g) 418 + } 419 + fields = append(fields, makeField("prefixItems", &ast.ListLit{ 420 + Elts: items, 421 + })) 422 + } 423 + if i.rest != nil { 424 + fields = append(fields, makeField("items", i.rest.generate(g))) 425 + } 426 + return makeSchemaStructLit(fields...) 411 427 } 412 428 413 429 func (i *itemItems) apply(f func(item) item) item { 414 - elem := i.elem.apply(f) 415 - if elem == i.elem { 416 - return i 430 + rest := i.rest 431 + if rest != nil { 432 + rest = f(rest) 417 433 } 418 - return &itemItems{elem: elem} 419 - } 420 - 421 - // itemPrefixItems represents prefixItems constraint for arrays 422 - type itemPrefixItems struct { 423 - elems []item 424 - } 425 - 426 - func (i *itemPrefixItems) generate(g *generator) ast.Expr { 427 - return singleKeyword("items", generateList(g, i.elems)) 428 - } 429 - 430 - func (i *itemPrefixItems) apply(f func(item) item) item { 431 - elems, changed := applyElems(i.elems, f) 432 - if !changed { 434 + prefix, changed := applyElems(i.prefix, f) 435 + if !changed && rest == i.rest { 433 436 return i 434 437 } 435 - return &itemPrefixItems{elems: elems} 438 + return &itemItems{prefix: prefix, rest: rest} 436 439 } 437 440 438 441 // itemContains represents a contains constraint for arrays
+33 -14
encoding/jsonschema/testdata/generate/callop.txtar
··· 2 2 package test 3 3 4 4 import ( 5 + "list" 6 + "math" 5 7 "strings" 6 - "math" 7 8 "time" 8 9 ) 9 10 ··· 18 19 dateTime?: time.Format("2006-01-02T15:04:05Z07:00") 19 20 date?: time.Format("2006-01-02") 20 21 timeOnly?: time.Format("15:04:05") 22 + 23 + // List constraints 24 + shortList?: list.MaxItems(3) 25 + longList?: list.MinItems(5) 26 + boundedList?: list.MaxItems(3) & list.MinItems(5) 21 27 22 28 // Combined constraints 23 29 userName?: string & strings.MinRunes(3) & strings.MaxRunes(20) ··· 72 78 $schema: "https://json-schema.org/draft/2020-12/schema" 73 79 type: "object" 74 80 properties: { 81 + boundedList: { 82 + type: "array" 83 + maxItems: 3 84 + minItems: 5 85 + } 75 86 date: { 76 87 type: "string" 77 88 format: "date" ··· 79 90 dateTime: { 80 91 type: "string" 81 92 format: "date-time" 93 + } 94 + longList: { 95 + type: "array" 96 + minItems: 5 82 97 } 83 98 longString: { 84 99 type: "string" ··· 96 111 multipleOf: 10 97 112 }] 98 113 } 114 + shortList: { 115 + type: "array" 116 + maxItems: 3 117 + } 99 118 shortString: { 100 119 type: "string" 101 120 maxLength: 10 ··· 113 132 } 114 133 -- out/generate-v3/badDate -- 115 134 badDate.data.date: invalid value "2025-10-40" (does not satisfy time.Format("2006-01-02")): error in call to time.Format: invalid time "2025-10-40": 116 - 1:113 135 + 1:170 117 136 ./datatest/tests.cue:30:14 118 137 -- out/generate-v3/badDateTime -- 119 138 badDateTime.data.dateTime: invalid value "2025-10-02T13" (does not satisfy time.Time): error in call to time.Time: invalid time "2025-10-02T13": 120 - 1:158 139 + 1:215 121 140 ./datatest/tests.cue:26:18 122 141 -- out/generate-v3/badScore2 -- 123 142 badScore2.data.score: invalid value 5 (does not satisfy math.MultipleOf(10)): 124 - 1:323 125 - 1:278 143 + 1:421 144 + 1:376 126 145 ./datatest/tests.cue:42:15 127 146 -- out/generate-v3/badTime -- 128 147 badTime.data.date: invalid value "25:00:10" (does not satisfy time.Format("2006-01-02")): error in call to time.Format: invalid time "25:00:10": 129 - 1:113 148 + 1:170 130 149 ./datatest/tests.cue:34:14 131 150 -- out/generate-v3/badUserName -- 132 151 badUserName.data.userName: invalid value "x" (does not satisfy strings.MinRunes(3)): 133 - 1:477 134 - 1:462 135 - 1:477 152 + 1:617 153 + 1:602 154 + 1:617 136 155 ./datatest/tests.cue:38:18 137 156 -- out/generate-v3/notMultiple -- 138 157 notMultiple.data.multiple: invalid value 1 (does not satisfy math.MultipleOf(5)): 139 - 1:253 158 + 1:351 140 159 ./datatest/tests.cue:22:18 141 160 -- out/generate-v3/ok -- 142 161 -- out/generate-v3/stringTooLong -- 143 162 stringTooLong.data.shortString: invalid value "01234567890" (does not satisfy strings.MaxRunes(10)): 144 - 1:373 145 - 1:373 163 + 1:513 164 + 1:513 146 165 ./datatest/tests.cue:14:21 147 166 -- out/generate-v3/stringTooShort -- 148 167 stringTooShort.data.longString: invalid value "x" (does not satisfy strings.MinRunes(5)): 149 - 1:210 150 - 1:210 168 + 1:308 169 + 1:308 151 170 ./datatest/tests.cue:18:20
+108
encoding/jsonschema/testdata/generate/lists.txtar
··· 1 + -- test.cue -- 2 + package test 3 + 4 + // Open list with element type 5 + stringList?: [...string] 6 + 7 + // Open list with number elements 8 + numberList?: [...number] 9 + 10 + // Closed list (tuple) 11 + tuple?: [string, int] 12 + 13 + // Mixed tuple with concrete values 14 + mixedTuple?: [string, 2, bool] 15 + 16 + // Empty list 17 + emptyList?: [] 18 + 19 + -- datatest/tests.cue -- 20 + package datatest 21 + 22 + ok1: data: { 23 + stringList: ["a", "b", "c"] 24 + numberList: [1, 2.5, 3] 25 + tuple: ["foo", 42] 26 + mixedTuple: ["bar", 2, true] 27 + emptyList: [] 28 + } 29 + 30 + ok2: data: {} 31 + 32 + badStringList: { 33 + data: stringList: [1, 2, 3] 34 + error: true 35 + } 36 + 37 + badTupleLength: { 38 + data: tuple: ["foo"] 39 + error: true 40 + } 41 + 42 + badTupleType: { 43 + data: tuple: [1, "bar"] 44 + error: true 45 + } 46 + -- out/generate-v3/schema -- 47 + { 48 + $schema: "https://json-schema.org/draft/2020-12/schema" 49 + type: "object" 50 + properties: { 51 + emptyList: { 52 + type: "array" 53 + maxLength: 0 54 + } 55 + mixedTuple: { 56 + type: "array" 57 + prefixItems: [{ 58 + type: "string" 59 + }, { 60 + const: 2 61 + }, { 62 + type: "boolean" 63 + }] 64 + maxLength: 3 65 + minLength: 3 66 + } 67 + numberList: { 68 + type: "array" 69 + items: { 70 + type: "number" 71 + } 72 + } 73 + stringList: { 74 + type: "array" 75 + items: { 76 + type: "string" 77 + } 78 + } 79 + tuple: { 80 + type: "array" 81 + prefixItems: [{ 82 + type: "string" 83 + }, { 84 + type: "integer" 85 + }] 86 + maxLength: 2 87 + minLength: 2 88 + } 89 + } 90 + } 91 + -- out/generate-v3/badStringList -- 92 + badStringList.data.stringList.0: conflicting values 1 and string (mismatched types int and string): 93 + ./datatest/tests.cue:14:21 94 + badStringList.data.stringList.1: conflicting values 2 and string (mismatched types int and string): 95 + ./datatest/tests.cue:14:24 96 + badStringList.data.stringList.2: conflicting values 3 and string (mismatched types int and string): 97 + ./datatest/tests.cue:14:27 98 + -- out/generate-v3/badTupleLength -- 99 + badTupleLength.data.tuple: incompatible list lengths (1 and 3): 100 + 1:391 101 + -- out/generate-v3/badTupleType -- 102 + badTupleType.data.tuple.0: conflicting values 1 and string (mismatched types int and string): 103 + ./datatest/tests.cue:24:16 104 + badTupleType.data.tuple.1: conflicting values "bar" and int (mismatched types string and int): 105 + 1:425 106 + ./datatest/tests.cue:24:19 107 + -- out/generate-v3/ok1 -- 108 + -- out/generate-v3/ok2 --