@@ -40,7 +40,7 @@ def __repr__(self):
4040
4141
4242class XOriginValidator :
43- def __init__ (self , api_dir : Path , schema_path : Path ):
43+ def __init__ (self , api_dir : Path , schema_path : Path , all_apis : Dict [ str , Dict [ str , Any ]] = None ):
4444 self .api_dir = api_dir
4545 self .api_name = api_dir .name
4646 self .api_path = api_dir / 'api.yaml'
@@ -49,6 +49,7 @@ def __init__(self, api_dir: Path, schema_path: Path):
4949 self .schema = None
5050 self .violations : List [XOriginViolation ] = []
5151 self .operation_ids : Set [str ] = set ()
52+ self .all_apis = all_apis or {} # Map of api_name -> {spec, operation_ids}
5253
5354 def load_schema (self ):
5455 """Load the x-origin JSON Schema"""
@@ -208,17 +209,86 @@ def check_operation_references(self):
208209 continue
209210
210211 operation = source ['operation' ]
211- api = source ['api' ]
212+ api_ref = source ['api' ]
213+ source_loc = f'{ location } .x-origin[{ idx } ]'
214+
215+ # Parse the API reference (could be "urn:api:api-name" or just "api-name")
216+ if api_ref .startswith ('urn:api:' ):
217+ target_api_name = api_ref .replace ('urn:api:' , '' )
218+ else :
219+ target_api_name = api_ref
212220
213- # Only validate references to the same API
214- if api == f'urn:api: { self . api_name } ' or api == self .api_name :
221+ # Check if referencing the same API
222+ if target_api_name == self .api_name :
215223 if operation not in self .operation_ids :
216224 self .violations .append (XOriginViolation (
217225 self .api_name ,
218226 'xorigin-invalid-operation-reference' ,
219- f'{ location } .x-origin[{ idx } ]' ,
220- f'References non-existent operationId: { operation } '
227+ source_loc ,
228+ f'References non-existent operationId "{ operation } " in same API'
229+ ))
230+ else :
231+ # Cross-API reference - check if target API exists
232+ if target_api_name not in self .all_apis :
233+ self .violations .append (XOriginViolation (
234+ self .api_name ,
235+ 'xorigin-invalid-api-reference' ,
236+ source_loc ,
237+ f'References non-existent API "{ target_api_name } " (from "{ api_ref } ")'
221238 ))
239+ else :
240+ # API exists, check if operation exists in target API
241+ target_operation_ids = self .all_apis [target_api_name ]['operation_ids' ]
242+ if operation not in target_operation_ids :
243+ self .violations .append (XOriginViolation (
244+ self .api_name ,
245+ 'xorigin-invalid-operation-reference' ,
246+ source_loc ,
247+ f'References non-existent operationId "{ operation } " in API "{ target_api_name } "'
248+ ))
249+
250+
251+ def load_all_apis (repo_root : Path ) -> Dict [str , Dict [str , Any ]]:
252+ """Load all API specs and their operation IDs"""
253+ all_apis = {}
254+ apis_dir = repo_root / 'apis'
255+
256+ if not apis_dir .exists ():
257+ return all_apis
258+
259+ for api_dir in sorted (apis_dir .iterdir ()):
260+ if not api_dir .is_dir () or api_dir .name .startswith ('.' ):
261+ continue
262+
263+ api_path = api_dir / 'api.yaml'
264+ if not api_path .exists ():
265+ continue
266+
267+ try :
268+ with open (api_path , 'r' ) as f :
269+ spec = yaml .safe_load (f )
270+
271+ # Extract operation IDs
272+ operation_ids = set ()
273+ paths = spec .get ('paths' , {})
274+ for path , path_item in paths .items ():
275+ if not isinstance (path_item , dict ):
276+ continue
277+ for method in ['get' , 'post' , 'put' , 'patch' , 'delete' , 'head' , 'options' ]:
278+ if method in path_item :
279+ operation = path_item [method ]
280+ op_id = operation .get ('operationId' )
281+ if op_id :
282+ operation_ids .add (op_id )
283+
284+ all_apis [api_dir .name ] = {
285+ 'spec' : spec ,
286+ 'operation_ids' : operation_ids
287+ }
288+ except Exception as e :
289+ print (f" Warning: Could not load { api_dir .name } : { e } " )
290+
291+ return all_apis
222292
223293
224294def validate_all_apis (repo_root : Path , schema_path : Path ) -> Dict [str , List [XOriginViolation ]]:
@@ -231,6 +301,12 @@ def validate_all_apis(repo_root: Path, schema_path: Path) -> Dict[str, List[XOri
231301 print (f" Warning: apis/ directory not found at { apis_dir } " )
232302 return results
233303
304+ # First pass: Load all APIs and their operation IDs
305+ print (" Loading all APIs..." )
306+ all_apis = load_all_apis (repo_root )
307+ print (f" Loaded { len (all_apis )} APIs\n " )
308+
309+ # Second pass: Validate each API with access to all others
234310 for api_dir in sorted (apis_dir .iterdir ()):
235311 if not api_dir .is_dir ():
236312 continue
@@ -244,7 +320,7 @@ def validate_all_apis(repo_root: Path, schema_path: Path) -> Dict[str, List[XOri
244320 continue
245321
246322 print (f" Validating: { api_dir .name } " )
247- validator = XOriginValidator (api_dir , schema_path )
323+ validator = XOriginValidator (api_dir , schema_path , all_apis )
248324 violations = validator .validate ()
249325
250326 if violations :
0 commit comments