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

rdf-elixir / jsonld-ex / 821274639a4f4544e94b3770488836b6fe2e1cbd

09 Apr 2025 05:46PM UTC coverage: 91.542% (-2.0%) from 93.513%
821274639a4f4544e94b3770488836b6fe2e1cbd

push

github

marcelotto
Update dependencies

1775 of 1939 relevant lines covered (91.54%)

4387.12 hits per line

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

96.81
/lib/json/ld/decoder.ex
1
defmodule JSON.LD.Decoder do
2
  @moduledoc """
3
  A decoder for JSON-LD serializations to `RDF.Dataset`s.
4

5
  As for all decoders of `RDF.Serialization.Format`s, you normally won't use these
6
  functions directly, but via one of the `read_` functions on the `JSON.LD` format
7
  module or the generic `RDF.Serialization` module.
8
  """
9

10
  use RDF.Serialization.Decoder
11

12
  import JSON.LD.{NodeIdentifierMap, Utils}
13

14
  alias JSON.LD.{NodeIdentifierMap, Options}
15
  alias RDF.{BlankNode, Dataset, Graph, IRI, Literal, NS, Statement, XSD}
16

17
  @rdf_direction RDF.__base_iri__() <> "direction"
18
  @rdf_language RDF.__base_iri__() <> "language"
19

20
  @impl RDF.Serialization.Decoder
21
  @spec decode(String.t(), keyword) :: {:ok, Dataset.t() | Graph.t()} | {:error, any}
22
  def decode(content, opts \\ []) do
23
    with {:ok, json_ld_object} <- parse_json(content) do
2,080✔
24
      {:ok, to_rdf(json_ld_object, opts)}
25
    end
26
  end
27

28
  @spec to_rdf(JSON.LD.input(), Options.t() | Enum.t()) :: Dataset.t() | Graph.t()
29
  def to_rdf(input, options \\ %Options{}) do
30
    {:ok, node_id_map} = NodeIdentifierMap.start_link()
2,096✔
31

32
    options = Options.new(options)
2,096✔
33

34
    try do
2,096✔
35
      input
36
      |> JSON.LD.expand(%{options | ordered: false})
37
      |> JSON.LD.node_map(node_id_map)
38
      |> do_to_rdf(node_id_map, options)
2,096✔
39
    after
40
      NodeIdentifierMap.stop(node_id_map)
2,096✔
41
    end
42
  end
43

44
  defp do_to_rdf(node_map, node_id_map, options) do
45
    node_map
46
    |> Enum.sort_by(fn {graph_name, _} -> graph_name end)
1,988✔
47
    |> Enum.reduce(Dataset.new(), fn {graph_name, graph}, dataset ->
1,688✔
48
      # 1.1)
49
      if graph_name != "@default" and not well_formed_iri?(graph_name) and
1,988✔
50
           not blank_node_id?(graph_name) do
212✔
51
        dataset
4✔
52
      else
53
        # 1.3)
54
        rdf_graph =
1,984✔
55
          graph
56
          |> Enum.sort_by(fn {subject, _} -> subject end)
4,708✔
57
          |> Enum.reduce(Graph.new(), fn {subject, node}, rdf_graph ->
58
            # 1.3.1)
59
            if not well_formed_iri?(subject) and not blank_node_id?(subject) do
4,708✔
60
              rdf_graph
24✔
61
            else
62
              # 1.3.2)
63
              node
64
              |> Enum.sort_by(fn {property, _} -> property end)
9,344✔
65
              |> Enum.reduce(rdf_graph, fn {property, values}, rdf_graph ->
4,684✔
66
                cond do
9,344✔
67
                  # 1.3.2.1)
68
                  property == "@type" ->
69
                    if subject = node_to_rdf(subject) do
420✔
70
                      objects = values |> Enum.map(&node_to_rdf/1) |> Enum.reject(&is_nil/1)
420✔
71

72
                      Graph.add(rdf_graph, {subject, NS.RDF.type(), objects})
420✔
73
                    else
74
                      rdf_graph
×
75
                    end
76

77
                  # 1.3.2.2)
78
                  JSON.LD.keyword?(property) ->
8,924✔
79
                    rdf_graph
4,784✔
80

81
                  # 1.3.2.3)
82
                  not options.produce_generalized_rdf and blank_node_id?(property) ->
4,140✔
83
                    rdf_graph
4✔
84

85
                  # 1.3.2.4)
86
                  not well_formed_iri?(property) ->
4,136✔
87
                    rdf_graph
16✔
88

89
                  # 1.3.2.5)
90
                  true ->
4,120✔
91
                    Enum.reduce(values, rdf_graph, fn item, rdf_graph ->
4,120✔
92
                      case object_to_rdf(item, node_id_map, options) do
4,744✔
93
                        {_list_triples, nil} ->
94
                          rdf_graph
12✔
95

96
                        {list_triples, object} ->
97
                          rdf_graph
98
                          |> Graph.add({node_to_rdf(subject), node_to_rdf(property), object})
99
                          |> Graph.add(list_triples)
4,732✔
100
                      end
101
                    end)
102
                end
103
              end)
104
            end
105
          end)
106

107
        if Enum.empty?(rdf_graph) do
1,984✔
108
          dataset
84✔
109
        else
110
          graph_name = if graph_name == "@default", do: nil, else: graph_name
1,900✔
111
          Dataset.add(dataset, rdf_graph, graph: graph_name)
1,900✔
112
        end
113
      end
114
    end)
