defmodule JSON.LD do @moduledoc """ An implementation of JSON-LD 1.1. It includes an implementation of the `RDF.Serialization.Format` behaviour of RDF.ex. See """ use RDF.Serialization.Format import RDF.Sigils alias JSON.LD.{ Compaction, Context, Expansion, Flattening, DocumentLoader, Encoder, Decoder, Options } alias JSON.LD.DocumentLoader.RemoteDocument alias RDF.{IRI, PropertyMap} @id ~I @name :jsonld @extension "jsonld" @media_type "application/ld+json" @keywords ~w[ @type @base @container @context @default @direction @graph @id @import @included @index @json @language @list @nest @none @prefix @propagate @protected @reverse @set @value @version @vocab : ] @type input :: map | [map] | String.t() | IRI.t() | RemoteDocument.t() @type context_convertible :: map | String.t() | nil | RDF.PropertyMap.t() | list(context_convertible) @spec options :: Options.t() def options, do: Options.new() @doc """ The set of all JSON-LD keywords. see """ @spec keywords :: [String.t()] def keywords, do: @keywords @doc """ Returns if the given value is a JSON-LD keyword. """ @spec keyword?(String.t() | any) :: boolean def keyword?(value) when is_binary(value) and value in @keywords, do: true def keyword?(_value), do: false @doc """ Expands the given input according to the steps in the JSON-LD Expansion Algorithm. > Expansion is the process of taking a JSON-LD document and applying a `@context` > such that all IRIs, types, and values are expanded so that the `@context` is > no longer necessary. -- Details at This is the `expand()` API function of the `JsonLdProcessor` interface as specified in """ @spec expand(input(), Options.convertible()) :: map | [map] | nil def expand(input, options \\ []) do {processor_options, options} = Options.extract(options) expand(input, options, processor_options) end defp expand(%IRI{} = iri, options, processor_options), do: iri |> IRI.to_string() |> expand(options, processor_options) defp expand(url, options, processor_options) when is_binary(url) do case DocumentLoader.load(url, processor_options) do {:ok, document} -> expand(document, options, processor_options) {:error, error} -> raise error end end defp expand(%RemoteDocument{} = document, options, processor_options) do %{ Context.new() | base_iri: processor_options.base || document.document_url, original_base_url: document.document_url || processor_options.base } |> expand( document.document, Keyword.put(options, :context_url, document.context_url), processor_options ) end defp expand(input, options, processor_options) do processor_options |> Context.new() |> expand(input, options, processor_options) end defp expand(active_context, input, options, processor_options) do active_context = if processor_options.expand_context do context = case processor_options.expand_context do %{"@context" => context} -> context %{} = context -> context context when is_binary(context) -> context invalid -> raise ArgumentError, "Invalid expand context value: #{inspect(invalid)}" end Context.update( active_context, context, Options.set_base(processor_options, active_context.original_base_url) ) else active_context end {active_context, options} = case Keyword.pop(options, :context_url) do {nil, options} -> {active_context, options} {context_url, options} -> {Context.update(active_context, context_url, base: context_url, processor_options: processor_options ), options} end case Expansion.expand(active_context, nil, input, options, processor_options) do result = %{"@graph" => graph} when map_size(result) == 1 -> graph nil -> [] result when not is_list(result) -> [result] result -> result end end @doc """ Compacts the given input according to the steps in the JSON-LD Compaction Algorithm. > Compaction is the process of applying a developer-supplied context to shorten > IRIs to terms or compact IRIs and JSON-LD values expressed in expanded form > to simple values such as strings or numbers. Often this makes it simpler to > work with document as the data is expressed in application-specific terms. > Compacted documents are also typically easier to read for humans. -- Details at This is the `compact()` API function of the `JsonLdProcessor` interface as specified in """ @spec compact(input(), context_convertible(), Options.convertible()) :: map def compact(input, context, options \\ []) do {processor_options, options} = Options.extract(options) compact(input, context, options, processor_options) end defp compact(%IRI{} = iri, context, options, processor_options), do: iri |> IRI.to_string() |> compact(context, options, processor_options) # 3) defp compact(url, context, options, processor_options) when is_binary(url) do case DocumentLoader.load(url, processor_options) do {:ok, document} -> compact(document, context, options, processor_options) {:error, error} -> raise error end end defp compact(input, context, _opts, popts) do # 4) expanded = JSON.LD.expand(input, %{popts | ordered: false}) # 5) context_base = if match?(%RemoteDocument{}, input), do: input.document_url, else: popts.base # 6) context = case context do %{"@context" => context} -> context context -> context end # 7) active_context = %{ context(context, %{popts | base: context_base}) | # 8) api_base_iri: popts.base || if(popts.compact_to_relative, do: context_base) } |> Context.set_inverse() # 9) result = case Compaction.compact(expanded, active_context, nil, popts, popts.compact_arrays) do [] -> %{} result when is_list(result) -> %{Compaction.compact_iri("@graph", active_context, popts) => result} result -> result end cond do is_binary(context) -> Map.put(result, "@context", context) is_nil(context) || Enum.empty?(context) -> result true -> Map.put(result, "@context", context) end end @doc """ Flattens the given input according to the steps in the JSON-LD Flattening Algorithm. > Flattening collects all properties of a node in a single JSON object and labels > all blank nodes with blank node identifiers. This ensures a shape of the data > and consequently may drastically simplify the code required to process JSON-LD > in certain applications. -- Details at This is the `flatten()` API function of the `JsonLdProcessor` interface as specified in """ @spec flatten(input(), context_convertible(), Options.convertible()) :: [map] def flatten(input, context \\ nil, options \\ %Options{}) do {processor_options, options} = Options.extract(options) flatten(input, context, options, processor_options) end defp flatten(%IRI{} = iri, context, options, processor_options), do: iri |> IRI.to_string() |> flatten(context, options, processor_options) # 3) defp flatten(url, context, options, processor_options) when is_binary(url) do case DocumentLoader.load(url, processor_options) do {:ok, document} -> flatten(document, context, options, processor_options) {:error, error} -> raise error end end defp flatten(input, context, _options, processor_options) do flattened = input |> expand(%{processor_options | ordered: false}) |> Flattening.flatten(processor_options) if context && !Enum.empty?(flattened) do compact( flattened, context, if( is_nil(processor_options.base) and processor_options.compact_to_relative and match?(%RemoteDocument{}, input), do: %{processor_options | base: input.document_url}, else: processor_options ) ) else flattened end end @doc """ Transforms the given `RDF.Dataset` into a JSON-LD document in expanded form. Details at This is the `toRdf()` API function of the `JsonLdProcessor` interface as specified in """ defdelegate from_rdf(data, options \\ %Options{}), to: Encoder @doc """ Transforms the given JSON-LD document into an `RDF.Dataset`. Details at This is the `fromRdf()` API function of the `JsonLdProcessor` interface as specified in """ defdelegate to_rdf(input, options \\ %Options{}), to: Decoder @doc """ Generator function for `JSON.LD.Context`s. You can either pass a map with a `"@context"` key having the JSON-LD context object its value, or the JSON-LD context object directly. This function can be used also to create `JSON.LD.Context` from a `RDF.PropertyMap`. """ @spec context(context_convertible(), Options.t()) :: Context.t() def context(context, options \\ %Options{}) do context |> normalize_context() |> do_context(options) end defp do_context(%{"@context" => _} = object, options), do: Context.create(object, options) defp do_context(context, options), do: Context.create(%{"@context" => context}, options) defp normalize_context(%PropertyMap{} = property_map) do Map.new(property_map, fn {property, iri} -> {to_string(property), to_string(iri)} end) end defp normalize_context(map) when is_map(map) do Map.new(map, fn {key, value} when is_map(value) -> {to_string(key), normalize_context(value)} {key, value} -> {to_string(key), value} end) end defp normalize_context(list) when is_list(list), do: Enum.map(list, &normalize_context/1) defp normalize_context(value), do: value @doc """ Generator function for JSON-LD node maps. """ defdelegate node_map(input, node_id_map \\ nil), to: Flattening end