1
0

emit_jsonschema.go 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. package main
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "io"
  6. "sort"
  7. "strconv"
  8. "strings"
  9. )
  10. func emitJSONSchema(w io.Writer, schemas []Schema, aliases []Alias) error {
  11. byName := make(map[string]Schema, len(schemas))
  12. for _, s := range schemas {
  13. byName[s.Name] = s
  14. }
  15. aliasByName := make(map[string]Alias, len(aliases))
  16. for _, a := range aliases {
  17. aliasByName[a.Name] = a
  18. }
  19. gen := &schemaGen{byName: byName, aliasByName: aliasByName}
  20. out := make(map[string]any, len(schemas))
  21. for _, s := range schemas {
  22. out[s.Name] = gen.objectSchema(s)
  23. }
  24. payload, err := json.MarshalIndent(out, "", " ")
  25. if err != nil {
  26. return err
  27. }
  28. if _, err := fmt.Fprintln(w, examplesHeader); err != nil {
  29. return err
  30. }
  31. if _, err := fmt.Fprintf(w, "export const SCHEMAS: Record<string, unknown> = %s;\n", payload); err != nil {
  32. return err
  33. }
  34. return nil
  35. }
  36. type schemaGen struct {
  37. byName map[string]Schema
  38. aliasByName map[string]Alias
  39. }
  40. func (g *schemaGen) objectSchema(s Schema) map[string]any {
  41. props := make(map[string]any, len(s.Fields))
  42. var required []string
  43. for _, f := range s.Fields {
  44. props[f.JSONName] = g.fieldSchema(f)
  45. if !f.Optional {
  46. required = append(required, f.JSONName)
  47. }
  48. }
  49. obj := map[string]any{"type": "object", "properties": props}
  50. if len(required) > 0 {
  51. sort.Strings(required)
  52. obj["required"] = required
  53. }
  54. if s.Doc != "" {
  55. obj["description"] = s.Doc
  56. }
  57. return obj
  58. }
  59. func (g *schemaGen) fieldSchema(f Field) map[string]any {
  60. sch := g.typeSchema(f.Type)
  61. if ref, ok := sch["$ref"]; ok {
  62. if f.Doc == "" && f.Example == "" {
  63. return sch
  64. }
  65. wrap := map[string]any{"allOf": []any{map[string]any{"$ref": ref}}}
  66. if f.Doc != "" {
  67. wrap["description"] = f.Doc
  68. }
  69. if f.Example != "" {
  70. wrap["example"] = coerceExample(f.Example, baseKind(f.Type))
  71. }
  72. return wrap
  73. }
  74. applyConstraints(sch, f.Type, f.Validate)
  75. if f.Doc != "" {
  76. sch["description"] = f.Doc
  77. }
  78. if f.Example != "" {
  79. sch["example"] = coerceExample(f.Example, baseKind(f.Type))
  80. }
  81. return sch
  82. }
  83. func (g *schemaGen) typeSchema(t TypeRef) map[string]any {
  84. switch t.Kind {
  85. case KindString:
  86. if t.Name == "datetime" {
  87. return map[string]any{"type": "string", "format": "date-time"}
  88. }
  89. return map[string]any{"type": "string"}
  90. case KindInt:
  91. return map[string]any{"type": "integer"}
  92. case KindNumber:
  93. return map[string]any{"type": "number"}
  94. case KindBool:
  95. return map[string]any{"type": "boolean"}
  96. case KindArray:
  97. return map[string]any{"type": "array", "items": g.typeSchema(*t.Element)}
  98. case KindMap:
  99. return map[string]any{"type": "object", "additionalProperties": g.typeSchema(*t.Value)}
  100. case KindAny, KindUnknown, KindRaw:
  101. return map[string]any{}
  102. case KindRef:
  103. if t.Name == "nullable" {
  104. inner := g.typeSchema(*t.Inner)
  105. if ref, ok := inner["$ref"]; ok {
  106. return map[string]any{"nullable": true, "allOf": []any{map[string]any{"$ref": ref}}}
  107. }
  108. inner["nullable"] = true
  109. return inner
  110. }
  111. if alias, ok := g.aliasByName[t.Name]; ok {
  112. return g.typeSchema(alias.Underlying)
  113. }
  114. if _, ok := g.byName[t.Name]; ok {
  115. return map[string]any{"$ref": "#/components/schemas/" + t.Name}
  116. }
  117. return map[string]any{}
  118. }
  119. return map[string]any{}
  120. }
  121. func applyConstraints(sch map[string]any, t TypeRef, rules []ValidateRule) {
  122. base := baseKind(t)
  123. numeric := base.Kind == KindInt || base.Kind == KindNumber
  124. str := base.Kind == KindString
  125. for _, r := range rules {
  126. switch r.Name {
  127. case "gte":
  128. if numeric {
  129. sch["minimum"] = coerceExample(r.Param, base)
  130. }
  131. case "lte":
  132. if numeric {
  133. sch["maximum"] = coerceExample(r.Param, base)
  134. }
  135. case "gt":
  136. if numeric {
  137. sch["minimum"] = coerceExample(r.Param, base)
  138. sch["exclusiveMinimum"] = true
  139. }
  140. case "lt":
  141. if numeric {
  142. sch["maximum"] = coerceExample(r.Param, base)
  143. sch["exclusiveMaximum"] = true
  144. }
  145. case "min":
  146. if numeric {
  147. sch["minimum"] = coerceExample(r.Param, base)
  148. } else if str {
  149. if n, err := strconv.Atoi(r.Param); err == nil {
  150. sch["minLength"] = n
  151. }
  152. }
  153. case "max":
  154. if numeric {
  155. sch["maximum"] = coerceExample(r.Param, base)
  156. } else if str {
  157. if n, err := strconv.Atoi(r.Param); err == nil {
  158. sch["maxLength"] = n
  159. }
  160. }
  161. case "oneof":
  162. vals := strings.Fields(r.Param)
  163. if len(vals) > 0 {
  164. enum := make([]any, len(vals))
  165. for i, v := range vals {
  166. enum[i] = v
  167. }
  168. sch["enum"] = enum
  169. }
  170. case "email":
  171. if str {
  172. sch["format"] = "email"
  173. }
  174. case "url":
  175. if str {
  176. sch["format"] = "uri"
  177. }
  178. }
  179. }
  180. }