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

rdf-elixir / jsonld-ex / de3ca9b1dae3a8dffda3dec6d69d62a58ee3622e

10 Apr 2025 03:31PM UTC coverage: 91.542%. Remained the same
de3ca9b1dae3a8dffda3dec6d69d62a58ee3622e

push

github

marcelotto
Remove unused HTML versions of test manifests

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

89.82
/lib/json/ld/encoder.ex
1
defmodule JSON.LD.Encoder do
2
  @moduledoc """
3
  An encoder for JSON-LD serializations of RDF.ex data structures.
4

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

9

10
  ## Options
11

12
  - `:context`: When a context map or remote context URL string is given,
13
    compaction is performed using this context
14
  - `:base`: : Allows to specify a base URI to be used during compaction
15
    (only when `:context` is provided).
16
    Default is the base IRI of the encoded graph or if none present or in case of
17
    encoded datasets the `RDF.default_base_iri/0`.
18
  - `:use_native_types`: If this flag is set to `true`, RDF literals with a datatype IRI
19
    that equals `xsd:integer` or `xsd:double` are converted to a JSON numbers and
20
    RDF literals with a datatype IRI that equals `xsd:boolean` are converted to `true`
21
    or `false` based on their lexical form. (default: `false`)
22
  - `:use_rdf_type`: Unless this flag is set to `true`, `rdf:type` predicates will be
23
    serialized as `@type` as long as the associated object is either an IRI or blank
24
    node identifier. (default: `false`)
25

26
  The given options are also passed through to `Jason.encode/2`, so you can also
27
  provide any the options this function supports, most notably the `:pretty` option.
28

29
  """
30

31
  use RDF.Serialization.Encoder
32

33
  alias JSON.LD.Options
34

35
  alias RDF.{
36
    BlankNode,
37
    Dataset,
38
    Description,
39
    Graph,
40
    IRI,
41
    LangString,
42
    Literal,
43
    NS,
44
    XSD
45
  }
46

47
  import JSON.LD.Utils
48
  import RDF.Guards
49

50
  @type input :: Dataset.t() | Description.t() | Graph.t()
51

52
  @rdf_type to_string(RDF.NS.RDF.type())
53
  @rdf_value to_string(RDF.NS.RDF.value())
54
  @rdf_nil to_string(RDF.NS.RDF.nil())
55
  @rdf_first to_string(RDF.NS.RDF.first())
56
  @rdf_rest to_string(RDF.NS.RDF.rest())
57
  @rdf_list to_string(RDF.uri(RDF.NS.RDF.List))
58
  @rdf_direction RDF.__base_iri__() <> "direction"
59
  @rdf_language RDF.__base_iri__() <> "language"
60

61
  @impl RDF.Serialization.Encoder
62
  @spec encode(RDF.Data.t(), keyword) :: {:ok, String.t()} | {:error, any}
63
  def encode(data, opts \\ []) do
64
    opts = set_base_iri(data, opts)
40✔
65

66
    with {:ok, json_ld_object} <- from_rdf(data, opts),
40✔
67
         {:ok, json_ld_object} <- maybe_compact(json_ld_object, opts) do
40✔
68
      encode_json(json_ld_object, opts)
40✔
69
    end
70
  end
71

72
  defp set_base_iri(%Graph{base_iri: base_iri}, opts) when not is_nil(base_iri) do
73
    Keyword.put_new(opts, :base, IRI.to_string(base_iri))
8✔
74
  end
75

76
  defp set_base_iri(_, opts) do
77
    if base = RDF.default_base_iri() do
32✔
78
      Keyword.put_new(opts, :base, IRI.to_string(base))
×
79
    else
80
      opts
32✔
81
    end
82
  end
83

84
  defp maybe_compact(json_ld_object, opts) do
85
    if context = Keyword.get(opts, :context) do
40✔
86
      {:ok, JSON.LD.compact(json_ld_object, context, opts)}
87
    else
88
      {:ok, json_ld_object}
89
    end
90
  end
