defmodule JSON.LD.Encoder do @moduledoc """ An encoder for JSON-LD serializations of RDF.ex data structures. As for all encoders of `RDF.Serialization.Format`s, you normally won't use these functions directly, but via one of the `write_` functions on the `JSON.LD` format module or the generic `RDF.Serialization` module. ## Options - `:context`: When a context map or remote context URL string is given, compaction is performed using this context - `:base`: : Allows to specify a base URI to be used during compaction (only when `:context` is provided). Default is the base IRI of the encoded graph or if none present or in case of encoded datasets the `RDF.default_base_iri/0`. - `:use_native_types`: If this flag is set to `true`, RDF literals with a datatype IRI that equals `xsd:integer` or `xsd:double` are converted to a JSON numbers and RDF literals with a datatype IRI that equals `xsd:boolean` are converted to `true` or `false` based on their lexical form. (default: `false`) - `:use_rdf_type`: Unless this flag is set to `true`, `rdf:type` predicates will be serialized as `@type` as long as the associated object is either an IRI or blank node identifier. (default: `false`) The given options are also passed through to `Jason.encode/2`, so you can also provide any the options this function supports, most notably the `:pretty` option. """ use RDF.Serialization.Encoder alias JSON.LD.Options alias RDF.{ BlankNode, Dataset, Description, Graph, IRI, LangString, Literal, NS, XSD } import JSON.LD.Utils import RDF.Guards @type input :: Dataset.t() | Description.t() | Graph.t() @rdf_type to_string(RDF.NS.RDF.type()) @rdf_value to_string(RDF.NS.RDF.value()) @rdf_nil to_string(RDF.NS.RDF.nil()) @rdf_first to_string(RDF.NS.RDF.first()) @rdf_rest to_string(RDF.NS.RDF.rest()) @rdf_list to_string(RDF.uri(RDF.NS.RDF.List)) @rdf_direction RDF.__base_iri__() <> "direction" @rdf_language RDF.__base_iri__() <> "language" @impl RDF.Serialization.Encoder @spec encode(RDF.Data.t(), keyword) :: {:ok, String.t()} | {:error, any} def encode(data, opts \\ []) do opts = set_base_iri(data, opts) with {:ok, json_ld_object} <- from_rdf(data, opts), {:ok, json_ld_object} <- maybe_compact(json_ld_object, opts) do encode_json(json_ld_object, opts) end end defp set_base_iri(%Graph{base_iri: base_iri}, opts) when not is_nil(base_iri) do Keyword.put_new(opts, :base, IRI.to_string(base_iri)) end defp set_base_iri(_, opts) do if base = RDF.default_base_iri() do Keyword.put_new(opts, :base, IRI.to_string(base)) else opts end end defp maybe_compact(json_ld_object, opts) do if context = Keyword.get(opts, :context) do {:ok, JSON.LD.compact(json_ld_object, context, opts)} else {:ok, json_ld_object} end end @spec from_rdf(RDF.Data.t(), Options.t() | Enum.t()) :: {:ok, [map]} | {:error, any} def from_rdf(dataset, options \\ %Options{}) do {:ok, from_rdf!(dataset, options)} rescue exception -> {:error, Exception.message(exception)} end @spec from_rdf!(RDF.Data.t(), Options.t() | Enum.t()) :: [map] def from_rdf!(rdf_data, options \\ %Options{}) def from_rdf!(%Dataset{} = dataset, options) do options = Options.new(options) {graph_map, referenced_once, compound_literal_subjects} = Enum.reduce(Dataset.graphs(dataset), {%{}, %{}, %{}}, fn graph, {graph_map, referenced_once, compound_literal_subjects} -> # 5.1) name = to_string(graph.name || "@default") # 5.2) graph_map = Map.put_new(graph_map, name, %{}) # 5.3) compound_literal_subjects = Map.put_new(compound_literal_subjects, name, %{}) # 5.4) graph_map = if graph.name && !get_in(graph_map, ["@default", name]) do Map.update(graph_map, "@default", %{name => %{"@id" => name}}, fn default_graph -> Map.put(default_graph, name, %{"@id" => name}) end) else graph_map end # 5.5) & 5.6) & 5.7) {node_map, referenced_once, compound_map} = process_graph_triples( graph, graph_map[name], referenced_once, compound_literal_subjects[name], options ) { Map.put(graph_map, name, node_map), referenced_once, Map.put(compound_literal_subjects, name, compound_map) } end) # 6) {graph_map, _referenced_once} = Enum.reduce(graph_map, {%{}, referenced_once}, fn {name, graph_object}, {graph_map, referenced_once} -> # 6.1) {graph_object, referenced_once} = process_compound_literals( graph_object, compound_literal_subjects[name], referenced_once ) # 6.2) - 6.4) {graph_object, referenced_once} = convert_list(graph_object, referenced_once) {Map.put(graph_map, name, graph_object), referenced_once} end) # 7+8) graph_map |> Map.get("@default", %{}) |> maybe_sort_by(options.ordered, fn {subject, _} -> subject end) |> Enum.reduce([], fn {subject, node}, result -> # 8.1) node = if Map.has_key?(graph_map, subject) do Map.put( node, "@graph", graph_map[subject] |> maybe_sort_by(options.ordered, fn {s, _} -> s end) |> Enum.reduce([], fn {_s, n}, graph_nodes -> n = Map.delete(n, :usages) if map_size(n) == 1 and Map.has_key?(n, "@id") do graph_nodes else [n | graph_nodes] end end) |> Enum.reverse() ) else node end # 8.2) node = Map.delete(node, :usages) if map_size(node) == 1 and Map.has_key?(node, "@id") do result else [node | result] end end) |> Enum.reverse() end def from_rdf!(rdf_data, options), do: rdf_data |> Dataset.new() |> from_rdf!(options) # Process triples in the graph (steps 5.5 through 5.7) defp process_graph_triples(graph, node_map, referenced_once, compound_map, options) do Enum.reduce(graph, {node_map, referenced_once, compound_map}, fn {subject, predicate, object}, {node_map, referenced_once, compound_map} -> subject = to_string(subject) predicate = to_string(predicate) # 5.7.1) node_map = Map.put_new(node_map, subject, %{"@id" => subject}) # 5.7.2) node = Map.get(node_map, subject) # 5.7.3) Handle the compound-literal direction case compound_map = if options.rdf_direction == "compound-literal" && predicate == @rdf_direction do Map.put(compound_map, subject, true) else compound_map end # 5.7.4) {object_id, node_map} = if is_node_object = is_rdf_resource(object) do object_id = to_string(object) node_map = Map.put_new(node_map, object_id, %{"@id" => object_id}) {object_id, node_map} else {nil, node_map} end # 5.7.5) {node, node_map, referenced_once} = if is_node_object and !options.use_rdf_type and predicate == @rdf_type do node = Map.update(node, "@type", [object_id], fn types -> if object_id in types, do: types, else: types ++ [object_id] end) {node, node_map, referenced_once} else # 5.7.6) value = rdf_to_object(object, options) # 5.7.7) & 5.7.8) node = Map.update(node, predicate, [value], fn objects -> if value in objects, do: objects, else: objects ++ [value] end) {node_map, referenced_once} = cond do # 5.7.9) object_id == @rdf_nil -> usage = %{node: subject, property: predicate, value: value} node_map = Map.update(node_map, @rdf_nil, %{usages: [usage]}, fn object_node -> Map.update(object_node, :usages, [usage], &[usage | &1]) end) {node_map, referenced_once} # 5.7.10) Map.has_key?(referenced_once, object_id) -> {node_map, Map.put(referenced_once, object_id, false)} # 5.7.11) is_rdf_bnode(object) -> {node_map, Map.put(referenced_once, object_id, %{ # We're using here the node id as the reference to the respective graph map entry. node: subject, property: predicate, value: value })} true -> {node_map, referenced_once} end {node, node_map, referenced_once} end {Map.put(node_map, subject, node), referenced_once, compound_map} end) end # 6.1) defp process_compound_literals(graph_object, nil, referenced_once), do: {graph_object, referenced_once} defp process_compound_literals(graph_object, compound_map, referenced_once) do Enum.reduce(compound_map, {graph_object, referenced_once}, fn {cl, _}, {graph_object, referenced_once} -> case referenced_once[cl] do # SPEC ISSUE: "6.1.4) Initialize value to value of value in cl entry." seems unnecessary, since value is never used %{node: node_id, property: property, value: _value} -> node = graph_object[node_id] # 6.1.5) case Map.pop(graph_object, cl) do {%{} = cl_node, graph_object} -> # 6.1.6) node |> Map.get(property, []) |> Enum.reduce({graph_object, referenced_once}, fn %{"@id" => ^cl} = cl_reference, {graph_object, referenced_once} -> cl_reference = cl_reference # 6.1.6.1) |> Map.delete("@id") # 6.1.6.2) |> Map.put( "@value", (List.first(cl_node[@rdf_value] || []) || %{})["@value"] ) # 6.1.6.3) cl_reference = case cl_node[@rdf_language] do [%{"@value" => language} | _] -> if not valid_language?(language) do raise JSON.LD.Error.invalid_language_tagged_string(language) end Map.put(cl_reference, "@language", language) _ -> cl_reference end # 6.1.6.4) cl_reference = case cl_node[@rdf_direction] do [%{"@value" => direction} | _] -> if direction not in ~w[ltr rtl] do raise JSON.LD.Error.invalid_base_direction(direction) end Map.put(cl_reference, "@direction", direction) _ -> cl_reference end { update_in(graph_object, [node_id, property], fn props -> Enum.map(props, fn %{"@id" => ^cl} -> cl_reference other -> other end) end), Map.delete(referenced_once, cl) } _, {graph_object, referenced_once} -> {graph_object, referenced_once} end) _ -> {graph_object, referenced_once} end _ -> {graph_object, referenced_once} end end) end # # 6.3) - 6.4) defp convert_list(%{@rdf_nil => %{usages: usages}} = graph_object, referenced_once) do Enum.reduce(usages, {graph_object, referenced_once}, fn # 6.4.1) # Note: original_head is always an rdf:nil node %{node: node_id, property: property, value: original_head}, {graph_object, referenced_once} -> node = graph_object[node_id] # 6.4.2) & 6.4.3) {list, list_nodes, head_path, head} = extract_list(node, property, original_head, referenced_once, graph_object) updated_head = head # 6.4.4) |> Map.delete("@id") # 6.4.5) is not needed since extract_list returns the list in reverse order already # 6.4.6) |> Map.put("@list", list) {graph_object, referenced_once} = update_head(graph_object, referenced_once, head_path, head, updated_head) # 6.4.7) graph_object = Enum.reduce(list_nodes, graph_object, fn node_id, graph_object -> Map.delete(graph_object, node_id) end) {graph_object, referenced_once} end) end defp convert_list(graph_object, referenced_once), do: {graph_object, referenced_once} # 6.4.2) & 6.4.3) defp extract_list( node, property, head, referenced_once, graph_object, list \\ [], list_nodes \\ [] ) defp extract_list( %{"@id" => "_:" <> _ = id, @rdf_rest => [_rest]} = node, @rdf_rest = property, head, referenced_once, graph_object, list, list_nodes ) do do_extract_list( node, referenced_once[id], property, head, referenced_once, graph_object, list, list_nodes ) end defp extract_list(node, property, head, _referenced_once, _graph_object, list, list_nodes) do {list, list_nodes, [node["@id"], property], head} end defp do_extract_list( %{ "@id" => "_:" <> _ = id, @rdf_first => [first], "@type" => [@rdf_list] } = node, %{node: next_node_id, property: next_property, value: next_head}, _property, _head, referenced_once, graph_object, list, list_nodes ) when map_size(node) == 4 do extract_list( graph_object[next_node_id], next_property, next_head, referenced_once, graph_object, [first | list], [id | list_nodes] ) end defp do_extract_list( %{ "@id" => "_:" <> _ = id, @rdf_first => [first] } = node, %{node: next_node_id, property: next_property, value: next_head}, _property, _head, referenced_once, graph_object, list, list_nodes ) when map_size(node) == 3 do extract_list( graph_object[next_node_id], next_property, next_head, referenced_once, graph_object, [first | list], [id | list_nodes] ) end defp do_extract_list( node, _id_ref, property, head, _referenced_once, _graph_object, list, list_nodes ) do {list, list_nodes, [node["@id"], property], head} end defp rdf_to_object(%IRI{} = iri, _) do %{"@id" => to_string(iri)} end defp rdf_to_object(%BlankNode{} = bnode, _) do %{"@id" => to_string(bnode)} end defp rdf_to_object(%Literal{literal: %datatype{}} = literal, options) do result = %{} value = Literal.value(literal) converted_value = literal type = nil {converted_value, type, result} = cond do options.use_native_types -> cond do datatype == XSD.String -> {value, type, result} datatype == XSD.Boolean -> if RDF.XSD.Boolean.valid?(literal) do {value, type, result} else {converted_value, NS.XSD.boolean(), result} end datatype in [XSD.Integer, XSD.Double] -> if Literal.valid?(literal) do {value, type, result} else {converted_value, type, result} end true -> {converted_value, Literal.datatype_id(literal), result} end options.processing_mode != "json-ld-1.0" and datatype == RDF.JSON -> if RDF.JSON.valid?(literal) do {value, "@json", result} else raise JSON.LD.Error.invalid_json_literal(literal) end (i18n_datatype_parts = i18n_datatype_parts(literal)) && options.rdf_direction == "i18n-datatype" -> {language, direction} = i18n_datatype_parts { value, type, if language do Map.put(result, "@language", language) else result end |> Map.put("@direction", direction) } datatype == LangString -> {converted_value, type, Map.put(result, "@language", Literal.language(literal))} datatype == XSD.String -> {converted_value, type, result} true -> {Literal.lexical(literal), Literal.datatype_id(literal), result} end result = (type && Map.put(result, "@type", to_string(type))) || result Map.put( result, "@value", (match?(%Literal{}, converted_value) && Literal.lexical(converted_value)) || converted_value ) end defp i18n_datatype_parts(%Literal{} = literal), do: literal |> Literal.datatype_id() |> i18n_datatype_parts() defp i18n_datatype_parts(%IRI{} = datatype), do: datatype |> to_string() |> i18n_datatype_parts() defp i18n_datatype_parts("https://www.w3.org/ns/i18n#" <> suffix) do case String.split(suffix, "_", parts: 2) do ["", direction] -> {nil, direction} [language, direction] -> {language, direction} _ -> nil end end defp i18n_datatype_parts(_), do: nil # # This function is necessary because we have no references and use this instead to update the head by path defp update_head(graph_object, referenced_once, [subject, property], old, new) do { deep_replace(graph_object, old, new), case old do %{"@id" => head_id} -> Map.new(referenced_once, fn {^head_id, %{node: ^subject, property: ^property} = usage} -> {head_id, %{usage | value: new}} other -> other end) _ -> Map.new(referenced_once, fn {key, %{node: ^subject, property: ^property} = usage} -> {key, %{usage | value: new}} other -> other end) end } end defp deep_replace(old, old, new), do: new defp deep_replace(map, old, new) when is_map(map) do Map.new(map, fn {:usages, value} -> {:usages, value} {key, value} -> {key, deep_replace(value, old, new)} end) end defp deep_replace(list, old, new) when is_list(list), do: Enum.map(list, &deep_replace(&1, old, new)) defp deep_replace(old, _, _), do: old defp encode_json(value, opts) do Jason.encode(value, opts) end end