Skip to content

Commit 4a38617

Browse files
authored
Enhance x-origin validator to check cross-API operation references (#19)
Previously only validated operation references within the same API. Now loads all API specs upfront and validates operation references across APIs, catching invalid API URNs and non-existent operations in target APIs.
1 parent 4564ae4 commit 4a38617

1 file changed

Lines changed: 83 additions & 7 deletions

File tree

scripts/build/validate_xorigin.py

Lines changed: 83 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def __repr__(self):
4040

4141

4242
class 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

224294
def 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

Comments
 (0)