91

92
  @spec from_rdf(RDF.Data.t(), Options.t() | Enum.t()) :: {:ok, [map]} | {:error, any}
93
  def from_rdf(dataset, options \\ %Options{}) do
94
    {:ok, from_rdf!(dataset, options)}
95
  rescue
96
    exception ->
×
97
      {:error, Exception.message(exception)}
98
  end
99

100
  @spec from_rdf!(RDF.Data.t(), Options.t() | Enum.t()) :: [map]
101
  def from_rdf!(rdf_data, options \\ %Options{})
×
102

103
  def from_rdf!(%Dataset{} = dataset, options) do
104
    options = Options.new(options)
804✔
105

106
    {graph_map, referenced_once, compound_literal_subjects} =
804✔
107
      Enum.reduce(Dataset.graphs(dataset), {%{}, %{}, %{}}, fn
108
        graph, {graph_map, referenced_once, compound_literal_subjects} ->
109
          # 5.1)
110
          name = to_string(graph.name || "@default")
860✔
111
          # 5.2)
112
          graph_map = Map.put_new(graph_map, name, %{})
860✔
113
          # 5.3)
114
          compound_literal_subjects = Map.put_new(compound_literal_subjects, name, %{})
860✔
115

116
          # 5.4)
117
          graph_map =
860✔
118
            if graph.name && !get_in(graph_map, ["@default", name]) do
860✔
119
              Map.update(graph_map, "@default", %{name => %{"@id" => name}}, fn default_graph ->
76✔
120
                Map.put(default_graph, name, %{"@id" => name})
36✔
121
              end)
122
            else
123
              graph_map
784✔
124
            end
125

126
          # 5.5) & 5.6) & 5.7)
127
          {node_map, referenced_once, compound_map} =
860✔
128
            process_graph_triples(
129
              graph,
130
              graph_map[name],
131
              referenced_once,
132
              compound_literal_subjects[name],
133
              options
134
            )
135

136
          {
852✔
137
            Map.put(graph_map, name, node_map),
138
            referenced_once,
139
            Map.put(compound_literal_subjects, name, compound_map)
140
          }
141
      end)
142

143
    # 6)
144
    {graph_map, _referenced_once} =
796✔
145
      Enum.reduce(graph_map, {%{}, referenced_once}, fn
146
        {name, graph_object}, {graph_map, referenced_once} ->
147
          # 6.1)
148
          {graph_object, referenced_once} =
892✔
149
            process_compound_literals(
150
              graph_object,
151
              compound_literal_subjects[name],
152
              referenced_once
153
            )
154

155
          # 6.2) - 6.4)
156
          {graph_object, referenced_once} = convert_list(graph_object, referenced_once)
892✔
157

158
          {Map.put(graph_map, name, graph_object), referenced_once}
159
      end)
160

161
    # 7+8)
162
    graph_map
163
    |> Map.get("@default", %{})
164
    |> maybe_sort_by(options.ordered, fn {subject, _} -> subject end)
796✔
165
    |> Enum.reduce([], fn {subject, node}, result ->
166
      # 8.1)
167
      node =
1,380✔
168
        if Map.has_key?(graph_map, subject) do
169
          Map.put(
100✔
170
            node,
171
            "@graph",
172
            graph_map[subject]
173
            |> maybe_sort_by(options.ordered, fn {s, _} -> s end)
100✔
174
            |> Enum.reduce([], fn {_s, n}, graph_nodes ->
175
              n = Map.delete(n, :usages)
276✔
176

177
              if map_size(n) == 1 and Map.has_key?(n, "@id") do
276✔
178
                graph_nodes
152✔
179
              else
180
                [n | graph_nodes]
181
              end
182
            end)
183
            |> Enum.reverse()
184
          )
185
        else
186
          node
1,280✔
187
        end
188

189
      # 8.2)
190
      node = Map.delete(node, :usages)
1,380✔
191

192
      if map_size(node) == 1 and Map.has_key?(node, "@id") do
1,380✔
193
        result
368✔
194
      else
195
        [node | result]
196
      end
197
    end)
