Wrap the build block definition in a function

This also fixes the undefined-function-warnings raised in the previous
version when using terms from non-strict vocabulary namespaces (incl.
the auto-generated ad-hoc vocabulary namespaces).
This commit is contained in:
Marcel Otto 2022-06-18 23:58:41 +02:00
parent eac696114f
commit 65bb0831b8
6 changed files with 182 additions and 137 deletions

View file

@ -53,7 +53,7 @@ jobs:
- run: mix compile --warnings-as-errors - run: mix compile --warnings-as-errors
if: ${{ matrix.lint }} if: ${{ matrix.lint }}
- run: MIX_ENV=test mix coveralls.github - run: MIX_ENV=test mix coveralls.github --warnings-as-errors
- name: Retrieve PLT Cache - name: Retrieve PLT Cache
uses: actions/cache@v1 uses: actions/cache@v1

View file

@ -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 - 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 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`. 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 - `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 which implement the `RDF.Data` protocol can be given also as the second argument
(previously custom structs with `RDF.Data` protocol implementations always (previously custom structs with `RDF.Data` protocol implementations always

View file

@ -342,5 +342,6 @@ defmodule RDF do
defdelegate __base_iri__(), to: RDF.NS.RDF defdelegate __base_iri__(), to: RDF.NS.RDF
defdelegate __terms__(), to: RDF.NS.RDF defdelegate __terms__(), to: RDF.NS.RDF
defdelegate __iris__(), to: RDF.NS.RDF defdelegate __iris__(), to: RDF.NS.RDF
defdelegate __strict__(), to: RDF.NS.RDF
defdelegate __resolve_term__(term), to: RDF.NS.RDF defdelegate __resolve_term__(term), to: RDF.NS.RDF
end end

View file

@ -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). 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 defmacro build(opts \\ [], do: block) do
Builder.build( Builder.build(block, __CALLER__, Builder.builder_mod(__CALLER__), opts)
block,
__CALLER__,
Builder.namespace_context_mod(__CALLER__),
opts
)
end end
@doc """ @doc """

View file

@ -18,40 +18,54 @@ defmodule RDF.Graph.Builder do
end end
def build(do_block, env, opts) do 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 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) 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) {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) base_string = base_string(base)
data = resolve_relative_iris(data, base_string) data = resolve_relative_iris(data, base_string)
declarations = resolve_relative_iris(declarations, 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} = mod_body =
extract_prefixes(declarations, env_aliases, namespace_context_mod, env) 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 quote do
alias RDF.XSD apply(unquote(builder_mod), :build, [unquote(opts)])
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)
)
end end
end end
def build(single, env, namespace_context_mod, opts) do def build(single, env, builder_mod, opts) do
build({:__block__, [], List.wrap(single)}, env, namespace_context_mod, opts) build({:__block__, [], List.wrap(single)}, env, builder_mod, opts)
end end
@doc false @doc false
@ -60,8 +74,8 @@ defmodule RDF.Graph.Builder do
|> Graph.add(Enum.filter(data, &rdf?/1)) |> Graph.add(Enum.filter(data, &rdf?/1))
end end
def namespace_context_mod(env) do def builder_mod(env) do
Module.concat(env.module, "GraphBuilderNS#{random_number()}") Module.concat(env.module, "GraphBuilder#{random_number()}")
end end
defp graph_opts(opts, prefixes, base) do defp graph_opts(opts, prefixes, base) do
@ -107,14 +121,11 @@ defmodule RDF.Graph.Builder do
end) end)
end end
defp extract_base(declarations, env_aliases) do defp extract_base(declarations) do
{base, declarations} = {base, declarations} =
Enum.reduce(declarations, {nil, []}, fn Enum.reduce(declarations, {nil, []}, fn
{:@, line, [{:base, _, [{:__aliases__, _, ns}] = aliases}]}, {_, declarations} -> {:@, line, [{:base, _, [{:__aliases__, _, ns}] = aliases}]}, {_, declarations} ->
{ {Module.concat(ns), [{:alias, line, aliases} | declarations]}
ns |> expand_module(env_aliases) |> Module.concat(),
[{:alias, line, aliases} | declarations]
}
{:@, _, [{:base, _, [base]}]}, {_, declarations} -> {:@, _, [{:base, _, [base]}]}, {_, declarations} ->
{base, declarations} {base, declarations}
@ -126,49 +137,53 @@ defmodule RDF.Graph.Builder do
{base, Enum.reverse(declarations)} {base, Enum.reverse(declarations)}
end end
defp extract_prefixes(declarations, env_aliases, namespace_context_mod, env) do defp extract_prefixes(declarations, builder_mod, env) do
{prefixes, declarations} = {prefixes, ad_hoc_ns, declarations} =
Enum.reduce(declarations, {[], []}, fn Enum.reduce(declarations, {[], [], []}, fn
{:@, line, [{:prefix, _, [{:__aliases__, _, ns}] = aliases}]}, {prefixes, declarations} -> {:@, 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] [{:alias, line, aliases} | declarations]
} }
{:@, line, [{:prefix, _, [[{prefix, {:__aliases__, _, ns} = aliases}]]}]}, {:@, 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] [{:alias, line, [aliases]} | declarations]
} }
{:@, line, [{:prefix, _, [[{prefix, uri}]]}]}, {prefixes, declarations} {:@, line, [{:prefix, _, [[{prefix, uri}]]}]}, {prefixes, ad_hoc_ns, declarations}
when is_binary(uri) -> 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] [{:alias, line, [{:__aliases__, line, ns}]} | declarations]
} }
{:@, _, [{:prefix, _, _}]} = expr, _ -> {:@, _, [{:prefix, _, _}]} = expr, _ ->
raise Error, "invalid @prefix expression:\n\t#{Macro.to_string(expr)}" raise Error, "invalid @prefix expression:\n\t#{Macro.to_string(expr)}"
declaration, {prefixes, declarations} -> declaration, {prefixes, ad_hoc_ns, declarations} ->
{prefixes, [declaration | declarations]} {prefixes, ad_hoc_ns, [declaration | declarations]}
end) end)
{prefixes, Enum.reverse(declarations)} {prefixes, ad_hoc_ns, Enum.reverse(declarations)}
end end
defp prefix(namespace, env_aliases) do defp prefix(namespace) do
namespace namespace
|> determine_prefix() |> determine_prefix()
|> prefix(namespace, env_aliases) |> prefix(namespace)
end end
defp prefix(prefix, namespace, env_aliases) do defp prefix(prefix, namespace) do
{prefix, namespace |> expand_module(env_aliases) |> Module.concat()} {prefix, Module.concat(namespace)}
end end
defp determine_prefix(namespace) do defp determine_prefix(namespace) do
@ -180,9 +195,9 @@ defmodule RDF.Graph.Builder do
|> String.to_atom() |> String.to_atom()
end end
defp ad_hoc_namespace(prefix, uri, namespace_context_mod, env) do defp ad_hoc_namespace(prefix, uri, builder_mod, env) do
{:module, module, _, _} = {:module, module, _, _} =
namespace_context_mod builder_mod
|> Module.concat(prefix |> Atom.to_string() |> Macro.camelize()) |> Module.concat(prefix |> Atom.to_string() |> Macro.camelize())
|> Vocabulary.Namespace.create!(uri, [], env, strict: false) |> Vocabulary.Namespace.create!(uri, [], env, strict: false)
@ -211,6 +226,19 @@ defmodule RDF.Graph.Builder do
raise Error, message: "invalid RDF data: #{inspect(invalid)}" raise Error, message: "invalid RDF data: #{inspect(invalid)}"
end 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 defp expand_module([first | rest] = module, env_aliases) do
if full = env_aliases[first] do if full = env_aliases[first] do
full ++ rest full ++ rest
@ -233,6 +261,23 @@ defmodule RDF.Graph.Builder do
String.to_atom(short) String.to_atom(short)
end 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 defp random_number do
:erlang.unique_integer([:positive]) :erlang.unique_integer([:positive])
end end

