Improve specs for datatypes

This commit is contained in:
rustra 2020-03-10 00:25:30 +01:00
parent 2244c36b78
commit bd52920e7c
13 changed files with 88 additions and 37 deletions

View file

@ -24,7 +24,7 @@ defmodule RDF.Datatype do
@doc """
Produces the lexical form of a value.
"""
@callback canonical_lexical(any) :: String.t | nil
@callback canonical_lexical(Literal.literal_value) :: String.t | nil
@doc """
Produces the lexical form of an invalid value of a typed Literal.
@ -51,7 +51,7 @@ defmodule RDF.Datatype do
of the lexical value space of an `xsd:boolean`, so the `RDF.Boolean`
implementation of this datatype calls `super`.
"""
@callback convert(any, keyword) :: any
@callback convert(any, map) :: any
@doc """
@ -60,13 +60,13 @@ defmodule RDF.Datatype do
If the given literal is invalid or can not be converted into this datatype
`nil` is returned.
"""
@callback cast(Literal.t) :: Literal.t | nil
@callback cast(Literal.t | any) :: Literal.t | nil
@doc """
Determines if the value of a `RDF.Literal` is a member of lexical value space of its datatype.
"""
@callback valid?(literal :: Literal.t) :: boolean | nil
@callback valid?(Literal.t) :: boolean | nil
@doc """
Checks if the value of two `RDF.Literal`s of this datatype are equal.
@ -78,7 +78,7 @@ defmodule RDF.Datatype do
The default implementation of the `_using__` macro compares the values of the
`canonical/1` forms of the given literals of this datatype.
"""
@callback equal_value?(literal1 :: Literal.t, literal2 :: Literal.t) :: boolean | nil
@callback equal_value?(Literal.t, Literal.t) :: boolean | nil
@doc """
Compares two `RDF.Literal`s.
@ -94,7 +94,7 @@ defmodule RDF.Datatype do
The default implementation of the `_using__` macro compares the values of the
`canonical/1` forms of the given literals of this datatype.
"""
@callback compare(literal1 :: Literal.t, literal2 :: Literal.t) :: :lt | :gt | :eq | :indeterminate | nil
@callback compare(Literal.t, Literal.t) :: :lt | :gt | :eq | :indeterminate | nil
@lang_string RDF.iri("http://www.w3.org/1999/02/22-rdf-syntax-ns#langString")
@ -122,7 +122,7 @@ defmodule RDF.Datatype do
The IRIs of all datatypes with a `RDF.Datatype` defined.
"""
@spec ids :: [IRI.t]
def ids, do: Map.keys(@mapping)
def ids, do: Map.keys(@mapping)
@doc """
All defined `RDF.Datatype` modules.
@ -151,6 +151,7 @@ defmodule RDF.Datatype do
def id, do: @id
@spec new(any, map | keyword) :: Literal.t
def new(value, opts \\ %{})
def new(value, opts) when is_list(opts),
@ -170,6 +171,8 @@ defmodule RDF.Datatype do
end
@dialyzer {:nowarn_function, build_literal_by_value: 2}
@spec build_literal_by_value(any, map) :: Literal.t
def build_literal_by_value(value, opts) do
case convert(value, opts) do
nil ->
@ -179,6 +182,7 @@ defmodule RDF.Datatype do
end
end
@dialyzer {:nowarn_function, build_literal_by_lexical: 2}
def build_literal_by_lexical(lexical, opts) do
case convert(lexical, opts) do
nil ->
@ -192,6 +196,7 @@ defmodule RDF.Datatype do
end
end
@spec build_literal(Literal.literal_value | nil, String.t | nil, map) :: Literal.t
def build_literal(value, lexical, %{canonicalize: true} = opts) do
build_literal(value, lexical, Map.delete(opts, :canonicalize))
|> canonical
@ -208,11 +213,11 @@ defmodule RDF.Datatype do
@impl unquote(__MODULE__)
def lexical(literal)
def lexical(%RDF.Literal{value: value, uncanonical_lexical: nil}) do
def lexical(%Literal{value: value, uncanonical_lexical: nil}) do
canonical_lexical(value)
end
def lexical(%RDF.Literal{uncanonical_lexical: lexical}) do
def lexical(%Literal{uncanonical_lexical: lexical}) do
lexical
end
@ -250,7 +255,7 @@ defmodule RDF.Datatype do
canonical(literal1).value == canonical(literal2).value
end
def equal_value?(%RDF.Literal{} = left, right) when not is_nil(right) do
def equal_value?(%Literal{} = left, right) when not is_nil(right) do
unless RDF.Term.term?(right) do
equal_value?(left, RDF.Term.coerce(right))
end
@ -259,9 +264,9 @@ defmodule RDF.Datatype do
def equal_value?(_, _), do: nil
def less_than?(literal1, literal2), do: RDF.Literal.less_than?(literal1, literal2)
def less_than?(literal1, literal2), do: Literal.less_than?(literal1, literal2)
def greater_than?(literal1, literal2), do: RDF.Literal.greater_than?(literal1, literal2)
def greater_than?(literal1, literal2), do: Literal.greater_than?(literal1, literal2)
@impl unquote(__MODULE__)