198
    |> Enum.reverse()
796✔
199
  end
200

201
  def from_rdf!(rdf_data, options),
202
    do: rdf_data |> Dataset.new() |> from_rdf!(options)
232✔
203

204
  # Process triples in the graph (steps 5.5 through 5.7)
205
  defp process_graph_triples(graph, node_map, referenced_once, compound_map, options) do
206
    Enum.reduce(graph, {node_map, referenced_once, compound_map}, fn
860✔
207
      {subject, predicate, object}, {node_map, referenced_once, compound_map} ->
208
        subject = to_string(subject)
2,604✔
209
        predicate = to_string(predicate)
2,604✔
210

211
        # 5.7.1)
212
        node_map = Map.put_new(node_map, subject, %{"@id" => subject})
2,604✔
213
        # 5.7.2)
214
        node = Map.get(node_map, subject)
2,604✔
215

216
        # 5.7.3) Handle the compound-literal direction case
217
        compound_map =
2,604✔
218
          if options.rdf_direction == "compound-literal" && predicate == @rdf_direction do
2,604✔
219
            Map.put(compound_map, subject, true)
40✔
220
          else
221
            compound_map
2,564✔
222
          end
223

224
        # 5.7.4)
225
        {object_id, node_map} =
2,604✔
226
          if is_node_object = is_rdf_resource(object) do
2,604✔
227
            object_id = to_string(object)
1,432✔
228
            node_map = Map.put_new(node_map, object_id, %{"@id" => object_id})
1,432✔
229
            {object_id, node_map}
230
          else
231
            {nil, node_map}
232
          end
233

234
        # 5.7.5)
235
        {node, node_map, referenced_once} =
2,604✔
236
          if is_node_object and !options.use_rdf_type and predicate == @rdf_type do
2,604✔
237
            node =
112✔
238
              Map.update(node, "@type", [object_id], fn types ->
239
                if object_id in types, do: types, else: types ++ [object_id]
×
240
              end)
241

242
            {node, node_map, referenced_once}
112✔
243
          else
244
            # 5.7.6)
245
            value = rdf_to_object(object, options)
2,492✔
246

247
            # 5.7.7) & 5.7.8)
248
            node =
2,484✔
249
              Map.update(node, predicate, [value], fn objects ->
250
                if value in objects, do: objects, else: objects ++ [value]
52✔
251
              end)
252

253
            {node_map, referenced_once} =
2,484✔
254
              cond do
255
                # 5.7.9)
256
                object_id == @rdf_nil ->
257
                  usage = %{node: subject, property: predicate, value: value}
336✔
258

259
                  node_map =
336✔
260
                    Map.update(node_map, @rdf_nil, %{usages: [usage]}, fn object_node ->
261
                      Map.update(object_node, :usages, [usage], &[usage | &1])
336✔
262
                    end)
263

264
                  {node_map, referenced_once}
265

266
                # 5.7.10)
267
                Map.has_key?(referenced_once, object_id) ->
2,148✔
268
                  {node_map, Map.put(referenced_once, object_id, false)}
269

270
                # 5.7.11)
271
                is_rdf_bnode(object) ->
2,124✔
272
                  {node_map,
273
                   Map.put(referenced_once, object_id, %{
274
                     # We're using here the node id as the reference to the respective graph map entry.
275
                     node: subject,
276
                     property: predicate,
277
                     value: value
278
                   })}
279

280
                true ->
1,396✔
281
                  {node_map, referenced_once}
282
              end
283

284
            {node, node_map, referenced_once}
2,484✔
285
          end
286

287
        {Map.put(node_map, subject, node), referenced_once, compound_map}
2,596✔
288
    end)
289
  end
290

291
  # 6.1)
292
  defp process_compound_literals(graph_object, nil, referenced_once),
40✔
293
    do: {graph_object, referenced_once}
294

295
  defp process_compound_literals(graph_object, compound_map, referenced_once) do