View file

@ -18,6 +18,7 @@ defmodule RDF.Graph.BuilderTest do
@compile {:no_warn_undefined, __MODULE__.TestNS.EX} @compile {:no_warn_undefined, __MODULE__.TestNS.EX}
@compile {:no_warn_undefined, __MODULE__.TestNS.Custom} @compile {:no_warn_undefined, __MODULE__.TestNS.Custom}
@compile {:no_warn_undefined, RDF.Test.Case.EX}
alias TestNS.EX alias TestNS.EX
alias RDF.NS alias RDF.NS
@ -317,51 +318,53 @@ defmodule RDF.Graph.BuilderTest do
end end
test "RDF.XSD is aliased" do test "RDF.XSD is aliased" do
# we're wrapping this in a function to isolate the alias
graph = graph =
(fn -> RDF.Graph.build do
RDF.Graph.build do EX.S |> EX.p(XSD.byte(42))
EX.S |> EX.p(XSD.byte(42)) end
end
end).()
assert graph == RDF.graph(EX.S |> EX.p(RDF.XSD.byte(42))) assert graph == RDF.graph(EX.S |> EX.p(RDF.XSD.byte(42)))
end end
test "default aliases" do test "default aliases" do
# we're wrapping this in a function to isolate the alias
graph = graph =
(fn -> RDF.Graph.build do
RDF.Graph.build do OWL.Class |> RDFS.subClassOf(RDFS.Class)
OWL.Class |> RDFS.subClassOf(RDFS.Class) end
end
end).()
assert graph == RDF.graph(NS.OWL.Class |> NS.RDFS.subClassOf(NS.RDFS.Class)) assert graph == RDF.graph(NS.OWL.Class |> NS.RDFS.subClassOf(NS.RDFS.Class))
end end
test "alias" do test "alias" do
# we're wrapping this in a function to isolate the alias
graph = graph =
(fn -> RDF.Graph.build do
RDF.Graph.build do alias TestNS.Custom
alias TestNS.Custom Custom.S |> Custom.p(Custom.O)
Custom.S |> Custom.p(Custom.O) end
end
end).()
assert graph == RDF.graph(TestNS.Custom.S |> TestNS.Custom.p(TestNS.Custom.O)) assert graph == RDF.graph(TestNS.Custom.S |> TestNS.Custom.p(TestNS.Custom.O))
end end
test "import" do test "aliasing an already taken name" do
# we're wrapping this in a function to isolate the import
graph = graph =
(fn -> RDF.Graph.build do
RDF.Graph.build do alias RDF.Test.Case.EX, as: EX2
import TestNS.ImportTest {EX2.S, EX.p(), EX2.foo()}
EX.S |> foo(TestNS.ImportTest.Bar) end
end
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)) assert graph == RDF.graph(EX.S |> TestNS.ImportTest.foo(TestNS.ImportTest.Bar))
end end
@ -394,15 +397,12 @@ defmodule RDF.Graph.BuilderTest do
describe "@prefix" do describe "@prefix" do
test "for vocabulary namespace with explicit prefix" do test "for vocabulary namespace with explicit prefix" do
# we're wrapping this in a function to isolate the alias
graph = graph =
(fn -> RDF.Graph.build do
RDF.Graph.build do @prefix cust: TestNS.Custom
@prefix cust: TestNS.Custom
Custom.S |> Custom.p(Custom.O) Custom.S |> Custom.p(Custom.O)
end end
end).()
assert graph == assert graph ==
RDF.graph(TestNS.Custom.S |> TestNS.Custom.p(TestNS.Custom.O), RDF.graph(TestNS.Custom.S |> TestNS.Custom.p(TestNS.Custom.O),
@ -411,15 +411,12 @@ defmodule RDF.Graph.BuilderTest do
end end
test "for vocabulary namespace with auto-generated prefix" do test "for vocabulary namespace with auto-generated prefix" do
# we're wrapping this in a function to isolate the alias
graph = graph =
(fn -> RDF.Graph.build do
RDF.Graph.build do @prefix TestNS.Custom
@prefix TestNS.Custom
Custom.S |> Custom.p(Custom.O) Custom.S |> Custom.p(Custom.O)
end end
end).()
assert graph == assert graph ==
RDF.graph(TestNS.Custom.S |> TestNS.Custom.p(TestNS.Custom.O), RDF.graph(TestNS.Custom.S |> TestNS.Custom.p(TestNS.Custom.O),
@ -428,15 +425,12 @@ defmodule RDF.Graph.BuilderTest do
end end
test "ad-hoc vocabulary namespace for URIs given as string" do test "ad-hoc vocabulary namespace for URIs given as string" do
# we're wrapping this in a function to isolate the alias
graph = graph =
(fn -> RDF.Graph.build do
RDF.Graph.build do @prefix ad: "http://example.com/ad-hoc/"
@prefix ad: "http://example.com/ad-hoc/"
Ad.S |> Ad.p(Ad.O) Ad.S |> Ad.p(Ad.O)
end end
end).()
assert graph == assert graph ==
RDF.graph( RDF.graph(
@ -450,35 +444,41 @@ defmodule RDF.Graph.BuilderTest do
end end
test "two ad-hoc vocabulary namespaces for the same URI in the same context" do 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 graph1 =
graph = RDF.Graph.build do
(fn -> @prefix ad: "http://example.com/ad-hoc/"
graph1 = @prefix ex1: "http://example.com/ad-hoc/ex1#"
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) Ad.S |> Ad.p(Ex1.O)
end end
RDF.Graph.build do graph2 =
@prefix ad: "http://example.com/ad-hoc/" RDF.Graph.build do
@prefix ex2: "http://example.com/ad-hoc/ex2#" @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) assert graph1 ==
end
end).()
assert graph ==
RDF.graph( RDF.graph(
[ [
{ {
RDF.iri("http://example.com/ad-hoc/S"), RDF.iri("http://example.com/ad-hoc/S"),
RDF.iri("http://example.com/ad-hoc/p"), RDF.iri("http://example.com/ad-hoc/p"),
RDF.iri("http://example.com/ad-hoc/ex1#O") 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/S"),
RDF.iri("http://example.com/ad-hoc/p"), RDF.iri("http://example.com/ad-hoc/p"),
@ -488,22 +488,18 @@ defmodule RDF.Graph.BuilderTest do
prefixes: prefixes:
RDF.default_prefixes( RDF.default_prefixes(
ad: "http://example.com/ad-hoc/", ad: "http://example.com/ad-hoc/",
ex1: "http://example.com/ad-hoc/ex1#",
ex2: "http://example.com/ad-hoc/ex2#" ex2: "http://example.com/ad-hoc/ex2#"
) )
) )
end end
test "merge with prefixes opt" do test "merge with prefixes opt" do
# we're wrapping this in a function to isolate the alias
graph = graph =
(fn -> RDF.Graph.build prefixes: [custom: EX] do
RDF.Graph.build prefixes: [custom: EX] do @prefix custom: TestNS.Custom
@prefix custom: TestNS.Custom
Custom.S |> Custom.p(Custom.O) Custom.S |> Custom.p(Custom.O)
end end
end).()
assert graph == assert graph ==
RDF.graph(TestNS.Custom.S |> TestNS.Custom.p(TestNS.Custom.O), 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 test "with vocabulary namespace" do
import RDF.Sigils import RDF.Sigils
# we're wrapping this in a function to isolate the alias
graph = graph =
(fn -> RDF.Graph.build do
RDF.Graph.build do @base TestNS.Custom
@base TestNS.Custom
~I<S> |> Custom.p(~I<O>) ~I<S> |> Custom.p(~I<O>)
{~I<foo>, ~I<bar>, ~I<baz>} {~I<foo>, ~I<bar>, ~I<baz>}
end end
end).()
assert graph == assert graph ==
RDF.graph( RDF.graph(
@ -546,6 +539,8 @@ defmodule RDF.Graph.BuilderTest do
{~I<#foo>, ~I<#bar>, ~I<#baz>} {~I<#foo>, ~I<#bar>, ~I<#baz>}
end end
import RDF.Sigils
assert graph == assert graph ==
RDF.graph( RDF.graph(
[ [
@ -566,6 +561,8 @@ defmodule RDF.Graph.BuilderTest do
{~I<#foo>, ~I<#bar>, ~I<#baz>} {~I<#foo>, ~I<#bar>, ~I<#baz>}
end end
import RDF.Sigils
assert graph == assert graph ==
RDF.graph( RDF.graph(
[ [
@ -585,6 +582,8 @@ defmodule RDF.Graph.BuilderTest do
~I<#S> |> EX.p(~I<#O>) ~I<#S> |> EX.p(~I<#O>)
end end
import RDF.Sigils
assert graph == assert graph ==
RDF.graph(~I<http://example.com/base#S> |> EX.p(~I<http://example.com/base#O>), RDF.graph(~I<http://example.com/base#S> |> EX.p(~I<http://example.com/base#O>),
base_iri: "http://example.com/base" base_iri: "http://example.com/base"