115
  end
116

117
  defp well_formed_iri?(iri) do
118
    valid_uri?(iri)
18,888✔
119
  end
120

121
  @spec parse_json(String.t(), [Jason.decode_opt()]) ::
122
          {:ok, map} | {:error, Jason.DecodeError.t()}
123
  def parse_json(content, _opts \\ []) do
124
    Jason.decode(content)
2,080✔
125
  end
126

127
  @spec parse_json!(String.t(), [Jason.decode_opt()]) :: map
128
  def parse_json!(content, _opts \\ []) do
129
    Jason.decode!(content)
×
130
  end
131

132
  @spec node_to_rdf(String.t()) :: IRI.t() | BlankNode.t() | nil
133
  def node_to_rdf(node) do
134
    cond do
12,600✔
135
      blank_node_id?(node) -> node |> String.trim_leading("_:") |> RDF.bnode()
2,856✔
136
      well_formed_iri?(node) -> RDF.uri(node)
9,744✔
137
      true -> nil
20✔
138
    end
139
  end
140

141
  # Object to RDF Conversion - https://www.w3.org/TR/json-ld11-api/#object-to-rdf-conversion
142
  defp object_to_rdf(item, node_id_map, options)
143
  # 1) and 2)
144
  defp object_to_rdf(%{"@id" => id}, _node_id_map, _options) do
2,160✔
145
    {[], node_to_rdf(id)}
146
  end
147

148
  # 3)
149
  defp object_to_rdf(%{"@list" => list}, node_id_map, options) do
150
    list_to_rdf(list, node_id_map, options)
232✔
151
  end
152

153
  # 4)
154
  defp object_to_rdf(%{"@value" => value} = item, _node_id_map, options) do
155
    # 5)
156
    datatype = item["@type"]
2,660✔
157

158
    {value, datatype} =
2,660✔
159
      cond do
160
        # 6)
161
        not is_nil(datatype) and relative_iri?(datatype) and datatype != "@json" ->
2,660✔
162
          {nil, datatype}
163

164
        # 7)
165
        Map.has_key?(item, "@language") and not relative_iri?(item["@language"]) ->
2,660✔
166
          {nil, datatype}
167

168
        # 8)
169
        datatype == "@json" ->
2,660✔
170
          value =
124✔
171
            value
172
            |> RDF.JSON.new(as_value: true)
173
            |> RDF.JSON.canonical()
174
            |> RDF.JSON.lexical()
175

176
          {value, RDF.iri(RDF.JSON)}
177

178
        # 9)
179
        is_boolean(value) ->
2,536✔
180
          value =
104✔
181
            value
182
            |> XSD.Boolean.new()
183
            |> XSD.Boolean.canonical()
184
            |> XSD.Boolean.lexical()
185

186
          datatype = if is_nil(datatype), do: NS.XSD.boolean(), else: datatype