296
    Enum.reduce(compound_map, {graph_object, referenced_once}, fn
852✔
297
      {cl, _}, {graph_object, referenced_once} ->
298
        case referenced_once[cl] do
40✔
299
          # SPEC ISSUE: "6.1.4) Initialize value to value of value in cl entry." seems unnecessary, since value is never used
300
          %{node: node_id, property: property, value: _value} ->
301
            node = graph_object[node_id]
40✔
302
            # 6.1.5)
303
            case Map.pop(graph_object, cl) do
40✔
304
              {%{} = cl_node, graph_object} ->
305
                # 6.1.6)
306
                node
307
                |> Map.get(property, [])
308
                |> Enum.reduce({graph_object, referenced_once}, fn
40✔
309
                  %{"@id" => ^cl} = cl_reference, {graph_object, referenced_once} ->
310
                    cl_reference =
40✔
311
                      cl_reference
312
                      # 6.1.6.1)
313
                      |> Map.delete("@id")
314
                      # 6.1.6.2)
315
                      |> Map.put(
316
                        "@value",
317
                        (List.first(cl_node[@rdf_value] || []) || %{})["@value"]
40✔
318
                      )
319

320
                    # 6.1.6.3)
321
                    cl_reference =
40✔
322
                      case cl_node[@rdf_language] do
323
                        [%{"@value" => language} | _] ->
324
                          if not valid_language?(language) do
16✔
325
                            raise JSON.LD.Error.invalid_language_tagged_string(language)
×
326
                          end
327

328
                          Map.put(cl_reference, "@language", language)
16✔
329

330
                        _ ->
331
                          cl_reference
24✔
332
                      end
333

334
                    # 6.1.6.4)
335
                    cl_reference =
40✔
336
                      case cl_node[@rdf_direction] do
337
                        [%{"@value" => direction} | _] ->
338
                          if direction not in ~w[ltr rtl] do
40✔
339
                            raise JSON.LD.Error.invalid_base_direction(direction)
×
340
                          end
341

342
                          Map.put(cl_reference, "@direction", direction)
40✔
343

344
                        _ ->
345
                          cl_reference
×
346
                      end
347

348
                    {
349
                      update_in(graph_object, [node_id, property], fn props ->
350
                        Enum.map(props, fn
40✔
351
                          %{"@id" => ^cl} -> cl_reference
40✔
352
                          other -> other
×
353
                        end)
354
                      end),
355
                      Map.delete(referenced_once, cl)
356
                    }
357

358
                  _, {graph_object, referenced_once} ->
×
359
                    {graph_object, referenced_once}
360
                end)
361

362
              _ ->
×
363
                {graph_object, referenced_once}
364
            end
365

366
          _ ->
×
367
            {graph_object, referenced_once}
368
        end
369
    end)
370
  end
371

372
  #  # 6.3) - 6.4)
373
  defp convert_list(%{@rdf_nil => %{usages: usages}} = graph_object, referenced_once) do
374
    Enum.reduce(usages, {graph_object, referenced_once}, fn
208✔
375
      # 6.4.1)
376
      # Note: original_head is always an rdf:nil node
377
      %{node: node_id, property: property, value: original_head}, {graph_object, referenced_once} ->
378
        node = graph_object[node_id]
336✔
379

380
        # 6.4.2) & 6.4.3)
381
        {list, list_nodes, head_path, head} =
336✔
382
          extract_list(node, property, original_head, referenced_once, graph_object)
383

384
        updated_head =
336✔
385
          head
386
          # 6.4.4)
387
          |> Map.delete("@id")
388
          # 6.4.5) is not needed since extract_list returns the list in reverse order already
389
          # 6.4.6)
390
          |> Map.put("@list", list)
391

392
        {graph_object, referenced_once} =
336✔
393
          update_head(graph_object, referenced_once, head_path, head, updated_head)
394

395
        # 6.4.7)
396
        graph_object =
336✔
397
          Enum.reduce(list_nodes, graph_object, fn node_id, graph_object ->
398
            Map.delete(graph_object, node_id)
504✔
399
          end)
400

401
        {graph_object, referenced_once}
402
    end)
