@@ -2,58 +2,99 @@ package jsonschema
22
33import (
44 "bytes"
5+ "encoding/json"
56 "fmt"
67 "io"
78 "reflect"
89 "regexp"
910
10- invopopjsonschema "github.com/invopop/jsonschema"
11+ gojsonschema "github.com/google/jsonschema-go/jsonschema"
12+ xjsonschema "go.jacobcolvin.com/x/jsonschema"
1113
1214 "github.com/macropower/kclipper/pkg/kclgen"
1315)
1416
15- type Reflector struct {
16- Reflector * invopopjsonschema.Reflector
17+ // reflectConfig holds the options for a [Reflect] call.
18+ type reflectConfig struct {
19+ comments bool
1720}
1821
19- func NewReflector () * Reflector {
20- return & Reflector {
21- Reflector : & invopopjsonschema.Reflector {
22- DoNotReference : true ,
23- ExpandedStruct : true ,
24- },
22+ // ReflectOpt configures [Reflect].
23+ //
24+ // Available options:
25+ // - [WithGoComments]
26+ type ReflectOpt func (* reflectConfig )
27+
28+ // WithGoComments is a [ReflectOpt] that enables extraction of Go doc comments
29+ // as schema descriptions. The source files of the reflected types must be
30+ // resolvable at generation time.
31+ func WithGoComments () ReflectOpt {
32+ return func (c * reflectConfig ) {
33+ c .comments = true
2534 }
2635}
2736
28- func (r * Reflector ) AddGoComments (pkg , path string ) error {
29- err := r .Reflector .AddGoComments (pkg , path , invopopjsonschema .WithFullComment ())
37+ // Reflect generates a schema for t and returns it wrapped in a [Reflected].
38+ //
39+ // Definitions are inlined rather than referenced via $ref, and nil-able Go
40+ // types are not made nullable, so the result is a single self-contained,
41+ // non-nullable schema suitable for KCL schema generation (a multi-type union
42+ // with null would otherwise collapse to any).
43+ func Reflect (t reflect.Type , opts ... ReflectOpt ) (* Reflected , error ) {
44+ var cfg reflectConfig
45+
46+ for _ , opt := range opts {
47+ opt (& cfg )
48+ }
49+
50+ genOpts := []xjsonschema.Option {
51+ // Inline every type instead of emitting a $defs/$ref graph; KCL schema
52+ // generation consumes a single flattened schema.
53+ xjsonschema .WithDefinitions (false ),
54+ // Keep slices, maps, and pointers non-nullable; KCL schema generation
55+ // renders a type union with null as any, losing the element type.
56+ xjsonschema .WithNullable (false ),
57+ }
58+ if cfg .comments {
59+ genOpts = append (genOpts , xjsonschema .WithComments (true ))
60+ }
61+
62+ s , err := xjsonschema .Generate (t , genOpts ... )
3063 if err != nil {
31- return fmt .Errorf ("add go comments from %q : %w" , pkg , err )
64+ return nil , fmt .Errorf ("reflect %s : %w" , t , err )
3265 }
3366
34- return nil
67+ return & Reflected { Schema : s , name : typeName ( t )}, nil
3568}
3669
37- func (r * Reflector ) Reflect (t reflect.Type , opts ... PropertyOpt ) * Reflected {
38- rs := r .Reflector .ReflectFromType (t )
39- for _ , opt := range opts {
40- opt (rs )
70+ // typeName returns the schema name for t, dereferencing pointers. KCL schema
71+ // generation names the root schema after this value, since the inlined schema
72+ // carries no $id for it to use instead.
73+ func typeName (t reflect.Type ) string {
74+ for t .Kind () == reflect .Pointer {
75+ t = t .Elem ()
4176 }
4277
43- return & Reflected { Schema : rs }
78+ return t . Name ()
4479}
4580
4681type replacement struct {
4782 re * regexp.Regexp
4883 repl string
4984}
5085
86+ // Reflected is a JSON Schema produced by [Reflect], ready to be converted to a
87+ // KCL schema. Create instances with [Reflect].
5188type Reflected struct {
52- Schema * invopopjsonschema.Schema
89+ // Schema is the underlying JSON Schema. It can be adjusted directly or via
90+ // [Reflected.SetProperty] and related methods before KCL generation.
91+ Schema * gojsonschema.Schema
5392
93+ name string
5494 replacements []replacement
5595}
5696
97+ // GenerateKCL converts the schema to a KCL schema and writes it to w.
5798func (r * Reflected ) GenerateKCL (w io.Writer , opts ... GenOpt ) error {
5899 for _ , opt := range opts {
59100 opt (r )
@@ -64,8 +105,15 @@ func (r *Reflected) GenerateKCL(w io.Writer, opts ...GenOpt) error {
64105 return fmt .Errorf ("marshal json schema: %w" , err )
65106 }
66107
108+ // KCL schema generation names the root schema after the file name when the
109+ // schema has no $id; pass the reflected type name so the schema keeps it.
110+ filename := r .name
111+ if filename == "" {
112+ filename = "chart"
113+ }
114+
67115 b := & bytes.Buffer {}
68- err = kclgen .Gen .GenKcl (b , "chart" , jsBytes , & kclgen.GenKclOptions {
116+ err = kclgen .Gen .GenKcl (b , filename , jsBytes , & kclgen.GenKclOptions {
69117 Mode : kclgen .ModeJSONSchema ,
70118 CastingOption : kclgen .OriginalName ,
71119 })
@@ -87,77 +135,120 @@ func (r *Reflected) GenerateKCL(w io.Writer, opts ...GenOpt) error {
87135 return nil
88136}
89137
138+ // SetProperty applies opts to the property named key, if it exists.
90139func (r * Reflected ) SetProperty (key string , opts ... PropertyOpt ) {
91- if cv , ok := r .Schema .Properties . Get ( key ) ; ok {
140+ if cv , ok := r .Schema .Properties [ key ] ; ok {
92141 for _ , opt := range opts {
93142 opt (cv )
94143 }
95144 }
96145}
97146
147+ // SetOrRemoveProperty applies opts to the property named key when setProperty
148+ // is true, and otherwise removes the property.
98149func (r * Reflected ) SetOrRemoveProperty (key string , setProperty bool , opts ... PropertyOpt ) {
99- if cv , ok := r .Schema .Properties .Get (key ); ok {
100- if setProperty {
101- for _ , opt := range opts {
102- opt (cv )
103- }
104- } else {
105- r .Schema .Properties .Delete (key )
150+ cv , ok := r .Schema .Properties [key ]
151+ if ! ok {
152+ return
153+ }
154+
155+ if setProperty {
156+ for _ , opt := range opts {
157+ opt (cv )
106158 }
159+
160+ return
107161 }
162+
163+ delete (r .Schema .Properties , key )
108164}
109165
166+ // RemoveProperty removes the property named key.
110167func (r * Reflected ) RemoveProperty (key string ) {
111- r .Schema .Properties . Delete ( key )
168+ delete ( r .Schema .Properties , key )
112169}
113170
171+ // GenOpt configures KCL generation from a [Reflected].
172+ //
173+ // Available options:
174+ // - [Replace]
114175type GenOpt func (* Reflected )
115176
177+ // Replace is a [GenOpt] that registers a regular-expression replacement applied
178+ // to the generated KCL schema text.
116179func Replace (re * regexp.Regexp , repl string ) GenOpt {
117180 return func (r * Reflected ) {
118181 r .replacements = append (r .replacements , replacement {re : re , repl : repl })
119182 }
120183}
121184
122- type PropertyOpt func (* invopopjsonschema.Schema )
185+ // PropertyOpt modifies a property [gojsonschema.Schema].
186+ //
187+ // Available options:
188+ // - [WithEnum]
189+ // - [WithItemsEnum]
190+ // - [WithDefault]
191+ // - [WithType]
192+ // - [WithNoContent]
193+ // - [WithAllowAdditionalProperties]
194+ type PropertyOpt func (* gojsonschema.Schema )
123195
196+ // WithEnum is a [PropertyOpt] that sets an enum on a property schema.
124197func WithEnum (enum []any ) PropertyOpt {
125- return func (s * invopopjsonschema .Schema ) {
198+ return func (s * gojsonschema .Schema ) {
126199 s .Enum = enum
127200 }
128201}
129202
130203// WithItemsEnum is a [PropertyOpt] that sets an enum on an array property's
131204// items schema.
132205func WithItemsEnum (enum []any ) PropertyOpt {
133- return func (s * invopopjsonschema .Schema ) {
206+ return func (s * gojsonschema .Schema ) {
134207 if s .Items != nil {
135208 s .Items .Enum = enum
136209 }
137210 }
138211}
139212
213+ // WithDefault is a [PropertyOpt] that sets the default on a property schema. A
214+ // nil value leaves the default unset, matching JSON omitempty semantics.
140215func WithDefault (defaultValue any ) PropertyOpt {
141- return func (s * invopopjsonschema.Schema ) {
142- s .Default = defaultValue
216+ return func (s * gojsonschema.Schema ) {
217+ if defaultValue == nil {
218+ return
219+ }
220+
221+ data , err := json .Marshal (defaultValue )
222+ if err != nil {
223+ return
224+ }
225+
226+ s .Default = data
143227 }
144228}
145229
230+ // WithType is a [PropertyOpt] that sets a single type on a property schema,
231+ // clearing any multi-type union.
146232func WithType (t string ) PropertyOpt {
147- return func (s * invopopjsonschema .Schema ) {
233+ return func (s * gojsonschema .Schema ) {
148234 s .Type = t
235+ s .Types = nil
149236 }
150237}
151238
239+ // WithNoContent is a [PropertyOpt] that removes the items and properties from a
240+ // property schema.
152241func WithNoContent () PropertyOpt {
153- return func (s * invopopjsonschema .Schema ) {
242+ return func (s * gojsonschema .Schema ) {
154243 s .Items = nil
155244 s .Properties = nil
156245 }
157246}
158247
248+ // WithAllowAdditionalProperties is a [PropertyOpt] that allows additional
249+ // properties on a property schema.
159250func WithAllowAdditionalProperties () PropertyOpt {
160- return func (s * invopopjsonschema .Schema ) {
161- s .AdditionalProperties = invopopjsonschema . TrueSchema
251+ return func (s * gojsonschema .Schema ) {
252+ s .AdditionalProperties = & gojsonschema. Schema {}
162253 }
163254}
0 commit comments