Skip to content

Commit 3589605

Browse files
origin: record scalar-valued map keys for location lookup
A scalar-valued map (e.g. OAuth flow scopes: map[string]string) decodes into a Go map that has no Origin field of its own, so the per-key locations present in the OriginTree are discarded by applyOriginsToMap. Capture them on the parent struct's Origin as a named sequence (Sequences[field]), addressable by key, so a consumer can locate an individual entry. Gated by isScalarValuedMapField, so it fires only for maps with scalar elements; pointer/struct-valued maps (e.g. properties) are untouched because their values already carry their own Origin. No yaml3 change is needed: the key locations are already in the OriginTree. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BqJA1X6suZYtR3tRbX8sLj
1 parent 2fd3d49 commit 3589605

2 files changed

Lines changed: 68 additions & 3 deletions

File tree

openapi3/origin.go

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package openapi3
22

33
import (
44
"reflect"
5+
"sort"
56
"strings"
67

78
"github.com/oasdiff/yaml"
@@ -131,6 +132,48 @@ func toInt(v any) int {
131132
return 0
132133
}
133134

135+
// isScalarValuedMapField reports whether v is a non-empty map whose element
136+
// type is a scalar (string, bool, or a numeric kind). Such a map decodes
137+
// without an Origin field of its own, unlike a pointer- or struct-valued map
138+
// whose elements each carry their own Origin.
139+
func isScalarValuedMapField(v reflect.Value) bool {
140+
if v.Kind() != reflect.Map || v.IsNil() || v.Len() == 0 {
141+
return false
142+
}
143+
switch v.Type().Elem().Kind() {
144+
case reflect.String, reflect.Bool,
145+
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
146+
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
147+
reflect.Float32, reflect.Float64:
148+
return true
149+
}
150+
return false
151+
}
152+
153+
// recordMapKeyLocations copies the map-key locations from a scalar-valued map's
154+
// own subtree onto parentOrigin.Sequences[field], so each key is addressable by
155+
// name (the same shape used for sequence items). It is a no-op when the child
156+
// carries no origin data. Keys are sorted for deterministic output.
157+
func recordMapKeyLocations(parentOrigin *Origin, field string, childTree *yaml.OriginTree) {
158+
s, ok := childTree.Origin.([]any)
159+
if !ok {
160+
return
161+
}
162+
childOrigin := originFromSeq(s)
163+
if childOrigin == nil || len(childOrigin.Fields) == 0 {
164+
return
165+
}
166+
locs := make([]Location, 0, len(childOrigin.Fields))
167+
for _, loc := range childOrigin.Fields {
168+
locs = append(locs, loc)
169+
}
170+
sort.Slice(locs, func(i, j int) bool { return locs[i].Name < locs[j].Name })
171+
if parentOrigin.Sequences == nil {
172+
parentOrigin.Sequences = make(map[string][]Location)
173+
}
174+
parentOrigin.Sequences[field] = locs
175+
}
176+
134177
// applyOrigins walks a Go struct tree and a parallel OriginTree, setting
135178
// Origin fields on each struct from the extracted origin data.
136179
func applyOrigins(v any, tree *yaml.OriginTree) {
@@ -168,12 +211,14 @@ func applyOriginsToStruct(val reflect.Value, ptr reflect.Value, tree *yaml.Origi
168211
typ := val.Type()
169212

170213
// Set Origin field for structs whose Origin field has a "-" json tag.
214+
var structOrigin *Origin
171215
if tree.Origin != nil {
172216
if sf, ok := typ.FieldByName("Origin"); ok && sf.Type == originPtrType {
173217
tag := sf.Tag.Get("json")
174218
if tag == "-" {
175219
if s, ok := tree.Origin.([]any); ok {
176-
val.FieldByName("Origin").Set(reflect.ValueOf(originFromSeq(s)))
220+
structOrigin = originFromSeq(s)
221+
val.FieldByName("Origin").Set(reflect.ValueOf(structOrigin))
177222
}
178223
}
179224
}
@@ -190,9 +235,19 @@ func applyOriginsToStruct(val reflect.Value, ptr reflect.Value, tree *yaml.Origi
190235
continue
191236
}
192237
childTree := tree.Fields[tag]
193-
if childTree != nil {
194-
applyOriginsToValue(val.Field(i), childTree)
238+
if childTree == nil {
239+
continue
240+
}
241+
// A scalar-valued map (e.g. OAuth scopes: map[string]string) decodes into
242+
// a Go map that has no Origin field of its own, so its per-key locations —
243+
// present in the child subtree — would otherwise be lost. Record them on
244+
// this struct's Origin as a named sequence so a consumer can locate each
245+
// entry by key. Object- or pointer-valued maps are excluded: their values
246+
// carry their own Origin via the recursion below.
247+
if structOrigin != nil && isScalarValuedMapField(val.Field(i)) {
248+
recordMapKeyLocations(structOrigin, tag, childTree)
195249
}
250+
applyOriginsToValue(val.Field(i), childTree)
196251
}
197252

198253
// Handle wrapper types whose inner struct has no json tag:

openapi3/origin_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,16 @@ func TestOrigin_Security(t *testing.T) {
393393
Name: "authorizationUrl",
394394
},
395395
base.Flows.Implicit.Origin.Fields["authorizationUrl"])
396+
397+
// scopes is a map[string]string, which decodes without an Origin of its own,
398+
// so its per-key locations are recorded on the flow's Origin as a named
399+
// sequence (sorted by key).
400+
require.Equal(t,
401+
[]openapi3.Location{
402+
{File: "testdata/origin/security.yaml", Line: 36, Column: 13, Name: "read:pets"},
403+
{File: "testdata/origin/security.yaml", Line: 35, Column: 13, Name: "write:pets"},
404+
},
405+
base.Flows.Implicit.Origin.Sequences["scopes"])
396406
}
397407

398408
func TestOrigin_Example(t *testing.T) {

0 commit comments

Comments
 (0)