• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rdf-elixir / rdf-ex / e13c6dddfa3dcff42f19061d70f6a1c7c0caafb8-PR-17

pending completion
e13c6dddfa3dcff42f19061d70f6a1c7c0caafb8-PR-17

Pull #17

github

marcelotto
Pull Request #17: Add implementation of the RDF Dataset canonicalization algorithm

166 of 166 new or added lines in 9 files covered. (100.0%)

5349 of 7224 relevant lines covered (74.04%)

567.51 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.57
/lib/rdf/model/statement.ex
1
defmodule RDF.Statement do
2
  @moduledoc """
3
  Helper functions for RDF statements.
4

5
  An RDF statement is either a `RDF.Triple` or a `RDF.Quad`.
6
  """
7

8
  alias RDF.{Resource, BlankNode, IRI, Literal, Quad, Term, Triple, PropertyMap}
9
  import RDF.Guards
10

11
  @type subject :: Resource.t()
12
  @type predicate :: Resource.t()
13
  @type object :: Resource.t() | Literal.t()
14
  @type graph_name :: Resource.t() | nil
15

16
  @type coercible_subject :: Resource.coercible()
17
  @type coercible_predicate :: Resource.coercible()
18
  @type coercible_object :: object | any
19
  @type coercible_graph_name :: graph_name | atom | String.t()
20

21
  @type position :: :subject | :predicate | :object | :graph_name
22
  @type qualified_term :: {position, Term.t() | nil}
23
  @type term_mapping :: (qualified_term -> any | nil)
24

25
  @type t :: Triple.t() | Quad.t()
26
  @type coercible :: Triple.coercible() | Quad.coercible()
27
  # deprecated: This will be removed in v0.11.
28
  @type coercible_t :: coercible
29

30
  @doc """
31
  Creates a `RDF.Triple` or `RDF.Quad` with proper RDF values.
32

33
  An error is raised when the given elements are not coercible to RDF values.
34

35
  Note: The `RDF.statement` function is a shortcut to this function.
36

37
  ## Examples
38

39
      iex> RDF.Statement.new({EX.S, EX.p, 42})
40
      {RDF.iri("http://example.com/S"), RDF.iri("http://example.com/p"), RDF.literal(42)}
41

42
      iex> RDF.Statement.new({EX.S, EX.p, 42, EX.Graph})
43
      {RDF.iri("http://example.com/S"), RDF.iri("http://example.com/p"), RDF.literal(42), RDF.iri("http://example.com/Graph")}
44

45
      iex> RDF.Statement.new({EX.S, :p, 42, EX.Graph}, RDF.PropertyMap.new(p: EX.p))
46
      {RDF.iri("http://example.com/S"), RDF.iri("http://example.com/p"), RDF.literal(42), RDF.iri("http://example.com/Graph")}
47
  """
48
  def new(tuple, property_map \\ nil)
2✔
49
  def new({_, _, _} = tuple, property_map), do: Triple.new(tuple, property_map)
1✔
50
  def new({_, _, _, _} = tuple, property_map), do: Quad.new(tuple, property_map)
2✔
51

52
  defdelegate new(s, p, o), to: Triple, as: :new
×
53
  defdelegate new(s, p, o, g), to: Quad, as: :new
×
54

55
  @doc """
56
  The subject component of a statement.
57

58
  ## Examples
59

60
      iex> RDF.Statement.subject {"http://example.com/S", "http://example.com/p", 42}
61
      ~I<http://example.com/S>
62
  """
63
  def subject(statement) when tuple_size(statement) in [3, 4],
64
    do: statement |> elem(0) |> coerce_subject()
8,797✔
65

66
  @doc """
67
  The predicate component of a statement.
68

69
  ## Examples
70

71
      iex> RDF.Statement.predicate {"http://example.com/S", "http://example.com/p", 42}
72
      ~I<http://example.com/p>
73
  """
74
  def predicate(statement) when tuple_size(statement) in [3, 4],
75
    do: statement |> elem(1) |> coerce_predicate()
8,793✔
76

