Current section
Files
Jump to
Current section
Files
lib/microdata/strategy/json_ld.ex
defmodule Microdata.Strategy.JSONLD do
@moduledoc """
`Microdata.Strategy.JSONLD` defines a strategy to extract linked data from a `Meeseeks.Document`, based on the W3C [JSON-LD standard](https://www.w3.org/TR/json-ld/).
### Caveats
- Only a small fraction of section 6 of the [JSON-LD specification](https://www.w3.org/TR/json-ld/) is implemented
"""
@behaviour Microdata.Strategy
import Meeseeks.XPath
alias Microdata.{Item, Property}
@impl true
def parse_items(doc) do
doc
|> Meeseeks.all(xpath("//script[@type=\"application/ld+json\"]"))
|> Enum.map(&parse_result(&1))
|> List.flatten()
|> Enum.reject(&is_nil/1)
end
defp parse_result(result) do
result
|> Meeseeks.data()
|> Poison.decode()
|> case do
{:ok, object} -> parse_object(object, %{})
{:error, _} -> nil
end
end
def parse_object(object, parent_context) when is_list(object) do
object
|> Enum.map(&parse_object(&1, parent_context))
end
def parse_object(object, parent_context) do
id = object["@id"]
context =
object
|> extract_context()
|> normalize_definitions(parent_context)
|> Enum.into(parent_context)
types =
object
|> extract_types()
|> normalize_types(context)
|> Enum.into(MapSet.new())
properties =
object
|> extract_properties()
|> map_terms(context)
|> normalize_values(context)
|> List.flatten()
|> to_property_list()
%Item{
id: id,
types: types,
properties: properties
}
end
defp extract_context(object) do
object
|> Map.get("@context")
|> case do
context when is_map(context) ->
context
context when is_binary(context) ->
context
|> download_context()
|> Poison.decode!()
|> extract_context()
_ ->
%{}
end
end
defp download_context("https://schema.org/") do
read_locally("schema.org.json")
end
defp download_context("http://schema.org/") do
read_locally("schema.org.json")
end
defp download_context("https://schema.org") do
read_locally("schema.org.json")
end
defp download_context("http://schema.org") do
read_locally("schema.org.json")
end
defp download_context(context) do
HTTPoison.get!(context, [Accept: "application/ld+json"], follow_redirect: true).body
end
defp read_locally(file) do
:code.priv_dir(:microdata)
|> Path.join("schemas")
|> Path.join(file)
|> File.read!()
end
defp normalize_definitions(context, parent_context) do
lookup_context = Map.merge(context, parent_context)
context
|> Enum.map(fn {term, mapping} ->
case mapping do
iri when is_binary(iri) -> {term, enlarge_iri(iri, lookup_context)}
%{"@id" => iri} -> {term, enlarge_iri(iri, lookup_context)}
end
end)
end
defp enlarge_iri(iri, context) do
iri
|> String.split(":")
|> case do
[iri] -> iri
[prefix, suffix] -> "#{context[prefix]}#{suffix}"
end
end
defp extract_types(object) do
case object["@type"] do
types when is_list(types) -> types
nil -> []
type -> [type]
end
end
defp normalize_types(types, context) do
types
|> Enum.map(fn type -> context[type] || type end)
end
defp extract_properties(object) do
object
|> Enum.reject(fn {term, _value} ->
term |> String.starts_with?("@")
end)
end
defp map_terms(properties, context) do
properties
|> Enum.map(fn {term, value} ->
{context[term] || term, value}
end)
end
defp normalize_values(properties, context) do
properties
|> Enum.map(fn {term, value} ->
case value do
list when is_list(list) ->
list |> Enum.map(&{term, &1}) |> normalize_values(context)
%{"@id" => value} ->
{term, value}
%{} = value ->
{term, parse_object(value, context)}
value ->
{term, value}
end
end)
end
defp to_property_list(properties) do
properties
|> Enum.map(fn {term, value} ->
%Property{
names: [term] |> MapSet.new(),
value: value
}
end)
end
end