this repo has no description
0
fork

Configure Feed

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

encoding/jsonschema: support `description` keyword

Add description to generated JSON schema documents based
on the CUE comments.

Fixes #4226

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

+207 -44
+26 -1
encoding/jsonschema/generate.go
··· 283 283 // makeItem returns an item representing the JSON Schema 284 284 // for v in naive form. 285 285 func (g *generator) makeItem(v cue.Value, mode closedMode) internItem { 286 - return g.unique.intern(g.makeItem0(v, mode)) 286 + it := g.unique.intern(g.makeItem0(v, mode)) 287 + if desc := docString(v); desc != "" { 288 + it = g.unique.intern(&itemDescription{description: desc, elem: it}) 289 + } 290 + return it 287 291 } 288 292 289 293 func (g *generator) makeItem0(v cue.Value, mode closedMode) item { ··· 1231 1235 return false 1232 1236 } 1233 1237 return (v.Kind() & (cue.StructKind | cue.ListKind)) == 0 1238 + } 1239 + 1240 + func docString(v cue.Value) string { 1241 + docs := v.Doc() 1242 + var txt string 1243 + switch len(docs) { 1244 + case 0: 1245 + return "" 1246 + case 1: 1247 + txt = strings.TrimSpace(docs[0].Text()) 1248 + default: 1249 + var b strings.Builder 1250 + for i, d := range docs { 1251 + if i > 0 { 1252 + b.WriteString("\n\n") 1253 + } 1254 + b.WriteString(strings.TrimSpace(d.Text())) 1255 + } 1256 + txt = b.String() 1257 + } 1258 + return txt 1234 1259 } 1235 1260 1236 1261 // DefaultNameFunc holds the default function used by [Generate]
+44 -3
encoding/jsonschema/generate_items.go
··· 326 326 return i 327 327 } 328 328 329 + type itemDescription struct { 330 + description string 331 + elem internItem 332 + } 333 + 334 + func (i *itemDescription) hash(h *maphash.Hash, u *uniqueItems) { 335 + h.WriteString(i.description) 336 + u.writeHash(h, i.elem) 337 + } 338 + 339 + func (i *itemDescription) generate(g *generator) ast.Expr { 340 + expr := i.elem.Value().generate(g) 341 + switch expr := expr.(type) { 342 + case *ast.StructLit: 343 + expr.Elts = append(expr.Elts, makeField("description", ast.NewString(i.description))) 344 + slices.SortFunc(expr.Elts, func(a, b ast.Decl) int { 345 + return cmpSchemaLabels(fieldLabel(a), fieldLabel(b)) 346 + }) 347 + return expr 348 + case *ast.BasicLit: 349 + if expr.Kind == token.TRUE { 350 + return makeSchemaStructLit(makeField("description", ast.NewString(i.description))) 351 + } 352 + return expr 353 + default: 354 + // All item generate methods return either *ast.StructLit 355 + // (via makeSchemaStructLit or singleKeyword) or *ast.BasicLit 356 + // (ast.NewBool in itemTrue and itemFalse), so this is unreachable. 357 + panic(fmt.Errorf("unexpected expression type in itemDescription: %T", expr)) 358 + } 359 + } 360 + 361 + func (i *itemDescription) apply(f func(internItem, *uniqueItems) internItem, u *uniqueItems) item { 362 + newElem := f(i.elem, u) 363 + if newElem == i.elem { 364 + return i 365 + } 366 + return &itemDescription{description: i.description, elem: newElem} 367 + } 368 + 329 369 // itemType represents a type constraint 330 370 type itemType struct { 331 371 kinds []string ··· 806 846 var labelPriorityValues = func() map[string]int { 807 847 // Always put these keywords at the start. 808 848 m := map[string]int{ 809 - "$schema": 0, 810 - "$defs": 1, 811 - "type": 2, 849 + "$schema": 0, 850 + "$defs": 1, 851 + "type": 2, 852 + "description": 3, 812 853 } 813 854 // It's nice to group related keywords together. 814 855 n := len(m)
+27 -22
encoding/jsonschema/testdata/generate/callop.txtar
··· 88 88 format: "date" 89 89 } 90 90 dateTime: { 91 - type: "string" 92 - format: "date-time" 91 + type: "string" 92 + description: "Time format constraints" 93 + format: "date-time" 93 94 } 94 95 longList: { 95 96 type: "array" ··· 100 101 minLength: 5 101 102 } 102 103 multiple: { 103 - type: "number" 104 - multipleOf: 5 104 + type: "number" 105 + description: "Math constraints" 106 + multipleOf: 5 105 107 } 106 108 score: { 107 109 allOf: [{ ··· 112 114 }] 113 115 } 114 116 shortList: { 115 - type: "array" 116 - maxItems: 3 117 + type: "array" 118 + description: "List constraints" 119 + maxItems: 3 117 120 } 118 121 shortString: { 119 - type: "string" 120 - maxLength: 10 122 + type: "string" 123 + description: "String rune constraints" 124 + maxLength: 10 121 125 } 122 126 timeOnly: { 123 127 type: "string" 124 128 format: "time" 125 129 } 126 130 userName: { 127 - type: "string" 128 - maxLength: 20 129 - minLength: 3 131 + type: "string" 132 + description: "Combined constraints" 133 + maxLength: 20 134 + minLength: 3 130 135 } 131 136 } 132 137 } ··· 137 142 -- out/generate-v3/badDateTime -- 138 143 badDateTime.data.dateTime: invalid value "2025-10-02T13" (does not satisfy time.Time): invalid time "2025-10-02T13": 139 144 ./datatest/tests.cue:26:18 140 - 1:215 145 + 1:255 141 146 -- out/generate-v3/badScore2 -- 142 147 badScore2.data.score: invalid value 5 (does not satisfy math.MultipleOf(10)): 143 - 1:421 148 + 1:494 144 149 ./datatest/tests.cue:42:15 145 - 1:376 150 + 1:449 146 151 -- out/generate-v3/badTime -- 147 152 badTime.data.date: invalid value "25:00:10" (does not satisfy time.Format("2006-01-02")): invalid time "25:00:10": 148 153 1:170 149 154 ./datatest/tests.cue:34:14 150 155 -- out/generate-v3/badUserName -- 151 156 badUserName.data.userName: invalid value "x" (does not satisfy strings.MinRunes(3)): 152 - 1:617 157 + 1:800 153 158 ./datatest/tests.cue:38:18 154 - 1:602 155 - 1:617 159 + 1:785 160 + 1:800 156 161 -- out/generate-v3/notMultiple -- 157 162 notMultiple.data.multiple: invalid value 1 (does not satisfy math.MultipleOf(5)): 158 - 1:351 163 + 1:424 159 164 ./datatest/tests.cue:22:18 160 165 -- out/generate-v3/ok -- 161 166 -- out/generate-v3/stringTooLong -- 162 167 stringTooLong.data.shortString: invalid value "01234567890" (does not satisfy strings.MaxRunes(10)): 163 - 1:513 168 + 1:659 164 169 ./datatest/tests.cue:14:21 165 - 1:513 170 + 1:659 166 171 -- out/generate-v3/stringTooShort -- 167 172 stringTooShort.data.longString: invalid value "x" (does not satisfy strings.MinRunes(5)): 168 - 1:308 173 + 1:348 169 174 ./datatest/tests.cue:18:20 170 - 1:308 175 + 1:348
+3 -2
encoding/jsonschema/testdata/generate/const.txtar
··· 118 118 const: "something" 119 119 } 120 120 unaryEquals: { 121 - const: 42 121 + description: "Test different forms of const as mentioned in constValueOf doc comment:" 122 + const: 42 122 123 } 123 124 } 124 125 } ··· 164 165 -- out/generate-v3/errorNonUnaryEquals -- 165 166 errorNonUnaryEquals.data.unaryEquals: conflicting values 42 and 99: 166 167 ./datatest/tests.cue:44:21 167 - 1:388 168 + 1:476
+8 -2
encoding/jsonschema/testdata/generate/contains.txtar
··· 41 41 type: "object" 42 42 properties: { 43 43 complexContains: { 44 - type: "array" 44 + type: "array" 45 + description: "Contains with complex schema" 45 46 contains: { 46 47 type: "object" 47 48 properties: { ··· 59 60 } 60 61 p1: { 61 62 type: "array" 63 + description: "Contains with default minimum (1)" 62 64 contains: true 63 65 minContains: 1 64 66 } 65 67 p2: { 66 68 type: "array" 69 + description: "Contains with minimum 0" 67 70 contains: true 68 71 minContains: 0 69 72 } 70 73 p3: { 71 74 type: "array" 75 + description: "Contains with minimum and maximum" 72 76 contains: true 73 77 maxContains: 6 74 78 minContains: 1 75 79 } 76 80 p4: { 77 81 type: "array" 82 + description: "Contains with minimum 3 and maximum 6" 78 83 contains: true 79 84 maxContains: 6 80 85 minContains: 3 81 86 } 82 87 stringContains: { 83 - type: "array" 88 + type: "array" 89 + description: "Contains with a specific schema" 84 90 contains: { 85 91 type: "string" 86 92 }
+68
encoding/jsonschema/testdata/generate/description.txtar
··· 1 + -- test.cue -- 2 + // This is the root description. 3 + package test 4 + 5 + // A name field. 6 + name!: string 7 + 8 + age?: int 9 + 10 + // A user definition. 11 + #User: { 12 + // The user's email. 13 + email!: string 14 + phone?: string 15 + } 16 + 17 + // The primary user. 18 + user?: #User 19 + 20 + -- datatest/tests.cue -- 21 + package datatest 22 + simple: { 23 + data: {name: "Alice", user: {email: "alice@example.com"}} 24 + } 25 + missingName: { 26 + data: {age: 30} 27 + error: true 28 + } 29 + -- out/generate-v3/schema -- 30 + { 31 + $schema: "https://json-schema.org/draft/2020-12/schema" 32 + $defs: { 33 + "#User": { 34 + type: "object" 35 + description: "A user definition." 36 + additionalProperties: false 37 + properties: { 38 + email: { 39 + type: "string" 40 + description: "The user's email." 41 + } 42 + phone: { 43 + type: "string" 44 + } 45 + } 46 + required: ["email"] 47 + } 48 + } 49 + type: "object" 50 + description: "This is the root description." 51 + properties: { 52 + age: { 53 + type: "integer" 54 + } 55 + name: { 56 + type: "string" 57 + description: "A name field." 58 + } 59 + user: { 60 + description: "The primary user." 61 + $ref: "#/$defs/%23User" 62 + } 63 + } 64 + required: ["name"] 65 + } 66 + -- out/generate-v3/simple -- 67 + -- out/generate-v3/missingName -- 68 + missingName.data.name: field is required but not present
+13 -7
encoding/jsonschema/testdata/generate/lists.txtar
··· 52 52 type: "object" 53 53 properties: { 54 54 emptyList: { 55 + description: "Empty list" 55 56 const: [] 56 57 } 57 58 emptyOpenList: { 58 - type: "array" 59 + type: "array" 60 + description: "Empty open list" 59 61 } 60 62 mixedTuple: { 61 - type: "array" 63 + type: "array" 64 + description: "Mixed tuple with concrete values" 62 65 prefixItems: [{ 63 66 type: "string" 64 67 }, { ··· 70 73 minItems: 3 71 74 } 72 75 numberList: { 73 - type: "array" 76 + type: "array" 77 + description: "Open list with number elements" 74 78 items: { 75 79 type: "number" 76 80 } 77 81 } 78 82 stringList: { 79 - type: "array" 83 + type: "array" 84 + description: "Open list with element type" 80 85 items: { 81 86 type: "string" 82 87 } 83 88 } 84 89 tuple: { 85 - type: "array" 90 + type: "array" 91 + description: "Closed list (tuple)" 86 92 prefixItems: [{ 87 93 type: "string" 88 94 }, { ··· 102 108 ./datatest/tests.cue:14:27 103 109 -- out/generate-v3/badTupleLength -- 104 110 badTupleLength.data.tuple: incompatible list lengths (1 and 3): 105 - 1:404 111 + 1:639 106 112 badTupleLength.data.tuple: incompatible list lengths (1 and 3): 107 - 1:470 113 + 1:705 108 114 -- out/generate-v3/badTupleType -- 109 115 badTupleType.data.tuple.0: conflicting values 1 and string (mismatched types int and string): 110 116 ./datatest/tests.cue:24:16
+6 -2
encoding/jsonschema/testdata/generate/matchif.txtar
··· 61 61 62 62 -- out/generate-v3/schema -- 63 63 { 64 - $schema: "https://json-schema.org/draft/2020-12/schema" 65 - type: "object" 64 + $schema: "https://json-schema.org/draft/2020-12/schema" 65 + type: "object" 66 + description: "matchIf patterns generated by Extract\nThese test the reverse transformation in Generate" 66 67 properties: { 67 68 basicIfThenElse: { 69 + description: "Basic if/then/else" 68 70 else: { 69 71 type: "object" 70 72 additionalProperties: true ··· 97 99 } 98 100 } 99 101 ifOnly: { 102 + description: "If only (both then and else are _)" 100 103 if: { 101 104 type: "object" 102 105 additionalProperties: true ··· 109 112 } 110 113 } 111 114 ifThenOnly: { 115 + description: "If/then only (else is _)" 112 116 if: { 113 117 type: "object" 114 118 additionalProperties: true
+12 -5
encoding/jsonschema/testdata/generate/matchn.txtar
··· 61 61 62 62 -- out/generate-v3/schema -- 63 63 { 64 - $schema: "https://json-schema.org/draft/2020-12/schema" 65 - type: "object" 64 + $schema: "https://json-schema.org/draft/2020-12/schema" 65 + type: "object" 66 + description: "matchN patterns generated by Extract\nThese test the reverse transformation in Generate" 66 67 properties: { 67 68 allOfTest: { 69 + description: "matchN(N, [...]) where N == len -> allOf" 68 70 allOf: [{ 69 71 type: "number" 70 72 }, { ··· 74 76 }] 75 77 } 76 78 anyOfTest: { 79 + description: "matchN(>=1, [...]) -> anyOf" 77 80 anyOf: [{ 78 81 type: "integer" 79 82 }, { ··· 93 96 } 94 97 } 95 98 complexOneOf: { 99 + description: "Combined with other constraints" 96 100 not: { 97 101 const: null 98 102 } ··· 111 115 }] 112 116 } 113 117 notTest: { 118 + description: "matchN(0, [x]) -> not" 114 119 not: { 115 120 type: "integer" 116 121 } 117 122 } 118 123 oneOfTest: { 124 + description: "matchN(1, [...]) -> oneOf" 119 125 oneOf: [{ 120 126 type: "integer" 121 127 }, { ··· 141 147 } 142 148 } 143 149 singleOneOf: { 150 + description: "Edge cases" 144 151 oneOf: [{ 145 152 type: "string" 146 153 }] ··· 153 160 oneOfMultipleMatch.data.oneOfTest: conflicting values 7 and string (mismatched types int and string): 154 161 ./datatest/tests.cue:19:19 155 162 oneOfMultipleMatch.data.oneOfTest: invalid value 7 (does not satisfy matchN): 2 matched, expected 1: 156 - 1:582 163 + 1:915 157 164 ./datatest/tests.cue:19:19 158 165 -- out/generate-v3/notMatch -- 159 166 notMatch.data.notTest: invalid value 5 (does not satisfy matchN): 1 matched, expected 0: 160 - 1:543 167 + 1:834 161 168 ./datatest/tests.cue:24:17 162 169 -- out/generate-v3/allOfNotMatch -- 163 170 allOfNotMatch.data.allOfTest: invalid value 200 (out of bound <100): 164 - 1:147 171 + 1:308 165 172 ./datatest/tests.cue:29:19