Current section

Files

Jump to
json_ld lib json ld decoder.ex
Raw

lib/json/ld/decoder.ex

defmodule JSON.LD.Decoder do
@moduledoc """
A decoder for JSON-LD serializations to `RDF.Dataset`s.
As for all decoders of `RDF.Serialization.Format`s, you normally won't use these
functions directly, but via one of the `read_` functions on the `JSON.LD` format
module or the generic `RDF.Serialization` module.
"""
use RDF.Serialization.Decoder
import JSON.LD.{NodeIdentifierMap, Utils}
alias JSON.LD.{NodeIdentifierMap, Options}
alias RDF.{BlankNode, Dataset, Graph, IRI, Literal, NS, Statement, XSD}
@rdf_direction RDF.__base_iri__() <> "direction"
@rdf_language RDF.__base_iri__() <> "language"
@impl RDF.Serialization.Decoder
@spec decode(String.t(), keyword) :: {:ok, Dataset.t() | Graph.t()} | {:error, any}
def decode(content, opts \\ []) do
with {:ok, json_ld_object} <- parse_json(content) do
{:ok, to_rdf(json_ld_object, opts)}
end
end
@spec to_rdf(JSON.LD.input(), Options.t() | Enum.t()) :: Dataset.t() | Graph.t()
def to_rdf(input, options \\ %Options{}) do
{:ok, node_id_map} = NodeIdentifierMap.start_link()
options = Options.new(options)
try do
input
|> JSON.LD.expand(%{options | ordered: false})
|> JSON.LD.node_map(node_id_map)
|> do_to_rdf(node_id_map, options)
after
NodeIdentifierMap.stop(node_id_map)
end
end
defp do_to_rdf(node_map, node_id_map, options) do
node_map
|> Enum.sort_by(fn {graph_name, _} -> graph_name end)
|> Enum.reduce(Dataset.new(), fn {graph_name, graph}, dataset ->
# 1.1)
if graph_name != "@default" and not well_formed_iri?(graph_name) and
not blank_node_id?(graph_name) do
dataset
else
# 1.3)
rdf_graph =
graph
|> Enum.sort_by(fn {subject, _} -> subject end)
|> Enum.reduce(Graph.new(), fn {subject, node}, rdf_graph ->
# 1.3.1)
if not well_formed_iri?(subject) and not blank_node_id?(subject) do
rdf_graph
else
# 1.3.2)
node
|> Enum.sort_by(fn {property, _} -> property end)
|> Enum.reduce(rdf_graph, fn {property, values}, rdf_graph ->
cond do
# 1.3.2.1)
property == "@type" ->
if subject = node_to_rdf(subject) do
objects = values |> Enum.map(&node_to_rdf/1) |> Enum.reject(&is_nil/1)
Graph.add(rdf_graph, {subject, NS.RDF.type(), objects})
else
rdf_graph
end
# 1.3.2.2)
JSON.LD.keyword?(property) ->
rdf_graph
# 1.3.2.3)
not options.produce_generalized_rdf and blank_node_id?(property) ->
rdf_graph
# 1.3.2.4)
not well_formed_iri?(property) ->
rdf_graph
# 1.3.2.5)
true ->
Enum.reduce(values, rdf_graph, fn item, rdf_graph ->
case object_to_rdf(item, node_id_map, options) do
{_list_triples, nil} ->
rdf_graph
{list_triples, object} ->
rdf_graph
|> Graph.add({node_to_rdf(subject), node_to_rdf(property), object})
|> Graph.add(list_triples)
end
end)
end
end)
end
end)
if Enum.empty?(rdf_graph) do
dataset
else
graph_name = if graph_name == "@default", do: nil, else: graph_name
Dataset.add(dataset, rdf_graph, graph: graph_name)
end
end
end)
end
defp well_formed_iri?(iri) do
valid_uri?(iri)
end
@spec parse_json(String.t(), [Jason.decode_opt()]) ::
{:ok, map} | {:error, Jason.DecodeError.t()}
def parse_json(content, _opts \\ []) do
Jason.decode(content)
end
@spec parse_json!(String.t(), [Jason.decode_opt()]) :: map
def parse_json!(content, _opts \\ []) do
Jason.decode!(content)
end
@spec node_to_rdf(String.t()) :: IRI.t() | BlankNode.t() | nil
def node_to_rdf(node) do
cond do
blank_node_id?(node) -> node |> String.trim_leading("_:") |> RDF.bnode()
well_formed_iri?(node) -> RDF.uri(node)
true -> nil
end
end
# Object to RDF Conversion - https://www.w3.org/TR/json-ld11-api/#object-to-rdf-conversion
defp object_to_rdf(item, node_id_map, options)
# 1) and 2)
defp object_to_rdf(%{"@id" => id}, _node_id_map, _options) do
{[], node_to_rdf(id)}
end
# 3)
defp object_to_rdf(%{"@list" => list}, node_id_map, options) do
list_to_rdf(list, node_id_map, options)
end
# 4)
defp object_to_rdf(%{"@value" => value} = item, _node_id_map, options) do
# 5)
datatype = item["@type"]
{value, datatype} =
cond do
# 6)
not is_nil(datatype) and relative_iri?(datatype) and datatype != "@json" ->
{nil, datatype}
# 7)
Map.has_key?(item, "@language") and not relative_iri?(item["@language"]) ->
{nil, datatype}
# 8)
datatype == "@json" ->
value =
value
|> RDF.JSON.new(as_value: true)
|> RDF.JSON.canonical()
|> RDF.JSON.lexical()
{value, RDF.iri(RDF.JSON)}
# 9)
is_boolean(value) ->
value =
value
|> XSD.Boolean.new()
|> XSD.Boolean.canonical()
|> XSD.Boolean.lexical()
datatype = if is_nil(datatype), do: NS.XSD.boolean(), else: datatype
{value, datatype}
# 10)
is_number(value) and
(datatype == to_string(NS.XSD.double()) or value != trunc(value) or
value >= 1.0e21) ->
value =
value
|> XSD.Double.new()
|> XSD.Double.canonical()
|> XSD.Double.lexical()
datatype = if is_nil(datatype), do: NS.XSD.double(), else: datatype
{value, datatype}
# 11)
is_number(value) ->
value =
if(is_float(value), do: trunc(value), else: value)
|> XSD.Integer.new()
|> XSD.Integer.canonical()
|> XSD.Integer.lexical()
datatype = if is_nil(datatype), do: NS.XSD.integer(), else: datatype
{value, datatype}
# 12)
is_nil(datatype) ->
datatype =
if Map.has_key?(item, "@language"), do: RDF.langString(), else: NS.XSD.string()
{value, datatype}
true ->
{value, datatype}
end
cond do
is_nil(value) ->
{[], nil}
# 13)
Map.has_key?(item, "@direction") and not is_nil(options.rdf_direction) ->
# 13.1)
language = String.downcase(item["@language"] || "")
direction = item["@direction"]
case options.rdf_direction do
# 13.2)
"i18n-datatype" ->
{[],
Literal.new(value, datatype: "https://www.w3.org/ns/i18n##{language}_#{direction}")}
# 13.3)
"compound-literal" ->
literal = RDF.bnode()
list_triples =
[
{literal, RDF.value(), value},
{literal, @rdf_direction, direction}
]
list_triples =
if Map.has_key?(item, "@language") do
[{literal, @rdf_language, language} | list_triples]
else
list_triples
end
{list_triples, literal}
end
# 14)
language = item["@language"] ->
{
[],
if(valid_language?(language),
do: Literal.new(value, language: language, canonicalize: true)
)
}
true ->
{[], Literal.new(value, datatype: datatype, canonicalize: true)}
end
end
@spec list_to_rdf([map], pid, Options.t()) :: {[Statement.t()], IRI.t() | BlankNode.t()}
defp list_to_rdf(list, node_id_map, options) do
{list_triples, first, last} =
Enum.reduce(list, {[], nil, nil}, fn item, {list_triples, first, last} ->
case object_to_rdf(item, node_id_map, options) do
{more_list_triples, object} ->
bnode = RDF.bnode(generate_blank_node_id(node_id_map))
list_triples = more_list_triples ++ list_triples
object_triples = if object, do: [{bnode, NS.RDF.first(), object}], else: []
if last do
{list_triples ++ [{last, NS.RDF.rest(), bnode} | object_triples], first, bnode}
else
{object_triples ++ list_triples, bnode, bnode}
end
end
end)
if last do
{list_triples ++ [{last, NS.RDF.rest(), NS.RDF.nil()}], first}
else
{[], NS.RDF.nil()}
end
end
end