View file

@ -5,12 +5,15 @@ defmodule RDF.Boolean do
use RDF.Datatype, id: RDF.Datatype.NS.XSD.boolean
import RDF.Literal.Guards
import Literal.Guards
alias Literal
@type value :: boolean
@type input :: value | String.t | number
@impl RDF.Datatype
@spec convert(input | any, map) :: value | nil
def convert(value, opts)
def convert(value, _) when is_boolean(value), do: value
@ -35,9 +38,9 @@ defmodule RDF.Boolean do
@impl RDF.Datatype
def cast(literal)
def cast(%RDF.Literal{datatype: datatype} = literal) do
def cast(%Literal{datatype: datatype} = literal) do
cond do
not RDF.Literal.valid?(literal) ->
not Literal.valid?(literal) ->
nil
is_xsd_boolean(datatype) ->
@ -92,11 +95,12 @@ defmodule RDF.Boolean do
see <https://www.w3.org/TR/xpath-functions/#func-not>
"""
@spec fn_not(value | input | Literal.t | any) :: Literal.t | nil
def fn_not(value) do
case ebv(value) do
%RDF.Literal{value: true} -> RDF.Boolean.Value.false
%RDF.Literal{value: false} -> RDF.Boolean.Value.true
nil -> nil
%Literal{value: true} -> RDF.Boolean.Value.false
%Literal{value: false} -> RDF.Boolean.Value.true
nil -> nil
end
end
@ -123,20 +127,22 @@ defmodule RDF.Boolean do
see <https://www.w3.org/TR/sparql11-query/#func-logical-and>
"""
@spec logical_and(value | input | Literal.t | any, value | input | Literal.t | any) ::
Literal.t | nil
def logical_and(left, right) do
case ebv(left) do
%RDF.Literal{value: false} ->
%Literal{value: false} ->
RDF.false
%RDF.Literal{value: true} ->
%Literal{value: true} ->
case ebv(right) do
%RDF.Literal{value: true} -> RDF.true
%RDF.Literal{value: false} -> RDF.false
nil -> nil
%Literal{value: true} -> RDF.true
%Literal{value: false} -> RDF.false
nil -> nil
end
nil ->
if match?(%RDF.Literal{value: false}, ebv(right)) do
if match?(%Literal{value: false}, ebv(right)) do
RDF.false
end
end
@ -165,20 +171,22 @@ defmodule RDF.Boolean do
see <https://www.w3.org/TR/sparql11-query/#func-logical-or>
"""
@spec logical_or(value | input | Literal.t | any, value | input | Literal.t | any) ::
Literal.t | nil
def logical_or(left, right) do
case ebv(left) do
%RDF.Literal{value: true} ->
%Literal{value: true} ->
RDF.true
%RDF.Literal{value: false} ->
%Literal{value: false} ->
case ebv(right) do
%RDF.Literal{value: true} -> RDF.true
%RDF.Literal{value: false} -> RDF.false
%Literal{value: true} -> RDF.true
%Literal{value: false} -> RDF.false
nil -> nil
end
nil ->
if match?(%RDF.Literal{value: true}, ebv(right)) do
if match?(%Literal{value: true}, ebv(right)) do
RDF.true
end
end
@ -201,23 +209,24 @@ defmodule RDF.Boolean do
- <https://www.w3.org/TR/sparql11-query/#ebv>
"""
@spec ebv(value | input | Literal.t | any) :: Literal.t | nil
def ebv(value)
def ebv(true), do: RDF.Boolean.Value.true
def ebv(false), do: RDF.Boolean.Value.false
def ebv(%RDF.Literal{value: nil, datatype: @xsd_boolean}), do: RDF.Boolean.Value.false
def ebv(%RDF.Literal{datatype: @xsd_boolean} = literal), do: literal
def ebv(%Literal{value: nil, datatype: @xsd_boolean}), do: RDF.Boolean.Value.false
def ebv(%Literal{datatype: @xsd_boolean} = literal), do: literal
def ebv(%RDF.Literal{datatype: datatype} = literal) do
def ebv(%Literal{datatype: datatype} = literal) do
cond do
RDF.Numeric.type?(datatype) ->
if RDF.Literal.valid?(literal) and
if Literal.valid?(literal) and
not (literal.value == 0 or literal.value == :nan),
do: RDF.Boolean.Value.true,
else: RDF.Boolean.Value.false
RDF.Literal.plain?(literal) ->
Literal.plain?(literal) ->
if String.length(literal.value) == 0,
do: RDF.Boolean.Value.false,
else: RDF.Boolean.Value.true
@ -228,7 +237,7 @@ defmodule RDF.Boolean do
end
def ebv(value) when is_binary(value) or is_number(value) do
value |> RDF.Literal.new() |> ebv()
value |> Literal.new() |> ebv()
end
def ebv(_), do: nil
@ -236,6 +245,7 @@ defmodule RDF.Boolean do
@doc """
Alias for `ebv/1`.
"""
@spec effective(value | input | Literal.t | any) :: Literal.t | nil
def effective(value), do: ebv(value)
end

