@@ -13,6 +13,7 @@ import (
1313 "github.com/pb33f/libopenapi/datamodel/low/base"
1414 low "github.com/pb33f/libopenapi/datamodel/low/v3"
1515 "github.com/pb33f/libopenapi/orderedmap"
16+ "github.com/pb33f/libopenapi/utils"
1617 "go.yaml.in/yaml/v4"
1718)
1819
@@ -173,8 +174,11 @@ func (c *Components) Render() ([]byte, error) {
173174
174175// MarshalYAML will create a ready to render YAML representation of the Response object.
175176func (c * Components ) MarshalYAML () (interface {}, error ) {
177+ c .warnPreservedComponentMapRefs ()
176178 nb := high .NewNodeBuilder (c , c .low )
177- return nb .Render (), nil
179+ rendered := nb .Render ()
180+ c .preserveInvalidComponentMapRefs (rendered )
181+ return rendered , nil
178182}
179183
180184// RenderInline will return a YAML representation of the Components object as a byte slice with references resolved.
@@ -185,7 +189,177 @@ func (c *Components) RenderInline() ([]byte, error) {
185189
186190// MarshalYAMLInline will create a ready to render YAML representation of the Components object with references resolved.
187191func (c * Components ) MarshalYAMLInline () (interface {}, error ) {
192+ c .warnPreservedComponentMapRefs ()
188193 nb := high .NewNodeBuilder (c , c .low )
189194 nb .Resolve = true
190- return nb .Render (), nil
195+ rendered := nb .Render ()
196+ c .preserveInvalidComponentMapRefs (rendered )
197+ return rendered , nil
198+ }
199+
200+ func (c * Components ) warnPreservedComponentMapRefs () {
201+ if c == nil || c .low == nil {
202+ return
203+ }
204+ idx := c .low .GetIndex ()
205+ if idx == nil {
206+ return
207+ }
208+ logger := idx .GetLogger ()
209+ if logger == nil {
210+ return
211+ }
212+
213+ warnComponentRefEntries (logger , low .SchemasLabel , c .low .Schemas .Value )
214+ warnComponentRefEntries (logger , low .ResponsesLabel , c .low .Responses .Value )
215+ warnComponentRefEntries (logger , low .ParametersLabel , c .low .Parameters .Value )
216+ warnComponentRefEntries (logger , base .ExamplesLabel , c .low .Examples .Value )
217+ warnComponentRefEntries (logger , low .RequestBodiesLabel , c .low .RequestBodies .Value )
218+ warnComponentRefEntries (logger , low .HeadersLabel , c .low .Headers .Value )
219+ warnComponentRefEntries (logger , low .SecuritySchemesLabel , c .low .SecuritySchemes .Value )
220+ warnComponentRefEntries (logger , low .LinksLabel , c .low .Links .Value )
221+ warnComponentRefEntries (logger , low .CallbacksLabel , c .low .Callbacks .Value )
222+ warnComponentRefEntries (logger , low .PathItemsLabel , c .low .PathItems .Value )
223+ warnComponentRefEntries (logger , low .MediaTypesLabel , c .low .MediaTypes .Value )
224+ }
225+
226+ func warnComponentRefEntries [T any ](
227+ logger interface {
228+ Warn (msg string , args ... any )
229+ },
230+ section string ,
231+ m * orderedmap.Map [lowmodel.KeyReference [string ], lowmodel.ValueReference [T ]],
232+ ) {
233+ if m == nil {
234+ return
235+ }
236+
237+ for pair := m .First (); pair != nil ; pair = pair .Next () {
238+ if pair .Key ().Value != "$ref" {
239+ continue
240+ }
241+ valueNode := pair .Value ().ValueNode
242+ if valueNode == nil || valueNode .Kind != yaml .ScalarNode {
243+ continue
244+ }
245+ logger .Warn (
246+ "preserving invalid component map $ref entry during render" ,
247+ "section" , section ,
248+ "ref" , valueNode .Value ,
249+ "line" , valueNode .Line ,
250+ "column" , valueNode .Column ,
251+ )
252+ }
253+ }
254+
255+ // preserveInvalidComponentMapRefs patches the rendered Components YAML tree so that invalid
256+ // map-level "$ref" entries under component sections survive a render cycle unchanged.
257+ //
258+ // Inputs like:
259+ //
260+ // components:
261+ // parameters:
262+ // $ref: "./params.yaml"
263+ //
264+ // are not valid OpenAPI component maps, but they do appear in the wild. The normal high-level
265+ // render path treats "$ref" as a literal component name and can otherwise collapse the scalar
266+ // value into an empty object. For these cases we preserve the original raw YAML nodes and pair
267+ // the behavior with a warning log, rather than silently rewriting the input.
268+ func (c * Components ) preserveInvalidComponentMapRefs (rendered * yaml.Node ) {
269+ if c == nil || c .low == nil || rendered == nil || rendered .Kind != yaml .MappingNode {
270+ return
271+ }
272+
273+ preserveComponentRefEntries (rendered , low .SchemasLabel , c .low .Schemas .Value )
274+ preserveComponentRefEntries (rendered , low .ResponsesLabel , c .low .Responses .Value )
275+ preserveComponentRefEntries (rendered , low .ParametersLabel , c .low .Parameters .Value )
276+ preserveComponentRefEntries (rendered , base .ExamplesLabel , c .low .Examples .Value )
277+ preserveComponentRefEntries (rendered , low .RequestBodiesLabel , c .low .RequestBodies .Value )
278+ preserveComponentRefEntries (rendered , low .HeadersLabel , c .low .Headers .Value )
279+ preserveComponentRefEntries (rendered , low .SecuritySchemesLabel , c .low .SecuritySchemes .Value )
280+ preserveComponentRefEntries (rendered , low .LinksLabel , c .low .Links .Value )
281+ preserveComponentRefEntries (rendered , low .CallbacksLabel , c .low .Callbacks .Value )
282+ preserveComponentRefEntries (rendered , low .PathItemsLabel , c .low .PathItems .Value )
283+ preserveComponentRefEntries (rendered , low .MediaTypesLabel , c .low .MediaTypes .Value )
284+ }
285+
286+ // preserveComponentRefEntries re-inserts a scalar "$ref" entry into the rendered YAML for a
287+ // specific component section. Only literal "$ref" keys backed by scalar low-level value nodes
288+ // are preserved; real component entries and malformed non-scalar values are ignored.
289+ func preserveComponentRefEntries [T any ](
290+ rendered * yaml.Node ,
291+ section string ,
292+ m * orderedmap.Map [lowmodel.KeyReference [string ], lowmodel.ValueReference [T ]],
293+ ) {
294+ if m == nil {
295+ return
296+ }
297+
298+ sectionNode := findMapValueNode (rendered , section )
299+ for pair := m .First (); pair != nil ; pair = pair .Next () {
300+ if pair .Key ().Value != "$ref" {
301+ continue
302+ }
303+
304+ valueNode := pair .Value ().ValueNode
305+ keyNode := pair .Key ().KeyNode
306+ if keyNode == nil || valueNode == nil || valueNode .Kind != yaml .ScalarNode {
307+ continue
308+ }
309+
310+ if sectionNode == nil {
311+ sectionNode = utils .CreateEmptyMapNode ()
312+ rendered .Content = append (
313+ rendered .Content ,
314+ utils .CreateStringNode (section ),
315+ sectionNode ,
316+ )
317+ }
318+ upsertMapNodeEntry (sectionNode , cloneYAMLNode (keyNode ), cloneYAMLNode (valueNode ))
319+ }
320+ }
321+
322+ // findMapValueNode returns the mapping value node for key from a YAML mapping node.
323+ func findMapValueNode (m * yaml.Node , key string ) * yaml.Node {
324+ if m == nil || m .Kind != yaml .MappingNode {
325+ return nil
326+ }
327+ for i := 0 ; i < len (m .Content ); i += 2 {
328+ if m .Content [i ].Value == key {
329+ return m .Content [i + 1 ]
330+ }
331+ }
332+ return nil
333+ }
334+
335+ // upsertMapNodeEntry replaces or appends a key/value pair in a YAML mapping node.
336+ func upsertMapNodeEntry (m * yaml.Node , keyNode , valueNode * yaml.Node ) {
337+ if m == nil || m .Kind != yaml .MappingNode || keyNode == nil || valueNode == nil {
338+ return
339+ }
340+ for i := 0 ; i < len (m .Content ); i += 2 {
341+ if m .Content [i ].Value == keyNode .Value {
342+ m .Content [i ] = keyNode
343+ m .Content [i + 1 ] = valueNode
344+ return
345+ }
346+ }
347+ m .Content = append (m .Content , keyNode , valueNode )
348+ }
349+
350+ // cloneYAMLNode deep-copies a YAML node tree so preserved low-level nodes can be spliced into
351+ // rendered output without mutating the original parsed model.
352+ func cloneYAMLNode (node * yaml.Node ) * yaml.Node {
353+ if node == nil {
354+ return nil
355+ }
356+
357+ cloned := * node
358+ if len (node .Content ) > 0 {
359+ cloned .Content = make ([]* yaml.Node , len (node .Content ))
360+ for i , child := range node .Content {
361+ cloned .Content [i ] = cloneYAMLNode (child )
362+ }
363+ }
364+ return & cloned
191365}
0 commit comments