Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 109 additions & 9 deletions lib/authoritex.ex
Original file line number Diff line number Diff line change
@@ -1,15 +1,37 @@
defmodule Authoritex do
@moduledoc "Elixir authority lookup behavior"

defmodule Record do
@moduledoc false
defstruct [
:id,
:label,
:qualified_label,
hint: nil,
variants: [],
related: []
]
end

defmodule SearchResult do
@moduledoc false
defstruct [:id, :label, :hint]
end

@type authority :: {module(), String.t(), String.t()}
@type fetch_result :: %{
@type fetch_result :: %__MODULE__.Record{
id: String.t(),
label: String.t(),
qualified_label: String.t(),
hint: String.t() | nil,
variants: list(String.t())
variants: list(String.t()),
related: list({atom(), any()})
}
@type search_result :: %__MODULE__.SearchResult{
id: String.t(),
label: String.t(),
hint: String.t() | nil
}
@type search_result :: %{id: String.t(), label: String.t(), hint: String.t() | nil}

@doc "Returns true if the module can resolve the given identifier"
@callback can_resolve?(String.t()) :: true | false
Expand All @@ -27,28 +49,86 @@ defmodule Authoritex do
@callback search(String.t(), integer()) :: {:ok, list(:search_result)} | {:error, term()}

@doc """
Returns a label given an id.
Returns term details given an id.

## Options

* `:redirect` - controls whether to follow redirects for obsolete terms (default: `false`)

Examples:
```
iex> Authoritex.fetch("http://id.loc.gov/authorities/names/no2011087251")
{:ok, "Valim, Jose"}
{:ok,
%{
id: "http://id.loc.gov/authorities/names/no2011087251",
label: "Valim, Jose",
hint: nil,
qualified_label: "Valim, Jose",
variants: [],
related: []
}}

iex> Authoritex.fetch("http://id.loc.gov/authorities/names/unknown-id")
{:error, 404}

iex> Authoritex.fetch("http://fake.authority.org/not-a-real-thing")
{:error, :unknown_authority}

iex> Authoritex.fetch("http://vocab.getty.edu/aat/300423926")
{:ok,
%{
id: "http://vocab.getty.edu/aat/300423926",
label: "eating fork",
qualified_label: "eating fork",
hint: nil,
variants: [],
related: [replaced_by: "https://vocab.getty.edu/aat/300043099"]
}}

iex> Authoritex.fetch("http://vocab.getty.edu/aat/300423926", redirect: true)
{:ok,
%Authoritex.Record{
id: "http://vocab.getty.edu/aat/300043099",
label: "forks (flatware)",
qualified_label: "forks (flatware)",
hint: nil,
variants: ["fork (flatware)", "eating fork", "叉子", "vork", "prakijzers",
"Gabeln (Essbestecke)", "tenedor"],
related: [replaces: "http://vocab.getty.edu/aat/300423926"]
}}
```
"""
@spec fetch(binary()) :: {:ok, fetch_result()} | {:error, term()}
def fetch(id) do
@spec fetch(binary(), keyword()) :: {:ok, fetch_result()} | {:error, term()}
def fetch(id, opts \\ []) do
opts = Keyword.validate!(opts, redirect: false)

case authority_for(id) do
nil -> {:error, :unknown_authority}
{authority, _, _} -> authority.fetch(id)
nil ->
{:error, :unknown_authority}

{authority, _, _} ->
authority.fetch(id)
|> maybe_refetch(opts[:redirect])
end
end

defp maybe_refetch({:ok, record}, true) do
case Keyword.get(record.related, :replaced_by) do
nil ->
{:ok, record}

new_id ->
{:ok, result} = fetch(new_id, redirect: true)

{:ok,
Map.update!(result, :related, fn related ->
Keyword.put(related, :replaces, record.id)
end)}
end
end

defp maybe_refetch(result, _), do: result

@doc """
Returns search results for a given query.

Expand Down Expand Up @@ -134,6 +214,26 @@ defmodule Authoritex do
|> Enum.find(fn {authority, _, _} -> authority.can_resolve?(id) end)
end

@doc """
Turns a fetch result map into a struct.
"""
@spec fetch_result({:ok, map()} | {:error, any()}) :: fetch_result()
def fetch_result({:ok, result}) do
{:ok, struct(Authoritex.Record, result)}
end

def fetch_result({:error, reason}), do: {:error, reason}

@doc """
Turns a list of search result maps into structs.
"""
@spec search_results({:ok, list(map())} | {:error, any()}) :: list(search_result())
def search_results({:ok, results}) do
{:ok, Enum.map(results, &struct(Authoritex.SearchResult, &1))}
end

def search_results({:error, reason}), do: {:error, reason}

defp find_authority(code) do
authorities()
|> Enum.find_value(fn
Expand Down
17 changes: 16 additions & 1 deletion lib/authoritex/fast/base.ex
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,20 @@ defmodule Authoritex.FAST.Base do
|> fetch()
end

def fetch(unquote(http_uri) <> "/" <> id) do
def fetch(unquote(http_uri) <> "/" <> id = uri) do
"https://fast.oclc.org/fast/#{id}"
|> HttpClient.get(
headers: [{"Accept", "application/rdf+xml"}, {"Content-Type", "application/json;"}]
)
|> case do
{:ok, response} ->
parse_fetch_result(response)
|> maybe_add_replaced_by(uri)

{:error, error} ->
{:error, error}
end
|> Authoritex.fetch_result()
end

@impl Authoritex
Expand All @@ -67,6 +69,7 @@ defmodule Authoritex.FAST.Base do
{:error, error} ->
{:error, error}
end
|> Authoritex.search_results()
end

defp parse_fetch_result(%{body: response, status: 200}) do
Expand Down Expand Up @@ -135,6 +138,18 @@ defmodule Authoritex.FAST.Base do
_ -> str <> "/"
end
end

defp maybe_add_replaced_by({:ok, %{id: id}} = result, id), do: result

defp maybe_add_replaced_by({:ok, result}, original_id) do
{:ok,
result
|> Map.put_new(:related, [])
|> put_in([:related, :replaced_by], result.id)
|> Map.put(:id, original_id)}
end

defp maybe_add_replaced_by(result, _original_id), do: result
end
end
end
2 changes: 2 additions & 0 deletions lib/authoritex/geonames.ex
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ defmodule Authoritex.GeoNames do
{:error, error}
end
end
|> Authoritex.fetch_result()
end

@impl Authoritex
Expand All @@ -82,6 +83,7 @@ defmodule Authoritex.GeoNames do
{:error, error} ->
{:error, error}
end
|> Authoritex.search_results()
end

defp parse_search_result(response) do
Expand Down
8 changes: 6 additions & 2 deletions lib/authoritex/getty/aat.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@ defmodule Authoritex.Getty.AAT do
"""
SELECT DISTINCT ?s ?name ?replacedBy (group_concat(?alt; separator="|") AS ?variants) {
BIND(<#{id}> as ?s)
OPTIONAL {?s gvp:prefLabelGVP/xl:literalForm ?name}
OPTIONAL {?s dcterms:isReplacedBy ?replacedBy}
OPTIONAL {?s gvp:prefLabelGVP/xl:literalForm ?prefLabel}
OPTIONAL {
?s dcterms:isReplacedBy ?replacedBy .
?s rdfs:label ?obsoleteLabel
}
OPTIONAL {?s xl:altLabel/xl:literalForm ?alt}
BIND(COALESCE(?prefLabel, ?obsoleteLabel) AS ?name)
}
GROUP BY ?s ?name ?replacedBy
LIMIT 1
Expand Down
29 changes: 16 additions & 13 deletions lib/authoritex/getty/base.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ defmodule Authoritex.Getty.Base do

@impl Authoritex
def can_resolve?(unquote(http_uri) <> _id), do: true

def can_resolve?(unquote(prefix) <> _ = id) do
unquote(prefix) != ":"
end

def can_resolve?(_), do: false

@impl Authoritex
Expand All @@ -35,20 +37,16 @@ defmodule Authoritex.Getty.Base do

def fetch(id) do
case sparql_fetch(id) |> send() |> parse_sparql_result() do
{:ok, [%{label: label} = result]}
when label == "" and not is_map_key(result, :replaced_by) ->
{:ok, [%{label: label} = result]} when label == "" ->
{:error, 404}

{:ok, [%{replaced_by: replaced_by}] = result} when replaced_by != "" ->
Logger.warning("#{id} is obsolete. Fetching replacement term #{replaced_by}.")
fetch(replaced_by)

{:ok, [result]} ->
{:ok,
result
|> Map.delete(:replaced_by)
|> ensure_variants()
|> put_qualified_label()}
|> put_qualified_label()
|> add_related()}
|> Authoritex.fetch_result()

other ->
other
Expand All @@ -61,8 +59,6 @@ defmodule Authoritex.Getty.Base do
defp ensure_variants(result), do: Map.put(result, :variants, [])

defp put_qualified_label(result) do
result = Map.delete(result, :replaced_by)

case result.hint do
nil -> Map.put(result, :qualified_label, result.label)
"" -> Map.put(result, :qualified_label, result.label)
Expand All @@ -75,6 +71,7 @@ defmodule Authoritex.Getty.Base do
sparql_search(query, max_results)
|> send()
|> parse_sparql_result()
|> Authoritex.search_results()
end

defp sanitize(query), do: query |> String.replace(~r"[^\w\s-]", "")
Expand Down Expand Up @@ -113,7 +110,6 @@ defmodule Authoritex.Getty.Base do
result
|> nilify_hint()
|> remove_empty_variants()
|> remove_replaced_by()
end)
|> Enum.map(&process_result/1)}
end
Expand All @@ -126,8 +122,15 @@ defmodule Authoritex.Getty.Base do
defp nilify_hint(result), do: result
defp remove_empty_variants(%{variants: [""]} = result), do: Map.delete(result, :variants)
defp remove_empty_variants(result), do: result
defp remove_replaced_by(%{replaced_by: ""} = result), do: Map.delete(result, :replaced_by)
defp remove_replaced_by(result), do: result