77
  @doc """
78
  The object component of a statement.
79

80
  ## Examples
81

82
      iex> RDF.Statement.object {"http://example.com/S", "http://example.com/p", 42}
83
      RDF.literal(42)
84
  """
85
  def object(statement) when tuple_size(statement) in [3, 4],
86
    do: statement |> elem(2) |> coerce_object()
8,797✔
87

88
  @doc """
89
  The graph name component of a statement.
90

91
  ## Examples
92

93
      iex> RDF.Statement.graph_name {"http://example.com/S", "http://example.com/p", 42, "http://example.com/Graph"}
94
      ~I<http://example.com/Graph>
95
      iex> RDF.Statement.graph_name {"http://example.com/S", "http://example.com/p", 42}
96
      nil
97
  """
98
  def graph_name(statement)
99
  def graph_name({_, _, _, graph_name}), do: coerce_graph_name(graph_name)
21✔
100
  def graph_name({_, _, _}), do: nil
8,777✔
101

102
  @doc """
103
  Creates a `RDF.Statement` tuple with proper RDF values.
104

105
  An error is raised when the given elements are not coercible to RDF values.
106

107
  ## Examples
108

109
      iex> RDF.Statement.coerce {"http://example.com/S", "http://example.com/p", 42}
110
      {~I<http://example.com/S>, ~I<http://example.com/p>, RDF.literal(42)}
111
      iex> RDF.Statement.coerce {"http://example.com/S", "http://example.com/p", 42, "http://example.com/Graph"}
112
      {~I<http://example.com/S>, ~I<http://example.com/p>, RDF.literal(42), ~I<http://example.com/Graph>}
113
  """
114
  @spec coerce(coercible(), PropertyMap.t() | nil) :: Triple.t() | Quad.t()
115
  def coerce(statement, property_map \\ nil)
2✔
116
  def coerce({_, _, _} = triple, property_map), do: Triple.new(triple, property_map)
1✔
117
  def coerce({_, _, _, _} = quad, property_map), do: Quad.new(quad, property_map)
1✔
118

119
  @doc false
120
  @spec coerce_subject(coercible_subject) :: subject
121
  def coerce_subject(iri)
122
  def coerce_subject(%IRI{} = iri), do: iri
14,269✔
123
  def coerce_subject(%BlankNode{} = bnode), do: bnode
37,259✔
124
  def coerce_subject("_:" <> identifier), do: RDF.bnode(identifier)
×
125
  def coerce_subject(iri) when maybe_ns_term(iri) or is_binary(iri), do: RDF.iri!(iri)
4,886✔
126
  def coerce_subject(arg), do: raise(RDF.Triple.InvalidSubjectError, subject: arg)
1✔
127

128
  @doc false
129
  @spec coerce_predicate(coercible_predicate) :: predicate
130
  def coerce_predicate(iri)
131
  def coerce_predicate(%IRI{} = iri), do: iri
50,273✔
132
  # Note: Although, RDF does not allow blank nodes for properties, JSON-LD allows
133
  # them, by introducing the notion of "generalized RDF".
134
  # TODO: Support an option `:strict_rdf` to explicitly disallow them or produce warnings or ...
135
  def coerce_predicate(%BlankNode{} = bnode), do: bnode
×
136
  def coerce_predicate(iri) when maybe_ns_term(iri) or is_binary(iri), do: RDF.iri!(iri)
1,624✔
137
  def coerce_predicate(arg), do: raise(RDF.Triple.InvalidPredicateError, predicate: arg)
×
138

139
  @doc false
140
  @spec coerce_predicate(coercible_predicate, PropertyMap.t()) :: predicate
141
  def coerce_predicate(term, context)
142

143
  def coerce_predicate(term, %PropertyMap{} = property_map) when is_atom(term) do
144
    PropertyMap.iri(property_map, term) || coerce_predicate(term)
75✔
145
  end
146

147
  def coerce_predicate(term, _), do: coerce_predicate(term)
20,737✔
148

149
  @doc false
150
  @spec coerce_object(coercible_object) :: object
