44 "context"
55 "errors"
66 "fmt"
7- "io"
8- "net/http"
97 "path/filepath"
108 "strings"
119
@@ -31,12 +29,14 @@ const (
3129
3230// LocalizeOptions represents the options available when localizing an OpenAPI document.
3331type LocalizeOptions struct {
34- // ResolveOptions are the options to use when resolving references during localization .
35- ResolveOptions ResolveOptions
32+ // DocumentLocation is the location of the document being localized .
33+ DocumentLocation string
3634 // TargetDirectory is the directory where localized files will be written.
3735 TargetDirectory string
3836 // VirtualFS is the file system interface used for reading and writing files.
3937 VirtualFS system.WritableVirtualFS
38+ // HTTPClient is the HTTP client to use for fetching remote references.
39+ HTTPClient system.Client
4040 // NamingStrategy determines how external reference files are named when localized.
4141 NamingStrategy LocalizeNamingStrategy
4242}
@@ -132,7 +132,13 @@ func Localize(ctx context.Context, doc *OpenAPI, opts LocalizeOptions) error {
132132 }
133133
134134 // Phase 1: Discover and collect all external references
135- if err := discoverExternalReferences (ctx , doc , opts .ResolveOptions , localizeStorage ); err != nil {
135+ if err := discoverExternalReferences (ctx , doc , ResolveOptions {
136+ RootDocument : doc ,
137+ TargetDocument : doc ,
138+ TargetLocation : opts .DocumentLocation ,
139+ VirtualFS : opts .VirtualFS ,
140+ HTTPClient : opts .HTTPClient ,
141+ }, localizeStorage ); err != nil {
136142 return fmt .Errorf ("failed to discover external references: %w" , err )
137143 }
138144
@@ -224,29 +230,25 @@ func discoverSchemaReference(ctx context.Context, schema *oas3.JSONSchema[oas3.R
224230 normalizedFilePath = normalizeFilePath (filePath )
225231 }
226232
227- // Always resolve the schema to enable recursive discovery
228- resolveOpts := oas3.ResolveOptions {
229- RootDocument : opts .RootDocument ,
230- TargetDocument : opts .TargetDocument ,
231- TargetLocation : opts .TargetLocation ,
232- VirtualFS : opts .VirtualFS ,
233- HTTPClient : opts .HTTPClient ,
234- }
235-
236- if _ , err := schema .Resolve (ctx , resolveOpts ); err != nil {
233+ if _ , err := schema .Resolve (ctx , opts ); err != nil {
237234 return fmt .Errorf ("failed to resolve external schema reference %s: %w" , ref , err )
238235 }
239236
240237 // Only store the file content if we haven't processed this file before
241238 if ! storage .externalRefs .Has (normalizedFilePath ) {
242- // Get the file content for this reference
243- content , err := getFileContentForReference (ctx , normalizedFilePath , opts )
244- if err != nil {
245- return fmt .Errorf ("failed to get content for reference %s: %w" , normalizedFilePath , err )
246- }
239+ // Get the cached reference document content that was loaded during resolution
240+ resolutionInfo := schema .GetReferenceResolutionInfo ()
241+ if resolutionInfo != nil {
242+ storage .externalRefs .Set (normalizedFilePath , "" ) // Will be filled in filename generation phase
247243
248- storage .externalRefs .Set (normalizedFilePath , "" ) // Will be filled in filename generation phase
249- storage .resolvedContent [normalizedFilePath ] = content
244+ if data , found := opts .RootDocument .GetCachedReferenceDocument (resolutionInfo .AbsoluteReference ); found {
245+ storage .resolvedContent [normalizedFilePath ] = data
246+ } else {
247+ return fmt .Errorf ("failed to get cached content for reference %s" , normalizedFilePath )
248+ }
249+ } else {
250+ return fmt .Errorf ("failed to get resolution info for reference %s" , normalizedFilePath )
251+ }
250252 }
251253
252254 // Get the resolved schema and recursively discover references within it
@@ -313,112 +315,65 @@ func discoverGenericReference[T any, V interfaces.Validator[T], C marshaller.Cor
313315 return fmt .Errorf ("failed to resolve external reference %s: %w" , refStr , err )
314316 }
315317
316- // Get the file content for this reference
317- content , err := getFileContentForReference (ctx , normalizedFilePath , opts )
318- if err != nil {
319- return fmt .Errorf ("failed to get content for reference %s: %w" , normalizedFilePath , err )
320- }
318+ // Get the cached reference document content that was loaded during resolution
319+ resolutionInfo := ref .GetReferenceResolutionInfo ()
320+ if resolutionInfo != nil {
321+ storage .externalRefs .Set (normalizedFilePath , "" ) // Will be filled in filename generation phase
321322
322- storage .externalRefs .Set (normalizedFilePath , "" ) // Will be filled in filename generation phase
323- storage .resolvedContent [normalizedFilePath ] = content
323+ if data , found := opts .RootDocument .GetCachedReferenceDocument (resolutionInfo .AbsoluteReference ); found {
324+ storage .resolvedContent [normalizedFilePath ] = data
325+ } else {
326+ return fmt .Errorf ("failed to get cached content for reference %s" , normalizedFilePath )
327+ }
328+ } else {
329+ return fmt .Errorf ("failed to get resolution info for reference %s" , normalizedFilePath )
330+ }
324331
325332 // Get the resolved object and recursively discover references within it
326333 resolvedValue := ref .GetObject ()
327334 if resolvedValue != nil {
328335 targetDocInfo := ref .GetReferenceResolutionInfo ()
329336
337+ resolveOpts := ResolveOptions {
338+ RootDocument : opts .RootDocument ,
339+ TargetDocument : targetDocInfo .ResolvedDocument ,
340+ TargetLocation : targetDocInfo .AbsoluteReference ,
341+ VirtualFS : opts .VirtualFS ,
342+ HTTPClient : opts .HTTPClient ,
343+ }
344+
330345 // Recursively discover references within the resolved object using Walk
331346 for item := range Walk (ctx , resolvedValue ) {
332347 err := item .Match (Matcher {
333348 Schema : func (s * oas3.JSONSchema [oas3.Referenceable ]) error {
334- return discoverSchemaReference (ctx , s , ResolveOptions {
335- RootDocument : opts .RootDocument ,
336- TargetDocument : targetDocInfo .ResolvedDocument ,
337- TargetLocation : targetDocInfo .AbsoluteReference ,
338- VirtualFS : opts .VirtualFS ,
339- HTTPClient : opts .HTTPClient ,
340- }, storage )
349+ return discoverSchemaReference (ctx , s , resolveOpts , storage )
341350 },
342351 ReferencedPathItem : func (r * ReferencedPathItem ) error {
343- return discoverGenericReference (ctx , r , ResolveOptions {
344- RootDocument : opts .RootDocument ,
345- TargetDocument : targetDocInfo .ResolvedDocument ,
346- TargetLocation : targetDocInfo .AbsoluteReference ,
347- VirtualFS : opts .VirtualFS ,
348- HTTPClient : opts .HTTPClient ,
349- }, storage )
352+ return discoverGenericReference (ctx , r , resolveOpts , storage )
350353 },
351354 ReferencedParameter : func (r * ReferencedParameter ) error {
352- return discoverGenericReference (ctx , r , ResolveOptions {
353- RootDocument : opts .RootDocument ,
354- TargetDocument : targetDocInfo .ResolvedDocument ,
355- TargetLocation : targetDocInfo .AbsoluteReference ,
356- VirtualFS : opts .VirtualFS ,
357- HTTPClient : opts .HTTPClient ,
358- }, storage )
355+ return discoverGenericReference (ctx , r , resolveOpts , storage )
359356 },
360357 ReferencedExample : func (r * ReferencedExample ) error {
361- return discoverGenericReference (ctx , r , ResolveOptions {
362- RootDocument : opts .RootDocument ,
363- TargetDocument : targetDocInfo .ResolvedDocument ,
364- TargetLocation : targetDocInfo .AbsoluteReference ,
365- VirtualFS : opts .VirtualFS ,
366- HTTPClient : opts .HTTPClient ,
367- }, storage )
358+ return discoverGenericReference (ctx , r , resolveOpts , storage )
368359 },
369360 ReferencedRequestBody : func (r * ReferencedRequestBody ) error {
370- return discoverGenericReference (ctx , r , ResolveOptions {
371- RootDocument : opts .RootDocument ,
372- TargetDocument : targetDocInfo .ResolvedDocument ,
373- TargetLocation : targetDocInfo .AbsoluteReference ,
374- VirtualFS : opts .VirtualFS ,
375- HTTPClient : opts .HTTPClient ,
376- }, storage )
361+ return discoverGenericReference (ctx , r , resolveOpts , storage )
377362 },
378363 ReferencedResponse : func (r * ReferencedResponse ) error {
379- return discoverGenericReference (ctx , r , ResolveOptions {
380- RootDocument : opts .RootDocument ,
381- TargetDocument : targetDocInfo .ResolvedDocument ,
382- TargetLocation : targetDocInfo .AbsoluteReference ,
383- VirtualFS : opts .VirtualFS ,
384- HTTPClient : opts .HTTPClient ,
385- }, storage )
364+ return discoverGenericReference (ctx , r , resolveOpts , storage )
386365 },
387366 ReferencedHeader : func (r * ReferencedHeader ) error {
388- return discoverGenericReference (ctx , r , ResolveOptions {
389- RootDocument : opts .RootDocument ,
390- TargetDocument : targetDocInfo .ResolvedDocument ,
391- TargetLocation : targetDocInfo .AbsoluteReference ,
392- VirtualFS : opts .VirtualFS ,
393- HTTPClient : opts .HTTPClient ,
394- }, storage )
367+ return discoverGenericReference (ctx , r , resolveOpts , storage )
395368 },
396369 ReferencedCallback : func (r * ReferencedCallback ) error {
397- return discoverGenericReference (ctx , r , ResolveOptions {
398- RootDocument : opts .RootDocument ,
399- TargetDocument : targetDocInfo .ResolvedDocument ,
400- TargetLocation : targetDocInfo .AbsoluteReference ,
401- VirtualFS : opts .VirtualFS ,
402- HTTPClient : opts .HTTPClient ,
403- }, storage )
370+ return discoverGenericReference (ctx , r , resolveOpts , storage )
404371 },
405372 ReferencedLink : func (r * ReferencedLink ) error {
406- return discoverGenericReference (ctx , r , ResolveOptions {
407- RootDocument : opts .RootDocument ,
408- TargetDocument : targetDocInfo .ResolvedDocument ,
409- TargetLocation : targetDocInfo .AbsoluteReference ,
410- VirtualFS : opts .VirtualFS ,
411- HTTPClient : opts .HTTPClient ,
412- }, storage )
373+ return discoverGenericReference (ctx , r , resolveOpts , storage )
413374 },
414375 ReferencedSecurityScheme : func (r * ReferencedSecurityScheme ) error {
415- return discoverGenericReference (ctx , r , ResolveOptions {
416- RootDocument : opts .RootDocument ,
417- TargetDocument : targetDocInfo .ResolvedDocument ,
418- TargetLocation : targetDocInfo .AbsoluteReference ,
419- VirtualFS : opts .VirtualFS ,
420- HTTPClient : opts .HTTPClient ,
421- }, storage )
376+ return discoverGenericReference (ctx , r , resolveOpts , storage )
422377 },
423378 })
424379 if err != nil {
@@ -430,90 +385,6 @@ func discoverGenericReference[T any, V interfaces.Validator[T], C marshaller.Cor
430385 return nil
431386}
432387
433- // getFileContentForReference retrieves the content of a file referenced by the given file path
434- func getFileContentForReference (ctx context.Context , filePath string , opts ResolveOptions ) ([]byte , error ) {
435- // First check if this is a URL or file path
436- classification , err := utils .ClassifyReference (filePath )
437- if err != nil {
438- return nil , fmt .Errorf ("failed to classify reference %s: %w" , filePath , err )
439- }
440-
441- var resolvedPath string
442- if classification .Type == utils .ReferenceTypeURL {
443- // For URLs, use the path as-is
444- resolvedPath = filePath
445- } else {
446- // For file paths, check if we need to resolve relative to a URL target location
447- resolvedPath = filePath
448- if ! filepath .IsAbs (filePath ) && opts .TargetLocation != "" {
449- // Check if target location is a URL
450- if targetClassification , targetErr := utils .ClassifyReference (opts .TargetLocation ); targetErr == nil && targetClassification .Type == utils .ReferenceTypeURL {
451- // Resolve relative file path against URL target location
452- resolved := resolveRelativeReference (filePath , opts .TargetLocation )
453- // Re-classify the resolved reference
454- if resolvedClassification , resolvedErr := utils .ClassifyReference (resolved ); resolvedErr == nil {
455- classification = resolvedClassification
456- resolvedPath = resolved
457- }
458- } else {
459- // Resolve relative to the target location directory (file path)
460- targetDir := filepath .Dir (opts .TargetLocation )
461- resolvedPath = filepath .Join (targetDir , filePath )
462- }
463- }
464- }
465-
466- switch classification .Type {
467- case utils .ReferenceTypeFilePath :
468- // Read from file system
469- file , err := opts .VirtualFS .Open (resolvedPath )
470- if err != nil {
471- return nil , fmt .Errorf ("failed to open file %s: %w" , resolvedPath , err )
472- }
473- defer file .Close ()
474-
475- content , err := io .ReadAll (file )
476- if err != nil {
477- return nil , fmt .Errorf ("failed to read file %s: %w" , resolvedPath , err )
478- }
479-
480- return content , nil
481-
482- case utils .ReferenceTypeURL :
483- // Fetch content via HTTP
484- httpClient := opts .HTTPClient
485- if httpClient == nil {
486- httpClient = http .DefaultClient
487- }
488-
489- req , err := http .NewRequestWithContext (ctx , http .MethodGet , resolvedPath , nil )
490- if err != nil {
491- return nil , fmt .Errorf ("failed to create HTTP request for %s: %w" , resolvedPath , err )
492- }
493-
494- resp , err := httpClient .Do (req )
495- if err != nil || resp == nil {
496- return nil , fmt .Errorf ("failed to fetch URL %s: %w" , resolvedPath , err )
497- }
498- defer resp .Body .Close ()
499-
500- // Check if the response was successful
501- if resp .StatusCode < 200 || resp .StatusCode >= 300 {
502- return nil , fmt .Errorf ("HTTP request failed with status %d for URL %s" , resp .StatusCode , resolvedPath )
503- }
504-
505- content , err := io .ReadAll (resp .Body )
506- if err != nil {
507- return nil , fmt .Errorf ("failed to read response body from %s: %w" , resolvedPath , err )
508- }
509-
510- return content , nil
511-
512- default :
513- return nil , fmt .Errorf ("unsupported reference type for localization: %s" , resolvedPath )
514- }
515- }
516-
517388// generateLocalizedFilenames creates conflict-free filenames for all external references
518389func generateLocalizedFilenames (storage * localizeStorage , strategy LocalizeNamingStrategy ) {
519390 // First pass: collect all base filenames to detect conflicts
0 commit comments