From 520a6ba58d8f9ce589903f192b0005cf04f42733 Mon Sep 17 00:00:00 2001 From: Marcel Otto Date: Tue, 16 Jun 2020 12:05:44 +0200 Subject: [PATCH] Add API documentation for BGP querying and some fixes for the API --- lib/rdf.ex | 2 + lib/rdf/graph.ex | 18 +++- lib/rdf/query.ex | 189 +++++++++++++++++++++++++++++++-- lib/rdf/query/bgp.ex | 11 +- lib/rdf/query/bgp/matcher.ex | 2 +- lib/rdf/query/bgp/simple.ex | 2 + lib/rdf/query/bgp/stream.ex | 2 + lib/rdf/query/builder.ex | 2 + test/support/rdf_case.ex | 7 +- test/unit/query/query_test.exs | 44 ++++++-- 10 files changed, 261 insertions(+), 18 deletions(-) diff --git a/lib/rdf.ex b/lib/rdf.ex index 7bed1a3..915d61c 100644 --- a/lib/rdf.ex +++ b/lib/rdf.ex @@ -9,6 +9,7 @@ defmodule RDF do - `RDF.IRI` - `RDF.BlankNode` - `RDF.Literal` + - the `RDF.Literal.Datatype` system - a facility for the mapping of URIs of a vocabulary to Elixir modules and functions: `RDF.Vocabulary.Namespace` - modules for the construction of statements @@ -22,6 +23,7 @@ defmodule RDF do - `RDF.Data` - `RDF.List` - `RDF.Diff` + - functions to construct and execute basic graph pattern queries: `RDF.Query` - functions for working with RDF serializations: `RDF.Serialization` - behaviours for the definition of RDF serialization formats - `RDF.Serialization.Format` diff --git a/lib/rdf/graph.ex b/lib/rdf/graph.ex index d7e1eda..42b3b85 100644 --- a/lib/rdf/graph.ex +++ b/lib/rdf/graph.ex @@ -479,10 +479,26 @@ defmodule RDF.Graph do Access.fetch(descriptions, coerce_subject(subject)) end + @doc """ + Execute the given `query` against the given `graph`. + + This is just a convenience delegator function to `RDF.Query.execute!/3` with + the first two arguments swapped so it can be used in a pipeline on a `RDF.Graph`. + + See `RDF.Query.execute/3` and `RDF.Query.execute!/3` for more information and examples. + """ def query(graph, query, opts \\ []) do RDF.Query.execute!(query, graph, opts) end + @doc """ + Returns a `Stream` for the execution of the given `query` against the given `graph`. + + This is just a convenience delegator function to `RDF.Query.stream!/3` with + the first two arguments swapped so it can be used in a pipeline on a `RDF.Graph`. + + See `RDF.Query.stream/3` and `RDF.Query.stream!/3` for more information and examples. + """ def query_stream(graph, query, opts \\ []) do RDF.Query.stream!(query, graph, opts) end @@ -854,7 +870,7 @@ defmodule RDF.Graph do def take(%RDF.Graph{} = graph, subjects, properties) do graph = take(graph, subjects, nil) - %RDF.Graph{graph | + %RDF.Graph{graph | descriptions: Map.new(graph.descriptions, fn {subject, description} -> {subject, Description.take(description, properties)} end) diff --git a/lib/rdf/query.ex b/lib/rdf/query.ex index ac884a6..270c79a 100644 --- a/lib/rdf/query.ex +++ b/lib/rdf/query.ex @@ -8,33 +8,136 @@ defmodule RDF.Query do @default_matcher RDF.Query.BGP.Stream + @doc """ + Execute the given `query` against the given `graph`. + The `query` can be given directly as `RDF.Query.BGP` struct created with one + of the builder functions in this module or as basic graph pattern expression + accepted by `bgp/1`. + + The result is a list of maps with the solutions for the variables in the graph + pattern query and will be returned in a `:ok` tuple. In case of an error a + `:error` tuple is returned. + + ## Example + + Let's assume we have an `example_graph` with these triples: + + ```turtle + @prefix foaf: . + @prefix ex: . + + ex:Outlaw + foaf:name "Johnny Lee Outlaw" ; + foaf:mbox . + + ex:Goodguy + foaf:name "Peter Goodguy" ; + foaf:mbox ; + foaf:friend ex:Outlaw . + ``` + + iex> {:_, FOAF.name, :name?} |> RDF.Query.execute(example_graph()) + {:ok, [%{name: ~L"Peter Goodguy"}, %{name: ~L"Johnny Lee Outlaw"}]} + + iex> [ + ...> {:_, FOAF.name, :name?}, + ...> {:_, FOAF.mbox, :mbox?}, + ...> ] |> RDF.Query.execute(example_graph()) + {:ok, [ + %{name: ~L"Peter Goodguy", mbox: ~I}, + %{name: ~L"Johnny Lee Outlaw", mbox: ~I} + ]} + + iex> query = [ + ...> {:_, FOAF.name, :name?}, + ...> {:_, FOAF.mbox, :mbox?}, + ...> ] |> RDF.Query.bgp() + ...> RDF.Query.execute(query, example_graph()) + {:ok, [ + %{name: ~L"Peter Goodguy", mbox: ~I}, + %{name: ~L"Johnny Lee Outlaw", mbox: ~I} + ]} + + iex> [ + ...> EX.Goodguy, FOAF.friend, FOAF.name, :name? + ...> ] |> RDF.Query.path() |> RDF.Query.execute(example_graph()) + {:ok, [%{name: ~L"Johnny Lee Outlaw"}]} + + """ def execute(query, graph, opts \\ []) def execute(%BGP{} = query, %Graph{} = graph, opts) do matcher = Keyword.get(opts, :matcher, @default_matcher) - matcher.execute(query, graph, opts) + {:ok, matcher.execute(query, graph, opts)} end def execute(query, graph, opts) when is_list(query) or is_tuple(query) do with {:ok, bgp} <- Builder.bgp(query) do - execute(bgp, graph, opts) + execute(bgp, graph, opts) end end - def execute!(query, graph, opts) do + @doc """ + Execute the given `query` against the given `graph`. + + As opposed to `execute/3` this returns the results directly or fails with an + exception. + """ + def execute!(query, graph, opts \\ []) do case execute(query, graph, opts) do {:ok, results} -> results {:error, error} -> raise error end end + @doc """ + Returns a `Stream` for the execution of the given `query` against the given `graph`. + Just like on `execute/3` the `query` can be given directly as `RDF.Query.BGP` struct + created with one of the builder functions in this module or as basic graph pattern + expression accepted by `bgp/1`. + + The stream of solutions for variable bindings will be returned in a `:ok` tuple. + In case of an error a `:error` tuple is returned. + + ## Example + + Let's assume we have an `example_graph` with these triples: + + ```turtle + @prefix foaf: . + @prefix ex: . + + ex:Outlaw + foaf:name "Johnny Lee Outlaw" ; + foaf:mbox . + + ex:Goodguy + foaf:name "Peter Goodguy" ; + foaf:mbox ; + foaf:friend ex:Outlaw . + ``` + + iex> {:ok, stream} = {:_, FOAF.name, :name?} |> RDF.Query.stream(example_graph()) + ...> Enum.to_list(stream) + [%{name: ~L"Peter Goodguy"}, %{name: ~L"Johnny Lee Outlaw"}] + + iex> {:ok, stream} = [ + ...> {:_, FOAF.name, :name?}, + ...> {:_, FOAF.mbox, :mbox?}, + ...> ] |> RDF.Query.stream(example_graph()) + ...> Enum.take(stream, 1) + [ + %{name: ~L"Peter Goodguy", mbox: ~I}, + ] + + """ def stream(query, graph, opts \\ []) def stream(%BGP{} = query, %Graph{} = graph, opts) do matcher = Keyword.get(opts, :matcher, @default_matcher) - matcher.stream(query, graph, opts) + {:ok, matcher.stream(query, graph, opts)} end def stream(query, graph, opts) when is_list(query) or is_tuple(query) do @@ -43,13 +146,87 @@ defmodule RDF.Query do end end - def stream!(query, graph, opts) do - case execute(query, graph, opts) do + @doc """ + Returns a `Stream` for the execution of the given `query` against the given `graph`. + + As opposed to `stream/3` this returns the stream directly or fails with an + exception. + """ + def stream!(query, graph, opts \\ []) do + case stream(query, graph, opts) do {:ok, results} -> results {:error, error} -> raise error end end + @doc """ + Creates a `RDF.Query.BGP` struct. + + A basic graph pattern consist of single or list of triple patterns. + A triple pattern is a tuple which consists of RDF terms or variables for + the subject, predicate and object of a RDF triple. + + As RDF terms `RDF.IRI`s, `RDF.BlankNode`s, `RDF.Literal`s or all Elixir + values which can be coerced to any of those are allowed, i.e. + `RDF.Vocabulary.Namespace` atoms or Elixir values which can be coerced to RDF + literals with `RDF.Literal.coerce/1` (only on object position). On predicate + position the `:a` atom can be used for the `rdf:type` property. + + Variables are written as atoms ending with a question mark. Blank nodes which + in a graph query patterns act like a variable which doesn't show up in the + results can be written as atoms starting with an underscore. + + Here's a basic graph pattern example: + + ```elixir + [ + {:s?, :a, EX.Foo}, + {:s?, :a, EX.Bar}, + {:s?, RDFS.label, "foo"}, + {:s?, :p?, :o?} + ] + ``` + + Multiple triple patterns sharing the same subject and/or predicate can be grouped: + + - Multiple objects to the same subject-predicate pair can be written by just + writing them one by one in the same triple pattern. + - Multiple predicate-objects pair on the same subject can be written by + grouping them with square brackets. + + With these, the previous example can be shortened to: + + ```elixir + { + :s?, + [:a, EX.Foo, EX.Bar], + [RDFS.label, "foo"], + [:p?, :o?] + } + ``` + + """ defdelegate bgp(query), to: Builder, as: :bgp! + + @doc """ + Creates a `RDF.Query.BGP` struct for a path through a graph. + + The elements of the path can consist of the same RDF terms and variable + expressions allowed in `bgp/1` expressions. + + ## Example + + The `RDF.Query.BGP` struct build with this: + + RDF.Query.path [EX.S, EX.p, RDFS.label, :name?] + + is the same as the one build by this `bgp/1` call: + + RDF.Query.bgp [ + {EX.S, EX.p, :_o}, + {:_o, RDFS.label, :name?}, + ] + + """ defdelegate path(query, opts \\ []), to: Builder, as: :path! end diff --git a/lib/rdf/query/bgp.ex b/lib/rdf/query/bgp.ex index c28cbc6..1104d13 100644 --- a/lib/rdf/query/bgp.ex +++ b/lib/rdf/query/bgp.ex @@ -1,4 +1,11 @@ defmodule RDF.Query.BGP do + @moduledoc """ + A struct for Basic Graph Pattern queries. + + See `RDF.Query` and its functions on how to construct this query struct and + apply it on `RDF.Graph`s. + """ + @enforce_keys [:triple_patterns] defstruct [:triple_patterns] @@ -13,10 +20,12 @@ defmodule RDF.Query.BGP do @type t :: %__MODULE__{triple_patterns: triple_patterns} - @doc """ Return a list of all variables in a BGP. """ + @spec variables(any) :: [atom] + def variables(bgp) + def variables(%__MODULE__{triple_patterns: triple_patterns}), do: variables(triple_patterns) def variables(triple_patterns) when is_list(triple_patterns) do diff --git a/lib/rdf/query/bgp/matcher.ex b/lib/rdf/query/bgp/matcher.ex index 82e69e9..63040d3 100644 --- a/lib/rdf/query/bgp/matcher.ex +++ b/lib/rdf/query/bgp/matcher.ex @@ -1,5 +1,5 @@ defmodule RDF.Query.BGP.Matcher do - @moduledoc """ + @moduledoc !""" An interface for various BGP matching algorithm implementations. """ diff --git a/lib/rdf/query/bgp/simple.ex b/lib/rdf/query/bgp/simple.ex index f73db86..0cf9fad 100644 --- a/lib/rdf/query/bgp/simple.ex +++ b/lib/rdf/query/bgp/simple.ex @@ -1,4 +1,6 @@ defmodule RDF.Query.BGP.Simple do + @moduledoc false + @behaviour RDF.Query.BGP.Matcher alias RDF.Query.BGP diff --git a/lib/rdf/query/bgp/stream.ex b/lib/rdf/query/bgp/stream.ex index 41959cf..6b2c807 100644 --- a/lib/rdf/query/bgp/stream.ex +++ b/lib/rdf/query/bgp/stream.ex @@ -1,4 +1,6 @@ defmodule RDF.Query.BGP.Stream do + @moduledoc false + @behaviour RDF.Query.BGP.Matcher alias RDF.Query.BGP diff --git a/lib/rdf/query/builder.ex b/lib/rdf/query/builder.ex index 73a2ad5..ee6c584 100644 --- a/lib/rdf/query/builder.ex +++ b/lib/rdf/query/builder.ex @@ -1,4 +1,6 @@ defmodule RDF.Query.Builder do + @moduledoc false + alias RDF.Query.BGP alias RDF.{IRI, BlankNode, Literal, Namespace} import RDF.Utils.Guards diff --git a/test/support/rdf_case.ex b/test/support/rdf_case.ex index 8c79dac..3612ceb 100644 --- a/test/support/rdf_case.ex +++ b/test/support/rdf_case.ex @@ -6,13 +6,17 @@ defmodule RDF.Test.Case do base_iri: "http://example.com/", terms: [], strict: false + defvocab FOAF, + base_iri: "http://xmlns.com/foaf/0.1/", + terms: [], strict: false + alias RDF.{Dataset, Graph, Description, IRI} import RDF, only: [iri: 1] using do quote do alias RDF.{Dataset, Graph, Description, IRI, XSD} - alias unquote(__MODULE__).EX + alias unquote(__MODULE__).{EX, FOAF} import RDF, only: [iri: 1, literal: 1, bnode: 1] import unquote(__MODULE__) @@ -20,6 +24,7 @@ defmodule RDF.Test.Case do import RDF.Sigils @compile {:no_warn_undefined, RDF.Test.Case.EX} + @compile {:no_warn_undefined, RDF.Test.Case.FOAF} end end diff --git a/test/unit/query/query_test.exs b/test/unit/query/query_test.exs index eab8e17..66505b1 100644 --- a/test/unit/query/query_test.exs +++ b/test/unit/query/query_test.exs @@ -1,27 +1,55 @@ defmodule RDF.QueryTest do use RDF.Query.Test.Case - @example_graph Graph.new([ - {EX.s1, EX.p1, EX.o1}, - {EX.s1, EX.p2, EX.o2}, - {EX.s3, EX.p3, EX.o2} - ]) + doctest RDF.Query - @example_query [{:s?, :p?, EX.o2}] + @example_graph """ + @prefix foaf: . + @prefix ex: . + + ex:Outlaw + foaf:name "Johnny Lee Outlaw" ; + foaf:mbox . + + ex:Goodguy + foaf:name "Peter Goodguy" ; + foaf:mbox ; + foaf:friend ex:Outlaw . + """ |> RDF.Turtle.read_string!() + + def example_graph, do: @example_graph + + @example_query [{:s?, FOAF.name, ~L"Peter Goodguy"}] test "execute/2" do assert RDF.Query.execute(RDF.Query.bgp(@example_query), @example_graph) == - BGP.Stream.execute(RDF.Query.bgp(@example_query), @example_graph) + {:ok, BGP.Stream.execute(RDF.Query.bgp(@example_query), @example_graph)} assert RDF.Query.execute(@example_query, @example_graph) == + {:ok, BGP.Stream.execute(RDF.Query.bgp(@example_query), @example_graph)} + end + + test "execute!/2" do + assert RDF.Query.execute!(RDF.Query.bgp(@example_query), @example_graph) == + BGP.Stream.execute(RDF.Query.bgp(@example_query), @example_graph) + + assert RDF.Query.execute!(@example_query, @example_graph) == BGP.Stream.execute(RDF.Query.bgp(@example_query), @example_graph) end test "stream/2" do assert RDF.Query.stream(RDF.Query.bgp(@example_query), @example_graph) == - BGP.Stream.stream(RDF.Query.bgp(@example_query), @example_graph) + {:ok, BGP.Stream.stream(RDF.Query.bgp(@example_query), @example_graph)} assert RDF.Query.stream(@example_query, @example_graph) == + {:ok, BGP.Stream.stream(RDF.Query.bgp(@example_query), @example_graph)} + end + + test "stream!/2" do + assert RDF.Query.stream!(RDF.Query.bgp(@example_query), @example_graph) == + BGP.Stream.stream(RDF.Query.bgp(@example_query), @example_graph) + + assert RDF.Query.stream!(@example_query, @example_graph) == BGP.Stream.stream(RDF.Query.bgp(@example_query), @example_graph) end end