Skip to content

Commit fb72538

Browse files
authored
Add enums and defaults into jsonschema (#14)
Encode enum and default value metadata into the jsonschema struct tag of the generated Go code. This allows github.com/invopop/jsonschema to generate a JSONSchema with enums and default values.
1 parent 31f35db commit fb72538

File tree

1 file changed

+80
-8
lines changed

1 file changed

+80
-8
lines changed

internal/generator/internal/codegen/generator.go

Lines changed: 80 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"generator/internal/spec"
2424
"io"
2525
"slices"
26+
"strconv"
2627
"strings"
2728
"unicode"
2829

@@ -267,7 +268,7 @@ func (b *Generator) addProperties(g *jen.Group, props []spec.Property) {
267268
s.Id("any")
268269
}
269270

270-
s.Tag(structTag(&p))
271+
s.Tag(b.structTag(&p))
271272
if p.Description != nil {
272273
s.Comment(comment(&p))
273274
}
@@ -370,19 +371,77 @@ func goIDName(name string) string {
370371
return strings.Join(allParts, "")
371372
}
372373

373-
// structTag returns a struct tag for JSON and YAML
374-
func structTag(p *spec.Property) map[string]string {
374+
// structTag returns a struct tag for json, yaml, and jsonschema.
375+
func (b *Generator) structTag(p *spec.Property) map[string]string {
376+
tag := jsonTag(p)
377+
tags := map[string]string{
378+
"json": tag,
379+
"yaml": tag,
380+
}
381+
382+
if tag = b.jsonschemaTag(p); tag != "" {
383+
tags["jsonschema"] = tag
384+
}
385+
return tags
386+
}
387+
388+
// jsonschemaTag generates the jsonschema struct tag value used by
389+
// github.com/invopop/jsonschema.
390+
func (b *Generator) jsonschemaTag(p *spec.Property) string {
391+
var tagParts []string
392+
393+
// Enum values.
394+
if p.Type.InstanceOf != nil {
395+
if typeInfo, found := b.allTypes[p.Type.TypeName]; found {
396+
if enum, ok := typeInfo.Value.(spec.Enum); ok {
397+
for _, member := range enum.Members {
398+
tagParts = append(tagParts, "enum="+member.Name)
399+
}
400+
}
401+
}
402+
}
403+
404+
// Default value.
405+
switch v := p.ServerDefault.(type) {
406+
case nil:
407+
case string:
408+
// Data cleaning.
409+
v = trimBalanced(v, '`')
410+
v = trimBalanced(v, '"')
411+
v = trimBalanced(v, '\'')
412+
413+
if v != "" && v != "null" {
414+
tagParts = append(tagParts, "default="+escapeTagPart(v))
415+
}
416+
case []any:
417+
for _, v := range v {
418+
switch v := v.(type) {
419+
case string:
420+
tagParts = append(tagParts, "default="+escapeTagPart(v))
421+
default:
422+
panic(fmt.Errorf("unhandled type %T for default found in array for %s", v, p.Name))
423+
}
424+
}
425+
case bool:
426+
tagParts = append(tagParts, "default="+strconv.FormatBool(v))
427+
case float64:
428+
tagParts = append(tagParts, "default="+strconv.FormatFloat(v, 'g', -1, 64))
429+
default:
430+
panic(fmt.Errorf("unhandled type %T for default found for %s", v, p.Name))
431+
}
432+
433+
return strings.Join(tagParts, ",")
434+
}
435+
436+
// jsonTag generates the json struct tag value for a property.
437+
func jsonTag(p *spec.Property) string {
375438
tag := p.Name
376439
required := p.Required != nil && *p.Required
377440

378441
if !required {
379442
tag += ",omitempty"
380443
}
381-
382-
return map[string]string{
383-
"json": tag,
384-
"yaml": tag,
385-
}
444+
return tag
386445
}
387446

388447
// comment returns the field description with punctuation and information about
@@ -405,3 +464,16 @@ func comment(p *spec.Property) string {
405464
}
406465
return sb.String()
407466
}
467+
468+
// trimBalanced removes the leading and trailing character if they are balanced.
469+
func trimBalanced(s string, c rune) string {
470+
if len(s) >= 2 && rune(s[0]) == c && rune(s[len(s)-1]) == c {
471+
return s[1 : len(s)-1]
472+
}
473+
return s
474+
}
475+
476+
// escapeTagPart escapes commas in struct tag values.
477+
func escapeTagPart(s string) string {
478+
return strings.ReplaceAll(s, ",", `\,`)
479+
}

0 commit comments

Comments
 (0)