Current section
Files
Jump to
Current section
Files
src/nori@ref@resolver.erl
-module(nori@ref@resolver).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/nori/ref/resolver.gleam").
-export([parse_json_pointer/1, parse_ref/1, resolve/2, resolve_file/1]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" $ref resolution engine for OpenAPI specifications.\n"
"\n"
" Operates on raw YamlValue trees (before typed decoding) to resolve\n"
" $ref pointers across files. Supports:\n"
" - Local refs: `#/components/schemas/User`\n"
" - File refs: `./components/schemas/user.yaml`\n"
" - File refs with pointer: `./schemas.yaml#/User`\n"
).
-file("src/nori/ref/resolver.gleam", 203).
?DOC(" Loads a file from disk or returns it from cache.\n").
-spec load_or_cache(binary(), nori@ref@types:ref_context()) -> {ok,
{taffy@value:yaml_value(), nori@ref@types:ref_context()}} |
{error, nori@ref@types:ref_error()}.
load_or_cache(Path, Ctx) ->
case gleam_stdlib:map_get(erlang:element(3, Ctx), Path) of
{ok, Cached} ->
{ok, {Cached, Ctx}};
{error, _} ->
case simplifile:read(Path) of
{error, _} ->
{error, {file_not_found, Path}};
{ok, Content} ->
case taffy:parse(Content) of
{error, Err} ->
{error, {parse_error, Path, erlang:element(2, Err)}};
{ok, Value} ->
New_ctx = {ref_context,
erlang:element(2, Ctx),
gleam@dict:insert(
erlang:element(3, Ctx),
Path,
Value
),
erlang:element(4, Ctx),
erlang:element(5, Ctx)},
{ok, {Value, New_ctx}}
end
end
end.
-file("src/nori/ref/resolver.gleam", 280).
?DOC(" Unescapes a JSON pointer segment: ~1 → /, ~0 → ~\n").
-spec unescape_pointer_segment(binary()) -> binary().
unescape_pointer_segment(Segment) ->
_pipe = Segment,
_pipe@1 = gleam@string:replace(_pipe, <<"~1"/utf8>>, <<"/"/utf8>>),
gleam@string:replace(_pipe@1, <<"~0"/utf8>>, <<"~"/utf8>>).
-file("src/nori/ref/resolver.gleam", 265).
?DOC(
" Parses a JSON pointer string into path segments.\n"
" E.g., \"/components/schemas/User\" → [\"components\", \"schemas\", \"User\"]\n"
" Handles ~0 (→ ~) and ~1 (→ /) escaping.\n"
).
-spec parse_json_pointer(binary()) -> list(binary()).
parse_json_pointer(Pointer) ->
case Pointer of
<<""/utf8>> ->
[];
<<"/"/utf8, Rest/binary>> ->
_pipe = Rest,
_pipe@1 = gleam@string:split(_pipe, <<"/"/utf8>>),
gleam@list:map(_pipe@1, fun unescape_pointer_segment/1);
_ ->
_pipe@2 = Pointer,
_pipe@3 = gleam@string:split(_pipe@2, <<"/"/utf8>>),
gleam@list:map(_pipe@3, fun unescape_pointer_segment/1)
end.
-file("src/nori/ref/resolver.gleam", 231).
?DOC(" Parses a $ref string into its components.\n").
-spec parse_ref(binary()) -> {ok, nori@ref@types:parsed_ref()} |
{error, nori@ref@types:ref_error()}.
parse_ref(Ref_str) ->
case Ref_str of
<<"#"/utf8, Rest/binary>> ->
Pointer = parse_json_pointer(Rest),
{ok, {local_ref, Pointer}};
_ ->
case gleam@string:split_once(Ref_str, <<"#"/utf8>>) of
{ok, {Path, Fragment}} ->
case Path of
<<""/utf8>> ->
{error, {invalid_ref_format, Ref_str}};
_ ->
Pointer@1 = parse_json_pointer(Fragment),
{ok, {file_ref_with_pointer, Path, Pointer@1}}
end;
{error, _} ->
case Ref_str of
<<""/utf8>> ->
{error, {invalid_ref_format, Ref_str}};
_ ->
{ok, {file_ref, Ref_str}}
end
end
end.
-file("src/nori/ref/resolver.gleam", 287).
?DOC(" Navigates a YamlValue tree using a JSON pointer path.\n").
-spec navigate_pointer(taffy@value:yaml_value(), list(binary())) -> gleam@option:option(taffy@value:yaml_value()).
navigate_pointer(Value, Pointer) ->
case Pointer of
[] ->
{some, Value};
[Segment | Rest] ->
case Value of
{mapping, Pairs} ->
case gleam@list:find(
Pairs,
fun(Pair) -> erlang:element(1, Pair) =:= Segment end
) of
{ok, {_, Child}} ->
navigate_pointer(Child, Rest);
{error, _} ->
none
end;
_ ->
none
end
end.
-file("src/nori/ref/resolver.gleam", 308).
?DOC(" Finds a $ref value in a mapping's pairs.\n").
-spec find_ref(list({binary(), taffy@value:yaml_value()})) -> gleam@option:option(binary()).
find_ref(Pairs) ->
case Pairs of
[] ->
none;
[{<<"$ref"/utf8>>, {string, Ref_str}} | _] ->
{some, Ref_str};
[_ | Rest] ->
find_ref(Rest)
end.
-file("src/nori/ref/resolver.gleam", 317).
?DOC(" Gets the directory part of a file path.\n").
-spec directory_of(binary()) -> binary().
directory_of(Path) ->
Parts = gleam@string:split(Path, <<"/"/utf8>>),
case lists:reverse(Parts) of
[_] ->
<<"."/utf8>>;
[_ | Rest] ->
_pipe = Rest,
_pipe@1 = lists:reverse(_pipe),
gleam@string:join(_pipe@1, <<"/"/utf8>>);
[] ->
<<"."/utf8>>
end.
-file("src/nori/ref/resolver.gleam", 327).
?DOC(" Resolves a relative path against a base directory.\n").
-spec resolve_path(binary(), binary()) -> binary().
resolve_path(Base_dir, Relative) ->
case gleam_stdlib:string_starts_with(Relative, <<"/"/utf8>>) of
true ->
Relative;
false ->
Relative@1 = case gleam_stdlib:string_starts_with(
Relative,
<<"./"/utf8>>
) of
true ->
gleam@string:drop_start(Relative, 2);
false ->
Relative
end,
<<<<Base_dir/binary, "/"/utf8>>/binary, Relative@1/binary>>
end.
-file("src/nori/ref/resolver.gleam", 77).
-spec resolve_mapping_pairs(
list({binary(), taffy@value:yaml_value()}),
list({binary(), taffy@value:yaml_value()}),
nori@ref@types:ref_context()
) -> {ok, {taffy@value:yaml_value(), nori@ref@types:ref_context()}} |
{error, nori@ref@types:ref_error()}.
resolve_mapping_pairs(Pairs, Acc, Ctx) ->
case Pairs of
[] ->
{ok, {{mapping, lists:reverse(Acc)}, Ctx}};
[{Key, Val} | Rest] ->
case resolve(Val, Ctx) of
{error, E} ->
{error, E};
{ok, {Resolved_val, New_ctx}} ->
resolve_mapping_pairs(
Rest,
[{Key, Resolved_val} | Acc],
New_ctx
)
end
end.
-file("src/nori/ref/resolver.gleam", 28).
?DOC(
" Resolves all $ref pointers in a YamlValue tree.\n"
"\n"
" Walks the tree recursively. When a mapping contains a \"$ref\" key,\n"
" the ref is resolved (loading files as needed) and the mapping is\n"
" replaced with the resolved value.\n"
).
-spec resolve(taffy@value:yaml_value(), nori@ref@types:ref_context()) -> {ok,
{taffy@value:yaml_value(), nori@ref@types:ref_context()}} |
{error, nori@ref@types:ref_error()}.
resolve(Value, Ctx) ->
case Value of
{mapping, Pairs} ->
resolve_mapping(Pairs, Ctx);
{sequence, Items} ->
resolve_sequence(Items, Ctx);
_ ->
{ok, {Value, Ctx}}
end.
-file("src/nori/ref/resolver.gleam", 63).
-spec resolve_mapping(
list({binary(), taffy@value:yaml_value()}),
nori@ref@types:ref_context()
) -> {ok, {taffy@value:yaml_value(), nori@ref@types:ref_context()}} |
{error, nori@ref@types:ref_error()}.
resolve_mapping(Pairs, Ctx) ->
case find_ref(Pairs) of
{some, Ref_str} ->
resolve_ref_string(Ref_str, Ctx);
none ->
resolve_mapping_pairs(Pairs, [], Ctx)
end.
-file("src/nori/ref/resolver.gleam", 40).
?DOC(" Convenience entry point: load a file and resolve all its $refs.\n").
-spec resolve_file(binary()) -> {ok,
{taffy@value:yaml_value(), nori@ref@types:ref_context()}} |
{error, nori@ref@types:ref_error()}.
resolve_file(Entry_path) ->
case simplifile:read(Entry_path) of
{error, _} ->
{error, {file_not_found, Entry_path}};
{ok, Content} ->
case taffy:parse(Content) of
{error, Err} ->
{error, {parse_error, Entry_path, erlang:element(2, Err)}};
{ok, Root} ->
Base_dir = directory_of(Entry_path),
Ctx = nori@ref@types:new_context(Base_dir, Root),
Ctx@1 = {ref_context,
erlang:element(2, Ctx),
gleam@dict:insert(
erlang:element(3, Ctx),
Entry_path,
Root
),
erlang:element(4, Ctx),
erlang:element(5, Ctx)},
resolve(Root, Ctx@1)
end
end.
-file("src/nori/ref/resolver.gleam", 101).
-spec resolve_sequence_items(
list(taffy@value:yaml_value()),
list(taffy@value:yaml_value()),
nori@ref@types:ref_context()
) -> {ok, {taffy@value:yaml_value(), nori@ref@types:ref_context()}} |
{error, nori@ref@types:ref_error()}.
resolve_sequence_items(Items, Acc, Ctx) ->
case Items of
[] ->
{ok, {{sequence, lists:reverse(Acc)}, Ctx}};
[Item | Rest] ->
case resolve(Item, Ctx) of
{error, E} ->
{error, E};
{ok, {Resolved, New_ctx}} ->
resolve_sequence_items(Rest, [Resolved | Acc], New_ctx)
end
end.
-file("src/nori/ref/resolver.gleam", 94).
-spec resolve_sequence(
list(taffy@value:yaml_value()),
nori@ref@types:ref_context()
) -> {ok, {taffy@value:yaml_value(), nori@ref@types:ref_context()}} |
{error, nori@ref@types:ref_error()}.
resolve_sequence(Items, Ctx) ->
resolve_sequence_items(Items, [], Ctx).
-file("src/nori/ref/resolver.gleam", 144).
?DOC(" Resolves a local ref (#/components/schemas/User) against the root document.\n").
-spec resolve_local_ref(list(binary()), binary(), nori@ref@types:ref_context()) -> {ok,
{taffy@value:yaml_value(), nori@ref@types:ref_context()}} |
{error, nori@ref@types:ref_error()}.
resolve_local_ref(Pointer, Ref_str, Ctx) ->
case navigate_pointer(erlang:element(5, Ctx), Pointer) of
{some, Value} ->
Ctx@1 = {ref_context,
erlang:element(2, Ctx),
erlang:element(3, Ctx),
gleam@set:delete(erlang:element(4, Ctx), Ref_str),
erlang:element(5, Ctx)},
resolve(Value, Ctx@1);
none ->
{error,
{ref_target_not_found,
Ref_str,
gleam@string:join(Pointer, <<"/"/utf8>>)}}
end.
-file("src/nori/ref/resolver.gleam", 160).
?DOC(" Resolves a file ref, optionally with a JSON pointer into the loaded file.\n").
-spec resolve_file_ref(
binary(),
list(binary()),
binary(),
nori@ref@types:ref_context()
) -> {ok, {taffy@value:yaml_value(), nori@ref@types:ref_context()}} |
{error, nori@ref@types:ref_error()}.
resolve_file_ref(Path, Pointer, Ref_str, Ctx) ->
Full_path = resolve_path(erlang:element(2, Ctx), Path),
case load_or_cache(Full_path, Ctx) of
{error, E} ->
{error, E};
{ok, {File_value, New_ctx}} ->
Target = case Pointer of
[] ->
{some, File_value};
_ ->
navigate_pointer(File_value, Pointer)
end,
case Target of
none ->
{error,
{ref_target_not_found,
Ref_str,
gleam@string:join(Pointer, <<"/"/utf8>>)}};
{some, Value} ->
File_base = directory_of(Full_path),
Resolve_ctx = {ref_context,
File_base,
erlang:element(3, New_ctx),
gleam@set:delete(erlang:element(4, New_ctx), Ref_str),
File_value},
case resolve(Value, Resolve_ctx) of
{error, E@1} ->
{error, E@1};
{ok, {Resolved, Final_ctx}} ->
Restored_ctx = {ref_context,
erlang:element(2, Ctx),
erlang:element(3, Final_ctx),
erlang:element(4, Final_ctx),
erlang:element(5, Ctx)},
{ok, {Resolved, Restored_ctx}}
end
end
end.
-file("src/nori/ref/resolver.gleam", 119).
?DOC(" Resolves a $ref string value.\n").
-spec resolve_ref_string(binary(), nori@ref@types:ref_context()) -> {ok,
{taffy@value:yaml_value(), nori@ref@types:ref_context()}} |
{error, nori@ref@types:ref_error()}.
resolve_ref_string(Ref_str, Ctx) ->
case gleam@set:contains(erlang:element(4, Ctx), Ref_str) of
true ->
{error,
{circular_reference, gleam@set:to_list(erlang:element(4, Ctx))}};
false ->
Ctx@1 = {ref_context,
erlang:element(2, Ctx),
erlang:element(3, Ctx),
gleam@set:insert(erlang:element(4, Ctx), Ref_str),
erlang:element(5, Ctx)},
case parse_ref(Ref_str) of
{error, E} ->
{error, E};
{ok, Parsed} ->
case Parsed of
{local_ref, Pointer} ->
resolve_local_ref(Pointer, Ref_str, Ctx@1);
{file_ref, Path} ->
resolve_file_ref(Path, [], Ref_str, Ctx@1);
{file_ref_with_pointer, Path@1, Pointer@1} ->
resolve_file_ref(Path@1, Pointer@1, Ref_str, Ctx@1)
end
end
end.