View file

@ -8,12 +8,14 @@ defmodule RDF.Date do
import RDF.Literal.Guards
@type value :: Date.t | {Date.t, String.t}
@type input :: value | String.t
@grammar ~r/\A(-?\d{4}-\d{2}-\d{2})((?:[\+\-]\d{2}:\d{2})|UTC|GMT|Z)?\Z/
@xsd_datetime RDF.Datatype.NS.XSD.dateTime
@impl RDF.Datatype
@spec convert(input | any, map) :: value | nil
def convert(value, opts)
def convert(%Date{} = value, %{tz: "+00:00"} = opts) do
@ -54,6 +56,7 @@ defmodule RDF.Date do
@impl RDF.Datatype
@spec canonical_lexical(value) :: String.t
def canonical_lexical(value)
def canonical_lexical(%Date{} = value) do

View file

@ -8,11 +8,13 @@ defmodule RDF.DateTime do
import RDF.Literal.Guards
@type value :: DateTime.t | NaiveDateTime.t
@type input :: value | String.t
@xsd_date RDF.Datatype.NS.XSD.date
@impl RDF.Datatype
@spec convert(input | any, map) :: value | nil
def convert(value, opts)
# Special case for date and dateTime, for which 0 is not a valid year
@ -65,6 +67,7 @@ defmodule RDF.DateTime do
@impl RDF.Datatype
@spec canonical_lexical(value) :: String.t
def canonical_lexical(value)
def canonical_lexical(%DateTime{} = value) do
@ -112,6 +115,7 @@ defmodule RDF.DateTime do
@doc """
Builds a `RDF.DateTime` literal for current moment in time.
"""
@spec now :: Literal.t
def now() do
new(DateTime.utc_now())
end
@ -120,6 +124,7 @@ defmodule RDF.DateTime do
@doc """
Extracts the timezone string from a `RDF.DateTime` literal.
"""
@spec tz(Literal.t) :: String.t | nil
def tz(literal)
def tz(%Literal{value: %NaiveDateTime{}}), do: ""
@ -136,6 +141,7 @@ defmodule RDF.DateTime do
@doc """
Converts a datetime literal to a canonical string, preserving the zone information.
"""
@spec canonical_lexical_with_zone(Literal.t) :: String.t | nil
def canonical_lexical_with_zone(%Literal{datatype: datatype} = literal)
when is_xsd_datetime(datatype) do
case tz(literal) do

