emit_jsonschema.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  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. sch := map[string]any{"type": "integer"}
  92. if t.Name == "int64" {
  93. sch["format"] = "int64"
  94. }
  95. return sch
  96. case KindNumber:
  97. return map[string]any{"type": "number"}
  98. case KindBool:
  99. return map[string]any{"type": "boolean"}
  100. case KindArray:
  101. return map[string]any{"type": "array", "items": g.typeSchema(*t.Element)}
  102. case KindMap:
  103. return map[string]any{"type": "object", "additionalProperties": g.typeSchema(*t.Value)}
  104. case KindAny, KindUnknown, KindRaw:
  105. return map[string]any{}
  106. case KindRef:
  107. if t.Name == "nullable" {
  108. inner := g.typeSchema(*t.Inner)
  109. if ref, ok := inner["$ref"]; ok {
  110. return map[string]any{"nullable": true, "allOf": []any{map[string]any{"$ref": ref}}}
  111. }
  112. inner["nullable"] = true
  113. return inner
  114. }
  115. if alias, ok := g.aliasByName[t.Name]; ok {
  116. return g.typeSchema(alias.Underlying)
  117. }
  118. if _, ok := g.byName[t.Name]; ok {
  119. return map[string]any{"$ref": "#/components/schemas/" + t.Name}
  120. }
  121. return map[string]any{}
  122. }
  123. return map[string]any{}
  124. }
  125. func applyConstraints(sch map[string]any, t TypeRef, rules []ValidateRule) {
  126. base := baseKind(t)
  127. numeric := base.Kind == KindInt || base.Kind == KindNumber
  128. str := base.Kind == KindString
  129. for _, r := range rules {
  130. switch r.Name {
  131. case "gte":
  132. if numeric {
  133. sch["minimum"] = coerceExample(r.Param, base)
  134. }
  135. case "lte":
  136. if numeric {
  137. sch["maximum"] = coerceExample(r.Param, base)
  138. }
  139. case "gt":
  140. if numeric {
  141. sch["minimum"] = coerceExample(r.Param, base)
  142. sch["exclusiveMinimum"] = true
  143. }
  144. case "lt":
  145. if numeric {
  146. sch["maximum"] = coerceExample(r.Param, base)
  147. sch["exclusiveMaximum"] = true
  148. }
  149. case "min":
  150. if numeric {
  151. sch["minimum"] = coerceExample(r.Param, base)
  152. } else if str {
  153. if n, err := strconv.Atoi(r.Param); err == nil {
  154. sch["minLength"] = n
  155. }
  156. }
  157. case "max":
  158. if numeric {
  159. sch["maximum"] = coerceExample(r.Param, base)
  160. } else if str {
  161. if n, err := strconv.Atoi(r.Param); err == nil {
  162. sch["maxLength"] = n
  163. }
  164. }
  165. case "oneof":
  166. vals := strings.Fields(r.Param)
  167. if len(vals) > 0 {
  168. enum := make([]any, len(vals))
  169. for i, v := range vals {
  170. enum[i] = v
  171. }
  172. sch["enum"] = enum
  173. }
  174. case "email":
  175. if str {
  176. sch["format"] = "email"
  177. }
  178. case "url":
  179. if str {
  180. sch["format"] = "uri"
  181. }
  182. }
  183. }
  184. }