403
  end
404

405
  defp convert_list(graph_object, referenced_once), do: {graph_object, referenced_once}
684✔
406

407
  # 6.4.2) & 6.4.3)
408
  defp extract_list(
409
         node,
410
         property,
411
         head,
412
         referenced_once,
413
         graph_object,
414
         list \\ [],
336✔
415
         list_nodes \\ []
416
       )
417

418
  defp extract_list(
419
         %{"@id" => "_:" <> _ = id, @rdf_rest => [_rest]} = node,
420
         @rdf_rest = property,
421
         head,
422
         referenced_once,
423
         graph_object,
424
         list,
425
         list_nodes
426
       ) do
427
    do_extract_list(
536✔
428
      node,
429
      referenced_once[id],
430
      property,
431
      head,
432
      referenced_once,
433
      graph_object,
434
      list,
435
      list_nodes
436
    )
437
  end
438

439
  defp extract_list(node, property, head, _referenced_once, _graph_object, list, list_nodes) do
440
    {list, list_nodes, [node["@id"], property], head}
304✔
441
  end
442

443
  defp do_extract_list(
444
         %{
445
           "@id" => "_:" <> _ = id,
446
           @rdf_first => [first],
447
           "@type" => [@rdf_list]
448
         } = node,
449
         %{node: next_node_id, property: next_property, value: next_head},
450
         _property,
451
         _head,
452
         referenced_once,
453
         graph_object,
454
         list,
455
         list_nodes
456
       )
457
       when map_size(node) == 4 do
458
    extract_list(
16✔
459
      graph_object[next_node_id],
460
      next_property,
461
      next_head,
462
      referenced_once,
463
      graph_object,
464
      [first | list],
465
      [id | list_nodes]
466
    )
467
  end
468

469
  defp do_extract_list(
470
         %{
471
           "@id" => "_:" <> _ = id,
472
           @rdf_first => [first]
473
         } = node,
474
         %{node: next_node_id, property: next_property, value: next_head},
475
         _property,
476
         _head,
477
         referenced_once,
478
         graph_object,
479
         list,
480
         list_nodes
481
       )
482
       when map_size(node) == 3 do
483
    extract_list(
488✔
484
      graph_object[next_node_id],
485
      next_property,
486
      next_head,
487
      referenced_once,
488
      graph_object,
489
      [first | list],
490
      [id | list_nodes]
491
    )
492
  end
493

494
  defp do_extract_list(
495
         node,
496
         _id_ref,
497
         property,
498
         head,
499
         _referenced_once,
500
         _graph_object,
501
         list,
502
         list_nodes
503
       ) do
504
    {list, list_nodes, [node["@id"], property], head}
32✔
505
  end
506

507
  defp rdf_to_object(%IRI{} = iri, _) do
508
    %{"@id" => to_string(iri)}
568✔
509
  end
510

511
  defp rdf_to_object(%BlankNode{} = bnode, _) do
512
    %{"@id" => to_string(bnode)}
752✔
513
  end
514

515
  defp rdf_to_object(%Literal{literal: %datatype{}} = literal, options) do
516
    result = %{}
1,172✔
517
    value = Literal.value(literal)
1,172✔
518
    converted_value = literal
1,172✔
519
    type = nil
1,172✔
520

521
    {converted_value, type, result} =
1,172✔
522
      cond do
523
        options.use_native_types ->
1,172✔
524
          cond do
64✔
525
            datatype == XSD.String ->
526
              {value, type, result}
16✔
527

528
            datatype == XSD.Boolean ->
48✔
529
              if RDF.XSD.Boolean.valid?(literal) do
8✔
530
                {value, type, result}
8✔
531
              else
532
                {converted_value, NS.XSD.boolean(), result}
×
533
              end
534

535
            datatype in [XSD.Integer, XSD.Double] ->
