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
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

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
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

View file

@ -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

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).
"""
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 """

View file

@ -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

View file

@ -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<S> |> Custom.p(~I<O>)
{~I<foo>, ~I<bar>, ~I<baz>}
end
end).()
~I<S> |> Custom.p(~I<O>)
{~I<foo>, ~I<bar>, ~I<baz>}
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<http://example.com/base#S> |> EX.p(~I<http://example.com/base#O>),
base_iri: "http://example.com/base"