104✔
187
          {value, datatype}
188

189
        # 10)
190
        is_number(value) and
2,216✔
191
            (datatype == to_string(NS.XSD.double()) or value != trunc(value) or
216✔
192
               value >= 1.0e21) ->
2,432✔
193
          value =
40✔
194
            value
195
            |> XSD.Double.new()
196
            |> XSD.Double.canonical()
197
            |> XSD.Double.lexical()
198

199
          datatype = if is_nil(datatype), do: NS.XSD.double(), else: datatype
40✔
200
          {value, datatype}
201

202
        # 11)
203
        is_number(value) ->
2,392✔
204
          value =
176✔
205
            if(is_float(value), do: trunc(value), else: value)
176✔
206
            |> XSD.Integer.new()
207
            |> XSD.Integer.canonical()
208
            |> XSD.Integer.lexical()
209

210
          datatype = if is_nil(datatype), do: NS.XSD.integer(), else: datatype
176✔
211
          {value, datatype}
212

213
        # 12)
214
        is_nil(datatype) ->
2,216✔
215
          datatype =
2,108✔
216
            if Map.has_key?(item, "@language"), do: RDF.langString(), else: NS.XSD.string()
2,108✔
217

218
          {value, datatype}
219

220
        true ->
108✔
221
          {value, datatype}
222
      end
223

224
    cond do
2,660✔
225
      is_nil(value) ->
×
226
        {[], nil}
227

228
      # 13)
229
      Map.has_key?(item, "@direction") and not is_nil(options.rdf_direction) ->
2,660✔
230
        # 13.1)
231
        language = String.downcase(item["@language"] || "")
32✔
232
        direction = item["@direction"]
32✔
233

234
        case options.rdf_direction do
32✔
235
          # 13.2)
236
          "i18n-datatype" ->
16✔
237
            {[],
238
             Literal.new(value, datatype: "https://www.w3.org/ns/i18n##{language}_#{direction}")}
16✔
239

240
          # 13.3)
241
          "compound-literal" ->
242
            literal = RDF.bnode()
16✔
243

244
            list_triples =
16✔
245
              [
246
                {literal, RDF.value(), value},
247
                {literal, @rdf_direction, direction}
248
              ]
249

250
            list_triples =
16✔
251
              if Map.has_key?(item, "@language") do
8✔
252
                [{literal, @rdf_language, language} | list_triples]
253
              else
254
                list_triples
8✔
255
              end
256

257
            {list_triples, literal}
258
        end
259

260
      # 14)
261
      language = item["@language"] ->
2,628✔
262
        {
263
          [],
264
          if(valid_language?(language),
4✔
265
            do: Literal.new(value, language: language, canonicalize: true)
208✔
266
          )
267
        }
268

269
      true ->
2,416✔
270
        {[], Literal.new(value, datatype: datatype, canonicalize: true)}
271
    end
272
  end
273

274
  @spec list_to_rdf([map], pid, Options.t()) :: {[Statement.t()], IRI.t() | BlankNode.t()}
275
  defp list_to_rdf(list, node_id_map, options) do
276
    {list_triples, first, last} =
232✔
277
      Enum.reduce(list, {[], nil, nil}, fn item, {list_triples, first, last} ->
278
        case object_to_rdf(item, node_id_map, options) do
308✔
279
          {more_list_triples, object} ->
280
            bnode = RDF.bnode(generate_blank_node_id(node_id_map))
308✔
281
            list_triples = more_list_triples ++ list_triples
308✔
282
            object_triples = if object, do: [{bnode, NS.RDF.first(), object}], else: []
308✔
283

284
            if last do
308✔
285
              {list_triples ++ [{last, NS.RDF.rest(), bnode} | object_triples], first, bnode}
124✔
286
            else
287
              {object_triples ++ list_triples, bnode, bnode}
184✔
288
            end
289
        end
290
      end)
291

292
    if last do
232✔
293
      {list_triples ++ [{last, NS.RDF.rest(), NS.RDF.nil()}], first}
294
    else
295
      {[], NS.RDF.nil()}
296
    end
297
  end
298
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

© 2026 Coveralls, Inc