/
statement.ex
214 lines (170 loc) · 8.58 KB
/
statement.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
defmodule RDF.Statement do
@moduledoc """
Helper functions for RDF statements.
A RDF statement is either a `RDF.Triple` or a `RDF.Quad`.
"""
alias RDF.{BlankNode, IRI, Literal, Quad, Term, Triple, PropertyMap}
import RDF.Guards
@type subject :: IRI.t() | BlankNode.t()
@type predicate :: IRI.t() | BlankNode.t()
@type object :: IRI.t() | BlankNode.t() | Literal.t()
@type graph_name :: IRI.t() | BlankNode.t() | nil
@type coercible_subject :: subject | atom | String.t()
@type coercible_predicate :: predicate | atom | String.t()
@type coercible_object :: object | any
@type coercible_graph_name :: graph_name | atom | String.t()
@type position :: :subject | :predicate | :object | :graph_name
@type qualified_term :: {position, Term.t() | nil}
@type term_mapping :: (qualified_term -> any | nil)
@type t :: Triple.t() | Quad.t()
@type coercible_t ::
{coercible_subject(), coercible_predicate(), coercible_object(), coercible_graph_name()}
| {coercible_subject(), coercible_predicate(), coercible_object()}
@doc """
Creates a `RDF.Statement` tuple with proper RDF values.
An error is raised when the given elements are not coercible to RDF values.
## Examples
iex> RDF.Statement.coerce {"http://example.com/S", "http://example.com/p", 42}
{~I<http://example.com/S>, ~I<http://example.com/p>, RDF.literal(42)}
iex> RDF.Statement.coerce {"http://example.com/S", "http://example.com/p", 42, "http://example.com/Graph"}
{~I<http://example.com/S>, ~I<http://example.com/p>, RDF.literal(42), ~I<http://example.com/Graph>}
"""
@spec coerce(coercible_t()) :: Triple.t() | Quad.t()
def coerce(statement)
def coerce({_, _, _} = triple), do: Triple.new(triple)
def coerce({_, _, _, _} = quad), do: Quad.new(quad)
@doc false
@spec coerce_subject(coercible_subject) :: subject
def coerce_subject(iri)
def coerce_subject(iri = %IRI{}), do: iri
def coerce_subject(bnode = %BlankNode{}), do: bnode
def coerce_subject("_:" <> identifier), do: RDF.bnode(identifier)
def coerce_subject(iri) when maybe_ns_term(iri) or is_binary(iri), do: RDF.iri!(iri)
def coerce_subject(arg), do: raise(RDF.Triple.InvalidSubjectError, subject: arg)
@doc false
@spec coerce_predicate(coercible_predicate) :: predicate
def coerce_predicate(iri)
def coerce_predicate(iri = %IRI{}), do: iri
# Note: Although, RDF does not allow blank nodes for properties, JSON-LD allows
# them, by introducing the notion of "generalized RDF".
# TODO: Support an option `:strict_rdf` to explicitly disallow them or produce warnings or ...
def coerce_predicate(bnode = %BlankNode{}), do: bnode
def coerce_predicate(iri) when maybe_ns_term(iri) or is_binary(iri), do: RDF.iri!(iri)
def coerce_predicate(arg), do: raise(RDF.Triple.InvalidPredicateError, predicate: arg)
@doc false
@spec coerce_predicate(coercible_predicate, PropertyMap.t()) :: predicate
def coerce_predicate(term, context)
def coerce_predicate(term, %PropertyMap{} = property_map) when is_atom(term) do
PropertyMap.iri(property_map, term) || coerce_predicate(term)
end
def coerce_predicate(term, _), do: coerce_predicate(term)
@doc false
@spec coerce_object(coercible_object) :: object
def coerce_object(iri)
def coerce_object(iri = %IRI{}), do: iri
def coerce_object(literal = %Literal{}), do: literal
def coerce_object(bnode = %BlankNode{}), do: bnode
def coerce_object(bool) when is_boolean(bool), do: Literal.new(bool)
def coerce_object(atom) when maybe_ns_term(atom), do: RDF.iri(atom)
def coerce_object(arg), do: Literal.new(arg)
@doc false
@spec coerce_graph_name(coercible_graph_name) :: graph_name
def coerce_graph_name(iri)
def coerce_graph_name(nil), do: nil
def coerce_graph_name(iri = %IRI{}), do: iri
def coerce_graph_name(bnode = %BlankNode{}), do: bnode
def coerce_graph_name("_:" <> identifier), do: RDF.bnode(identifier)
def coerce_graph_name(iri) when maybe_ns_term(iri) or is_binary(iri), do: RDF.iri!(iri)
def coerce_graph_name(arg),
do: raise(RDF.Quad.InvalidGraphContextError, graph_context: arg)
@doc """
Returns a tuple of native Elixir values from a `RDF.Statement` of RDF terms.
When a `:context` option is given with a `RDF.PropertyMap`, predicates will
be mapped to the terms defined in the `RDF.PropertyMap`, if present.
Returns `nil` if one of the components of the given tuple is not convertible via `RDF.Term.value/1`.
## Examples
iex> RDF.Statement.values {~I<http://example.com/S>, ~I<http://example.com/p>, RDF.literal(42)}
{"http://example.com/S", "http://example.com/p", 42}
iex> RDF.Statement.values {~I<http://example.com/S>, ~I<http://example.com/p>, RDF.literal(42), ~I<http://example.com/Graph>}
{"http://example.com/S", "http://example.com/p", 42, "http://example.com/Graph"}
iex> {~I<http://example.com/S>, ~I<http://example.com/p>, RDF.literal(42)}
...> |> RDF.Statement.values(context: %{p: ~I<http://example.com/p>})
{"http://example.com/S", :p, 42}
"""
@spec values(t, keyword) :: Triple.t_values() | Quad.t_values() | nil
def values(quad, opts \\ [])
def values({_, _, _} = triple, opts), do: Triple.values(triple, opts)
def values({_, _, _, _} = quad, opts), do: Quad.values(quad, opts)
@doc """
Returns a tuple of native Elixir values from a `RDF.Statement` of RDF terms.
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> {~I<http://example.com/S>, ~I<http://example.com/p>, RDF.literal(42), ~I<http://example.com/Graph>}
...> |> RDF.Statement.map(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<http://example.com/Graph>}
"""
@spec map(t, term_mapping()) :: Triple.t_values() | Quad.t_values() | nil | nil
def map(statement, fun)
def map({_, _, _} = triple, fun), do: RDF.Triple.map(triple, fun)
def map({_, _, _, _} = quad, fun), do: RDF.Quad.map(quad, fun)
@doc false
@spec default_term_mapping(qualified_term) :: any | nil
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)
@spec default_property_mapping(PropertyMap.t()) :: term_mapping
def default_property_mapping(%PropertyMap{} = property_map) do
fn
{:predicate, predicate} ->
PropertyMap.term(property_map, predicate) || default_term_mapping({:predicate, predicate})
other ->
default_term_mapping(other)
end
end
@doc """
Checks if the given tuple is a valid RDF statement, i.e. RDF triple or quad.
The elements of a valid RDF statement must be RDF terms. On the subject
position only IRIs and blank nodes allowed, while on the predicate and graph
context position only IRIs allowed. The object position can be any RDF term.
"""
@spec valid?(Triple.t() | Quad.t() | any) :: boolean
def valid?(tuple)
def valid?({subject, predicate, object}) do
valid_subject?(subject) && valid_predicate?(predicate) && valid_object?(object)
end
def valid?({subject, predicate, object, graph_name}) do
valid_subject?(subject) && valid_predicate?(predicate) && valid_object?(object) &&
valid_graph_name?(graph_name)
end
def valid?(_), do: false
@spec valid_subject?(subject | any) :: boolean
def valid_subject?(%IRI{}), do: true
def valid_subject?(%BlankNode{}), do: true
def valid_subject?(_), do: false
@spec valid_predicate?(predicate | any) :: boolean
def valid_predicate?(%IRI{}), do: true
def valid_predicate?(_), do: false
@spec valid_object?(object | any) :: boolean
def valid_object?(%IRI{}), do: true
def valid_object?(%BlankNode{}), do: true
def valid_object?(%Literal{}), do: true
def valid_object?(_), do: false
@spec valid_graph_name?(graph_name | any) :: boolean
def valid_graph_name?(%IRI{}), do: true
def valid_graph_name?(_), do: false
end