json_ld: initial JSON-LD context processing and expansion implementation
This commit is contained in:
parent
514233555c
commit
8a6bf65aae
10 changed files with 2677 additions and 13 deletions
26
README.md
26
README.md
|
@ -1,23 +1,27 @@
|
|||
# JSON.LD
|
||||
|
||||
**TODO: Add description**
|
||||
An implementation of the [JSON-LD](https://en.wikipedia.org/wiki/JSON-LD) standard.
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
If [available in Hex](https://hex.pm/docs/publish), the package can be installed as:
|
||||
|
||||
1. Add `json_ld` to your list of dependencies in `mix.exs`:
|
||||
The package can be installed by adding `json_ld` to your list of dependencies in `mix.exs`:
|
||||
|
||||
```elixir
|
||||
def deps do
|
||||
[{:json_ld, "~> 0.1.0"}]
|
||||
[
|
||||
{:json_ld, "~> 0.1.0"}
|
||||
]
|
||||
end
|
||||
```
|
||||
|
||||
2. Ensure `json_ld` is started before your application:
|
||||
Then, update your dependencies:
|
||||
|
||||
```sh-session
|
||||
$ mix deps.get
|
||||
```
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
|
||||
```elixir
|
||||
def application do
|
||||
[applications: [:json_ld]]
|
||||
end
|
||||
```
|
||||
|
|
311
lib/json/ld/context.ex
Normal file
311
lib/json/ld/context.ex
Normal file
|
@ -0,0 +1,311 @@
|
|||
defmodule JSON.LD.Context do
|
||||
defstruct term_defs: %{},
|
||||
vocab: nil,
|
||||
base_iri: nil,
|
||||
default_language: nil
|
||||
|
||||
import JSON.LD
|
||||
|
||||
alias JSON.LD.Context.TermDefinition
|
||||
|
||||
@keywords JSON.LD.keywords # to allow this to be used in function guard clauses, we redefine this here
|
||||
|
||||
|
||||
def new(opts \\ [])
|
||||
|
||||
def new([base: base_iri]), do: %JSON.LD.Context{base_iri: base_iri}
|
||||
def new(_), do: %JSON.LD.Context{}
|
||||
|
||||
|
||||
def create(%{"@context" => json_ld_context}, opts),
|
||||
do: new(opts) |> update(json_ld_context, Keyword.get(opts, :remote, []))
|
||||
|
||||
|
||||
def update(active, local, remote \\ [])
|
||||
|
||||
def update(%JSON.LD.Context{} = active, local, remote) when is_list(local) do
|
||||
Enum.reduce local, active, fn (local, result) ->
|
||||
do_update(result, local, remote)
|
||||
end
|
||||
end
|
||||
|
||||
# 2) If local context is not an array, set it to an array containing only local context.
|
||||
def update(%JSON.LD.Context{} = active, local, remote) do
|
||||
update(active, [local], remote)
|
||||
end
|
||||
|
||||
# 3.1) If context is null, set result to a newly-initialized active context and continue with the next context. The base IRI of the active context is set to the IRI of the currently being processed document (which might be different from the currently being processed context), if available; otherwise to null. If set, the base option of a JSON-LD API Implementation overrides the base IRI.
|
||||
defp do_update(%JSON.LD.Context{} = active, nil, remote) do
|
||||
# TODO: "If set, the base option of a JSON-LD API Implementation overrides the base IRI."
|
||||
JSON.LD.Context.new(base: active.base_iri)
|
||||
end
|
||||
|
||||
# 3.2) If context is a string, [it's interpreted as a remote context]
|
||||
defp do_update(%JSON.LD.Context{} = active, local, remote) when is_binary(local) do
|
||||
# TODO: fetch remote context and call recursively with remote updated
|
||||
end
|
||||
|
||||
# 3.4) - 3.8)
|
||||
defp do_update(%JSON.LD.Context{} = active, local, remote) when is_map(local) do
|
||||
with {base, local} <- Map.pop(local, "@base", :undefined),
|
||||
{vocab, local} <- Map.pop(local, "@vocab", :undefined),
|
||||
{language, local} <- Map.pop(local, "@language", :undefined) do
|
||||
active
|
||||
|> set_base(base, remote)
|
||||
|> set_vocab(vocab)
|
||||
|> set_language(language)
|
||||
|> create_term_definitions(local)
|
||||
end
|
||||
end
|
||||
|
||||
# 3.3) If context is not a JSON object, an invalid local context error has been detected and processing is aborted.
|
||||
defp do_update(_, local, _),
|
||||
do: raise JSON.LD.InvalidLocalContextError,
|
||||
message: "#{inspect local} is not a valid @context value"
|
||||
|
||||
|
||||
defp set_base(active, :undefined, _),
|
||||
do: active
|
||||
defp set_base(active, _, remote) when is_list(remote) and length(remote) > 0,
|
||||
do: active
|
||||
defp set_base(active, base, _) do
|
||||
cond do
|
||||
is_nil(base) or absolute_iri?(base) ->
|
||||
%JSON.LD.Context{active | base_iri: base}
|
||||
not is_nil(active.base_iri) ->
|
||||
%JSON.LD.Context{active | base_iri: absolute_iri(base, active.base_iri)}
|
||||
true ->
|
||||
raise JSON.LD.InvalidBaseURIError,
|
||||
message: "#{inspect base} is a relative IRI, but no active base IRI defined"
|
||||
end
|
||||
end
|
||||
|
||||
defp set_vocab(active, :undefined), do: active
|
||||
defp set_vocab(active, vocab) do
|
||||
if is_nil(vocab) or absolute_iri?(vocab) or blank_node_id?(vocab) do
|
||||
%JSON.LD.Context{active | vocab: vocab}
|
||||
else
|
||||
raise JSON.LD.InvalidVocabMappingError,
|
||||
message: "#{inspect vocab} is not a valid vocabulary mapping"
|
||||
end
|
||||
end
|
||||
|
||||
defp set_language(active, :undefined), do: active
|
||||
defp set_language(active, nil),
|
||||
do: %JSON.LD.Context{active | default_language: nil}
|
||||
defp set_language(active, language) when is_binary(language),
|
||||
do: %JSON.LD.Context{active | default_language: String.downcase(language)}
|
||||
defp set_language(_, language),
|
||||
do: raise JSON.LD.InvalidDefaultLanguageError,
|
||||
message: "#{inspect language} is not a valid language"
|
||||
|
||||
defp create_term_definitions(active, local, defined \\ %{}) do
|
||||
{active, _} =
|
||||
Enum.reduce local, {active, defined}, fn ({term, value}, {active, defined}) ->
|
||||
create_term_definition(active, local, term, value, defined)
|
||||
end
|
||||
active
|
||||
end
|
||||
|
||||
@doc """
|
||||
Expands the given input according to the steps in the JSON-LD Create Term Definition Algorithm.
|
||||
|
||||
see https://www.w3.org/TR/json-ld-api/#create-term-definition
|
||||
"""
|
||||
def create_term_definition(active, local, term, value, defined)
|
||||
|
||||
def create_term_definition(active, _, "@base", _, defined), do: {active, defined}
|
||||
def create_term_definition(active, _, "@vocab", _, defined), do: {active, defined}
|
||||
def create_term_definition(active, _, "@language", _, defined), do: {active, defined}
|
||||
|
||||
def create_term_definition(active, local, term, value, defined) do
|
||||
# 3)
|
||||
if term in JSON.LD.keywords,
|
||||
do: raise JSON.LD.KeywordRedefinitionError,
|
||||
message: "#{inspect term} is a keyword and can not be defined in context"
|
||||
# 1)
|
||||
case defined[term] do
|
||||
true -> {active, defined}
|
||||
false -> raise JSON.LD.CyclicIRIMappingError #, message: "#{inspect term} .."
|
||||
nil -> do_create_term_definition(active, local, term, value,
|
||||
Map.put(defined, term, false)) # 2)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_create_term_definition(active, _local, term, nil, defined) do
|
||||
{
|
||||
# (if Map.has_key?(active.term_defs, term),
|
||||
# do: put_in(active, [:term_defs, term], nil),
|
||||
# else: raise "NotImplemented"),
|
||||
%JSON.LD.Context{active | term_defs: Map.put(active.term_defs, term, nil)},
|
||||
Map.put(defined, term, true)}
|
||||
end
|
||||
|
||||
defp do_create_term_definition(active, local, term, %{"@id" => nil}, defined),
|
||||
do: do_create_term_definition(active, local, term, nil, defined)
|
||||
|
||||
defp do_create_term_definition(active, local, term, value, defined) when is_binary(value),
|
||||
do: do_create_term_definition(active, local, term, %{"@id" => value}, defined)
|
||||
|
||||
defp do_create_term_definition(active, local, term, %{} = value, defined) do
|
||||
definition = %TermDefinition{} # 9)
|
||||
{definition, active, defined} =
|
||||
do_create_type_definition(definition, active, local, value, defined)
|
||||
{done, definition, active, defined} =
|
||||
do_create_reverse_definition(definition, active, local, value, defined)
|
||||
unless done do
|
||||
{definition, active, defined} =
|
||||
do_create_id_definition(definition, active, local, term, value, defined)
|
||||
definition = do_create_container_definition(definition, value)
|
||||
definition = do_create_language_definition(definition, value)
|
||||
end
|
||||
# 18 / 11.6) Set the term definition of term in active context to definition and set the value associated with defined's key term to true.
|
||||
{%JSON.LD.Context{active | term_defs: Map.put(active.term_defs, term, definition)},
|
||||
Map.put(defined, term, true)}
|
||||
end
|
||||
|
||||
defp do_create_term_definition(_, _, _, value, _),
|
||||
do: raise JSON.LD.InvalidTermDefinitionError,
|
||||
message: "#{inspect value} is not a valid term definition"
|
||||
|
||||
|
||||
# 10.1)
|
||||
# TODO: RDF.rb implementation says: "SPEC FIXME: @type may be nil"
|
||||
defp do_create_type_definition(_, _, _, %{"@type" => type}, _) when not is_binary(type),
|
||||
do: raise JSON.LD.InvalidTypeMappingError,
|
||||
message: "#{inspect type} is not a valid type mapping"
|
||||
|
||||
# 10.2) and 10.3)
|
||||
defp do_create_type_definition(definition, active, local, %{"@type" => type}, defined) do
|
||||
{expanded_type, active, defined} =
|
||||
expand_iri(type, active, false, true, local, defined)
|
||||
if absolute_iri?(expanded_type) or expanded_type in ~w[@id @vocab] do
|
||||
{%TermDefinition{definition | type_mapping: expanded_type}, active, defined}
|
||||
else
|
||||
raise JSON.LD.InvalidTypeMappingError,
|
||||
message: "#{inspect type} is not a valid type mapping"
|
||||
end
|
||||
end
|
||||
|
||||
defp do_create_type_definition(definition, active, _, _, defined),
|
||||
do: {definition, active, defined}
|
||||
|
||||
# 11) If value contains the key @reverse
|
||||
defp do_create_reverse_definition(definition, active, local,
|
||||
%{"@reverse" => reverse} = value, defined) do
|
||||
cond do
|
||||
Map.has_key?(value, "@id") -> # 11.1)
|
||||
raise JSON.LD.InvalidReversePropertyError,
|
||||
message: "#{inspect reverse} is not a valid reverse property"
|
||||
not is_binary(reverse) -> # 11.2)
|
||||
raise JSON.LD.InvalidIRIMappingError,
|
||||
message: "#{inspect reverse} is not a valid IRI mapping"
|
||||
true -> # 11.3)
|
||||
{expanded_reverse, active, defined} =
|
||||
expand_iri(reverse, active, false, true, local, defined)
|
||||
if absolute_iri?(expanded_reverse) or blank_node_id?(expanded_reverse) do
|
||||
definition = %TermDefinition{definition | iri_mapping: expanded_reverse}
|
||||
else
|
||||
raise JSON.LD.InvalidIRIMappingError,
|
||||
message: "#{inspect reverse} is not a valid IRI mapping"
|
||||
end
|
||||
case Map.get(value, "@container", {:undefined}) do # 11.4)
|
||||
{:undefined} -> nil
|
||||
container when is_nil(container) or container in ~w[@set @index] ->
|
||||
definition = %TermDefinition{definition | container_mapping: container}
|
||||
_ ->
|
||||
raise JSON.LD.InvalidReversePropertyError,
|
||||
message: "#{inspect reverse} is not a valid reverse property; reverse properties only support set- and index-containers"
|
||||
end
|
||||
# 11.5) & 11.6)
|
||||
{true, %TermDefinition{definition | reverse_property: true}, active, defined}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_create_reverse_definition(definition, active, _, _, defined),
|
||||
do: {false, definition, active, defined}
|
||||
|
||||
|
||||
# 13)
|
||||
defp do_create_id_definition(definition, active, local, term,
|
||||
%{"@id" => id}, defined) when id != term do
|
||||
if is_binary(id) do
|
||||
# 13.2)
|
||||
{expanded_id, active, defined} =
|
||||
expand_iri(id, active, false, true, local, defined)
|
||||
cond do
|
||||
expanded_id == "@context" ->
|
||||
raise JSON.LD.InvalidKeywordAliasError,
|
||||
message: "#{inspect id} is an invalid keyword alias"
|
||||
keyword?(expanded_id) or
|
||||
absolute_iri?(expanded_id) or
|
||||
blank_node_id?(expanded_id) ->
|
||||
{%TermDefinition{definition | iri_mapping: expanded_id}, active, defined}
|
||||
true ->
|
||||
raise JSON.LD.InvalidIRIMappingError,
|
||||
message: "#{inspect id} is not a valid IRI mapping"
|
||||
end
|
||||
else # 13.1)
|
||||
raise JSON.LD.InvalidIRIMappingError,
|
||||
message: "#{inspect id} is not a valid IRI mapping"
|
||||
end
|
||||
end
|
||||
|
||||
defp do_create_id_definition(definition, active, local, term, _, defined) do
|
||||
# 14)
|
||||
# TODO: The W3C spec seems to contain an error by requiring only to check for a collon.
|
||||
# What's when an absolute IRI is given and an "http" term is defined in the context?
|
||||
if String.contains?(term, ":") do
|
||||
case compact_iri_parts(term) do
|
||||
[prefix, suffix] ->
|
||||
if prefix_mapping = local[prefix] do
|
||||
{active, defined} = do_create_term_definition(active, local, prefix, prefix_mapping, defined)
|
||||
end
|
||||
if prefix_def = active.term_defs[prefix] do
|
||||
{%TermDefinition{definition | iri_mapping: prefix_def.iri_mapping <> suffix}, active, defined}
|
||||
else
|
||||
{%TermDefinition{definition | iri_mapping: term}, active, defined}
|
||||
end
|
||||
nil -> {%TermDefinition{definition | iri_mapping: term}, active, defined}
|
||||
end
|
||||
# 15)
|
||||
else
|
||||
if active.vocab do
|
||||
{%TermDefinition{definition | iri_mapping: active.vocab <> term}, active, defined}
|
||||
else
|
||||
raise JSON.LD.InvalidIRIMappingError,
|
||||
message: "#{inspect term} is not a valid IRI mapping"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# 16.1)
|
||||
defp do_create_container_definition(_, %{"@container" => container})
|
||||
when not container in ~w[@list @set @index @language],
|
||||
do: raise JSON.LD.InvalidContainerMappingError,
|
||||
message: "#{inspect container} is not a valid container mapping; only @list, @set, @index, or @language allowed"
|
||||
# 16.2)
|
||||
defp do_create_container_definition(definition, %{"@container" => container}),
|
||||
do: %TermDefinition{definition | container_mapping: container}
|
||||
defp do_create_container_definition(definition, _),
|
||||
do: definition
|
||||
|
||||
|
||||
# 17)
|
||||
defp do_create_language_definition(definition, %{"@language" => language} = value) do
|
||||
unless Map.has_key?(value, "@type") do
|
||||
case language do
|
||||
language when is_binary(language) ->
|
||||
%TermDefinition{definition | language_mapping: String.downcase(language)}
|
||||
language when is_nil(language) ->
|
||||
%TermDefinition{definition | language_mapping: nil}
|
||||
_ ->
|
||||
raise JSON.LD.InvalidLanguageMappingError,
|
||||
message: "#{inspect language} is not a valid language mapping"
|
||||
end
|
||||
end
|
||||
end
|
||||
defp do_create_language_definition(definition, _), do: definition
|
||||
|
||||
end
|
7
lib/json/ld/context/term_definition.ex
Normal file
7
lib/json/ld/context/term_definition.ex
Normal file
|
@ -0,0 +1,7 @@
|
|||
defmodule JSON.LD.Context.TermDefinition do
|
||||
defstruct iri_mapping: nil,
|
||||
reverse_property: false,
|
||||
type_mapping: nil, language_mapping: :undefined,
|
||||
container_mapping: nil
|
||||
|
||||
end
|
245
lib/json/ld/exceptions.ex
Normal file
245
lib/json/ld/exceptions.ex
Normal file
|
@ -0,0 +1,245 @@
|
|||
defmodule JSON.LD.LoadingDocumentFailedError do
|
||||
@moduledoc """
|
||||
The document could not be loaded or parsed as JSON.
|
||||
"""
|
||||
defexception code: "loading document failed", message: nil
|
||||
end
|
||||
|
||||
defmodule JSON.LD.ListOfListsError do
|
||||
@moduledoc """
|
||||
A list of lists was detected. List of lists are not supported in this version of JSON-LD due to the algorithmic complexity.
|
||||
"""
|
||||
defexception code: "list of lists", message: nil
|
||||
end
|
||||
|
||||
defmodule JSON.LD.InvalidIndexValueError do
|
||||
@moduledoc """
|
||||
An @index member was encountered whose value was not a string.
|
||||
"""
|
||||
defexception code: "invalid @index value", message: nil
|
||||
end
|
||||
|
||||
defmodule JSON.LD.ConflictingIndexesError do
|
||||
@moduledoc """
|
||||
Multiple conflicting indexes have been found for the same node.
|
||||
"""
|
||||
defexception code: "conflicting indexes", message: nil
|
||||
end
|
||||
|
||||
defmodule JSON.LD.InvalidIdValueError do
|
||||
@moduledoc """
|
||||
An @id member was encountered whose value was not a string.
|
||||
"""
|
||||
defexception code: "invalid @id value", message: nil
|
||||
end
|
||||
|
||||
defmodule JSON.LD.InvalidLocalContextError do
|
||||
@moduledoc """
|
||||
An invalid local context was detected.
|
||||
"""
|
||||
defexception code: "invalid local context", message: nil
|
||||
end
|
||||
|
||||
defmodule JSON.LD.MultipleContextLinkHeadersError do
|
||||
@moduledoc """
|
||||
Multiple HTTP Link Headers [RFC5988] using the http://www.w3.org/ns/json-ld#context link relation have been detected.
|
||||
"""
|
||||
defexception code: "multiple context link headers", message: nil
|
||||
end
|
||||
|
||||
defmodule JSON.LD.LoadingRemoteContextFailedError do
|
||||
@moduledoc """
|
||||
There was a problem encountered loading a remote context.
|
||||
"""
|
||||
defexception code: "loading remote context failed", message: nil
|
||||
end
|
||||
|
||||
defmodule JSON.LD.InvalidRemoteContextError do
|
||||
@moduledoc """
|
||||
No valid context document has been found for a referenced, remote context.
|
||||
"""
|
||||
defexception code: "invalid remote context", message: nil
|
||||
end
|
||||
|
||||
defmodule JSON.LD.RecursiveContextInclusionError do
|
||||
@moduledoc """
|
||||
A cycle in remote context inclusions has been detected.
|
||||
"""
|
||||
defexception code: "recursive context inclusion", message: nil
|
||||
end
|
||||
|
||||
defmodule JSON.LD.InvalidBaseURIError do
|
||||
@moduledoc """
|
||||
An invalid base IRI has been detected, i.e., it is neither an absolute IRI nor null.
|
||||
"""
|
||||
defexception code: "invalid base IRI", message: nil
|
||||
end
|
||||
|
||||
defmodule JSON.LD.InvalidVocabMappingError do
|
||||
@moduledoc """
|
||||
An invalid vocabulary mapping has been detected, i.e., it is neither an absolute IRI nor null.
|
||||
"""
|
||||
defexception code: "invalid vocab mapping", message: nil
|
||||
end
|
||||
|
||||
defmodule JSON.LD.InvalidDefaultLanguageError do
|
||||
@moduledoc """
|
||||
The value of the default language is not a string or null and thus invalid.
|
||||
"""
|
||||
defexception code: "invalid default language", message: nil
|
||||
end
|
||||
|
||||
defmodule JSON.LD.KeywordRedefinitionError do
|
||||
@moduledoc """
|
||||
A keyword redefinition has been detected.
|
||||
"""
|
||||
defexception code: "keyword redefinition", message: nil
|
||||
end
|
||||
|
||||
defmodule JSON.LD.InvalidTermDefinitionError do
|
||||
@moduledoc """
|
||||
An invalid term definition has been detected.
|
||||
"""
|
||||
defexception code: "invalid term definition", message: nil
|
||||
end
|
||||
|
||||
defmodule JSON.LD.InvalidReversePropertyError do
|
||||
@moduledoc """
|
||||
An invalid reverse property definition has been detected.
|
||||
"""
|
||||
defexception code: "invalid reverse property", message: nil
|
||||
end
|
||||
|
||||
defmodule JSON.LD.InvalidIRIMappingError do
|
||||
@moduledoc """
|
||||
A local context contains a term that has an invalid or missing IRI mapping..
|
||||
"""
|
||||
defexception code: "invalid IRI mapping", message: nil
|
||||
end
|
||||
|
||||
defmodule JSON.LD.CyclicIRIMappingError do
|
||||
@moduledoc """
|
||||
A cycle in IRI mappings has been detected.
|
||||
"""
|
||||
defexception code: "cyclic IRI mapping", message: nil
|
||||
end
|
||||
|
||||
defmodule JSON.LD.InvalidKeywordAliasError do
|
||||
@moduledoc """
|
||||
An invalid keyword alias definition has been encountered.
|
||||
"""
|
||||
defexception code: "invalid keyword alias", message: nil
|
||||
end
|
||||
|
||||
defmodule JSON.LD.InvalidTypeMappingError do
|
||||
@moduledoc """
|
||||
An @type member in a term definition was encountered whose value could not be expanded to an absolute IRI.
|
||||
"""
|
||||
defexception code: "invalid type mapping", message: nil
|
||||
end
|
||||
|
||||
defmodule JSON.LD.InvalidLanguageMappingError do
|
||||
@moduledoc """
|
||||
An @language member in a term definition was encountered whose value was neither a string nor null and thus invalid.
|
||||
"""
|
||||
defexception code: "invalid language mapping", message: nil
|
||||
end
|
||||
|
||||
defmodule JSON.LD.CollidingKeywordsError do
|
||||
@moduledoc """
|
||||
Two properties which expand to the same keyword have been detected. This might occur if a keyword and an alias thereof are used at the same time.
|
||||
"""
|
||||
defexception code: "colliding keywords", message: nil
|
||||
end
|
||||
|
||||
defmodule JSON.LD.InvalidContainerMappingError do
|
||||
@moduledoc """
|
||||
An @container member was encountered whose value was not one of the following strings: @list, @set, or @index.
|
||||
"""
|
||||
defexception code: "invalid container mapping", message: nil
|
||||
end
|
||||
|
||||
defmodule JSON.LD.InvalidTypeValueError do
|
||||
@moduledoc """
|
||||
An invalid value for an @type member has been detected, i.e., the value was neither a string nor an array of strings.
|
||||
"""
|
||||
defexception code: "invalid type value", message: nil
|
||||
end
|
||||
|
||||
defmodule JSON.LD.InvalidValueObjectError do
|
||||
@moduledoc """
|
||||
A value object with disallowed members has been detected.
|
||||
"""
|
||||
defexception code: "invalid value object", message: nil
|
||||
end
|
||||
|
||||
defmodule JSON.LD.InvalidValueObjectValueError do
|
||||
@moduledoc """
|
||||
An invalid value for the @value member of a value object has been detected, i.e., it is neither a scalar nor null.
|
||||
"""
|
||||
defexception code: "invalid value object value", message: nil
|
||||
end
|
||||
|
||||
defmodule JSON.LD.InvalidLanguageTaggedStringError do
|
||||
@moduledoc """
|
||||
A language-tagged string with an invalid language value was detected.
|
||||
"""
|
||||
defexception code: "invalid language-tagged string", message: nil
|
||||
end
|
||||
|
||||
|
||||
defmodule JSON.LD.InvalidLanguageTaggedValueError do
|
||||
@moduledoc """
|
||||
A number, true, or false with an associated language tag was detected.
|
||||
"""
|
||||
defexception code: "invalid language-tagged value", message: nil
|
||||
end
|
||||
|
||||
defmodule JSON.LD.InvalidTypedValueError do
|
||||
@moduledoc """
|
||||
A typed value with an invalid type was detected.
|
||||
"""
|
||||
defexception code: "invalid typed value", message: nil
|
||||
end
|
||||
|
||||
defmodule JSON.LD.InvalidSetOrListObjectError do
|
||||
@moduledoc """
|
||||
A set object or list object with disallowed members has been detected.
|
||||
"""
|
||||
defexception code: "invalid set or list object", message: nil
|
||||
end
|
||||
|
||||
defmodule JSON.LD.InvalidLanguageMapValueError do
|
||||
@moduledoc """
|
||||
An invalid value in a language map has been detected. It has to be a string or an array of strings.
|
||||
"""
|
||||
defexception code: "invalid language map value", message: nil
|
||||
end
|
||||
|
||||
defmodule JSON.LD.CompactionToListOfListsError do
|
||||
@moduledoc """
|
||||
The compacted document contains a list of lists as multiple lists have been compacted to the same term.
|
||||
"""
|
||||
defexception code: "compaction to list of lists", message: nil
|
||||
end
|
||||
|
||||
defmodule JSON.LD.InvalidReversePropertyMapError do
|
||||
@moduledoc """
|
||||
CollidingKeywordsError
|
||||
"""
|
||||
defexception code: "invalid reverse property map", message: nil
|
||||
end
|
||||
|
||||
defmodule JSON.LD.InvalidReverseValueError do
|
||||
@moduledoc """
|
||||
An invalid value for an @reverse member has been detected, i.e., the value was not a JSON object.
|
||||
"""
|
||||
defexception code: "invalid @reverse value", message: nil
|
||||
end
|
||||
|
||||
defmodule JSON.LD.InvalidReversePropertyValueError do
|
||||
@moduledoc """
|
||||
An invalid value for a reverse property has been detected. The value of an inverse property must be a node object.
|
||||
"""
|
||||
defexception code: "invalid reverse property value", message: nil
|
||||
end
|
554
lib/json_ld.ex
554
lib/json_ld.ex
|
@ -1,2 +1,556 @@
|
|||
defmodule JSON.LD do
|
||||
|
||||
# see https://www.w3.org/TR/2014/REC-json-ld-20140116/#syntax-tokens-and-keywords
|
||||
@keywords ~w[
|
||||
@base
|
||||
@container
|
||||
@context
|
||||
@graph
|
||||
@id
|
||||
@index
|
||||
@language
|
||||
@list
|
||||
@reverse
|
||||
@set
|
||||
@type
|
||||
@value
|
||||
@vocab
|
||||
:
|
||||
]
|
||||
|
||||
def keywords, do: @keywords
|
||||
|
||||
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.
|
||||
|
||||
see http://json-ld.org/spec/latest/json-ld-api/#expansion-algorithm
|
||||
"""
|
||||
def expand(json_ld_object, opts \\ []) do
|
||||
case do_expand(JSON.LD.Context.new(opts), nil, json_ld_object, Keyword.delete(opts, :base)) do
|
||||
result = %{"@graph" => graph} when map_size(result) == 1 ->
|
||||
graph
|
||||
nil ->
|
||||
[]
|
||||
result when not is_list(result) ->
|
||||
[result]
|
||||
result -> result
|
||||
end
|
||||
end
|
||||
|
||||
defp do_expand(active_context, active_property, element, opts \\ [])
|
||||
|
||||
# 1) If element is null, return null.
|
||||
defp do_expand(_, _, nil, _), do: nil
|
||||
|
||||
# 2) If element is a scalar, ...
|
||||
defp do_expand(active_context, active_property, element, opts)
|
||||
when is_binary(element) or is_number(element) or is_boolean(element) do
|
||||
if active_property in [nil, "@graph"] do
|
||||
nil
|
||||
else
|
||||
expand_value(active_context, active_property, element)
|
||||
end
|
||||
end
|
||||
|
||||
# 3) If element is an array, ...
|
||||
defp do_expand(active_context, active_property, element, opts)
|
||||
when is_list(element) do
|
||||
term_def = active_context.term_defs[active_property]
|
||||
container_mapping = term_def && term_def.container_mapping
|
||||
element
|
||||
|> Enum.reduce([], fn (item, result) ->
|
||||
expanded_item = do_expand(active_context, active_property, item)
|
||||
if (active_property == "@list" or container_mapping == "@list") and
|
||||
(is_list(expanded_item) or Map.has_key?(expanded_item, "@list")),
|
||||
do: raise JSON.LD.ListOfListsError,
|
||||
message: "List of lists in #{inspect element}"
|
||||
case expanded_item do
|
||||
nil -> result
|
||||
list when is_list(list) ->
|
||||
result ++ list
|
||||
expanded_item ->
|
||||
result ++ [expanded_item]
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
# 4) - 13)
|
||||
defp do_expand(active_context, active_property, element, opts)
|
||||
when is_map(element) do
|
||||
# 5)
|
||||
if Map.has_key?(element, "@context") do
|
||||
active_context = JSON.LD.Context.update(active_context, Map.get(element, "@context"))
|
||||
end
|
||||
# 6) and 7)
|
||||
result = element
|
||||
|> Enum.sort_by(fn {key, _} -> key end)
|
||||
|> Enum.reduce(%{}, fn ({key, value}, result) ->
|
||||
if (key != "@context") && # 7.1)
|
||||
(expanded_property = expand_iri(key, active_context, false, true)) && # 7.2)
|
||||
(String.contains?(expanded_property, ":") || keyword?(expanded_property)) do # 7.3)
|
||||
if keyword?(expanded_property) do # 7.4)
|
||||
if active_property == "@reverse", # 7.4.1)
|
||||
do: raise JSON.LD.InvalidReversePropertyMapError,
|
||||
message: "An invalid reverse property map has been detected. No keywords apart from @context are allowed in reverse property maps."
|
||||
if Map.has_key?(result, expanded_property), # 7.4.2)
|
||||
do: raise JSON.LD.CollidingKeywordsError,
|
||||
message: "Two properties which expand to the same keyword have been detected. This might occur if a keyword and an alias thereof are used at the same time."
|
||||
|
||||
expanded_value = case expanded_property do
|
||||
"@id" -> # 7.4.3)
|
||||
if is_binary(value) do
|
||||
expand_iri(value, active_context, true)
|
||||
else
|
||||
raise JSON.LD.InvalidIdValueError,
|
||||
message: "#{inspect value} is not a valid @id value"
|
||||
end
|
||||
"@type" -> # 7.4.4)
|
||||
cond do
|
||||
is_binary(value) ->
|
||||
expand_iri(value, active_context, true, true)
|
||||
is_list(value) and Enum.all?(value, &is_binary/1) ->
|
||||
Enum.map value, fn item ->
|
||||
expand_iri(item, active_context, true, true) end
|
||||
true ->
|
||||
raise JSON.LD.InvalidTypeValueError,
|
||||
message: "#{inspect value} is not a valid @type value"
|
||||
end
|
||||
"@graph" -> # 7.4.5)
|
||||
do_expand(active_context, "@graph", value, opts)
|
||||
"@value" -> # 7.4.6)
|
||||
if scalar?(value) or is_nil(value) do
|
||||
if is_nil(value) do
|
||||
{:skip, Map.put(result, "@value", nil)}
|
||||
else
|
||||
value
|
||||
end
|
||||
else
|
||||
raise JSON.LD.InvalidValueObjectValueError,
|
||||
message: "#{inspect value} is not a valid value for the @value member of a value object; neither a scalar nor null"
|
||||
end
|
||||
"@language" -> # 7.4.7)
|
||||
if is_binary(value),
|
||||
do: String.downcase(value),
|
||||
else: raise JSON.LD.InvalidLanguageTaggedStringError,
|
||||
message: "#{inspect value} is not a valid language-tag"
|
||||
"@index" -> # 7.4.8)
|
||||
if is_binary(value),
|
||||
do: value,
|
||||
else: raise JSON.LD.InvalidIndexValueError,
|
||||
message: "#{inspect value} is not a valid @index value"
|
||||
"@list" -> # 7.4.9)
|
||||
if active_property in [nil, "@graph"] do # 7.4.9.1)
|
||||
{:skip, result}
|
||||
else
|
||||
value = do_expand(active_context, active_property, value, opts)
|
||||
|
||||
# Spec FIXME: need to be sure that result is a list [from RDF.rb implementation]
|
||||
value = if is_list(value),
|
||||
do: value,
|
||||
else: [value]
|
||||
|
||||
# If expanded value is a list object, a list of lists error has been detected and processing is aborted.
|
||||
# Spec FIXME: Also look at each object if result is a list [from RDF.rb implementation]
|
||||
if Enum.any?(value, fn v -> Map.has_key?(v, "@list") end),
|
||||
do: raise JSON.LD.ListOfListsError,
|
||||
message: "List of lists in #{inspect value}"
|
||||
value
|
||||
end
|
||||
"@set" -> # 7.4.10)
|
||||
do_expand(active_context, active_property, value, opts)
|
||||
"@reverse" -> # 7.4.11)
|
||||
unless is_map(value),
|
||||
do: raise JSON.LD.InvalidReverseValueError,
|
||||
message: "#{inspect value} is not a valid @reverse value"
|
||||
expanded_value = do_expand(active_context, "@reverse", value, opts) # 7.4.11.1)
|
||||
new_result =
|
||||
if Map.has_key?(expanded_value, "@reverse") do # 7.4.11.2) If expanded value contains an @reverse member, i.e., properties that are reversed twice, execute for each of its property and item the following steps:
|
||||
Enum.reduce expanded_value["@reverse"], result,
|
||||
fn ({property, item}, new_result) ->
|
||||
items = if is_list(item),
|
||||
do: item,
|
||||
else: [item]
|
||||
Map.update(new_result, property, items, fn members ->
|
||||
members ++ items
|
||||
end)
|
||||
end
|
||||
else
|
||||
result
|
||||
end
|
||||
if Map.keys(expanded_value) != ["@reverse"] do # 7.4.11.3)
|
||||
reverse_map =
|
||||
Enum.reduce expanded_value, Map.get(new_result, "@reverse", %{}), fn
|
||||
({property, items}, reverse_map) when property != "@reverse" ->
|
||||
Enum.each(items, fn item ->
|
||||
if Map.has_key?(item, "@value") or Map.has_key?(item, "@list"),
|
||||
do: raise JSON.LD.InvalidReversePropertyValueError,
|
||||
message: "invalid value for a reverse property in #{inspect item}"
|
||||
end)
|
||||
Map.update(reverse_map, property, items, fn members ->
|
||||
members ++ items
|
||||
end)
|
||||
(_, reverse_map) -> reverse_map
|
||||
end
|
||||
new_result = Map.put(new_result, "@reverse", reverse_map)
|
||||
end
|
||||
{:skip, new_result}
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
# 7.4.12)
|
||||
case expanded_value do
|
||||
nil ->
|
||||
result
|
||||
{:skip, new_result} ->
|
||||
new_result
|
||||
expanded_value ->
|
||||
Map.put(result, expanded_property, expanded_value)
|
||||
end
|
||||
|
||||
else # expanded_property is not a keyword
|
||||
term_def = active_context.term_defs[key]
|
||||
expanded_value = cond do
|
||||
# 7.5) Otherwise, if key's container mapping in active context is @language and value is a JSON object then value is expanded from a language map as follows:
|
||||
is_map(value) && term_def && term_def.container_mapping == "@language" ->
|
||||
value
|
||||
# |> IO.inspect(label: "value")
|
||||
|> Enum.sort_by(fn {language, _} -> language end)
|
||||
|> Enum.reduce([], fn ({language, language_value}, language_map_result) ->
|
||||
language_map_result ++ (
|
||||
if(is_list(language_value),
|
||||
do: language_value,
|
||||
else: [language_value])
|
||||
|> Enum.map(fn
|
||||
item when is_binary(item) ->
|
||||
%{
|
||||
"@value" => item,
|
||||
"@language" => String.downcase(language)
|
||||
}
|
||||
item ->
|
||||
raise JSON.LD.InvalidLanguageMapValueError,
|
||||
message: "#{inspect item} is not a valid language map value"
|
||||
|
||||
end)
|
||||
)
|
||||
# |> IO.inspect(label: "result")
|
||||
end)
|
||||
# 7.6)
|
||||
is_map(value) && term_def && term_def.container_mapping == "@index" ->
|
||||
value
|
||||
|> Enum.sort_by(fn {index, _} -> index end)
|
||||
|> Enum.reduce([], fn ({index, index_value}, index_map_result) ->
|
||||
index_map_result ++ (
|
||||
index_value = if(is_list(index_value),
|
||||
do: index_value,
|
||||
else: [index_value])
|
||||
index_value = do_expand(active_context, key, index_value, opts)
|
||||
Enum.map(index_value, fn item ->
|
||||
Map.put_new(item, "@index", index)
|
||||
end)
|
||||
)
|
||||
end)
|
||||
# 7.7)
|
||||
true ->
|
||||
do_expand(active_context, key, value, opts)
|
||||
end
|
||||
# 7.8)
|
||||
if is_nil(expanded_value) do
|
||||
result
|
||||
else
|
||||
# 7.9)
|
||||
if (term_def && term_def.container_mapping == "@list") &&
|
||||
!(is_map(expanded_value) && Map.has_key?(expanded_value, "@list")) do
|
||||
expanded_value = %{"@list" =>
|
||||
(if is_list(expanded_value),
|
||||
do: expanded_value,
|
||||
else: [expanded_value])}
|
||||
end
|
||||
# 7.10) Otherwise, if the term definition associated to key indicates that it is a reverse property
|
||||
# Spec FIXME: this is not an otherwise [from RDF.rb implementation]
|
||||
if term_def && term_def.reverse_property do
|
||||
reverse_map = Map.get(result, "@reverse", %{})
|
||||
reverse_map =
|
||||
if(is_list(expanded_value),
|
||||
do: expanded_value,
|
||||
else: [expanded_value])
|
||||
|> Enum.reduce(reverse_map, fn (item, reverse_map) ->
|
||||
if Map.has_key?(item, "@value") or Map.has_key?(item, "@list"),
|
||||
do: raise JSON.LD.InvalidReversePropertyValueError,
|
||||
message: "invalid value for a reverse property in #{inspect item}"
|
||||
Map.update reverse_map, expanded_property, [item], fn members ->
|
||||
members ++ [item]
|
||||
end
|
||||
end)
|
||||
Map.put(result, "@reverse", reverse_map)
|
||||
else # 7.11)
|
||||
expanded_value = if is_list(expanded_value),
|
||||
do: expanded_value,
|
||||
else: [expanded_value]
|
||||
Map.update result, expanded_property, expanded_value,
|
||||
fn values -> expanded_value ++ values end
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
result
|
||||
end
|
||||
end)
|
||||
|
||||
result = case result do
|
||||
# 8)
|
||||
%{"@value" => value} ->
|
||||
with keys = Map.keys(result) do # 8.1)
|
||||
if Enum.any?(keys, &(not &1 in ~w[@value @language @type @index])) ||
|
||||
("@language" in keys and "@type" in keys) do
|
||||
raise JSON.LD.InvalidValueObjectError,
|
||||
message: "value object with disallowed members"
|
||||
end
|
||||
end
|
||||
cond do
|
||||
value == nil -> nil # 8.2)
|
||||
!is_binary(value) and Map.has_key?(result, "@language") -> # 8.3)
|
||||
raise JSON.LD.InvalidLanguageTaggedValueError,
|
||||
message: "@value '#{inspect value}' is tagged with a language"
|
||||
(type = result["@type"]) && !RDF.uri?(type) -> # 8.4)
|
||||
raise JSON.LD.InvalidTypedValueError,
|
||||
message: "@value '#{inspect value}' has invalid type #{inspect type}"
|
||||
true -> result
|
||||
end
|
||||
# 9)
|
||||
%{"@type" => type} when not is_list(type) ->
|
||||
Map.put(result, "@type", [type])
|
||||
# 10)
|
||||
%{"@set" => set} ->
|
||||
validate_set_or_list_object(result)
|
||||
set
|
||||
%{"@list" => list} ->
|
||||
validate_set_or_list_object(result)
|
||||
result
|
||||
_ -> result
|
||||
end
|
||||
|
||||
# 11) If result contains only the key @language, set result to null.
|
||||
if is_map(result) and map_size(result) == 1 and Map.has_key?(result, "@language"),
|
||||
do: result = nil
|
||||
|
||||
# 12) If active property is null or @graph, drop free-floating values as follows:
|
||||
if active_property in [nil, "@graph"] and (
|
||||
Enum.empty?(result) or
|
||||
Map.has_key?(result, "@value") or Map.has_key?(result, "@list") or
|
||||
(map_size(result) == 1 and Map.has_key?(result, "@id"))),
|
||||
do: result = nil
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
defp validate_set_or_list_object(object) when map_size(object) == 1, do: true
|
||||
defp validate_set_or_list_object(object = %{"@index" => _})
|
||||
when map_size(object) == 2, do: true
|
||||
defp validate_set_or_list_object(object) do
|
||||
raise JSON.LD.InvalidSetOrListObjectError,
|
||||
message: "set or list object with disallowed members: #{inspect object}"
|
||||
end
|
||||
|
||||
|
||||
@doc """
|
||||
see http://json-ld.org/spec/latest/json-ld-api/#value-expansion
|
||||
"""
|
||||
def expand_value(active_context, active_property, value) do
|
||||
with term_def when term_def != nil <- active_context.term_defs[active_property] do
|
||||
cond do
|
||||
term_def.type_mapping == "@id" ->
|
||||
%{"@id" => expand_iri(value, active_context, true, false)}
|
||||
term_def.type_mapping == "@vocab" ->
|
||||
%{"@id" => expand_iri(value, active_context, true, true)}
|
||||
type_mapping = term_def.type_mapping ->
|
||||
%{"@value" => value, "@type" => type_mapping}
|
||||
is_binary(value) ->
|
||||
language_mapping = term_def.language_mapping
|
||||
cond do
|
||||
not language_mapping in [nil, :undefined] ->
|
||||
%{"@value" => value, "@language" => language_mapping}
|
||||
language_mapping == :undefined && active_context.default_language ->
|
||||
%{"@value" => value, "@language" => active_context.default_language}
|
||||
true ->
|
||||
%{"@value" => value}
|
||||
end
|
||||
true ->
|
||||
%{"@value" => value}
|
||||
end
|
||||
else
|
||||
_ -> %{"@value" => value}
|
||||
end
|
||||
end
|
||||
|
||||
###########################################################################
|
||||
|
||||
@doc """
|
||||
|
||||
"""
|
||||
def flatten(json_ld_object, context \\ nil, opts \\ []) do
|
||||
json_ld_object
|
||||
end
|
||||
|
||||
###########################################################################
|
||||
|
||||
@doc """
|
||||
|
||||
"""
|
||||
def compact(json_ld_object, context, opts \\ []) do
|
||||
json_ld_object
|
||||
end
|
||||
|
||||
###########################################################################
|
||||
|
||||
@doc """
|
||||
see http://json-ld.org/spec/latest/json-ld-api/#iri-expansion
|
||||
"""
|
||||
def expand_iri(value, active_context, doc_relative \\ false, vocab \\ false,
|
||||
local_context \\ nil, defined \\ nil)
|
||||
|
||||
# 1) If value is a keyword or null, return value as is.
|
||||
def expand_iri(value, active_context, _, _, local_context, defined)
|
||||
when is_nil(value) or value in @keywords do
|
||||
if local_context || defined do
|
||||
{value, active_context, defined}
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
def expand_iri(value, active_context, doc_relative, vocab, local_context, defined) do
|
||||
# 2)
|
||||
if local_context && (local_def = local_context[value]) && defined[value] != true do
|
||||
{active_context, defined} =
|
||||
JSON.LD.Context.create_term_definition(
|
||||
active_context, local_context, value, local_def, defined)
|
||||
end
|
||||
|
||||
result = cond do
|
||||
# 3) If vocab is true and the active context has a term definition for value, return the associated IRI mapping.
|
||||
vocab && (term_def = active_context.term_defs[value]) ->
|
||||
term_def.iri_mapping
|
||||
# 4) If value contains a colon (:), it is either an absolute IRI, a compact IRI, or a blank node identifier
|
||||
String.contains?(value, ":") ->
|
||||
case compact_iri_parts(value) do
|
||||
[prefix, suffix] ->
|
||||
# 4.3)
|
||||
if local_context && (local_def = local_context[prefix]) && defined[prefix] != true do
|
||||
{active_context, defined} =
|
||||
JSON.LD.Context.create_term_definition(
|
||||
active_context, local_context, prefix, local_def, defined)
|
||||
end
|
||||
# 4.4)
|
||||
if prefix_def = active_context.term_defs[prefix] do
|
||||
prefix_def.iri_mapping <> suffix
|
||||
else
|
||||
value # 4.5)
|
||||
end
|
||||
nil -> value # 4.2)
|
||||
end
|
||||
# 5) If vocab is true, and active context has a vocabulary mapping, return the result of concatenating the vocabulary mapping with value.
|
||||
vocab && (vocabulary_mapping = active_context.vocab) ->
|
||||
vocabulary_mapping <> value
|
||||
# 6) Otherwise, if document relative is true, set value to the result of resolving value against the base IRI. Only the basic algorithm in section 5.2 of [RFC3986] is used; neither Syntax-Based Normalization nor Scheme-Based Normalization are performed. Characters additionally allowed in IRI references are treated in the same way that unreserved characters are treated in URI references, per section 6.5 of [RFC3987].
|
||||
doc_relative ->
|
||||
absolute_iri(value, active_context.base_iri)
|
||||
# TODO: RDF.rb's implementation differs from the spec here, by checking if base_iri is actually present in the previous clause and adding the following additional clause. Another Spec error?
|
||||
# if local_context && RDF::URI(value).relative?
|
||||
# # If local context is not null and value is not an absolute IRI, an invalid IRI mapping error has been detected and processing is aborted.
|
||||
# raise JSON.LD.InvalidIRIMappingError, message: "not an absolute IRI: #{value}"
|
||||
# 7) Return value as is.
|
||||
true -> value
|
||||
end
|
||||
|
||||
if local_context do
|
||||
{result, active_context, defined}
|
||||
else
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
###########################################################################
|
||||
|
||||
@doc """
|
||||
Resolves a relative IRI against a base IRI.
|
||||
|
||||
as specified in [section 5.1 Establishing a Base URI of RFC3986](http://tools.ietf.org/html/rfc3986#section-5.1).
|
||||
Only the basic algorithm in [section 5.2 of RFC3986](http://tools.ietf.org/html/rfc3986#section-5.2)
|
||||
is used; neither Syntax-Based Normalization nor Scheme-Based Normalization are performed.
|
||||
|
||||
Characters additionally allowed in IRI references are treated in the same way that unreserved
|
||||
characters are treated in URI references, per [section 6.5 of RFC3987](http://tools.ietf.org/html/rfc3987#section-6.5)
|
||||
"""
|
||||
# TODO: This should be part of a dedicated URI/IRI implementation and properly tested.
|
||||
def absolute_iri(value, base_iri)
|
||||
|
||||
def absolute_iri(value, nil), do: value
|
||||
|
||||
def absolute_iri(value, base_iri) do
|
||||
case URI.parse(value) do
|
||||
# absolute?
|
||||
uri = %URI{scheme: scheme} when not is_nil(scheme) -> uri
|
||||
# relative
|
||||
_ ->
|
||||
URI.merge(base_iri, value)
|
||||
end
|
||||
|> to_string
|
||||
end
|
||||
|
||||
|
||||
@doc """
|
||||
Checks if the given value is an absolute IRI.
|
||||
|
||||
An absolute IRI is defined in [RFC3987](http://www.ietf.org/rfc/rfc3987.txt)
|
||||
containing a scheme along with a path and optional query and fragment segments.
|
||||
|
||||
see https://www.w3.org/TR/json-ld-api/#dfn-absolute-iri
|
||||
"""
|
||||
# TODO: This should be part of a dedicated URI/IRI implementation and properly tested.
|
||||
def absolute_iri?(value), do: RDF.uri?(value)
|
||||
|
||||
def compact_iri_parts(compact_iri, exclude_bnode \\ true) do
|
||||
with [prefix, suffix] when not(binary_part(suffix, 0, 2) == "//") and
|
||||
not(exclude_bnode and prefix == "_") <-
|
||||
String.split(compact_iri, ":", parts: 2) do
|
||||
[prefix, suffix]
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@doc """
|
||||
Checks if the given value is a blank node identifier.
|
||||
|
||||
A blank node identifier is a string that can be used as an identifier for a
|
||||
blank node within the scope of a JSON-LD document.
|
||||
|
||||
Blank node identifiers begin with `_:`
|
||||
|
||||
see https://www.w3.org/TR/json-ld-api/#dfn-blank-node-identifier
|
||||
"""
|
||||
def blank_node_id?("_:" <> _), do: true
|
||||
def blank_node_id?(_), do: false
|
||||
|
||||
|
||||
defp scalar?(value) when is_binary(value) or is_number(value) or
|
||||
is_boolean(value), do: true
|
||||
defp scalar?(_), do: false
|
||||
|
||||
###########################################################################
|
||||
|
||||
|
||||
@doc """
|
||||
Generator function for `JSON.LD.Context`s.
|
||||
"""
|
||||
def context(args, opts \\ [])
|
||||
def context(%{"@context" => _} = object, opts),
|
||||
do: JSON.LD.Context.create(object, opts)
|
||||
def context(context, opts),
|
||||
do: JSON.LD.Context.create(%{"@context" => context}, opts)
|
||||
|
||||
end
|
||||
|
|
1
mix.exs
1
mix.exs
|
@ -38,6 +38,7 @@ defmodule JSON.LD.Mixfile do
|
|||
defp deps do
|
||||
[
|
||||
{:rdf_core, in_umbrella: true},
|
||||
{:poison, "~> 3.0"},
|
||||
{:dialyxir, "~> 0.4", only: [:dev, :test]},
|
||||
{:credo, "~> 0.5", only: [:dev, :test]},
|
||||
{:ex_doc, "~> 0.14", only: :dev},
|
||||
|
|
|
@ -2,7 +2,18 @@ defmodule JSON.LDTest do
|
|||
use ExUnit.Case
|
||||
doctest JSON.LD
|
||||
|
||||
test "the truth" do
|
||||
assert 1 + 1 == 2
|
||||
describe "compact_iri_parts" do
|
||||
test "returns the prefix and suffix of a compact IRI" do
|
||||
assert JSON.LD.compact_iri_parts("foo:bar") == ["foo", "bar"]
|
||||
end
|
||||
|
||||
test "returns nil on absolute IRIs" do
|
||||
assert JSON.LD.compact_iri_parts("http://example.com/") == nil
|
||||
end
|
||||
|
||||
test "returns nil on blank nodes" do
|
||||
assert JSON.LD.compact_iri_parts("_:bar") == nil
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
|
263
test/unit/context_test.exs
Normal file
263
test/unit/context_test.exs
Normal file
|
@ -0,0 +1,263 @@
|
|||
defmodule JSON.LD.ContextTest do
|
||||
use ExUnit.Case
|
||||
|
||||
doctest JSON.LD.Context
|
||||
|
||||
describe "create from Hash" do
|
||||
test "extracts @base" do
|
||||
assert JSON.LD.context(%{"@base" => "http://base/"}).base_iri == "http://base/"
|
||||
end
|
||||
|
||||
test "extracts @language" do
|
||||
assert JSON.LD.context(%{"@language" => "en"}).default_language == "en"
|
||||
end
|
||||
|
||||
test "extracts @vocab" do
|
||||
assert JSON.LD.context(%{"@vocab" => "http://schema.org/"}).vocab ==
|
||||
"http://schema.org/"
|
||||
end
|
||||
|
||||
test "maps term with IRI value" do
|
||||
c = JSON.LD.context(%{"foo" => "http://example.com/"})
|
||||
assert c.term_defs["foo"]
|
||||
assert c.term_defs["foo"].iri_mapping == "http://example.com/"
|
||||
end
|
||||
|
||||
test "maps term with @id" do
|
||||
c = JSON.LD.context(%{"foo" => %{"@id" => "http://example.com/"}})
|
||||
assert c.term_defs["foo"]
|
||||
assert c.term_defs["foo"].iri_mapping == "http://example.com/"
|
||||
end
|
||||
|
||||
test "associates @list container mapping with predicate" do
|
||||
c = JSON.LD.context(%{"foo" =>
|
||||
%{"@id" => "http://example.com/", "@container" => "@list"}})
|
||||
assert c.term_defs["foo"]
|
||||
assert c.term_defs["foo"].iri_mapping == "http://example.com/"
|
||||
assert c.term_defs["foo"].container_mapping == "@list"
|
||||
end
|
||||
|
||||
test "associates @set container mapping with predicate" do
|
||||
c = JSON.LD.context(%{"foo" =>
|
||||
%{"@id" => "http://example.com/", "@container" => "@set"}})
|
||||
assert c.term_defs["foo"]
|
||||
assert c.term_defs["foo"].iri_mapping == "http://example.com/"
|
||||
assert c.term_defs["foo"].container_mapping == "@set"
|
||||
end
|
||||
|
||||
test "associates @id container mapping with predicate" do
|
||||
c = JSON.LD.context(%{"foo" =>
|
||||
%{"@id" => "http://example.com/", "@type" => "@id"}})
|
||||
assert c.term_defs["foo"]
|
||||
assert c.term_defs["foo"].iri_mapping == "http://example.com/"
|
||||
assert c.term_defs["foo"].type_mapping == "@id"
|
||||
end
|
||||
|
||||
test "associates type mapping with predicate" do
|
||||
c = JSON.LD.context(%{"foo" =>
|
||||
%{"@id" => "http://example.com/", "@type" => to_string(RDF.XSD.string)}})
|
||||
assert c.term_defs["foo"]
|
||||
assert c.term_defs["foo"].iri_mapping == "http://example.com/"
|
||||
assert c.term_defs["foo"].type_mapping == to_string(RDF.XSD.string)
|
||||
end
|
||||
|
||||
test "associates language mapping with predicate" do
|
||||
c = JSON.LD.context(%{"foo" =>
|
||||
%{"@id" => "http://example.com/", "@language" => "en"}})
|
||||
assert c.term_defs["foo"]
|
||||
assert c.term_defs["foo"].iri_mapping == "http://example.com/"
|
||||
assert c.term_defs["foo"].language_mapping == "en"
|
||||
end
|
||||
|
||||
test "expands chains of term definition/use with string values" do
|
||||
assert JSON.LD.context(%{
|
||||
"foo" => "bar",
|
||||
"bar" => "baz",
|
||||
"baz" => "http://example.com/"
|
||||
}) |> iri_mappings == %{
|
||||
"foo" => "http://example.com/",
|
||||
"bar" => "http://example.com/",
|
||||
"baz" => "http://example.com/"
|
||||
}
|
||||
end
|
||||
|
||||
test "expands terms using @vocab" do
|
||||
c = JSON.LD.context(%{
|
||||
"foo" => "bar",
|
||||
"@vocab" => "http://example.com/"})
|
||||
assert c.term_defs["foo"]
|
||||
assert c.term_defs["foo"].iri_mapping == "http://example.com/bar"
|
||||
end
|
||||
end
|
||||
|
||||
describe "create from Array/List" do
|
||||
test "merges definitions from each context" do
|
||||
assert JSON.LD.context([
|
||||
%{"foo" => "http://example.com/foo"},
|
||||
%{"bar" => "foo"}
|
||||
]) |> iri_mappings == %{
|
||||
"foo" => "http://example.com/foo",
|
||||
"bar" => "http://example.com/foo"
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
describe "term definitions with null values" do
|
||||
test "removes @language if set to null" do
|
||||
assert JSON.LD.context([
|
||||
%{ "@language" => "en" },
|
||||
%{ "@language" => nil }
|
||||
]).default_language == nil
|
||||
end
|
||||
|
||||
test "removes @vocab if set to null" do
|
||||
assert JSON.LD.context([
|
||||
%{ "@vocab" => "http://schema.org/" },
|
||||
%{ "@vocab" => nil }
|
||||
]).vocab == nil
|
||||
end
|
||||
|
||||
test "removes term if set to null with @vocab" do
|
||||
assert JSON.LD.context([%{
|
||||
"@vocab" => "http://schema.org/",
|
||||
"term" => nil
|
||||
}]) |> iri_mappings == %{
|
||||
"term" => nil
|
||||
}
|
||||
end
|
||||
|
||||
test "removes a term definition" do
|
||||
assert JSON.LD.context(%{"name" => nil}).term_defs["name"] == nil
|
||||
end
|
||||
|
||||
test "loads initial context" do
|
||||
init_ec = JSON.LD.Context.new
|
||||
nil_ec = JSON.LD.context(nil)
|
||||
assert nil_ec.default_language == init_ec.default_language
|
||||
assert nil_ec |> coercions == init_ec |> coercions
|
||||
assert nil_ec |> containers == init_ec |> containers
|
||||
assert nil_ec |> languages == init_ec |> languages
|
||||
assert nil_ec |> iri_mappings == init_ec |> iri_mappings
|
||||
end
|
||||
end
|
||||
|
||||
describe "errors" do
|
||||
%{
|
||||
"no @id, @type, or @container" => %{
|
||||
input: %{"foo" => %{}},
|
||||
exception: JSON.LD.InvalidIRIMappingError
|
||||
},
|
||||
"value as array" => %{
|
||||
input: %{"foo" => []},
|
||||
exception: JSON.LD.InvalidTermDefinitionError
|
||||
},
|
||||
"@id as object" => %{
|
||||
input: %{"foo" => %{"@id" => %{}}},
|
||||
exception: JSON.LD.InvalidIRIMappingError
|
||||
},
|
||||
"@id as array of object" => %{
|
||||
input: %{"foo" => %{"@id" => [{}]}},
|
||||
exception: JSON.LD.InvalidIRIMappingError
|
||||
},
|
||||
"@id as array of null" => %{
|
||||
input: %{"foo" => %{"@id" => [nil]}},
|
||||
exception: JSON.LD.InvalidIRIMappingError
|
||||
},
|
||||
"@type as object" => %{
|
||||
input: %{"foo" => %{"@type" => %{}}},
|
||||
exception: JSON.LD.InvalidTypeMappingError
|
||||
},
|
||||
"@type as array" => %{
|
||||
input: %{"foo" => %{"@type" => []}},
|
||||
exception: JSON.LD.InvalidTypeMappingError
|
||||
},
|
||||
"@type as @list" => %{
|
||||
input: %{"foo" => %{"@type" => "@list"}},
|
||||
exception: JSON.LD.InvalidTypeMappingError
|
||||
},
|
||||
"@type as @set" => %{
|
||||
input: %{"foo" => %{"@type" => "@set"}},
|
||||
exception: JSON.LD.InvalidTypeMappingError
|
||||
},
|
||||
"@container as object" => %{
|
||||
input: %{"foo" => %{"@container" => %{}}},
|
||||
exception: JSON.LD.InvalidIRIMappingError
|
||||
},
|
||||
"@container as array" => %{
|
||||
input: %{"foo" => %{"@container" => []}},
|
||||
exception: JSON.LD.InvalidIRIMappingError
|
||||
},
|
||||
"@container as string" => %{
|
||||
input: %{"foo" => %{"@container" => "true"}},
|
||||
exception: JSON.LD.InvalidIRIMappingError
|
||||
},
|
||||
"@language as @id" => %{
|
||||
input: %{"@language" => %{"@id" => "http://example.com/"}},
|
||||
exception: JSON.LD.InvalidDefaultLanguageError
|
||||
},
|
||||
"@vocab as @id" => %{
|
||||
input: %{"@vocab" => %{"@id" => "http://example.com/"}},
|
||||
exception: JSON.LD.InvalidVocabMappingError
|
||||
},
|
||||
}
|
||||
|> Enum.each(fn ({title, data}) ->
|
||||
@tag data: data
|
||||
test title, %{data: data} do
|
||||
assert_raise data.exception, fn ->
|
||||
JSON.LD.context(data.input)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
(JSON.LD.keywords -- ~w[@base @language @vocab])
|
||||
|> Enum.each(fn keyword ->
|
||||
@tag keyword: keyword
|
||||
test "does not redefine #{keyword} as a string", %{keyword: keyword} do
|
||||
assert_raise JSON.LD.KeywordRedefinitionError, fn ->
|
||||
JSON.LD.context(%{"@context" => %{keyword => "http://example.com/"}})
|
||||
end
|
||||
end
|
||||
|
||||
@tag keyword: keyword
|
||||
test "does not redefine #{keyword} with an @id", %{keyword: keyword} do
|
||||
assert_raise JSON.LD.KeywordRedefinitionError, fn ->
|
||||
JSON.LD.context(%{"@context" => %{keyword => %{"@id" => "http://example.com/"}}})
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
# TODO: "To avoid forward-compatibility issues, a term should not start with an @ character as future versions of JSON-LD may introduce additional keywords." -- https://www.w3.org/TR/json-ld/#terms
|
||||
@tag :skip
|
||||
test "an empty string is not a valid term"
|
||||
|
||||
# TODO: "Furthermore, the term must not be an empty string ("") as not all programming languages are able to handle empty JSON keys." -- https://www.w3.org/TR/json-ld/#terms
|
||||
@tag :skip
|
||||
test "warn on terms starting with a @"
|
||||
|
||||
|
||||
def iri_mappings(%JSON.LD.Context{term_defs: term_defs}) do
|
||||
Enum.reduce term_defs, %{}, fn ({term, term_def}, iri_mappings) ->
|
||||
Map.put iri_mappings, term, (term_def && term_def.iri_mapping) || nil
|
||||
end
|
||||
end
|
||||
|
||||
def languages(%JSON.LD.Context{term_defs: term_defs}) do
|
||||
Enum.reduce term_defs, %{}, fn ({term, term_def}, language_mappings) ->
|
||||
Map.put language_mappings, term, term_def.language_mapping
|
||||
end
|
||||
end
|
||||
|
||||
def coercions(%JSON.LD.Context{term_defs: term_defs}) do
|
||||
Enum.reduce term_defs, %{}, fn ({term, term_def}, type_mappings) ->
|
||||
Map.put type_mappings, term, term_def.type_mapping
|
||||
end
|
||||
end
|
||||
|
||||
def containers(%JSON.LD.Context{term_defs: term_defs}) do
|
||||
Enum.reduce term_defs, %{}, fn ({term, term_def}, type_mappings) ->
|
||||
Map.put type_mappings, term, term_def.container_mapping
|
||||
end
|
||||
end
|
||||
|
||||
end
|
133
test/unit/expand_iri_test.exs
Normal file
133
test/unit/expand_iri_test.exs
Normal file
|
@ -0,0 +1,133 @@
|
|||
defmodule JSON.LD.ExpandIRITest do
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
setup do
|
||||
context = JSON.LD.context(%{
|
||||
"@base" => "http://base/",
|
||||
"@vocab" => "http://vocab/",
|
||||
"ex" => "http://example.org/",
|
||||
"" => "http://empty/",
|
||||
"_" => "http://underscore/"
|
||||
})
|
||||
%{example_context: context}
|
||||
end
|
||||
|
||||
test "bnode", %{example_context: context} do
|
||||
assert JSON.LD.expand_iri("_:a", context) == "_:a"
|
||||
end
|
||||
|
||||
describe "relative IRI with no options" do
|
||||
# TODO: Test this with RDF.URIs and RDF.BlankNodes
|
||||
# %{
|
||||
# "absolute IRI" => ["http://example.org/", RDF.uri("http://example.org/"],
|
||||
# "term" => ["ex", RDF.uri("ex")],
|
||||
# "prefix:suffix" => ["ex:suffix", RDF.uri("http://example.org/suffix")],
|
||||
# "keyword" => ["@type", "@type"],
|
||||
# "empty" => [":suffix", RDF.uri("http://empty/suffix")],
|
||||
# "unmapped" => ["foo", RDF.uri("foo")],
|
||||
# "empty term" => ["", RDF.uri("")],
|
||||
# "another abs IRI"=>["ex://foo", RDF.uri("ex://foo")],
|
||||
# "absolute IRI looking like a curie" =>
|
||||
# ["foo:bar", RDF.uri("foo:bar")],
|
||||
# "bnode" => ["_:t0", RDF.bnode("t0")],
|
||||
# "_" => ["_", RDF.uri("_")],
|
||||
# }
|
||||
%{
|
||||
"absolute IRI" => ["http://example.org/", "http://example.org/"],
|
||||
"term" => ["ex", "ex"],
|
||||
"prefix:suffix" => ["ex:suffix", "http://example.org/suffix"],
|
||||
"keyword" => ["@type", "@type"],
|
||||
"empty" => [":suffix", "http://empty/suffix"],
|
||||
"unmapped" => ["foo", "foo"],
|
||||
"empty term" => ["", ""],
|
||||
"another abs IRI"=>["ex://foo", "ex://foo"],
|
||||
"absolute IRI looking like a curie" =>
|
||||
["foo:bar", "foo:bar"],
|
||||
"bnode" => ["_:t0", "_:t0"],
|
||||
"_" => ["_", "_"],
|
||||
}
|
||||
|> Enum.each(fn ({title, data}) ->
|
||||
@tag data: data
|
||||
test title, %{data: [input, result], example_context: context} do
|
||||
assert JSON.LD.expand_iri(input, context) == result
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
describe "relative IRI with base IRI" do
|
||||
# TODO: Test this with RDF.URIs and RDF.BlankNodes
|
||||
# %{
|
||||
# "absolute IRI" => ["http://example.org/", RDF.uri("http://example.org/")],
|
||||
# "term" => ["ex", RDF.uri("http://base/ex")],
|
||||
# "prefix:suffix" => ["ex:suffix", RDF.uri("http://example.org/suffix")],
|
||||
# "keyword" => ["@type", "@type"],
|
||||
# "empty" => [":suffix", RDF.uri("http://empty/suffix")],
|
||||
# "unmapped" => ["foo", RDF.uri("http://base/foo")],
|
||||
# "empty term" => ["", RDF.uri("http://base/")],
|
||||
# "another abs IRI"=>["ex://foo", RDF.uri("ex://foo")],
|
||||
# "absolute IRI looking like a curie" =>
|
||||
# ["foo:bar", RDF.uri("foo:bar")],
|
||||
# "bnode" => ["_:t0", RDF.bnode("t0")],
|
||||
# "_" => ["_", RDF.uri("http://base/_")],
|
||||
# }
|
||||
%{
|
||||
"absolute IRI" => ["http://example.org/", "http://example.org/"],
|
||||
"term" => ["ex", "http://base/ex"],
|
||||
"prefix:suffix" => ["ex:suffix", "http://example.org/suffix"],
|
||||
"keyword" => ["@type", "@type"],
|
||||
"empty" => [":suffix", "http://empty/suffix"],
|
||||
"unmapped" => ["foo", "http://base/foo"],
|
||||
"empty term" => ["", "http://base/"],
|
||||
"another abs IRI"=>["ex://foo", "ex://foo"],
|
||||
"absolute IRI looking like a curie" =>
|
||||
["foo:bar", "foo:bar"],
|
||||
"bnode" => ["_:t0", "_:t0"],
|
||||
"_" => ["_", "http://base/_"],
|
||||
}
|
||||
|> Enum.each(fn ({title, data}) ->
|
||||
@tag data: data
|
||||
test title, %{data: [input, result], example_context: context} do
|
||||
assert JSON.LD.expand_iri(input, context, true) == result
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
describe "relative IRI @vocab" do
|
||||
# TODO: Test this with RDF.URIs and RDF.BlankNodes
|
||||
# %{
|
||||
# "absolute IRI" => ["http://example.org/", RDF.uri("http://example.org/")],
|
||||
# "term" => ["ex", RDF.uri("http://example.org/")],
|
||||
# "prefix:suffix" => ["ex:suffix", RDF.uri("http://example.org/suffix")],
|
||||
# "keyword" => ["@type", "@type"],
|
||||
# "empty" => [":suffix", RDF.uri("http://empty/suffix")],
|
||||
# "unmapped" => ["foo", RDF.uri("http://vocab/foo")],
|
||||
# "empty term" => ["", RDF.uri("http://empty/")],
|
||||
# "another abs IRI"=>["ex://foo", RDF.uri("ex://foo")],
|
||||
# "absolute IRI looking like a curie" =>
|
||||
# ["foo:bar", RDF.uri("foo:bar")],
|
||||
# "bnode" => ["_:t0", RDF.bode("t0")],
|
||||
# "_" => ["_", RDF.uri("http://underscore/")],
|
||||
# }
|
||||
%{
|
||||
"absolute IRI" => ["http://example.org/", "http://example.org/"],
|
||||
"term" => ["ex", "http://example.org/"],
|
||||
"prefix:suffix" => ["ex:suffix", "http://example.org/suffix"],
|
||||
"keyword" => ["@type", "@type"],
|
||||
"empty" => [":suffix", "http://empty/suffix"],
|
||||
"unmapped" => ["foo", "http://vocab/foo"],
|
||||
"empty term" => ["", "http://empty/"],
|
||||
"another abs IRI"=>["ex://foo", "ex://foo"],
|
||||
"absolute IRI looking like a curie" =>
|
||||
["foo:bar", "foo:bar"],
|
||||
"bnode" => ["_:t0", "_:t0"],
|
||||
"_" => ["_", "http://underscore/"],
|
||||
}
|
||||
|> Enum.each(fn ({title, data}) ->
|
||||
@tag data: data
|
||||
test title, %{data: [input, result], example_context: context} do
|
||||
assert JSON.LD.expand_iri(input, context, false, true) == result
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
end
|
1135
test/unit/expand_test.exs
Normal file
1135
test/unit/expand_test.exs
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue