diff --git a/VERSIONS b/VERSIONS index ade2e24..747c467 100644 --- a/VERSIONS +++ b/VERSIONS @@ -54,4 +54,9 @@ - deprecates `:content_type` option and renames it `:encoding` # 1.1.1 -- updates elixir versioning requirements \ No newline at end of file +- updates elixir versioning requirements + +# 1.1.2 +- adds extra message when the reference pointer is not found +- fixes bug where references are not recursively cached when the entrypoint + isn't the root entrypoint \ No newline at end of file diff --git a/lib/exonerate.ex b/lib/exonerate.ex index 4e5560a..8504cae 100644 --- a/lib/exonerate.ex +++ b/lib/exonerate.ex @@ -510,6 +510,7 @@ defmodule Exonerate do if id = id_from(schema) do resource = id Cache.put_schema(caller.module, resource, schema) + resource else resource_uri diff --git a/lib/exonerate/cache.ex b/lib/exonerate/cache.ex index 9891b29..190960c 100644 --- a/lib/exonerate/cache.ex +++ b/lib/exonerate/cache.ex @@ -161,6 +161,11 @@ defmodule Exonerate.Cache do put_schema(module, resource_uri, new_schema) :ok + rescue + KeyError -> + raise CompileError, + description: + "reference points to: #{JsonPtr.to_uri(pointer)}, this location not found in the schema" end @spec has_schema?(module, resource_uri :: String.t()) :: boolean @@ -178,6 +183,10 @@ defmodule Exonerate.Cache do {tgt_resource, tgt_pointer} end + defmatchspecp get_ref_pointers_ms(module, tgt_resource) do + {{:ref, {^module, ^tgt_resource, _}}, {^module, ^tgt_resource, tgt_pointer}} -> tgt_pointer + end + def register_ref(module, ref_resource, ref_pointer, tgt_resource, tgt_pointer) do :ets.insert( get_table(), @@ -189,11 +198,17 @@ defmodule Exonerate.Cache do def traverse_ref!(module, ref_resource, ref_pointer) do case :ets.select(get_table(), get_ref_ms(module, ref_resource, ref_pointer)) do - [] -> raise "ref not found" + [] -> raise "ref at #{JsonPtr.to_uri(ref_pointer)} not found" [ref] -> ref end end + def all_ref_pointers(module, ref_resource) do + get_table() + |> :ets.select(get_ref_pointers_ms(module, ref_resource)) + |> MapSet.new() + end + # CONTEXTS def register_context(module, call) when is_atom(call) do @@ -212,7 +227,6 @@ defmodule Exonerate.Cache do end end - require MatchSpec @all MatchSpec.fun2ms(fn any -> any end) def dump do :ets.select(get_table(), @all) diff --git a/lib/exonerate/schema.ex b/lib/exonerate/schema.ex index 140b5b4..b8deec8 100644 --- a/lib/exonerate/schema.ex +++ b/lib/exonerate/schema.ex @@ -17,8 +17,26 @@ defmodule Exonerate.Schema do json_decoded |> Degeneracy.canonicalize(opts) |> tap(&Cache.put_schema(caller.module, resource, &1)) + |> cache_assignments(caller, resource, opts) + end + + defp cache_assignments(schema, caller, resource, opts, seen \\ MapSet.new()) do + schema |> Id.prescan(caller.module, resource, opts) |> ref_prescan(caller, resource, opts) + + # recursively jump into references and go ahead run cache assignments on them. + caller.module + |> Cache.all_ref_pointers(resource) + |> Enum.reject(&(&1 in seen)) + |> Enum.reduce(seen, fn pointer, seen_so_far -> + new_seen = MapSet.put(seen_so_far, pointer) + new_opts = Keyword.replace(opts, :entrypoint, JsonPtr.to_path(pointer)) + cache_assignments(schema, caller, resource, new_opts, new_seen) + Cache.all_ref_pointers(caller.module, resource) + end) + + schema end @doc """ diff --git a/mix.exs b/mix.exs index 699ba08..86b4b25 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Exonerate.MixProject do def project do [ app: :exonerate, - version: "1.1.1", + version: "1.1.2", elixir: "~> 1.14", start_permanent: Mix.env() == :prod, deps: deps(), diff --git a/test/errors/ref_not_found_root.exs b/test/errors/ref_not_found_root.exs new file mode 100644 index 0000000..ffcbfd6 --- /dev/null +++ b/test/errors/ref_not_found_root.exs @@ -0,0 +1,15 @@ +defmodule ExonerateTest.RefNotFoundRoot do + require Exonerate + + Exonerate.function_from_string( + :defp, + :yaml, + """ + type: object + parameters: + foo: + $ref: "#/definitions/foo" + """, + encoding: "application/yaml" + ) +end diff --git a/test/errors/ref_not_found_test.exs b/test/errors/ref_not_found_test.exs new file mode 100644 index 0000000..fcae623 --- /dev/null +++ b/test/errors/ref_not_found_test.exs @@ -0,0 +1,13 @@ +defmodule ExonerateTest.RefNotFoundTest do + use ExUnit.Case, async: true + + test "if there's a missing ref, we get a CompileError" do + assert_raise CompileError, + "reference points to: #/definitions/foo, this location not found in the schema", + fn -> + __DIR__ + |> Path.join("ref_not_found_root.exs") + |> Code.compile_file() + end + end +end diff --git a/test/format_test.exs b/test/format_test.exs index 5a2fdb1..5573a91 100644 --- a/test/format_test.exs +++ b/test/format_test.exs @@ -1,6 +1,5 @@ defmodule ExonerateTest.FormatTest do use ExUnit.Case, async: true - require Exonerate describe "builtin formats:" do diff --git a/test/regression_test.exs b/test/regression/general_test.exs similarity index 100% rename from test/regression_test.exs rename to test/regression/general_test.exs diff --git a/test/regression/nested_ref_test.exs b/test/regression/nested_ref_test.exs new file mode 100644 index 0000000..298d806 --- /dev/null +++ b/test/regression/nested_ref_test.exs @@ -0,0 +1,25 @@ +defmodule ExonerateTest.Regression.NestedRefTest do + require Exonerate + + # combining an entrypoint with a $ref fails. + Exonerate.function_from_string( + :def, + :ref_trace_entrypoint, + ~S""" + schema: + type: object + properties: + bar: + $ref: '#/one' + one: + type: object + properties: + foo: + $ref: '#/two' + two: + type: string + """, + encoding: "application/yaml", + entrypoint: "/schema" + ) +end