151
  def coerce_object(iri)
152
  def coerce_object(%IRI{} = iri), do: iri
8,857✔
153
  def coerce_object(%Literal{} = literal), do: literal
4,970✔
154
  def coerce_object(%BlankNode{} = bnode), do: bnode
26,403✔
155
  def coerce_object(bool) when is_boolean(bool), do: Literal.new(bool)
10✔
156
  def coerce_object(atom) when maybe_ns_term(atom), do: RDF.iri(atom)
4,007✔
157
  def coerce_object(arg), do: Literal.new(arg)
2,059✔
158

159
  @doc false
160
  @spec coerce_graph_name(coercible_graph_name) :: graph_name
161
  def coerce_graph_name(iri)
162
  def coerce_graph_name(nil), do: nil
33,461✔
163
  def coerce_graph_name(%IRI{} = iri), do: iri
537✔
164
  def coerce_graph_name(%BlankNode{} = bnode), do: bnode
283✔
165
  def coerce_graph_name("_:" <> identifier), do: RDF.bnode(identifier)
×
166
  def coerce_graph_name(iri) when maybe_ns_term(iri) or is_binary(iri), do: RDF.iri!(iri)
599✔
167

168
  def coerce_graph_name(arg),
169
    do: raise(RDF.Quad.InvalidGraphContextError, graph_context: arg)
×
170

171
  @doc """
172
  Returns a tuple of native Elixir values from a `RDF.Statement` of RDF terms.
173

174
  When a `:context` option is given with a `RDF.PropertyMap`, predicates will
175
  be mapped to the terms defined in the `RDF.PropertyMap`, if present.
176

177
  Returns `nil` if one of the components of the given tuple is not convertible via `RDF.Term.value/1`.
178

179
  ## Examples
180

181
      iex> RDF.Statement.values {~I<http://example.com/S>, ~I<http://example.com/p>, RDF.literal(42)}
182
      {"http://example.com/S", "http://example.com/p", 42}
183

184
      iex> RDF.Statement.values {~I<http://example.com/S>, ~I<http://example.com/p>, RDF.literal(42), ~I<http://example.com/Graph>}
185
      {"http://example.com/S", "http://example.com/p", 42, "http://example.com/Graph"}
186

187
      iex> {~I<http://example.com/S>, ~I<http://example.com/p>, RDF.literal(42)}
188
      ...> |> RDF.Statement.values(context: %{p: ~I<http://example.com/p>})
189
      {"http://example.com/S", :p, 42}
190

191
  """
192
  @spec values(t, keyword) :: Triple.mapping_value() | Quad.mapping_value() | nil
193
  def values(quad, opts \\ [])
2✔
194
  def values({_, _, _} = triple, opts), do: Triple.values(triple, opts)
2✔
195
  def values({_, _, _, _} = quad, opts), do: Quad.values(quad, opts)
1✔
196

197
  @doc """
198
  Returns a tuple of native Elixir values from a `RDF.Statement` of RDF terms.
199

200
  Returns `nil` if one of the components of the given tuple is not convertible via `RDF.Term.value/1`.
201

202
  The optional second argument allows to specify a custom mapping with a function
203
  which will receive a tuple `{statement_position, rdf_term}` where
204
  `statement_position` is one of the atoms `:subject`, `:predicate`, `:object` or
205
  `:graph_name`, while `rdf_term` is the RDF term to be mapped. When the given
206
  function returns `nil` this will be interpreted as an error and will become
207
  the overhaul result of the `values/2` call.
208

209
  ## Examples
210

211
      iex> {~I<http://example.com/S>, ~I<http://example.com/p>, RDF.literal(42), ~I<http://example.com/Graph>}
212
      ...> |> RDF.Statement.map(fn
213
      ...>      {:subject, subject} ->
214
      ...>        subject |> to_string() |> String.last()
215
      ...>      {:predicate, predicate} ->
216
      ...>        predicate |> to_string() |> String.last() |> String.to_atom()
217
      ...>      {:object, object} ->
218
      ...>        RDF.Term.value(object)
219
      ...>      {:graph_name, graph_name} ->
220
      ...>        graph_name
221
      ...>    end)
222
      {"S", :p, 42, ~I<http://example.com/Graph>}
223

224
  """
