11package openapi3
22
3+ import (
4+ "encoding/json"
5+ "reflect"
6+ "strings"
7+
8+ "github.com/oasdiff/yaml"
9+ )
10+
311const originKey = "__origin__"
412
13+ var originPtrType = reflect .TypeOf ((* Origin )(nil ))
14+
515// Origin contains the origin of a collection.
616// Key is the location of the collection itself.
717// Fields is a map of the location of each scalar field in the collection.
@@ -20,6 +30,245 @@ type Location struct {
2030 Name string `json:"name,omitempty" yaml:"name,omitempty"`
2131}
2232
33+ // originFromSeq parses the compact []any sequence produced by yaml3's addOrigin.
34+ //
35+ // Format: [file, key_name, key_line, key_col, nf, f1_name, f1_delta, f1_col, ..., ns, s1_name, s1_count, s1_l0_delta, s1_c0, ...]
36+ func originFromSeq (s []any ) * Origin {
37+ // Need at least: file, key_name, key_line, key_col, nf, ns
38+ if len (s ) < 6 {
39+ return nil
40+ }
41+ file , _ := s [0 ].(string )
42+ keyName , _ := s [1 ].(string )
43+ keyLine := toInt (s [2 ])
44+ keyCol := toInt (s [3 ])
45+
46+ o := & Origin {
47+ Key : & Location {
48+ File : file ,
49+ Line : keyLine ,
50+ Column : keyCol ,
51+ Name : keyName ,
52+ },
53+ }
54+
55+ idx := 4
56+ nf := toInt (s [idx ])
57+ idx ++
58+ if nf > 0 && idx + nf * 3 <= len (s ) {
59+ o .Fields = make (map [string ]Location , nf )
60+ for i := 0 ; i < nf ; i ++ {
61+ fname , _ := s [idx ].(string )
62+ delta := toInt (s [idx + 1 ])
63+ col := toInt (s [idx + 2 ])
64+ o .Fields [fname ] = Location {
65+ File : file ,
66+ Line : keyLine + delta ,
67+ Column : col ,
68+ Name : fname ,
69+ }
70+ idx += 3
71+ }
72+ }
73+
74+ if idx >= len (s ) {
75+ return o
76+ }
77+ ns := toInt (s [idx ])
78+ idx ++
79+ if ns > 0 {
80+ o .Sequences = make (map [string ][]Location , ns )
81+ for i := 0 ; i < ns ; i ++ {
82+ if idx >= len (s ) {
83+ break
84+ }
85+ sname , _ := s [idx ].(string )
86+ idx ++
87+ count := toInt (s [idx ])
88+ idx ++
89+ locs := make ([]Location , count )
90+ for j := 0 ; j < count && idx + 1 < len (s ); j ++ {
91+ delta := toInt (s [idx ])
92+ col := toInt (s [idx + 1 ])
93+ locs [j ] = Location {File : file , Line : keyLine + delta , Column : col }
94+ idx += 2
95+ }
96+ o .Sequences [sname ] = locs
97+ }
98+ }
99+ return o
100+ }
101+
102+ // UnmarshalJSON parses the compact []any sequence produced by yaml3's addOrigin.
103+ // This allows __origin__ to be decoded directly during JSON unmarshaling without
104+ // a separate applyOrigins pass when the caller does not use UnmarshalWithOriginTree.
105+ func (o * Origin ) UnmarshalJSON (data []byte ) error {
106+ var seq []any
107+ if err := json .Unmarshal (data , & seq ); err != nil {
108+ return err
109+ }
110+ if parsed := originFromSeq (seq ); parsed != nil {
111+ * o = * parsed
112+ }
113+ return nil
114+ }
115+
116+ // toInt converts numeric types to int. Handles int/uint64 from YAML decoding
117+ // and float64 from JSON decoding of []any sequences.
118+ func toInt (v any ) int {
119+ switch n := v .(type ) {
120+ case int :
121+ return n
122+ case uint64 :
123+ return int (n )
124+ case float64 :
125+ return int (n )
126+ }
127+ return 0
128+ }
129+
130+ // applyOrigins walks a Go struct tree and a parallel OriginTree, setting
131+ // Origin fields on each struct from the extracted origin data.
132+ func applyOrigins (v any , tree * yaml.OriginTree ) {
133+ if tree == nil {
134+ return
135+ }
136+ applyOriginsToValue (reflect .ValueOf (v ), tree )
137+ }
138+
139+ func applyOriginsToValue (val reflect.Value , tree * yaml.OriginTree ) {
140+ // Keep track of the last pointer so we can pass it to struct handlers
141+ // (needed for calling methods like Map() on maplike types).
142+ var ptr reflect.Value
143+ for val .Kind () == reflect .Ptr || val .Kind () == reflect .Interface {
144+ if val .IsNil () {
145+ return
146+ }
147+ if val .Kind () == reflect .Ptr {
148+ ptr = val
149+ }
150+ val = val .Elem ()
151+ }
152+
153+ switch val .Kind () {
154+ case reflect .Struct :
155+ applyOriginsToStruct (val , ptr , tree )
156+ case reflect .Map :
157+ applyOriginsToMap (val , tree )
158+ case reflect .Slice :
159+ applyOriginsToSlice (val , tree )
160+ }
161+ }
162+
163+ func applyOriginsToStruct (val reflect.Value , ptr reflect.Value , tree * yaml.OriginTree ) {
164+ typ := val .Type ()
165+
166+ // Set Origin field if tagged with json:"__origin__" or json:"-" (maplike types).
167+ // Skip *Ref types which have Origin with no json tag — those pass through to Value.
168+ if tree .Origin != nil {
169+ if sf , ok := typ .FieldByName ("Origin" ); ok && sf .Type == originPtrType {
170+ tag := sf .Tag .Get ("json" )
171+ if strings .Contains (tag , originKey ) || tag == "-" {
172+ if s , ok := tree .Origin .([]any ); ok {
173+ val .FieldByName ("Origin" ).Set (reflect .ValueOf (originFromSeq (s )))
174+ }
175+ }
176+ }
177+ }
178+
179+ // Recurse into exported struct fields using json tags
180+ for i := 0 ; i < typ .NumField (); i ++ {
181+ sf := typ .Field (i )
182+ if ! sf .IsExported () {
183+ continue
184+ }
185+ tag := jsonTagName (sf )
186+ if tag == "" || tag == "-" {
187+ continue
188+ }
189+ childTree := tree .Fields [tag ]
190+ if childTree != nil {
191+ applyOriginsToValue (val .Field (i ), childTree )
192+ }
193+ }
194+
195+ // Handle wrapper types whose inner struct has no json tag:
196+ // - *Ref types (e.g. SchemaRef, ResponseRef) have a "Value" field
197+ // - AdditionalProperties has a "Schema" field
198+ // The origin tree data applies to the inner struct, not a sub-key.
199+ for _ , fieldName := range []string {"Value" , "Schema" } {
200+ vf := val .FieldByName (fieldName )
201+ if ! vf .IsValid () || vf .Kind () != reflect .Ptr || vf .IsNil () {
202+ continue
203+ }
204+ sf , _ := typ .FieldByName (fieldName )
205+ if sf .Tag .Get ("json" ) == "" {
206+ applyOriginsToValue (vf , tree )
207+ }
208+ }
209+
210+ // Handle "maplike" types (Paths, Responses, Callback) whose items are
211+ // stored in an unexported map accessible via a Map() method.
212+ // Use the original pointer (if available) since dereferenced values
213+ // are not addressable.
214+ receiver := val
215+ if ptr .IsValid () {
216+ receiver = ptr
217+ } else if val .CanAddr () {
218+ receiver = val .Addr ()
219+ }
220+ if receiver .Kind () == reflect .Ptr {
221+ if mapMethod := receiver .MethodByName ("Map" ); mapMethod .IsValid () {
222+ results := mapMethod .Call (nil )
223+ if len (results ) == 1 {
224+ applyOriginsToMap (results [0 ], tree )
225+ }
226+ }
227+ }
228+ }
229+
230+ func applyOriginsToMap (val reflect.Value , tree * yaml.OriginTree ) {
231+ if tree .Fields == nil {
232+ return
233+ }
234+ for _ , key := range val .MapKeys () {
235+ childTree := tree .Fields [key .String ()]
236+ if childTree == nil {
237+ continue
238+ }
239+ elem := val .MapIndex (key )
240+ // Map values are not addressable. For pointer-typed values we can
241+ // recurse directly. For value types we must copy, apply, and set back.
242+ if elem .Kind () == reflect .Ptr || elem .Kind () == reflect .Interface {
243+ applyOriginsToValue (elem , childTree )
244+ } else if elem .Kind () == reflect .Struct {
245+ // Copy to a settable value
246+ cp := reflect .New (elem .Type ()).Elem ()
247+ cp .Set (elem )
248+ applyOriginsToStruct (cp , reflect.Value {}, childTree )
249+ val .SetMapIndex (key , cp )
250+ }
251+ }
252+ }
253+
254+ func applyOriginsToSlice (val reflect.Value , tree * yaml.OriginTree ) {
255+ for i := 0 ; i < val .Len () && i < len (tree .Items ); i ++ {
256+ if tree .Items [i ] != nil {
257+ applyOriginsToValue (val .Index (i ), tree .Items [i ])
258+ }
259+ }
260+ }
261+
262+ // jsonTagName returns the JSON field name from a struct field's json tag.
263+ func jsonTagName (f reflect.StructField ) string {
264+ tag := f .Tag .Get ("json" )
265+ if tag == "" {
266+ return ""
267+ }
268+ name , _ , _ := strings .Cut (tag , "," )
269+ return name
270+ }
271+
23272// stripOriginFromAny recursively removes the __origin__ key from any
24273// map[string]any value. This is needed for interface{}/any-typed fields
25274// (e.g. Schema.Enum, Schema.Default, Parameter.Example) that have no
0 commit comments