diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bb19d6..bca6aad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ The generated namespaces are much more flexible now and compile faster. - `RDF.Namespace` builders `defnamespace/3` and `create/4` - `RDF.Vocabulary.Namespace.create/5` for dynamic creation of `RDF.Vocabulary.Namespace`s +- 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` ### Changed diff --git a/lib/rdf/namespace/iri.ex b/lib/rdf/namespace/iri.ex new file mode 100644 index 0000000..3d1abff --- /dev/null +++ b/lib/rdf/namespace/iri.ex @@ -0,0 +1,54 @@ +defmodule RDF.Namespace.IRI do + @moduledoc """ + Provides the `iri/1` macro to resolve IRI values inside of pattern matches. + """ + + @doc """ + A macro which allows to resolve IRI values inside of pattern matches. + + Terms of a `RDF.Namespace` (which includes terms of `RDF.Vocabulary.Namespace`) + can't be resolved in pattern matches. This macro allows just that, by wrapping + the terms in a pattern match with a call of this macro. + + Note: Only literal values are allowed as arguments of this macro, since the argument + expression needs to be evaluated at compile-time. + + + ## Example + + import RDF.Namespace.IRI + + case expr do + iri(EX.Foo) -> ... + iri(EX.bar()) -> ... + ... + end + + """ + defmacro iri({{:., _, [{:__aliases__, _, _} = module_alias, _fun_name]}, _, []} = expr) do + {module, _} = Code.eval_quoted(module_alias, [], __CALLER__) + + if RDF.Namespace.namespace?(module) do + resolve_to_iri(expr, __CALLER__) + else + forbidden_iri_expr(expr) + end + end + + defmacro iri({:__aliases__, _, _} = expr), do: resolve_to_iri(expr, __CALLER__) + + defmacro iri(expr), do: forbidden_iri_expr(expr) + + defp resolve_to_iri(expr, env) do + {value, _} = Code.eval_quoted(expr, [], env) + iri = RDF.IRI.new(value) + + quote do + unquote(Macro.escape(iri)) + end + end + + defp forbidden_iri_expr(expr) do + raise ArgumentError, "forbidden expression in RDF.Guard.iri/1 call: #{Macro.to_string(expr)}" + end +end diff --git a/test/support/rdf_case.ex b/test/support/rdf_case.ex index 1fce0e9..770137d 100644 --- a/test/support/rdf_case.ex +++ b/test/support/rdf_case.ex @@ -16,7 +16,8 @@ defmodule RDF.Test.Case do alias RDF.NS.{RDFS, OWL} alias unquote(__MODULE__).{EX, FOAF} - import RDF, only: [iri: 1, literal: 1, bnode: 1] + import RDF, only: [literal: 1, bnode: 1] + import RDF.Namespace.IRI import unquote(__MODULE__) import RDF.Sigils diff --git a/test/unit/dataset_test.exs b/test/unit/dataset_test.exs index ca39229..d5af384 100644 --- a/test/unit/dataset_test.exs +++ b/test/unit/dataset_test.exs @@ -15,7 +15,7 @@ defmodule RDF.DatasetTest do test "creating an empty dataset with a coercible dataset name" do assert named_dataset("http://example.com/DatasetName") - |> named_dataset?(iri("http://example.com/DatasetName")) + |> named_dataset?(~I) assert named_dataset(EX.Foo) |> named_dataset?(iri(EX.Foo)) end diff --git a/test/unit/graph_test.exs b/test/unit/graph_test.exs index 2675acc..859f102 100644 --- a/test/unit/graph_test.exs +++ b/test/unit/graph_test.exs @@ -23,7 +23,7 @@ defmodule RDF.GraphTest do test "creating an empty graph with a coercible graph name" do assert named_graph("http://example.com/graph/GraphName") - |> named_graph?(iri("http://example.com/graph/GraphName")) + |> named_graph?(~I) assert named_graph(EX.Foo) |> named_graph?(iri(EX.Foo)) end diff --git a/test/unit/list_test.exs b/test/unit/list_test.exs index 272aa3f..3aca811 100644 --- a/test/unit/list_test.exs +++ b/test/unit/list_test.exs @@ -9,8 +9,6 @@ defmodule RDF.ListTest do use RDF.Vocabulary.Namespace - defvocab EX, base_iri: "http://example.org/#", terms: [], strict: false - setup do {:ok, empty: RDF.List.new(RDF.nil(), Graph.new()), diff --git a/test/unit/namespace/iri_test.exs b/test/unit/namespace/iri_test.exs new file mode 100644 index 0000000..7aba872 --- /dev/null +++ b/test/unit/namespace/iri_test.exs @@ -0,0 +1,56 @@ +defmodule RDF.Namespace.IRITest do + use RDF.Test.Case + + doctest RDF.Namespace.IRI + + import RDF.Namespace.IRI + + describe "iri/1" do + test "with a property function from a vocabulary namespace" do + assert iri(EX.foo()) == EX.foo() + assert iri(RDF.NS.OWL.sameAs()) == RDF.NS.OWL.sameAs() + end + + test "with a term atom from a vocabulary namespace" do + assert iri(EX.Foo) == RDF.iri(EX.Foo) + end + + test "constant function calls from non-vocabulary namespace module results in a compile error" do + assert_raise ArgumentError, ~r[forbidden expression in RDF.Guard.iri/1], fn -> + ast = + quote do + import RDF.Guards + + iri(Mix.env()) + end + + Code.eval_quoted(ast, [], __ENV__) + end + end + + test "other forms result in a compile error" do + assert_raise ArgumentError, ~r[forbidden expression in RDF.Guard.iri/1], fn -> + ast = + quote do + import RDF.Guards + var = ~I + iri(var) + end + + Code.eval_quoted(ast, [], __ENV__) + end + end + + test "in pattern matches" do + assert (case EX.foo() do + iri(EX.foo()) -> "match" + _ -> {:mismatch, iri(EX.foo())} + end) == "match" + + assert (case RDF.iri(EX.Bar) do + iri(EX.Bar) -> "match" + _ -> {:mismatch, iri(EX.Bar)} + end) == "match" + end + end +end diff --git a/test/unit/sigils_test.exs b/test/unit/sigils_test.exs index 0051741..5f1d6ad 100644 --- a/test/unit/sigils_test.exs +++ b/test/unit/sigils_test.exs @@ -13,6 +13,13 @@ defmodule RDF.SigilsTest do test "escaping" do assert ~I == RDF.iri("http://example.com/f\\no") end + + test "in pattern matches" do + assert (case RDF.iri("http://example.com/foo") do + ~I -> "match" + _ -> :mismatch + end) == "match" + end end describe "~i sigil" do