diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9e741b5..bb5dc6b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,7 +53,7 @@ jobs: - run: mix compile --warnings-as-errors if: ${{ matrix.lint }} - - run: MIX_ENV=test mix coveralls.github + - run: MIX_ENV=test mix coveralls.github --warnings-as-errors - name: Retrieve PLT Cache uses: actions/cache@v1 diff --git a/CHANGELOG.md b/CHANGELOG.md index e11755f..eea3499 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,11 @@ The generated namespaces are much more flexible now and compile faster. - When defining an alias for a term of vocabulary which would be invalid as an Elixir term, the original term is now implicitly ignored and won't any longer be returned by the `__terms__/0` function of a `RDF.Vocabulary.Namespace`. +- `RDF.Graph.build/2` blocks are now wrapped in a function, so the aliases and + import no longer affect the caller context. `alias`es in the caller context are + still available in the build block, but `import`s not and must be reimported in + the build block. Variables in the caller context are also no longer available + build block. - `RDF.Data.merge/2` and `RDF.Data.equal?/2` are now commutative, i.e. structs which implement the `RDF.Data` protocol can be given also as the second argument (previously custom structs with `RDF.Data` protocol implementations always diff --git a/lib/rdf.ex b/lib/rdf.ex index 0cf2fd7..dafc91d 100644 --- a/lib/rdf.ex +++ b/lib/rdf.ex @@ -342,5 +342,6 @@ defmodule RDF do defdelegate __base_iri__(), to: RDF.NS.RDF defdelegate __terms__(), to: RDF.NS.RDF defdelegate __iris__(), to: RDF.NS.RDF + defdelegate __strict__(), to: RDF.NS.RDF defdelegate __resolve_term__(term), to: RDF.NS.RDF end diff --git a/lib/rdf/graph.ex b/lib/rdf/graph.ex index 7717cf7..0a1c212 100644 --- a/lib/rdf/graph.ex +++ b/lib/rdf/graph.ex @@ -145,12 +145,7 @@ defmodule RDF.Graph do For a description of the DSL see [this guide](https://rdf-elixir.dev/rdf-ex/description-and-graph-dsl.html). """ defmacro build(opts \\ [], do: block) do - Builder.build( - block, - __CALLER__, - Builder.namespace_context_mod(__CALLER__), - opts - ) + Builder.build(block, __CALLER__, Builder.builder_mod(__CALLER__), opts) end @doc """ diff --git a/lib/rdf/graph_builder.ex b/lib/rdf/graph_builder.ex index f4a06db..2180fb3 100644 --- a/lib/rdf/graph_builder.ex +++ b/lib/rdf/graph_builder.ex @@ -18,40 +18,54 @@ defmodule RDF.Graph.Builder do end def build(do_block, env, opts) do - build(do_block, env, namespace_context_mod(env), opts) + build(do_block, env, builder_mod(env), opts) end - def build({:__block__, _, block}, env, namespace_context_mod, opts) do + def build({:__block__, _, block}, env, builder_mod, opts) do env_aliases = env_aliases(env) + block = expand_aliased_modules(block, env_aliases) + non_strict_ns = extract_non_strict_ns(block) {declarations, data} = Enum.split_with(block, &declaration?/1) - {base, declarations} = extract_base(declarations, env_aliases) + {base, declarations} = extract_base(declarations) base_string = base_string(base) data = resolve_relative_iris(data, base_string) declarations = resolve_relative_iris(declarations, base_string) + {prefixes, ad_hoc_ns, declarations} = extract_prefixes(declarations, builder_mod, env) + non_strict_ns = (non_strict_ns ++ ad_hoc_ns) |> Enum.uniq() - {prefixes, declarations} = - extract_prefixes(declarations, env_aliases, namespace_context_mod, env) + mod_body = + quote do + for mod <- unquote(non_strict_ns) do + @compile {:no_warn_undefined, mod} + end + + def build(opts) do + alias RDF.XSD + alias RDF.NS.{RDFS, OWL} + + import RDF.Sigils + import Helper + + unquote(declarations) + + RDF.Graph.Builder.do_build( + unquote(data), + opts, + unquote(prefixes), + unquote(base_string) + ) + end + end + + Module.create(builder_mod, mod_body, Macro.Env.location(env)) quote do - alias RDF.XSD - alias RDF.NS.{RDFS, OWL} - - import RDF.Sigils - import Helper - - unquote(declarations) - - RDF.Graph.Builder.do_build( - unquote(data), - unquote(opts), - unquote(prefixes), - unquote(base_string) - ) + apply(unquote(builder_mod), :build, [unquote(opts)]) end end - def build(single, env, namespace_context_mod, opts) do - build({:__block__, [], List.wrap(single)}, env, namespace_context_mod, opts) + def build(single, env, builder_mod, opts) do + build({:__block__, [], List.wrap(single)}, env, builder_mod, opts) end @doc false @@ -60,8 +74,8 @@ defmodule RDF.Graph.Builder do |> Graph.add(Enum.filter(data, &rdf?/1)) end - def namespace_context_mod(env) do - Module.concat(env.module, "GraphBuilderNS#{random_number()}") + def builder_mod(env) do + Module.concat(env.module, "GraphBuilder#{random_number()}") end defp graph_opts(opts, prefixes, base) do @@ -107,14 +121,11 @@ defmodule RDF.Graph.Builder do end) end - defp extract_base(declarations, env_aliases) do + defp extract_base(declarations) do {base, declarations} = Enum.reduce(declarations, {nil, []}, fn {:@, line, [{:base, _, [{:__aliases__, _, ns}] = aliases}]}, {_, declarations} -> - { - ns |> expand_module(env_aliases) |> Module.concat(), - [{:alias, line, aliases} | declarations] - } + {Module.concat(ns), [{:alias, line, aliases} | declarations]} {:@, _, [{:base, _, [base]}]}, {_, declarations} -> {base, declarations} @@ -126,49 +137,53 @@ defmodule RDF.Graph.Builder do {base, Enum.reverse(declarations)} end - defp extract_prefixes(declarations, env_aliases, namespace_context_mod, env) do - {prefixes, declarations} = - Enum.reduce(declarations, {[], []}, fn - {:@, line, [{:prefix, _, [{:__aliases__, _, ns}] = aliases}]}, {prefixes, declarations} -> + defp extract_prefixes(declarations, builder_mod, env) do + {prefixes, ad_hoc_ns, declarations} = + Enum.reduce(declarations, {[], [], []}, fn + {:@, line, [{:prefix, _, [{:__aliases__, _, ns}] = aliases}]}, + {prefixes, ad_hoc_ns, declarations} -> { - [prefix(ns, env_aliases) | prefixes], + [prefix(ns) | prefixes], + ad_hoc_ns, [{:alias, line, aliases} | declarations] } {:@, line, [{:prefix, _, [[{prefix, {:__aliases__, _, ns} = aliases}]]}]}, - {prefixes, declarations} -> + {prefixes, ad_hoc_ns, declarations} -> { - [prefix(prefix, ns, env_aliases) | prefixes], + [prefix(prefix, ns) | prefixes], + ad_hoc_ns, [{:alias, line, [aliases]} | declarations] } - {:@, line, [{:prefix, _, [[{prefix, uri}]]}]}, {prefixes, declarations} + {:@, line, [{:prefix, _, [[{prefix, uri}]]}]}, {prefixes, ad_hoc_ns, declarations} when is_binary(uri) -> - ns = ad_hoc_namespace(prefix, uri, namespace_context_mod, env) + ns = ad_hoc_namespace(prefix, uri, builder_mod, env) { - [prefix(prefix, ns, env_aliases) | prefixes], + [prefix(prefix, ns) | prefixes], + [Module.concat(ns) | ad_hoc_ns], [{:alias, line, [{:__aliases__, line, ns}]} | declarations] } {:@, _, [{:prefix, _, _}]} = expr, _ -> raise Error, "invalid @prefix expression:\n\t#{Macro.to_string(expr)}" - declaration, {prefixes, declarations} -> - {prefixes, [declaration | declarations]} + declaration, {prefixes, ad_hoc_ns, declarations} -> + {prefixes, ad_hoc_ns, [declaration | declarations]} end) - {prefixes, Enum.reverse(declarations)} + {prefixes, ad_hoc_ns, Enum.reverse(declarations)} end - defp prefix(namespace, env_aliases) do + defp prefix(namespace) do namespace |> determine_prefix() - |> prefix(namespace, env_aliases) + |> prefix(namespace) end - defp prefix(prefix, namespace, env_aliases) do - {prefix, namespace |> expand_module(env_aliases) |> Module.concat()} + defp prefix(prefix, namespace) do + {prefix, Module.concat(namespace)} end defp determine_prefix(namespace) do @@ -180,9 +195,9 @@ defmodule RDF.Graph.Builder do |> String.to_atom() end - defp ad_hoc_namespace(prefix, uri, namespace_context_mod, env) do + defp ad_hoc_namespace(prefix, uri, builder_mod, env) do {:module, module, _, _} = - namespace_context_mod + builder_mod |> Module.concat(prefix |> Atom.to_string() |> Macro.camelize()) |> Vocabulary.Namespace.create!(uri, [], env, strict: false) @@ -211,6 +226,19 @@ defmodule RDF.Graph.Builder do raise Error, message: "invalid RDF data: #{inspect(invalid)}" end + defp expand_aliased_modules(ast, env_aliases) do + Macro.prewalk(ast, fn + {:__aliases__, [alias: false], _} = alias -> + alias + + {:__aliases__, _, module} -> + {:__aliases__, [alias: false], expand_module(module, env_aliases)} + + other -> + other + end) + end + defp expand_module([first | rest] = module, env_aliases) do if full = env_aliases[first] do full ++ rest @@ -233,6 +261,23 @@ defmodule RDF.Graph.Builder do String.to_atom(short) end + defp extract_non_strict_ns(block) do + modules = + block + |> Macro.prewalker() + |> Enum.reduce([], fn + {:__aliases__, _, mod}, modules -> [Module.concat(mod) | modules] + _, modules -> modules + end) + |> Enum.uniq() + + for module <- modules, non_strict_vocab_namespace?(module), do: module + end + + defp non_strict_vocab_namespace?(mod) do + Vocabulary.Namespace.vocabulary_namespace?(mod) and not mod.__strict__() + end + defp random_number do :erlang.unique_integer([:positive]) end diff --git a/test/unit/graph_builder_test.exs b/test/unit/graph_builder_test.exs index f175127..a733a13 100644 --- a/test/unit/graph_builder_test.exs +++ b/test/unit/graph_builder_test.exs @@ -18,6 +18,7 @@ defmodule RDF.Graph.BuilderTest do @compile {:no_warn_undefined, __MODULE__.TestNS.EX} @compile {:no_warn_undefined, __MODULE__.TestNS.Custom} + @compile {:no_warn_undefined, RDF.Test.Case.EX} alias TestNS.EX alias RDF.NS @@ -317,51 +318,53 @@ defmodule RDF.Graph.BuilderTest do end test "RDF.XSD is aliased" do - # we're wrapping this in a function to isolate the alias graph = - (fn -> - RDF.Graph.build do - EX.S |> EX.p(XSD.byte(42)) - end - end).() + RDF.Graph.build do + EX.S |> EX.p(XSD.byte(42)) + end assert graph == RDF.graph(EX.S |> EX.p(RDF.XSD.byte(42))) end test "default aliases" do - # we're wrapping this in a function to isolate the alias graph = - (fn -> - RDF.Graph.build do - OWL.Class |> RDFS.subClassOf(RDFS.Class) - end - end).() + RDF.Graph.build do + OWL.Class |> RDFS.subClassOf(RDFS.Class) + end assert graph == RDF.graph(NS.OWL.Class |> NS.RDFS.subClassOf(NS.RDFS.Class)) end test "alias" do - # we're wrapping this in a function to isolate the alias graph = - (fn -> - RDF.Graph.build do - alias TestNS.Custom - Custom.S |> Custom.p(Custom.O) - end - end).() + RDF.Graph.build do + alias TestNS.Custom + Custom.S |> Custom.p(Custom.O) + end assert graph == RDF.graph(TestNS.Custom.S |> TestNS.Custom.p(TestNS.Custom.O)) end - test "import" do - # we're wrapping this in a function to isolate the import + test "aliasing an already taken name" do graph = - (fn -> - RDF.Graph.build do - import TestNS.ImportTest - EX.S |> foo(TestNS.ImportTest.Bar) - end - end).() + RDF.Graph.build do + alias RDF.Test.Case.EX, as: EX2 + {EX2.S, EX.p(), EX2.foo()} + end + + quote do + alias RDF.Test.Case.EX, as: EX2 + end + + assert graph == RDF.graph(RDF.Test.Case.EX.S |> EX.p(RDF.Test.Case.EX.foo())) + end + + test "import" do + graph = + RDF.Graph.build do + import TestNS.ImportTest + EX.S |> foo(TestNS.ImportTest.Bar) + end assert graph == RDF.graph(EX.S |> TestNS.ImportTest.foo(TestNS.ImportTest.Bar)) end @@ -394,15 +397,12 @@ defmodule RDF.Graph.BuilderTest do describe "@prefix" do test "for vocabulary namespace with explicit prefix" do - # we're wrapping this in a function to isolate the alias graph = - (fn -> - RDF.Graph.build do - @prefix cust: TestNS.Custom + RDF.Graph.build do + @prefix cust: TestNS.Custom - Custom.S |> Custom.p(Custom.O) - end - end).() + Custom.S |> Custom.p(Custom.O) + end assert graph == RDF.graph(TestNS.Custom.S |> TestNS.Custom.p(TestNS.Custom.O), @@ -411,15 +411,12 @@ defmodule RDF.Graph.BuilderTest do end test "for vocabulary namespace with auto-generated prefix" do - # we're wrapping this in a function to isolate the alias graph = - (fn -> - RDF.Graph.build do - @prefix TestNS.Custom + RDF.Graph.build do + @prefix TestNS.Custom - Custom.S |> Custom.p(Custom.O) - end - end).() + Custom.S |> Custom.p(Custom.O) + end assert graph == RDF.graph(TestNS.Custom.S |> TestNS.Custom.p(TestNS.Custom.O), @@ -428,15 +425,12 @@ defmodule RDF.Graph.BuilderTest do end test "ad-hoc vocabulary namespace for URIs given as string" do - # we're wrapping this in a function to isolate the alias graph = - (fn -> - RDF.Graph.build do - @prefix ad: "http://example.com/ad-hoc/" + RDF.Graph.build do + @prefix ad: "http://example.com/ad-hoc/" - Ad.S |> Ad.p(Ad.O) - end - end).() + Ad.S |> Ad.p(Ad.O) + end assert graph == RDF.graph( @@ -450,35 +444,41 @@ defmodule RDF.Graph.BuilderTest do end test "two ad-hoc vocabulary namespaces for the same URI in the same context" do - # we're wrapping this in a function to isolate the alias - graph = - (fn -> - graph1 = - RDF.Graph.build do - @prefix ad: "http://example.com/ad-hoc/" - @prefix ex1: "http://example.com/ad-hoc/ex1#" + graph1 = + RDF.Graph.build do + @prefix ad: "http://example.com/ad-hoc/" + @prefix ex1: "http://example.com/ad-hoc/ex1#" - Ad.S |> Ad.p(Ex1.O) - end + Ad.S |> Ad.p(Ex1.O) + end - RDF.Graph.build do - @prefix ad: "http://example.com/ad-hoc/" - @prefix ex2: "http://example.com/ad-hoc/ex2#" + graph2 = + RDF.Graph.build do + @prefix ad: "http://example.com/ad-hoc/" + @prefix ex2: "http://example.com/ad-hoc/ex2#" - graph1 + Ad.S |> Ad.p(Ex2.O) + end - Ad.S |> Ad.p(Ex2.O) - end - end).() - - assert graph == + assert graph1 == RDF.graph( [ { RDF.iri("http://example.com/ad-hoc/S"), RDF.iri("http://example.com/ad-hoc/p"), RDF.iri("http://example.com/ad-hoc/ex1#O") - }, + } + ], + prefixes: + RDF.default_prefixes( + ad: "http://example.com/ad-hoc/", + ex1: "http://example.com/ad-hoc/ex1#" + ) + ) + + assert graph2 == + RDF.graph( + [ { RDF.iri("http://example.com/ad-hoc/S"), RDF.iri("http://example.com/ad-hoc/p"), @@ -488,22 +488,18 @@ defmodule RDF.Graph.BuilderTest do prefixes: RDF.default_prefixes( ad: "http://example.com/ad-hoc/", - ex1: "http://example.com/ad-hoc/ex1#", ex2: "http://example.com/ad-hoc/ex2#" ) ) end test "merge with prefixes opt" do - # we're wrapping this in a function to isolate the alias graph = - (fn -> - RDF.Graph.build prefixes: [custom: EX] do - @prefix custom: TestNS.Custom + RDF.Graph.build prefixes: [custom: EX] do + @prefix custom: TestNS.Custom - Custom.S |> Custom.p(Custom.O) - end - end).() + Custom.S |> Custom.p(Custom.O) + end assert graph == RDF.graph(TestNS.Custom.S |> TestNS.Custom.p(TestNS.Custom.O), @@ -516,16 +512,13 @@ defmodule RDF.Graph.BuilderTest do test "with vocabulary namespace" do import RDF.Sigils - # we're wrapping this in a function to isolate the alias graph = - (fn -> - RDF.Graph.build do - @base TestNS.Custom + RDF.Graph.build do + @base TestNS.Custom - ~I |> Custom.p(~I) - {~I, ~I, ~I} - end - end).() + ~I |> Custom.p(~I) + {~I, ~I, ~I} + end assert graph == RDF.graph( @@ -546,6 +539,8 @@ defmodule RDF.Graph.BuilderTest do {~I<#foo>, ~I<#bar>, ~I<#baz>} end + import RDF.Sigils + assert graph == RDF.graph( [ @@ -566,6 +561,8 @@ defmodule RDF.Graph.BuilderTest do {~I<#foo>, ~I<#bar>, ~I<#baz>} end + import RDF.Sigils + assert graph == RDF.graph( [ @@ -585,6 +582,8 @@ defmodule RDF.Graph.BuilderTest do ~I<#S> |> EX.p(~I<#O>) end + import RDF.Sigils + assert graph == RDF.graph(~I |> EX.p(~I), base_iri: "http://example.com/base"