View file

@ -1,6 +1,7 @@
defmodule RDF.DateTimeUtils do
@moduledoc false
@spec tz(String.t) :: String.t
def tz(string) do
case Regex.run(~r/([+-])(\d\d:\d\d)/, string) do
[_, sign, zone] ->

View file

@ -8,12 +8,14 @@ defmodule RDF.Decimal do
alias Elixir.Decimal, as: D
@type value :: Decimal.t | :nan
@type value :: Decimal.t
@type input :: value | number | String.t
@xsd_integer RDF.Datatype.NS.XSD.integer
@impl RDF.Datatype
@spec convert(input | any, map) :: value | nil
def convert(value, opts)
def convert(%D{coef: coef} = value, opts) when coef in ~w[qNaN sNaN inf]a,
@ -120,6 +122,7 @@ defmodule RDF.Decimal do
@doc """
The number of digits in the XML Schema canonical form of the literal value.
"""
@spec digit_count(Literal.t) :: non_neg_integer
def digit_count(%RDF.Literal{datatype: @id} = literal) do
if valid?(literal) do
literal
@ -137,6 +140,7 @@ defmodule RDF.Decimal do
@doc """
The number of digits to the right of the decimal point in the XML Schema canonical form of the literal value.
"""
@spec fraction_digit_count(Literal.t) :: non_neg_integer
def fraction_digit_count(%RDF.Literal{datatype: @id} = literal) do
if valid?(literal) do
[_, fraction] =

View file

@ -7,7 +7,8 @@ defmodule RDF.Double do
import RDF.Literal.Guards
@type value :: float
@type value :: float | :positive_infinity | :negative_infinity | :nan
@type input :: value | number | String.t
def build_literal_by_value(value, opts) do
@ -22,6 +23,7 @@ defmodule RDF.Double do
end
@impl RDF.Datatype
@spec convert(input | any, map) :: value | nil
def convert(value, opts)
def convert(value, _) when is_float(value), do: value
@ -57,6 +59,7 @@ defmodule RDF.Double do
@impl RDF.Datatype
@spec canonical_lexical(value) :: String.t
def canonical_lexical(value)
def canonical_lexical(:nan), do: "NaN"

View file

@ -8,9 +8,11 @@ defmodule RDF.Integer do
import RDF.Literal.Guards
@type value :: integer
@type input :: value | String.t
@impl RDF.Datatype
@spec convert(input | any, map) :: value | nil
def convert(value, opts)
def convert(value, _) when is_integer(value), do: value
@ -79,6 +81,7 @@ defmodule RDF.Integer do
@doc """
The number of digits in the XML Schema canonical form of the literal value.
"""
@spec digit_count(Literal.t) :: non_neg_integer
def digit_count(%RDF.Literal{datatype: @id} = literal) do
if valid?(literal) do
literal

View file

