@@ -83,6 +83,38 @@ func (of *OperationFinder) FindByID(operationID string) *OperationInfo {
8383 return nil
8484}
8585
86+ // parseQualifiedOperationID parses the Arazzo spec form
87+ // "$sourceDescriptions.NAME.operationId" into (sourceName, operationId, true).
88+ // Returns ("", "", false) for any other string.
89+ func parseQualifiedOperationID (expr string ) (string , string , bool ) {
90+ const prefix = "$sourceDescriptions."
91+ if ! strings .HasPrefix (expr , prefix ) {
92+ return "" , "" , false
93+ }
94+ rest := expr [len (prefix ):]
95+ dot := strings .Index (rest , "." )
96+ if dot < 0 {
97+ return "" , "" , false
98+ }
99+ return rest [:dot ], rest [dot + 1 :], true
100+ }
101+
102+ // FindByIDInSource finds an operation by its operationId within a single named
103+ // source description. Used when the step specifies a qualified operationId like
104+ // "$sourceDescriptions.petStoreDescription.loginUser".
105+ // It reuses FindByID by constructing a single-entry finder scoped to that source.
106+ func (of * OperationFinder ) FindByIDInSource (sourceName , operationID string ) * OperationInfo {
107+ sourceDescRaw , ok := of .SourceDescriptions [sourceName ]
108+ if ! ok {
109+ log .Printf ("Source description %q not found" , sourceName )
110+ return nil
111+ }
112+ scoped := & OperationFinder {
113+ SourceDescriptions : map [string ]interface {}{sourceName : sourceDescRaw },
114+ }
115+ return scoped .FindByID (operationID )
116+ }
117+
86118// FindByHTTPPathAndMethod finds an operation by its HTTP path and method.
87119func (of * OperationFinder ) FindByHTTPPathAndMethod (httpPath , httpMethod string ) * OperationInfo {
88120 targetMethod := strings .ToLower (httpMethod )
@@ -166,7 +198,12 @@ func (of *OperationFinder) FindByHTTPPathAndMethod(httpPath, httpMethod string)
166198
167199// FindByPath finds an operation by source URL and JSON pointer.
168200// operationPath format: sourceURL#jsonPointer
201+ // sourceURL may be a bare name, a URL, or a braced expression like
202+ // "{$sourceDescriptions.petStoreDescription.url}".
169203func (of * OperationFinder ) FindByPath (sourceURL , jsonPointer string ) * OperationInfo {
204+ // Strip surrounding braces and resolve "$sourceDescriptions.NAME.url" to "NAME"
205+ sourceURL = resolveSourceDescriptionRef (strings .Trim (sourceURL , "{}" ))
206+
170207 // Find the source description
171208 sourceName , sourceDesc := of .findSourceDescription (sourceURL )
172209 if sourceDesc == nil {
@@ -177,6 +214,18 @@ func (of *OperationFinder) FindByPath(sourceURL, jsonPointer string) *OperationI
177214 return of .parseOperationPointer (jsonPointer , sourceName , sourceDesc )
178215}
179216
217+ // resolveSourceDescriptionRef converts a "$sourceDescriptions.NAME.url" expression
218+ // (already stripped of surrounding braces) to just "NAME", which directly matches
219+ // the key in the SourceDescriptions map. Any other string is returned unchanged.
220+ func resolveSourceDescriptionRef (expr string ) string {
221+ const prefix = "$sourceDescriptions."
222+ const suffix = ".url"
223+ if strings .HasPrefix (expr , prefix ) && strings .HasSuffix (expr , suffix ) {
224+ return expr [len (prefix ) : len (expr )- len (suffix )]
225+ }
226+ return expr
227+ }
228+
180229// findSourceDescription finds a source description by URL or name.
181230func (of * OperationFinder ) findSourceDescription (sourceURL string ) (string , map [string ]interface {}) {
182231 // Exact name match
@@ -219,6 +268,13 @@ func (of *OperationFinder) parseOperationPointer(jsonPointer, sourceName string,
219268 return info
220269 }
221270
271+ // Approach 4: Path-only pointer (no HTTP method) — picks the first available method.
272+ // Handles e.g. /paths/~1pet~1findByStatus without a trailing /get.
273+ info = of .resolvePathOnly (jsonPointer , sourceName , sourceDesc )
274+ if info != nil {
275+ return info
276+ }
277+
222278 log .Printf ("Could not parse operation pointer: %s" , jsonPointer )
223279 return nil
224280}
@@ -427,6 +483,62 @@ func (of *OperationFinder) handleSpecialCases(jsonPointer, sourceName string, so
427483 return nil
428484}
429485
486+ // resolvePathOnly handles JSON pointers that reference a path item rather than a
487+ // specific operation, e.g. /paths/~1pet~1findByStatus (no HTTP method suffix).
488+ // It returns the first HTTP method found for the decoded path, in httpMethods order.
489+ func (of * OperationFinder ) resolvePathOnly (jsonPointer , sourceName string , sourceDesc map [string ]interface {}) * OperationInfo {
490+ if ! strings .HasPrefix (jsonPointer , "/paths/" ) {
491+ return nil
492+ }
493+
494+ // Everything after "/paths/" is the single encoded path token (e.g. ~1pet~1findByStatus).
495+ encodedPath := strings .TrimPrefix (jsonPointer , "/paths/" )
496+ if encodedPath == "" {
497+ return nil
498+ }
499+
500+ // Decode JSON Pointer encoding: ~1 → /, ~0 → ~
501+ httpPath := strings .ReplaceAll (encodedPath , "~1" , "/" )
502+ httpPath = strings .ReplaceAll (httpPath , "~0" , "~" )
503+ if ! strings .HasPrefix (httpPath , "/" ) {
504+ httpPath = "/" + httpPath
505+ }
506+
507+ paths := toMap (sourceDesc ["paths" ])
508+ if paths == nil {
509+ return nil
510+ }
511+
512+ pathItem := toMap (paths [httpPath ])
513+ if pathItem == nil {
514+ return nil
515+ }
516+
517+ baseURL , err := getBaseURL (sourceDesc )
518+ if err != nil {
519+ return nil
520+ }
521+
522+ // Return the first HTTP method found for this path
523+ for _ , method := range httpMethods {
524+ operation := toMap (pathItem [method ])
525+ if operation == nil {
526+ continue
527+ }
528+ opID , _ := operation ["operationId" ].(string )
529+ return & OperationInfo {
530+ Source : sourceName ,
531+ Path : httpPath ,
532+ Method : method ,
533+ URL : baseURL + httpPath ,
534+ Operation : operation ,
535+ OperationID : opID ,
536+ }
537+ }
538+
539+ return nil
540+ }
541+
430542// GetOperationsForWorkflow finds all operation references in a workflow dict.
431543func (of * OperationFinder ) GetOperationsForWorkflow (workflow map [string ]interface {}) []* OperationInfo {
432544 var operations []* OperationInfo
@@ -439,7 +551,13 @@ func (of *OperationFinder) GetOperationsForWorkflow(workflow map[string]interfac
439551 }
440552
441553 if opID , ok := step ["operationId" ].(string ); ok && opID != "" {
442- if info := of .FindByID (opID ); info != nil {
554+ var info * OperationInfo
555+ if sourceName , bareID , ok := parseQualifiedOperationID (opID ); ok {
556+ info = of .FindByIDInSource (sourceName , bareID )
557+ } else {
558+ info = of .FindByID (opID )
559+ }
560+ if info != nil {
443561 operations = append (operations , info )
444562 }
445563 } else if opPath , ok := step ["operationPath" ].(string ); ok && opPath != "" {
0 commit comments