From 71733b5fa0db1378a27190f0acda6be3b42a5d9e Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Sun, 11 Oct 2026 10:17:48 -0400 Subject: [PATCH] Resolve local OpenAPI URI-fragment pointers --- cli/openbb_cli/dispatchers/openapi_schema.py | 14 ++-- cli/tests/test_openapi_pointer_fragments.py | 72 ++++++++++++++++++++ 2 files changed, 79 insertions(+), 7 deletions(-) create mode 100644 cli/tests/test_openapi_pointer_fragments.py diff --git a/cli/openbb_cli/dispatchers/openapi_schema.py b/cli/openbb_cli/dispatchers/openapi_schema.py index 04fe7ce25652..1ec51662524e 100644 --- a/cli/openbb_cli/dispatchers/openapi_schema.py +++ b/cli/openbb_cli/dispatchers/openapi_schema.py @@ -98,14 +98,12 @@ def param_provider_membership( def resolve_ref(spec: dict[str, Any], ref: str) -> dict[str, Any]: """Resolve a local OpenAPI ``$ref`` JSON pointer against ``spec``.""" - if not ref.startswith("#/"): + if not ref.startswith("#"): + return {} + try: + node = _resolve_json_pointer(spec, ref[1:]) + except (KeyError, IndexError, ValueError): return {} - node: Any = spec - for raw_part in ref[2:].split("/"): - part = raw_part.replace("~1", "/").replace("~0", "~") - if not isinstance(node, dict) or part not in node: - return {} - node = node[part] return node if isinstance(node, dict) else {} @@ -874,6 +872,8 @@ def _resolve_json_pointer(document: Any, fragment: str) -> Any: if isinstance(node, dict): node = node[part] elif isinstance(node, list): + if not re.fullmatch(r"0|[1-9][0-9]*", part): + raise ValueError(f"Invalid OpenAPI array reference index: {part}") node = node[int(part)] else: raise ValueError(f"Invalid OpenAPI reference fragment: #{fragment}") diff --git a/cli/tests/test_openapi_pointer_fragments.py b/cli/tests/test_openapi_pointer_fragments.py new file mode 100644 index 000000000000..3281656687ac --- /dev/null +++ b/cli/tests/test_openapi_pointer_fragments.py @@ -0,0 +1,72 @@ +"""URI-fragment references share local and external pointer semantics.""" + +import json +from contextlib import contextmanager + +import httpx +import pytest + +from openbb_cli.dispatchers.openapi_schema import ( + _bundle_external_refs, + _resolve_json_pointer, + build_parser_from_operation, + resolve_ref, +) + + +@pytest.mark.parametrize( + "fragment", ["/defs/limit%20param", "/parameters/0", "/defs/a~1b~0c"] +) +def test_local_reference_preserves_typed_parameters(fragment): + parameter = {"name": "limit", "in": "query", "schema": {"type": "integer"}} + spec = { + "defs": {"limit param": parameter, "a/b~c": parameter}, + "parameters": [parameter], + } + assert resolve_ref(spec, "#" + fragment) == parameter + parser = build_parser_from_operation( + {"parameters": [{"$ref": "#" + fragment}]}, spec + ) + assert parser.parse_args(["--limit", "7"]).limit == 7 + + +@pytest.mark.parametrize("index", ["-1", "01", "+0", " 0", "١", "1", "nope"]) +def test_noncanonical_or_missing_list_reference_is_rejected(index): + spec = {"parameters": [{"type": "integer"}]} + assert resolve_ref(spec, "#/parameters/" + index) == {} + with pytest.raises((ValueError, IndexError)): + _resolve_json_pointer(spec, "/parameters/" + index) + + +@pytest.mark.parametrize( + "fragment", ["/defs/limit%20param", "/parameters/0", "/defs/a~1b~0c"] +) +def test_external_reference_has_local_pointer_parity(monkeypatch, fragment): + parameter = {"name": "limit", "in": "query", "schema": {"type": "integer"}} + doc = { + "defs": {"limit param": parameter, "a/b~c": parameter}, + "parameters": [parameter], + } + + @contextmanager + def stream(_method, url, **_kwargs): + yield httpx.Response( + 200, content=json.dumps(doc), request=httpx.Request("GET", url) + ) + + monkeypatch.setattr(httpx, "stream", stream) + result = _bundle_external_refs( + {"parameter": {"$ref": "defs.json#" + fragment}}, + "https://api.example/root.json", + timeout=1, + headers={}, + ) + assert result["parameter"] == resolve_ref(doc, "#" + fragment) + + +def test_dictionary_numeric_keys_and_missing_targets_keep_existing_semantics(): + spec = {"defs": {"01": {"type": "integer"}}} + assert resolve_ref(spec, "#/defs/01") == {"type": "integer"} + assert resolve_ref(spec, "#/missing") == {} + assert resolve_ref(spec, "https://elsewhere.example/spec.json#/defs/01") == {} + assert resolve_ref(spec, "#anchor") == {}