@ -5,6 +5,8 @@ defmodule RDF.LangString do
use RDF.Datatype, id: RDF.uri("http://www.w3.org/1999/02/22-rdf-syntax-ns#langString")
@type value :: String.t
def build_literal(value, lexical, %{language: language} = opts)
when is_binary(language) and language != "" do
@ -17,6 +19,7 @@ defmodule RDF.LangString do
@impl RDF.Datatype
@spec convert(any, map) :: value
def convert(value, _), do: to_string(value)
@ -43,6 +46,7 @@ defmodule RDF.LangString do
see <https://www.w3.org/TR/sparql11-query/#func-langMatches>
"""
@spec match_language?(Literal.t | String.t, String.t) :: boolean
def match_language?(language_tag, language_range)
def match_language?(%Literal{language: nil}, _), do: false

View file

@ -11,6 +11,7 @@ defmodule RDF.Numeric do
import RDF.Literal.Guards
import Kernel, except: [abs: 1, floor: 1, ceil: 1]
@type value :: RDF.Decimal.value | RDF.Integer.value | RDF.Double.value
@types MapSet.new [
XSD.integer,
@ -47,6 +48,7 @@ defmodule RDF.Numeric do
@doc """
Returns if a given literal has a numeric datatype.
"""
@spec literal?(Literal.t | any) :: boolean
def literal?(%Literal{datatype: datatype}), do: type?(datatype)
def literal?(_), do: false
@ -138,6 +140,7 @@ defmodule RDF.Numeric do
def compare(_, _), do: nil
@dialyzer {:nowarn_function, compare_decimal_value: 2}
defp compare_decimal_value(%D{} = left, %D{} = right), do: D.cmp(left, right)
defp compare_decimal_value(%D{} = left, right), do: compare_decimal_value(left, new_decimal(right))
defp compare_decimal_value(left, %D{} = right), do: compare_decimal_value(new_decimal(left), right)

View file

@ -31,6 +31,7 @@ defmodule RDF.String do
@impl RDF.Datatype
@spec convert(any, map) :: value
def convert(value, _), do: to_string(value)

View file

@ -8,12 +8,14 @@ defmodule RDF.Time do
import RDF.Literal.Guards
@type value :: Time.t
@type input :: value | String.t
@grammar ~r/\A(\d{2}:\d{2}:\d{2}(?:\.\d+)?)((?:[\+\-]\d{2}:\d{2})|UTC|GMT|Z)?\Z/
@tz_grammar ~r/\A(?:([\+\-])(\d{2}):(\d{2}))\Z/
@impl RDF.Datatype
@spec convert(input | any, map) :: value | nil
def convert(value, opts)
def convert(%Time{} = value, %{tz: tz} = opts) do
@ -120,6 +122,7 @@ defmodule RDF.Time do
@doc """
Extracts the timezone string from a `RDF.Time` literal.
"""
@spec tz(Literal.t) :: String.t | nil
def tz(time_literal) do
if valid?(time_literal) do
time_literal
@ -132,6 +135,7 @@ defmodule RDF.Time do
@doc """
Converts a time literal to a canonical string, preserving the zone information.
"""
@spec canonical_lexical_with_zone(Literal.t) :: String.t | nil
def canonical_lexical_with_zone(%Literal{datatype: datatype} = literal)
when is_xsd_time(datatype) do
case tz(literal) do

View file

@ -58,9 +58,11 @@ defmodule RDF.Vocabulary.Namespace do
end
@base_iri unquote(base_iri)
@spec __base_iri__ :: String.t
def __base_iri__, do: @base_iri
@strict unquote(strict)
@spec __strict__ :: boolean
def __strict__, do: @strict
@terms unquote(Macro.escape(terms))
@ -72,6 +74,7 @@ defmodule RDF.Vocabulary.Namespace do
@doc """
Returns all known IRIs of the vocabulary.
"""
@spec __iris__ :: [Elixir.RDF.IRI.t]
def __iris__ do
@terms
|> Enum.map(fn
@ -84,6 +87,7 @@ defmodule RDF.Vocabulary.Namespace do
define_vocab_terms unquote(lowercased_terms), unquote(base_iri)
@impl Elixir.RDF.Namespace
@dialyzer {:nowarn_function, __resolve_term__: 1}
def __resolve_term__(term) do
case @terms[term] do
nil ->