40✔
536
              if Literal.valid?(literal) do
24✔
537
                {value, type, result}
24✔
538
              else
539
                {converted_value, type, result}
×
540
              end
541

542
            true ->
16✔
543
              {converted_value, Literal.datatype_id(literal), result}
16✔
544
          end
545

546
        options.processing_mode != "json-ld-1.0" and datatype == RDF.JSON ->
1,108✔
547
          if RDF.JSON.valid?(literal) do
144✔
548
            {value, "@json", result}
136✔
549
          else
550
            raise JSON.LD.Error.invalid_json_literal(literal)
8✔
551
          end
552

553
        (i18n_datatype_parts = i18n_datatype_parts(literal)) &&
892✔
554
            options.rdf_direction == "i18n-datatype" ->
964✔
555
          {language, direction} = i18n_datatype_parts
40✔
556

557
          {
40✔
558
            value,
559
            type,
560
            if language do
561
              Map.put(result, "@language", language)
16✔
562
            else
563
              result
24✔
564
            end
565
            |> Map.put("@direction", direction)
566
          }
567

568
        datatype == LangString ->
924✔
569
          {converted_value, type, Map.put(result, "@language", Literal.language(literal))}
24✔
570

571
        datatype == XSD.String ->
900✔
572
          {converted_value, type, result}
752✔
573

574
        true ->
148✔
575
          {Literal.lexical(literal), Literal.datatype_id(literal), result}
148✔
576
      end
577

578
    result = (type && Map.put(result, "@type", to_string(type))) || result
1,164✔
579

580
    Map.put(
1,164✔
581
      result,
582
      "@value",
583
      (match?(%Literal{}, converted_value) && Literal.lexical(converted_value)) || converted_value
1,164✔
584
    )
585
  end
586

587
  defp i18n_datatype_parts(%Literal{} = literal),
588
    do: literal |> Literal.datatype_id() |> i18n_datatype_parts()
964✔
589

590
  defp i18n_datatype_parts(%IRI{} = datatype),
591
    do: datatype |> to_string() |> i18n_datatype_parts()
964✔
592

593
  defp i18n_datatype_parts("https://www.w3.org/ns/i18n#" <> suffix) do
594
    case String.split(suffix, "_", parts: 2) do
72✔
595
      ["", direction] -> {nil, direction}
40✔
596
      [language, direction] -> {language, direction}
32✔
597
      _ -> nil
×
598
    end
599
  end
600

601
  defp i18n_datatype_parts(_), do: nil
892✔
602

603
  #  # This function is necessary because we have no references and use this instead to update the head by path
604
  defp update_head(graph_object, referenced_once, [subject, property], old, new) do
336✔
605
    {
606
      deep_replace(graph_object, old, new),
607
      case old do
608
        %{"@id" => head_id} ->
609
          Map.new(referenced_once, fn
336✔
610
            {^head_id, %{node: ^subject, property: ^property} = usage} ->
232✔
611
              {head_id, %{usage | value: new}}
612

613
            other ->
614
              other
1,224✔
615
          end)
616

617
        _ ->
618
          Map.new(referenced_once, fn
×
619
            {key, %{node: ^subject, property: ^property} = usage} -> {key, %{usage | value: new}}
×
620
            other -> other
×
621
          end)
622
      end
623
    }
624
  end
625

626
  defp deep_replace(old, old, new), do: new
360✔
627

628
  defp deep_replace(map, old, new) when is_map(map) do
629
    Map.new(map, fn
4,656✔
630
      {:usages, value} -> {:usages, value}
336✔
631
      {key, value} -> {key, deep_replace(value, old, new)}
8,640✔
632
    end)
633
  end
634

635
  defp deep_replace(list, old, new) when is_list(list),
636
    do: Enum.map(list, &deep_replace(&1, old, new))
2,744✔
637

638
  defp deep_replace(old, _, _), do: old
4,144✔
639

640
  defp encode_json(value, opts) do
641
    Jason.encode(value, opts)
40✔
642
  end
643
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