Creation of ad-hoc namespaces in RDF.Graph.build/2

This commit is contained in:
Marcel Otto 2022-06-13 03:02:21 +02:00
parent e9fd42430b
commit eac696114f
4 changed files with 126 additions and 11 deletions

View file

@ -17,6 +17,8 @@ The generated namespaces are much more flexible now and compile faster.
- macro `RDF.Namespace.IRI.iri/1` which allows to resolve `RDF.Namespace` terms
inside of pattern matches
- `RDF.IRI.starts_with?/2` and `RDF.IRI.ends_with?/2`
- `RDF.Graph.build/2` now supports the creation of ad-hoc vocabulary namespaces
with a `@prefix` declaration providing the URI of the namespace as a string
### Changed

View file

@ -145,7 +145,12 @@ 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__, opts)
Builder.build(
block,
__CALLER__,
Builder.namespace_context_mod(__CALLER__),
opts
)
end
@doc """

View file

@ -1,5 +1,6 @@
defmodule RDF.Graph.Builder do
alias RDF.{Description, Graph, Dataset, PrefixMap, IRI}
@moduledoc false
alias RDF.{Description, Graph, Dataset, PrefixMap, IRI, Vocabulary}
defmodule Error do
defexception [:message]
@ -16,14 +17,20 @@ defmodule RDF.Graph.Builder do
def exclude(_), do: nil
end
def build({:__block__, _, block}, env, opts) do
def build(do_block, env, opts) do
build(do_block, env, namespace_context_mod(env), opts)
end
def build({:__block__, _, block}, env, namespace_context_mod, opts) do
env_aliases = env_aliases(env)
{declarations, data} = Enum.split_with(block, &declaration?/1)
{base, declarations} = extract_base(declarations, env_aliases)
base_string = base_string(base)
data = resolve_relative_iris(data, base_string)
declarations = resolve_relative_iris(declarations, base_string)
{prefixes, declarations} = extract_prefixes(declarations, env_aliases)
{prefixes, declarations} =
extract_prefixes(declarations, env_aliases, namespace_context_mod, env)
quote do
alias RDF.XSD
@ -43,8 +50,8 @@ defmodule RDF.Graph.Builder do
end
end
def build(single, env, opts) do
build({:__block__, [], List.wrap(single)}, env, opts)
def build(single, env, namespace_context_mod, opts) do
build({:__block__, [], List.wrap(single)}, env, namespace_context_mod, opts)
end
@doc false
@ -53,6 +60,10 @@ 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()}")
end
defp graph_opts(opts, prefixes, base) do
opts
|> set_base_opt(base)
@ -115,16 +126,33 @@ defmodule RDF.Graph.Builder do
{base, Enum.reverse(declarations)}
end
defp extract_prefixes(declarations, env_aliases) do
defp extract_prefixes(declarations, env_aliases, namespace_context_mod, env) do
{prefixes, declarations} =
Enum.reduce(declarations, {[], []}, fn
{:@, line, [{:prefix, _, [{:__aliases__, _, ns}] = aliases}]}, {prefixes, declarations} ->
{
[prefix(ns, env_aliases) | prefixes],
[{:alias, line, aliases} | declarations]
}
{:@, line, [{:prefix, _, [[{prefix, {:__aliases__, _, ns} = aliases}]]}]},
{prefixes, declarations} ->
{[prefix(prefix, ns, env_aliases) | prefixes],
[{:alias, line, [aliases]} | declarations]}
{
[prefix(prefix, ns, env_aliases) | prefixes],
[{:alias, line, [aliases]} | declarations]
}
{:@, line, [{:prefix, _, [{:__aliases__, _, ns}] = aliases}]}, {prefixes, declarations} ->
{[prefix(ns, env_aliases) | prefixes], [{:alias, line, aliases} | declarations]}
{:@, line, [{:prefix, _, [[{prefix, uri}]]}]}, {prefixes, declarations}
when is_binary(uri) ->
ns = ad_hoc_namespace(prefix, uri, namespace_context_mod, env)
{
[prefix(prefix, ns, env_aliases) | prefixes],
[{: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]}
@ -152,6 +180,15 @@ defmodule RDF.Graph.Builder do
|> String.to_atom()
end
defp ad_hoc_namespace(prefix, uri, namespace_context_mod, env) do
{:module, module, _, _} =
namespace_context_mod
|> Module.concat(prefix |> Atom.to_string() |> Macro.camelize())
|> Vocabulary.Namespace.create!(uri, [], env, strict: false)
module |> Module.split() |> Enum.map(&String.to_atom/1)
end
defp declaration?({:=, _, _}), do: true
defp declaration?({:@, _, [{:prefix, _, _}]}), do: true
defp declaration?({:@, _, [{:base, _, _}]}), do: true
@ -195,4 +232,8 @@ defmodule RDF.Graph.Builder do
[short] = Module.split(module)
String.to_atom(short)
end
defp random_number do
:erlang.unique_integer([:positive])
end
end

View file

@ -427,6 +427,73 @@ 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/"
Ad.S |> Ad.p(Ad.O)
end
end).()
assert graph ==
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/O")
},
prefixes: RDF.default_prefixes(ad: "http://example.com/ad-hoc/")
)
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#"
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#"
graph1
Ad.S |> Ad.p(Ex2.O)
end
end).()
assert graph ==
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")
},
{
RDF.iri("http://example.com/ad-hoc/S"),
RDF.iri("http://example.com/ad-hoc/p"),
RDF.iri("http://example.com/ad-hoc/ex2#O")
}
],
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 =