defp add_related(result) do
result
|> Enum.reduce(%{related: []}, fn
{:replaced_by, ""}, acc -> acc
{:replaced_by, value}, acc -> put_in(acc, [:related, :replaced_by], value)
{key, value}, acc -> Map.put(acc, key, value)
end)
end

defp parse_sparql_result({:ok, response}), do: {:error, response.status}
defp parse_sparql_result({:error, error}), do: {:error, error}
Expand Down
8 changes: 6 additions & 2 deletions lib/authoritex/getty/tgn.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@ defmodule Authoritex.Getty.TGN do
"""
SELECT DISTINCT ?s ?name ?hint ?replacedBy (group_concat(?alt; separator="|") AS ?variants) {
BIND(<#{id}> as ?s)
OPTIONAL {?s gvp:prefLabelGVP/xl:literalForm ?name}
OPTIONAL {?s gvp:prefLabelGVP/xl:literalForm ?prefLabel}
OPTIONAL {?s gvp:parentString ?hint}
OPTIONAL {?s dcterms:isReplacedBy ?replacedBy}
OPTIONAL {
?s dcterms:isReplacedBy ?replacedBy .
?s rdfs:label ?obsoleteLabel
}
OPTIONAL {?s xl:altLabel/xl:literalForm ?alt}
BIND(COALESCE(?prefLabel, ?obsoleteLabel) AS ?name)
}
GROUP BY ?s ?name ?hint ?replacedBy
LIMIT 1
Expand Down
8 changes: 6 additions & 2 deletions lib/authoritex/getty/ulan.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@ defmodule Authoritex.Getty.ULAN do
"""
SELECT DISTINCT ?s ?name ?hint ?replacedBy (group_concat(?alt; separator="|") AS ?variants) {
BIND(<#{id}> as ?s)
OPTIONAL {?s gvp:prefLabelGVP [skosxl:literalForm ?name]}
OPTIONAL {?s gvp:prefLabelGVP [skosxl:literalForm ?prefLabel]}
OPTIONAL {?s foaf:focus/gvp:biographyPreferred [schema:description ?hint]}
OPTIONAL {?s dcterms:isReplacedBy ?replacedBy}
OPTIONAL {
?s dcterms:isReplacedBy ?replacedBy .
?s rdfs:label ?obsoleteLabel
}
OPTIONAL {?s xl:altLabel/xl:literalForm ?alt}
BIND(COALESCE(?prefLabel, ?obsoleteLabel) AS ?name)
}
GROUP BY ?s ?name ?hint ?replacedBy
LIMIT 1
Expand Down
2 changes: 2 additions & 0 deletions lib/authoritex/homosaurus.ex
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ defmodule Authoritex.Homosaurus do
{:error, error} ->
{:error, error}
end
|> Authoritex.fetch_result()
end

@impl Authoritex
Expand All @@ -55,6 +56,7 @@ defmodule Authoritex.Homosaurus do
{:error, error} ->
{:error, error}
end
|> Authoritex.search_results()
end

defp parse_search_result(response) do
Expand Down
Loading