Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions cli/openbb_cli/dispatchers/openapi_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}


Expand Down Expand Up @@ -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}")
Expand Down
72 changes: 72 additions & 0 deletions cli/tests/test_openapi_pointer_fragments.py
Original file line number Diff line number Diff line change
@@ -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") == {}