2017-02-19 02:47:05 +00:00
defmodule JSON.LD.Expansion do
@moduledoc nil
2017-03-21 23:35:05 +00:00
import JSON.LD . { IRIExpansion , Utils }
def expand ( input , options \\ % JSON.LD.Options { } ) do
with options = JSON.LD.Options . new ( options ) ,
active_context = JSON.LD.Context . new ( options )
do
active_context =
case options . expand_context do
%{ " @context " = > context } ->
JSON.LD.Context . update ( active_context , context )
%{ } = context ->
JSON.LD.Context . update ( active_context , context )
nil ->
active_context
end
2017-02-19 02:47:05 +00:00
2017-03-21 23:35:05 +00:00
case do_expand ( active_context , nil , input , options ) do
result = %{ " @graph " = > graph } when map_size ( result ) == 1 ->
graph
nil ->
[ ]
result when not is_list ( result ) ->
[ result ]
result -> result
end
2017-02-19 02:47:05 +00:00
end
end
2017-03-21 23:35:05 +00:00
defp do_expand ( active_context , active_property , element , options )
2017-02-19 02:47:05 +00:00
# 1) If element is null, return null.
defp do_expand ( _ , _ , nil , _ ) , do : nil
# 2) If element is a scalar, ...
2017-09-13 23:41:12 +00:00
defp do_expand ( active_context , active_property , element , _options )
2017-02-19 02:47:05 +00:00
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, ...
2017-03-21 23:35:05 +00:00
defp do_expand ( active_context , active_property , element , options )
2017-02-19 02:47:05 +00:00
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 ) ->
2017-03-21 23:35:05 +00:00
expanded_item = do_expand ( active_context , active_property , item , options )
2017-02-19 02:47:05 +00:00
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)
2017-03-21 23:35:05 +00:00
defp do_expand ( active_context , active_property , element , options )
2017-02-19 02:47:05 +00:00
when is_map ( element ) do
# 5)
2017-09-13 23:41:12 +00:00
active_context =
if Map . has_key? ( element , " @context " ) do
JSON.LD.Context . update ( active_context , Map . get ( element , " @context " ) , [ ] , options )
else
active_context
end
2017-02-19 02:47:05 +00:00
# 6) and 7)
result = element
|> Enum . sort_by ( fn { key , _ } -> key end )
|> Enum . reduce ( %{ } , fn ( { key , value } , result ) ->
2017-09-13 23:41:12 +00:00
if key != " @context " do # 7.1)
expanded_property = expand_iri ( key , active_context , false , true )
if expanded_property && # 7.2)
2017-03-21 00:01:55 +00:00
( String . contains? ( expanded_property , " : " ) || JSON.LD . keyword? ( expanded_property ) ) do # 7.3)
2017-09-13 23:41:12 +00:00
if JSON.LD . 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. "
2017-02-19 02:47:05 +00:00
2017-09-13 23:41:12 +00:00
expanded_value = case expanded_property do
" @id " -> # 7.4.3)
if is_binary ( value ) do
expand_iri ( value , active_context , true )
2017-02-19 02:47:05 +00:00
else
2017-09-13 23:41:12 +00:00
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 , options )
" @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 , options )
# 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 } "
2017-02-19 02:47:05 +00:00
value
end
2017-09-13 23:41:12 +00:00
" @set " -> # 7.4.10)
do_expand ( active_context , active_property , value , options )
" @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 , options ) # 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
new_result =
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
Map . put ( new_result , " @reverse " , reverse_map )
else
new_result
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
2017-02-19 02:47:05 +00:00
2017-09-13 23:41:12 +00:00
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
|> 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 "
2017-02-19 02:47:05 +00:00
2017-09-13 23:41:12 +00:00
end )
)
end )
# 7.6)
is_map ( value ) && term_def && term_def . container_mapping == " @index " ->
2017-02-19 02:47:05 +00:00
value
2017-09-13 23:41:12 +00:00
|> 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 , options )
Enum . map ( index_value , fn item ->
Map . put_new ( item , " @index " , index )
end )
)
end )
# 7.7)
true ->
do_expand ( active_context , key , value , options )
end
# 7.8)
if is_nil ( expanded_value ) do
result
else
# 7.9)
expanded_value =
if ( term_def && term_def . container_mapping == " @list " ) &&
! ( is_map ( expanded_value ) && Map . has_key? ( expanded_value , " @list " ) ) do
%{ " @list " = >
( if is_list ( expanded_value ) ,
do : expanded_value ,
else : [ expanded_value ] ) }
2017-02-19 02:47:05 +00:00
else
2017-09-13 23:41:12 +00:00
expanded_value
2017-02-19 02:47:05 +00:00
end
2017-09-13 23:41:12 +00:00
# 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 " , %{ } )
2017-02-19 02:47:05 +00:00
reverse_map =
2017-09-13 23:41:12 +00:00
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
2017-02-19 02:47:05 +00:00
end )
2017-09-13 23:41:12 +00:00
Map . put ( result , " @reverse " , reverse_map )
else # 7.11)
expanded_value = if is_list ( expanded_value ) ,
2017-02-19 02:47:05 +00:00
do : expanded_value ,
2017-09-13 23:41:12 +00:00
else : [ expanded_value ]
Map . update result , expanded_property , expanded_value ,
fn values -> expanded_value ++ values end
end
2017-02-19 02:47:05 +00:00
end
end
2017-09-13 23:41:12 +00:00
else
result
2017-02-19 02:47:05 +00:00
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
2017-03-03 20:49:42 +00:00
%{ " @list " = > _ } ->
2017-02-19 02:47:05 +00:00
validate_set_or_list_object ( result )
result
_ -> result
end
# 11) If result contains only the key @language, set result to null.
2017-09-13 23:41:12 +00:00
result = if is_map ( result ) and map_size ( result ) == 1 and Map . has_key? ( result , " @language " ) ,
do : nil , else : result
2017-02-19 02:47:05 +00:00
# 12) If active property is null or @graph, drop free-floating values as follows:
2017-03-24 22:25:12 +00:00
# Spec FIXME: Due to case 10) we might land with a list here; other implementations deal with that, by just returning in step 10)
2017-09-13 23:41:12 +00:00
result = if is_map ( result ) and active_property in [ nil , " @graph " ] and (
2017-02-19 02:47:05 +00:00
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 " ) ) ) ,
2017-09-13 23:41:12 +00:00
do : nil , else : result
2017-02-19 02:47:05 +00:00
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 """
Details at < http :/ / json - ld . org / spec / latest / json - ld - api / #value-expansion>
"""
def expand_value ( active_context , active_property , value ) do
2017-03-24 22:25:12 +00:00
with term_def = Map . get ( active_context . term_defs , active_property ,
% JSON.LD.Context.TermDefinition { } ) do
2017-02-19 02:47:05 +00:00
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
2017-02-24 15:40:16 +00:00
language_mapping ->
2017-02-19 02:47:05 +00:00
%{ " @value " = > value , " @language " = > language_mapping }
2017-02-24 15:40:16 +00:00
language_mapping == false && active_context . default_language ->
2017-02-19 02:47:05 +00:00
%{ " @value " = > value , " @language " = > active_context . default_language }
true ->
%{ " @value " = > value }
end
true ->
%{ " @value " = > value }
end
end
end
end