Add API documentation for BGP querying and some fixes for the API
This commit is contained in:
parent
1ef433bb59
commit
520a6ba58d
10 changed files with 261 additions and 18 deletions
|
@ -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`
|
||||
|
|
|
@ -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)
|
||||
|
|
189
lib/rdf/query.ex
189
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: <http://xmlns.com/foaf/0.1/> .
|
||||
@prefix ex: <http://example.com/> .
|
||||
|
||||
ex:Outlaw
|
||||
foaf:name "Johnny Lee Outlaw" ;
|
||||
foaf:mbox <mailto:jlow@example.com> .
|
||||
|
||||
ex:Goodguy
|
||||
foaf:name "Peter Goodguy" ;
|
||||
foaf:mbox <mailto:peter@example.org> ;
|
||||
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<mailto:peter@example.org>},
|
||||
%{name: ~L"Johnny Lee Outlaw", mbox: ~I<mailto:jlow@example.com>}
|
||||
]}
|
||||
|
||||
iex> query = [
|
||||
...> {:_, FOAF.name, :name?},
|
||||
...> {:_, FOAF.mbox, :mbox?},
|
||||
...> ] |> RDF.Query.bgp()
|
||||
...> RDF.Query.execute(query, example_graph())
|
||||
{:ok, [
|
||||
%{name: ~L"Peter Goodguy", mbox: ~I<mailto:peter@example.org>},
|
||||
%{name: ~L"Johnny Lee Outlaw", mbox: ~I<mailto:jlow@example.com>}
|
||||
]}
|
||||
|
||||
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: <http://xmlns.com/foaf/0.1/> .
|
||||
@prefix ex: <http://example.com/> .
|
||||
|
||||
ex:Outlaw
|
||||
foaf:name "Johnny Lee Outlaw" ;
|
||||
foaf:mbox <mailto:jlow@example.com> .
|
||||
|
||||
ex:Goodguy
|
||||
foaf:name "Peter Goodguy" ;
|
||||
foaf:mbox <mailto:peter@example.org> ;
|
||||
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<mailto:peter@example.org>},
|
||||
]
|
||||
|
||||
"""
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
defmodule RDF.Query.BGP.Matcher do
|
||||
@moduledoc """
|
||||
@moduledoc !"""
|
||||
An interface for various BGP matching algorithm implementations.
|
||||
"""
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
defmodule RDF.Query.BGP.Simple do
|
||||
@moduledoc false
|
||||
|
||||
@behaviour RDF.Query.BGP.Matcher
|
||||
|
||||
alias RDF.Query.BGP
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
defmodule RDF.Query.BGP.Stream do
|
||||
@moduledoc false
|
||||
|
||||
@behaviour RDF.Query.BGP.Matcher
|
||||
|
||||
alias RDF.Query.BGP
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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: <http://xmlns.com/foaf/0.1/> .
|
||||
@prefix ex: <http://example.com/> .
|
||||
|
||||
ex:Outlaw
|
||||
foaf:name "Johnny Lee Outlaw" ;
|
||||
foaf:mbox <mailto:jlow@example.com> .
|
||||
|
||||
ex:Goodguy
|
||||
foaf:name "Peter Goodguy" ;
|
||||
foaf:mbox <mailto:peter@example.org> ;
|
||||
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
|
||||
|
|
Loading…
Reference in a new issue