225
  @spec map(t, term_mapping()) :: Triple.mapping_value() | Quad.mapping_value() | nil | nil
226
  def map(statement, fun)
227
  def map({_, _, _} = triple, fun), do: RDF.Triple.map(triple, fun)
266✔
228
  def map({_, _, _, _} = quad, fun), do: RDF.Quad.map(quad, fun)
7,633✔
229

230
  @doc false
231
  @spec default_term_mapping(qualified_term) :: any | nil
232
  def default_term_mapping(qualified_term)
233
  def default_term_mapping({:graph_name, nil}), do: nil
6✔
234
  def default_term_mapping({_, term}), do: RDF.Term.value(term)
157✔
235

236
  @spec default_property_mapping(PropertyMap.t()) :: term_mapping
237
  def default_property_mapping(%PropertyMap{} = property_map) do
238
    fn
18✔
239
      {:predicate, predicate} ->
240
        PropertyMap.term(property_map, predicate) || default_term_mapping({:predicate, predicate})
21✔
241

242
      other ->
243
        default_term_mapping(other)
48✔
244
    end
245
  end
246

247
  @doc """
248
  Checks if the given tuple is a valid RDF statement, i.e. RDF triple or quad.
249

250
  The elements of a valid RDF statement must be RDF terms. On the subject
251
  position only IRIs and blank nodes allowed, while on the predicate and graph
252
  context position only IRIs allowed. The object position can be any RDF term.
253
  """
254
  @spec valid?(Triple.t() | Quad.t() | any) :: boolean
255
  def valid?(tuple)
256

257
  def valid?({subject, predicate, object}) do
258
    valid_subject?(subject) && valid_predicate?(predicate) && valid_object?(object)
27✔
259
  end
260

261
  def valid?({subject, predicate, object, graph_name}) do
262
    valid_subject?(subject) && valid_predicate?(predicate) && valid_object?(object) &&
26✔
263
      valid_graph_name?(graph_name)
20✔
264
  end
265

266
  def valid?(_), do: false
7✔
267

268
  @spec valid_subject?(subject | any) :: boolean
269
  def valid_subject?(%IRI{}), do: true
123✔
270
  def valid_subject?(%BlankNode{}), do: true
32✔
271
  def valid_subject?(_), do: false
16✔
272

273
  @spec valid_predicate?(predicate | any) :: boolean
274
  def valid_predicate?(%IRI{}), do: true
151✔
275
  def valid_predicate?(_), do: false
24✔
276

277
  @spec valid_object?(object | any) :: boolean
278
  def valid_object?(%IRI{}), do: true
72✔
279
  def valid_object?(%BlankNode{}), do: true
16✔
280
  def valid_object?(%Literal{}), do: true
35✔
281
  def valid_object?(_), do: false
4✔
282

283
  @spec valid_graph_name?(graph_name | any) :: boolean
284
  def valid_graph_name?(%IRI{}), do: true
44✔
285
  def valid_graph_name?(_), do: false
8✔
286

287
  def has_bnode?({_, _, _, _} = quad), do: Quad.has_bnode?(quad)
12✔
288
  def has_bnode?({_, _, _} = triple), do: Triple.has_bnode?(triple)
325✔
289

290
  @doc """
291
  Returns a list of all `RDF.BlankNode`s within the given `statement`.
292
  """
293
  @spec bnodes(t) :: list(BlankNode.t())
294
  def bnodes(statement)
295
  def bnodes({_, _, _, _} = quad), do: Quad.bnodes(quad)
15✔
296
  def bnodes({_, _, _} = triple), do: Triple.bnodes(triple)
328✔
297

298
  def include_value?({_, _, _, _} = quad, value), do: Quad.include_value?(quad, value)
×
299
  def include_value?({_, _, _} = triple, value), do: Triple.include_value?(triple, value)
×
300
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc