diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ad0b36..2ea279d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,9 @@ This project adheres to [Semantic Versioning](http://semver.org/) and - `RDF.Description.values/1`, `RDF.Graph.values/1`, `RDF.Dataset.values/1` and `RDF.Data.values/1` returning a map of `RDF.Term.value/1` converted native Elixir values from the respective structure of RDF terms +- for all of aforementioned `values/1` functions a variant `values/2` which + allows to specify custom mapping function to be applied when creating the resp. + structure - `RDF.Literal.compare/2`, `RDF.Literal.less_than?/2` and `RDF.Literal.greater_than?/2` for `RDF.Datatype` aware comparisons of `RDF.Literal`s diff --git a/README.md b/README.md index a8af3ce..0b6ac67 100644 --- a/README.md +++ b/README.md @@ -689,6 +689,83 @@ iex> RDF.list(["foo", EX.Bar, ~B, [1, 2]]) |> RDF.List.values %RDF.Literal{value: 2, datatype: ~I}]] ``` +### Mapping of RDF terms and structures + +The `RDF.Term.value/1` function converts RDF terms to Elixir values: + +```elixir +iex> RDF.Term.value(~I) +"http://example.com/" +iex> RDF.Term.value(~L"foo") +"foo" +iex> RDF.integer(42) |> RDF.Term.value() +42 +``` + +It returns `nil` if no conversion is possible. + +All structures of RDF terms also support a `values` function. On `RDF.Triple.values`, `RDF.Quad` and `RDF.Statement` the tuple of RDF terms is converted to an tuple of the resp. Elixir values. On all of the other RDF data structures (`RDF.Description`, `RDF.Graph` and `RDF.Dataset`) and the general `RDF.Data` protocol the `values` functions produces a map of the converted Elixir values. + +```elixir +iex> RDF.Triple.values {~I, ~I, RDF.literal(42)} +{"http://example.com/S", "http://example.com/p", 42} + +iex> {~I, ~I, ~L"Foo"} +...> |> RDF.Description.new() +...> |> RDF.Description.values() +%{"http://example.com/p" => ["Foo"]} + +iex> [ +...> {~I, ~I, ~L"Foo"}, +...> {~I, ~I, RDF.integer(42)} +...> ] +...> |> RDF.Graph.new() +...> |> RDF.Graph.values() +%{ + "http://example.com/S1" => %{"http://example.com/p" => ["Foo"]}, + "http://example.com/S2" => %{"http://example.com/p" => [42]} +} + +iex> [ +...> {~I, ~I, ~L"Foo", ~I}, +...> {~I, ~I, RDF.integer(42), } +...> ] +...> |> RDF.Dataset.new() +...> |> RDF.Dataset.values() +%{ + "http://example.com/Graph" => %{ + "http://example.com/S" => %{"http://example.com/p" => ["Foo"]} + }, + nil => %{ + "http://example.com/S" => %{"http://example.com/p" => [42]} + } +} +``` + +All of these `values` functions also support an optional second argument for a function with a custom mapping of the terms depending on their statement position. The function will be called with a tuple `{statement_position, rdf_term}` where `statement_position` is one of the atoms `:subject`, `:predicate`, `:object` or `:graph_name`, while `rdf_term` is the RDF term to be mapped. + +```elixir +iex> [ +...> {~I, ~I, ~L"Foo"}, +...> {~I, ~I, RDF.integer(42)} +...> ] +...> |> RDF.Graph.new() +...> |> RDF.Graph.values(fn +...> {:predicate, predicate} -> +...> predicate +...> |> to_string() +...> |> String.split("/") +...> |> List.last() +...> |> String.to_atom() +...> {_, term} -> +...> RDF.Term.value(term) +...> end) +%{ + "http://example.com/S1" => %{p: ["Foo"]}, + "http://example.com/S2" => %{p: [42]} +} +``` + ### Serializations @@ -739,7 +816,7 @@ The `Date` and `DateTime` modules of Elixir versions < 1.7.2 don't handle negati ## Getting help - [Documentation](http://hexdocs.pm/rdf) -- [A tutorial about working with RDF.ex vocabularies](https://medium.com/@tonyhammond/early-steps-in-elixir-and-rdf-5078a4ebfe0f) +- [A tutorial about working with RDF.ex vocabularies by Tony Hammond](https://medium.com/@tonyhammond/early-steps-in-elixir-and-rdf-5078a4ebfe0f) - [Google Group](https://groups.google.com/d/forum/rdfex) @@ -772,7 +849,7 @@ see [CONTRIBUTING](CONTRIBUTING.md) for details. [RDF.ex]: https://hex.pm/packages/rdf [rdf_vocab]: https://hex.pm/packages/rdf_vocab [JSON-LD.ex]: https://hex.pm/packages/json_ld -[SPARQL.ex]: https://hex.pm/packages/sparql +[SPARQL.ex]: https://hex.pm/packages/sparql [SPARQL.Client]: https://hex.pm/packages/sparql_client [N-Triples]: https://www.w3.org/TR/n-triples/ [N-Quads]: https://www.w3.org/TR/n-quads/ diff --git a/lib/rdf/data.ex b/lib/rdf/data.ex index 3717d10..edefa65 100644 --- a/lib/rdf/data.ex +++ b/lib/rdf/data.ex @@ -97,6 +97,10 @@ defprotocol RDF.Data do """ def values(data) + @doc """ + Returns a nested map of the native Elixir values of a RDF data structure with values mapped with the given function. + """ + def values(data, mapping) end defimpl RDF.Data, for: RDF.Description do @@ -164,6 +168,7 @@ defimpl RDF.Data, for: RDF.Description do def subject_count(_), do: 1 def statement_count(description), do: RDF.Description.count(description) def values(description), do: RDF.Description.values(description) + def values(description, mapping), do: RDF.Description.values(description, mapping) end @@ -225,6 +230,7 @@ defimpl RDF.Data, for: RDF.Graph do def subject_count(graph), do: RDF.Graph.subject_count(graph) def statement_count(graph), do: RDF.Graph.triple_count(graph) def values(graph), do: RDF.Graph.values(graph) + def values(graph, mapping), do: RDF.Graph.values(graph, mapping) end @@ -279,4 +285,5 @@ defimpl RDF.Data, for: RDF.Dataset do def subject_count(dataset), do: dataset |> subjects |> Enum.count def statement_count(dataset), do: RDF.Dataset.statement_count(dataset) def values(dataset), do: RDF.Dataset.values(dataset) + def values(dataset, mapping), do: RDF.Dataset.values(dataset, mapping) end diff --git a/lib/rdf/dataset.ex b/lib/rdf/dataset.ex index cc0d304..6eb824c 100644 --- a/lib/rdf/dataset.ex +++ b/lib/rdf/dataset.ex @@ -731,6 +731,11 @@ defmodule RDF.Dataset do @doc """ Returns a nested map of the native Elixir values of a `RDF.Dataset`. + The optional second argument allows to specify a custom mapping with a function + which will receive a tuple `{statement_position, rdf_term}` where + `statement_position` is one of the atoms `:subject`, `:predicate`, `:object`, + or `graph_name` while `rdf_term` is the RDF term to be mapped. + ## Examples iex> [ @@ -748,10 +753,38 @@ defmodule RDF.Dataset do } } + iex> [ + ...> {~I, ~I, ~L"Foo", ~I}, + ...> {~I, ~I, RDF.integer(42), } + ...> ] + ...> |> RDF.Dataset.new() + ...> |> RDF.Dataset.values(fn + ...> {:graph_name, graph_name} -> + ...> graph_name + ...> {:predicate, predicate} -> + ...> predicate + ...> |> to_string() + ...> |> String.split("/") + ...> |> List.last() + ...> |> String.to_atom() + ...> {_, term} -> + ...> RDF.Term.value(term) + ...> end) + %{ + ~I => %{ + "http://example.com/S" => %{p: ["Foo"]} + }, + nil => %{ + "http://example.com/S" => %{p: [42]} + } + } + """ - def values(%RDF.Dataset{graphs: graphs}) do + def values(dataset, mapping \\ &RDF.Statement.default_term_mapping/1) + + def values(%RDF.Dataset{graphs: graphs}, mapping) do Map.new graphs, fn {graph_name, graph} -> - {RDF.Term.value(graph_name), Graph.values(graph)} + {mapping.({:graph_name, graph_name}), Graph.values(graph, mapping)} end end diff --git a/lib/rdf/description.ex b/lib/rdf/description.ex index f14b7ba..9cb9efc 100644 --- a/lib/rdf/description.ex +++ b/lib/rdf/description.ex @@ -578,6 +578,11 @@ defmodule RDF.Description do The subject is not part of the result. It can be converted separately with `RDF.Term.value/1`. + The optional second argument allows to specify a custom mapping with a function + which will receive a tuple `{statement_position, rdf_term}` where + `statement_position` is one of the atoms `:predicate` or `:object`, + while `rdf_term` is the RDF term to be mapped. + ## Examples iex> {~I, ~I, ~L"Foo"} @@ -585,16 +590,33 @@ defmodule RDF.Description do ...> |> RDF.Description.values() %{"http://example.com/p" => ["Foo"]} + iex> {~I, ~I, ~L"Foo"} + ...> |> RDF.Description.new() + ...> |> RDF.Description.values(fn + ...> {:predicate, predicate} -> + ...> predicate + ...> |> to_string() + ...> |> String.split("/") + ...> |> List.last() + ...> |> String.to_atom() + ...> {_, term} -> + ...> RDF.Term.value(term) + ...> end) + %{p: ["Foo"]} + """ - def values(%RDF.Description{subject: subject, predications: predications}) do + def values(description, mapping \\ &RDF.Statement.default_term_mapping/1) + + def values(%RDF.Description{predications: predications}, mapping) do Map.new predications, fn {predicate, objects} -> { - RDF.Term.value(predicate), - objects |> Map.keys() |> Enum.map(&RDF.Term.value/1) + mapping.({:predicate, predicate}), + objects |> Map.keys() |> Enum.map(&(mapping.({:object, &1}))) } end end + defimpl Enumerable do def member?(desc, triple), do: {:ok, RDF.Description.include?(desc, triple)} def count(desc), do: {:ok, RDF.Description.count(desc)} diff --git a/lib/rdf/graph.ex b/lib/rdf/graph.ex index 3052bc0..accae89 100644 --- a/lib/rdf/graph.ex +++ b/lib/rdf/graph.ex @@ -605,6 +605,11 @@ defmodule RDF.Graph do @doc """ Returns a nested map of the native Elixir values of a `RDF.Graph`. + The optional second argument allows to specify a custom mapping with a function + which will receive a tuple `{statement_position, rdf_term}` where + `statement_position` is one of the atoms `:subject`, `:predicate` or `:object`, + while `rdf_term` is the RDF term to be mapped. + ## Examples iex> [ @@ -618,10 +623,32 @@ defmodule RDF.Graph do "http://example.com/S2" => %{"http://example.com/p" => [42]} } + iex> [ + ...> {~I, ~I, ~L"Foo"}, + ...> {~I, ~I, RDF.integer(42)} + ...> ] + ...> |> RDF.Graph.new() + ...> |> RDF.Graph.values(fn + ...> {:predicate, predicate} -> + ...> predicate + ...> |> to_string() + ...> |> String.split("/") + ...> |> List.last() + ...> |> String.to_atom() + ...> {_, term} -> + ...> RDF.Term.value(term) + ...> end) + %{ + "http://example.com/S1" => %{p: ["Foo"]}, + "http://example.com/S2" => %{p: [42]} + } + """ - def values(%RDF.Graph{descriptions: descriptions}) do + def values(graph, mapping \\ &RDF.Statement.default_term_mapping/1) + + def values(%RDF.Graph{descriptions: descriptions}, mapping) do Map.new descriptions, fn {subject, description} -> - {RDF.Term.value(subject), Description.values(description)} + {mapping.({:subject, subject}), Description.values(description, mapping)} end end diff --git a/lib/rdf/quad.ex b/lib/rdf/quad.ex index 788493b..86c068f 100644 --- a/lib/rdf/quad.ex +++ b/lib/rdf/quad.ex @@ -54,23 +54,44 @@ defmodule RDF.Quad do Returns `nil` if one of the components of the given tuple is not convertible via `RDF.Term.value/1`. + The optional second argument allows to specify a custom mapping with a function + which will receive a tuple `{statement_position, rdf_term}` where + `statement_position` is one of the atoms `:subject`, `:predicate`, `:object` or + `:graph_name`, while `rdf_term` is the RDF term to be mapped. When the given + function returns `nil` this will be interpreted as an error and will become + the overhaul result of the `values/2` call. + ## Examples iex> RDF.Quad.values {~I, ~I, RDF.literal(42), ~I} {"http://example.com/S", "http://example.com/p", 42, "http://example.com/Graph"} + iex> {~I, ~I, RDF.literal(42), ~I} + ...> |> RDF.Quad.values(fn + ...> {:object, object} -> + ...> RDF.Term.value(object) + ...> {:graph_name, graph_name} -> + ...> graph_name + ...> {_, resource} -> + ...> resource |> to_string() |> String.last() |> String.to_atom() + ...> end) + {:S, :p, 42, ~I} + """ - def values({subject, predicate, object, graph_context}) do - with subject_value when not is_nil(subject_value) <- Term.value(subject), - predicate_value when not is_nil(predicate_value) <- Term.value(predicate), - object_value when not is_nil(object_value) <- Term.value(object), - graph_context_value when not is_nil(graph_context_value) or is_nil(graph_context) <- - Term.value(graph_context) + def values(quad, mapping \\ &Statement.default_term_mapping/1) + + def values({subject, predicate, object, graph_context}, mapping) do + with subject_value when not is_nil(subject_value) <- mapping.({:subject, subject}), + predicate_value when not is_nil(predicate_value) <- mapping.({:predicate, predicate}), + object_value when not is_nil(object_value) <- mapping.({:object, object}), + graph_context_value <- mapping.({:graph_name, graph_context}) do {subject_value, predicate_value, object_value, graph_context_value} + else + _ -> nil end end - def values(_), do: nil + def values(_, _), do: nil end diff --git a/lib/rdf/statement.ex b/lib/rdf/statement.ex index 44d2ebb..bf13185 100644 --- a/lib/rdf/statement.ex +++ b/lib/rdf/statement.ex @@ -25,9 +25,9 @@ defmodule RDF.Statement do ## Examples - iex> RDF.Statement.new {"http://example.com/S", "http://example.com/p", 42} + iex> RDF.Statement.coerce {"http://example.com/S", "http://example.com/p", 42} {~I, ~I, RDF.literal(42)} - iex> RDF.Statement.new {"http://example.com/S", "http://example.com/p", 42, "http://example.com/Graph"} + iex> RDF.Statement.coerce {"http://example.com/S", "http://example.com/p", 42, "http://example.com/Graph"} {~I, ~I, RDF.literal(42), ~I} """ def coerce(statement) @@ -77,6 +77,13 @@ defmodule RDF.Statement do Returns `nil` if one of the components of the given tuple is not convertible via `RDF.Term.value/1`. + The optional second argument allows to specify a custom mapping with a function + which will receive a tuple `{statement_position, rdf_term}` where + `statement_position` is one of the atoms `:subject`, `:predicate`, `:object` or + `:graph_name`, while `rdf_term` is the RDF term to be mapped. When the given + function returns `nil` this will be interpreted as an error and will become + the overhaul result of the `values/2` call. + ## Examples iex> RDF.Statement.values {~I, ~I, RDF.literal(42)} @@ -84,9 +91,28 @@ defmodule RDF.Statement do iex> RDF.Statement.values {~I, ~I, RDF.literal(42), ~I} {"http://example.com/S", "http://example.com/p", 42, "http://example.com/Graph"} + iex> {~I, ~I, RDF.literal(42), ~I} + ...> |> RDF.Statement.values(fn + ...> {:subject, subject} -> + ...> subject |> to_string() |> String.last() + ...> {:predicate, predicate} -> + ...> predicate |> to_string() |> String.last() |> String.to_atom() + ...> {:object, object} -> + ...> RDF.Term.value(object) + ...> {:graph_name, graph_name} -> + ...> graph_name + ...> end) + {"S", :p, 42, ~I} + """ - def values({_, _, _} = triple), do: RDF.Triple.values(triple) - def values({_, _, _, _} = quad), do: RDF.Quad.values(quad) - def values(_), do: nil + def values(statement, mapping \\ &default_term_mapping/1) + def values({_, _, _} = triple, mapping), do: RDF.Triple.values(triple, mapping) + def values({_, _, _, _} = quad, mapping), do: RDF.Quad.values(quad, mapping) + def values(_, _), do: nil + + @doc false + def default_term_mapping(qualified_term) + def default_term_mapping({:graph_name, nil}), do: nil + def default_term_mapping({_, term}), do: RDF.Term.value(term) end diff --git a/lib/rdf/triple.ex b/lib/rdf/triple.ex index a033582..8d7b49d 100644 --- a/lib/rdf/triple.ex +++ b/lib/rdf/triple.ex @@ -52,21 +52,39 @@ defmodule RDF.Triple do Returns `nil` if one of the components of the given tuple is not convertible via `RDF.Term.value/1`. + The optional second argument allows to specify a custom mapping with a function + which will receive a tuple `{statement_position, rdf_term}` where + `statement_position` is one of the atoms `:subject`, `:predicate` or `:object`, + while `rdf_term` is the RDF term to be mapped. When the given function returns + `nil` this will be interpreted as an error and will become the overhaul result + of the `values/2` call. + ## Examples iex> RDF.Triple.values {~I, ~I, RDF.literal(42)} {"http://example.com/S", "http://example.com/p", 42} + iex> {~I, ~I, RDF.literal(42)} + ...> |> RDF.Triple.values(fn + ...> {:object, object} -> RDF.Term.value(object) + ...> {_, term} -> term |> to_string() |> String.last() + ...> end) + {"S", "p", 42} + """ - def values({subject, predicate, object}) do - with subject_value when not is_nil(subject_value) <- Term.value(subject), - predicate_value when not is_nil(predicate_value) <- Term.value(predicate), - object_value when not is_nil(object_value) <- Term.value(object) + def values(triple, mapping \\ &Statement.default_term_mapping/1) + + def values({subject, predicate, object}, mapping) do + with subject_value when not is_nil(subject_value) <- mapping.({:subject, subject}), + predicate_value when not is_nil(predicate_value) <- mapping.({:predicate, predicate}), + object_value when not is_nil(object_value) <- mapping.({:object, object}) do {subject_value, predicate_value, object_value} + else + _ -> nil end end - def values(_), do: nil + def values(_, _), do: nil end diff --git a/test/unit/data_test.exs b/test/unit/data_test.exs index 87b5ec5..67e39d6 100644 --- a/test/unit/data_test.exs +++ b/test/unit/data_test.exs @@ -423,7 +423,7 @@ defmodule RDF.DataTest do assert RDF.Data.statement_count(dataset) == 14 end - test "values", %{dataset: dataset} do + test "values/1", %{dataset: dataset} do assert RDF.Data.values(dataset) == %{ nil => %{ @@ -459,6 +459,52 @@ defmodule RDF.DataTest do } } end + + test "values/2", %{dataset: dataset} do + mapping = fn + {:graph_name, graph_name} -> + graph_name + {:predicate, predicate} -> + predicate |> to_string() |> String.split("/") |> List.last() |> String.to_atom() + {_, term} -> + RDF.Term.value(term) + end + + assert RDF.Data.values(dataset, mapping) == + %{ + nil => %{ + RDF.Term.value(RDF.iri(EX.S)) => %{ + p1: [ + RDF.Term.value(RDF.iri(EX.O1)), + RDF.Term.value(RDF.iri(EX.O2)) + ], + p2: [RDF.Term.value(RDF.iri(EX.O3))], + p3: ["_:foo", "bar"], + }, + RDF.Term.value(RDF.iri(EX.S2)) => %{ + p2: [ + RDF.Term.value(RDF.iri(EX.O3)), + RDF.Term.value(RDF.iri(EX.O4)) + ], + }, + }, + RDF.iri(EX.NamedGraph) => %{ + RDF.Term.value(RDF.iri(EX.S)) => %{ + p1: [ + RDF.Term.value(RDF.iri(EX.O1)), + RDF.Term.value(RDF.iri(EX.O2)) + ], + p2: [RDF.Term.value(RDF.iri(EX.O3))], + p3: ["_:foo", RDF.Term.value(RDF.iri(EX.O5)), "bar"], + }, + RDF.Term.value(RDF.iri(EX.S3)) => %{ + p3: [ + RDF.Term.value(RDF.iri(EX.O5)) + ], + }, + } + } + end end end diff --git a/test/unit/dataset_test.exs b/test/unit/dataset_test.exs index 74f6023..9ad8560 100644 --- a/test/unit/dataset_test.exs +++ b/test/unit/dataset_test.exs @@ -715,6 +715,29 @@ defmodule RDF.DatasetTest do } end + test "values/2" do + mapping = fn + {:graph_name, graph_name} -> + graph_name + {:predicate, predicate} -> + predicate |> to_string() |> String.split("/") |> List.last() |> String.to_atom() + {_, term} -> + RDF.Term.value(term) + end + + assert Dataset.new() |> Dataset.values(mapping) == %{} + assert Dataset.new([{EX.s1, EX.p, EX.o1}, {EX.s2, EX.p, EX.o2, EX.graph}]) + |> Dataset.values(mapping) == + %{ + nil => %{ + RDF.Term.value(EX.s1) => %{p: [RDF.Term.value(EX.o1)]} + }, + EX.graph => %{ + RDF.Term.value(EX.s2) => %{p: [RDF.Term.value(EX.o2)]}, + } + } + end + describe "Enumerable protocol" do test "Enum.count" do diff --git a/test/unit/description_test.exs b/test/unit/description_test.exs index 6cf56a4..82f768b 100644 --- a/test/unit/description_test.exs +++ b/test/unit/description_test.exs @@ -340,9 +340,22 @@ defmodule RDF.DescriptionTest do end test "values/1" do - assert Description.new(EX.s) |> Description.values() == %{} - assert Description.new({EX.s, EX.p, ~L"Foo"}) |> Description.values() == - %{RDF.Term.value(EX.p) => ["Foo"]} + assert Description.new(EX.s) |> Description.values() == %{} + assert Description.new({EX.s, EX.p, ~L"Foo"}) |> Description.values() == + %{RDF.Term.value(EX.p) => ["Foo"]} + end + + test "values/2" do + mapping = fn + {:predicate, predicate} -> + predicate |> to_string() |> String.split("/") |> List.last() |> String.to_atom() + {_, term} -> + RDF.Term.value(term) + end + + assert Description.new(EX.s) |> Description.values(mapping) == %{} + assert Description.new({EX.s, EX.p, ~L"Foo"}) |> Description.values(mapping) == + %{p: ["Foo"]} end describe "Enumerable protocol" do diff --git a/test/unit/graph_test.exs b/test/unit/graph_test.exs index d8a1eb0..2532f40 100644 --- a/test/unit/graph_test.exs +++ b/test/unit/graph_test.exs @@ -374,6 +374,23 @@ defmodule RDF.GraphTest do } end + test "values/2" do + mapping = fn + {:predicate, predicate} -> + predicate |> to_string() |> String.split("/") |> List.last() |> String.to_atom() + {_, term} -> + RDF.Term.value(term) + end + + assert Graph.new() |> Graph.values(mapping) == %{} + assert Graph.new([{EX.s1, EX.p, EX.o1}, {EX.s2, EX.p, EX.o2}]) + |> Graph.values(mapping) == + %{ + RDF.Term.value(EX.s1) => %{p: [RDF.Term.value(EX.o1)]}, + RDF.Term.value(EX.s2) => %{p: [RDF.Term.value(EX.o2)]}, + } + end + describe "Enumerable protocol" do test "Enum.count" do diff --git a/test/unit/quad_test.exs b/test/unit/quad_test.exs index a711d88..3f789e6 100644 --- a/test/unit/quad_test.exs +++ b/test/unit/quad_test.exs @@ -18,4 +18,16 @@ defmodule RDF.QuadTest do refute Quad.values({self(), self(), self(), self()}) end end + + test "values/2" do + assert {~I, ~I, RDF.integer(42), ~I} + |> Quad.values(fn + {:subject, subject} -> subject |> to_string() |> String.last() |> String.to_atom() + {:predicate, _} -> :p + {:object, object} -> object |> RDF.Term.value() |> Kernel.+(1) + {:graph_name, _} -> nil + end) + == {:S, :p, 43, nil} + end + end diff --git a/test/unit/statement_test.exs b/test/unit/statement_test.exs new file mode 100644 index 0000000..7d56ad6 --- /dev/null +++ b/test/unit/statement_test.exs @@ -0,0 +1,6 @@ +defmodule RDF.StatementTest do + use RDF.Test.Case + + doctest RDF.Statement + +end diff --git a/test/unit/triple_test.exs b/test/unit/triple_test.exs index 56a3866..ed8d7b3 100644 --- a/test/unit/triple_test.exs +++ b/test/unit/triple_test.exs @@ -16,4 +16,14 @@ defmodule RDF.TripleTest do refute Triple.values({self(), self(), self()}) end end + + test "values/2" do + assert {~I, ~I, RDF.integer(42)} + |> Triple.values(fn + {:object, object} -> object |> RDF.Term.value() |> Kernel.+(1) + {_, term} -> term |> to_string() |> String.last() + end) + == {"S", "p", 43} + end + end