diff --git a/CHANGELOG.md b/CHANGELOG.md index db7b61a..e11755f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/lib/rdf/graph.ex b/lib/rdf/graph.ex index ec317b7..7717cf7 100644 --- a/lib/rdf/graph.ex +++ b/lib/rdf/graph.ex @@ -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 """ diff --git a/lib/rdf/graph_builder.ex b/lib/rdf/graph_builder.ex index ebcb9b9..f4a06db 100644 --- a/lib/rdf/graph_builder.ex +++ b/lib/rdf/graph_builder.ex @@ -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 diff --git a/test/unit/graph_builder_test.exs b/test/unit/graph_builder_test.exs index 15f6622..f175127 100644 --- a/test/unit/graph_builder_test.exs +++ b/test/unit/graph_builder_test.exs @@ -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 =