1515package openapi3
1616
1717import (
18+ "context"
1819 "errors"
1920 "fmt"
2021 "io"
22+ "net/url"
23+ "path"
24+ "path/filepath"
25+ "strings"
2126
2227 kopenapi3 "github.com/getkin/kin-openapi/openapi3"
2328 "github.com/gohugoio/hugo/cache/dynacache"
29+ "github.com/gohugoio/hugo/common/hashing"
30+ "github.com/gohugoio/hugo/common/maps"
2431 "github.com/gohugoio/hugo/deps"
2532 "github.com/gohugoio/hugo/identity"
2633 "github.com/gohugoio/hugo/parser/metadecoders"
34+ "github.com/gohugoio/hugo/resources"
2735 "github.com/gohugoio/hugo/resources/resource"
36+ resourcestpl "github.com/gohugoio/hugo/tpl/resources"
37+ "github.com/mitchellh/mapstructure"
2838)
2939
3040// New returns a new instance of the openapi3-namespaced template functions.
@@ -37,8 +47,9 @@ func New(deps *deps.Deps) *Namespace {
3747
3848// Namespace provides template functions for the "openapi3".
3949type Namespace struct {
40- cache * dynacache.Partition [string , * OpenAPIDocument ]
41- deps * deps.Deps
50+ cache * dynacache.Partition [string , * OpenAPIDocument ]
51+ deps * deps.Deps
52+ resourcesNs * resourcestpl.Namespace
4253}
4354
4455// OpenAPIDocument represents an OpenAPI 3 document.
@@ -51,13 +62,35 @@ func (o *OpenAPIDocument) GetIdentityGroup() identity.Identity {
5162 return o .identityGroup
5263}
5364
65+ type unmarshalOptions struct {
66+ // Options passed to resources.GetRemote when resolving remote $ref.
67+ GetRemote map [string ]any
68+ }
69+
5470// Unmarshal unmarshals the given resource into an OpenAPI 3 document.
55- func (ns * Namespace ) Unmarshal (r resource.UnmarshableResource ) (* OpenAPIDocument , error ) {
71+ func (ns * Namespace ) Unmarshal (ctx context.Context , args ... any ) (* OpenAPIDocument , error ) {
72+ if len (args ) < 1 || len (args ) > 2 {
73+ return nil , errors .New ("must provide a Resource and optionally an options map" )
74+ }
75+
76+ r := args [0 ].(resource.UnmarshableResource )
5677 key := r .Key ()
5778 if key == "" {
5879 return nil , errors .New ("no Key set in Resource" )
5980 }
6081
82+ var opts unmarshalOptions
83+ if len (args ) > 1 {
84+ optsm , err := maps .ToStringMapE (args [1 ])
85+ if err != nil {
86+ return nil , err
87+ }
88+ if err := mapstructure .WeakDecode (optsm , & opts ); err != nil {
89+ return nil , err
90+ }
91+ key += "_" + hashing .HashString (optsm )
92+ }
93+
6194 v , err := ns .cache .GetOrCreate (key , func (string ) (* OpenAPIDocument , error ) {
6295 f := metadecoders .FormatFromStrings (r .MediaType ().Suffixes ()... )
6396 if f == "" {
@@ -86,13 +119,84 @@ func (ns *Namespace) Unmarshal(r resource.UnmarshableResource) (*OpenAPIDocument
86119 return nil , err
87120 }
88121
89- err = kopenapi3 .NewLoader ().ResolveRefsIn (s , nil )
122+ var resourcePath string
123+ if res , ok := r .(resource.Resource ); ok {
124+ resourcePath = resources .InternalResourceSourcePath (res )
125+ }
126+ var relDir string
127+ if resourcePath != "" {
128+ if rel , ok := ns .deps .Assets .MakePathRelative (filepath .FromSlash (resourcePath ), true ); ok {
129+ relDir = filepath .Dir (rel )
130+ }
131+ }
132+
133+ var idm identity.Manager = identity .NopManager
134+ if v := identity .GetDependencyManager (r ); v != nil {
135+ idm = v
136+ }
137+ idg := identity .FirstIdentity (r )
138+
139+ resolver := & refResolver {
140+ ctx : ctx ,
141+ idm : idm ,
142+ opts : opts ,
143+ relBase : filepath .ToSlash (relDir ),
144+ ns : ns ,
145+ }
90146
91- return & OpenAPIDocument {T : s , identityGroup : identity .FirstIdentity (r )}, err
147+ loader := kopenapi3 .NewLoader ()
148+ loader .IsExternalRefsAllowed = true
149+ loader .ReadFromURIFunc = resolver .resolveExternalRef
150+ err = loader .ResolveRefsIn (s , nil )
151+
152+ return & OpenAPIDocument {T : s , identityGroup : idg }, err
92153 })
93154 if err != nil {
94155 return nil , err
95156 }
96157
97158 return v , nil
98159}
160+
161+ type refResolver struct {
162+ ctx context.Context
163+ idm identity.Manager
164+ opts unmarshalOptions
165+ relBase string
166+ ns * Namespace
167+ }
168+
169+ // resolveExternalRef resolves external references in OpenAPI documents by either fetching
170+ // remote URLs or loading local files from the assets directory, depending on the reference location.
171+ func (r * refResolver ) resolveExternalRef (loader * kopenapi3.Loader , loc * url.URL ) ([]byte , error ) {
172+ if loc .Scheme != "" && loc .Host != "" {
173+ res , err := r .ns .resourcesNs .GetRemote (loc .String (), r .opts .GetRemote )
174+ if err != nil {
175+ return nil , fmt .Errorf ("failed to get remote ref %q: %w" , loc .String (), err )
176+ }
177+ content , err := resources .InternalResourceSourceContent (r .ctx , res )
178+ if err != nil {
179+ return nil , fmt .Errorf ("failed to read remote ref %q: %w" , loc .String (), err )
180+ }
181+ r .idm .AddIdentity (identity .FirstIdentity (res ))
182+ return []byte (content ), nil
183+ }
184+
185+ var filename string
186+ if strings .HasPrefix (loc .Path , "/" ) {
187+ filename = loc .Path
188+ } else {
189+ filename = path .Join (r .relBase , loc .Path )
190+ }
191+
192+ res := r .ns .resourcesNs .Get (filename )
193+ if res == nil {
194+ return nil , fmt .Errorf ("local ref %q not found" , loc .String ())
195+ }
196+ content , err := resources .InternalResourceSourceContent (r .ctx , res )
197+ if err != nil {
198+ return nil , fmt .Errorf ("failed to read local ref %q: %w" , loc .String (), err )
199+ }
200+ r .idm .AddIdentity (identity .FirstIdentity (res ))
201+ return []byte (content ), nil
202+ }
0 commit comments