diff --git a/lib/rdf/blank_node.ex b/lib/rdf/blank_node.ex index ec9607f..bd21340 100644 --- a/lib/rdf/blank_node.ex +++ b/lib/rdf/blank_node.ex @@ -7,7 +7,7 @@ defmodule RDF.BlankNode do """ @type t :: %__MODULE__{ - id: binary + id: String.t } @enforce_keys [:id] @@ -16,6 +16,7 @@ defmodule RDF.BlankNode do @doc """ Creates a `RDF.BlankNode` with an arbitrary internal id. """ + @spec new :: t def new, do: new(make_ref()) @@ -27,6 +28,7 @@ defmodule RDF.BlankNode do iex> RDF.bnode(:foo) %RDF.BlankNode{id: "foo"} """ + @spec new(reference | String.t | atom | integer) :: t def new(id) def new(id) when is_binary(id), @@ -44,6 +46,7 @@ defmodule RDF.BlankNode do Returns `nil` when the given arguments are not comparable as blank nodes. """ + @spec equal_value?(t, t) :: boolean | nil def equal_value?(left, right) def equal_value?(%RDF.BlankNode{id: left}, %RDF.BlankNode{id: right}), diff --git a/lib/rdf/blank_node/generator_algorithm.ex b/lib/rdf/blank_node/generator_algorithm.ex index c458a79..3d10724 100644 --- a/lib/rdf/blank_node/generator_algorithm.ex +++ b/lib/rdf/blank_node/generator_algorithm.ex @@ -8,7 +8,7 @@ defmodule RDF.BlankNode.Generator.Algorithm do @doc """ Returns the initial state of the algorithm. """ - @callback init(opts :: map | Keyword.t()) :: map + @callback init(opts :: map | keyword) :: map @doc """ Generates a blank node. diff --git a/lib/rdf/dataset.ex b/lib/rdf/dataset.ex index 3f2357c..a35d66f 100644 --- a/lib/rdf/dataset.ex +++ b/lib/rdf/dataset.ex @@ -15,24 +15,28 @@ defmodule RDF.Dataset do @behaviour Access - alias RDF.{Graph, Description} + alias RDF.{Description, Graph, IRI, Statement} import RDF.Statement - @type graphs_key :: RDF.IRI.t | nil + @type graphs_key :: IRI.t | nil @type t :: %__MODULE__{ - name: RDF.IRI.t | nil, - graphs: %{graphs_key => RDF.Graph.t} + name: IRI.t | nil, + graphs: %{graphs_key => Graph.t} } + @type t_param :: Graph.t_param | t + + @type update_graph_fun :: (Graph.t -> {Graph.t, t_param} | :pop) + defstruct name: nil, graphs: %{} @doc """ Creates an empty unnamed `RDF.Dataset`. """ - def new, - do: %RDF.Dataset{} + @spec new :: t + def new, do: %RDF.Dataset{} @doc """ Creates an `RDF.Dataset`. @@ -49,6 +53,7 @@ defmodule RDF.Dataset do RDF.Graph.new(name: EX.GraphName) """ + @spec new(t_param | [t_param] | keyword) :: t def new(data_or_options) def new(data_or_options) @@ -78,6 +83,7 @@ defmodule RDF.Dataset do - `name`: the name of the dataset to be created """ + @spec new(t_param | [t_param], keyword) :: t def new(data, options) def new(%RDF.Dataset{} = graph, options) do @@ -98,6 +104,7 @@ defmodule RDF.Dataset do destination graph to which the statements are added, ignoring the graph context of given quads or the name of given graphs. """ + @spec add(t, t_param | [t_param], boolean | nil) :: t def add(dataset, statements, graph_context \\ false) def add(dataset, statements, graph_context) when is_list(statements) do @@ -179,6 +186,7 @@ defmodule RDF.Dataset do ...> RDF.Dataset.put([{EX.S1, EX.P2, EX.O3}, {EX.S2, EX.P2, EX.O3}]) RDF.Dataset.new([{EX.S1, EX.P1, EX.O1}, {EX.S1, EX.P2, EX.O3}, {EX.S2, EX.P2, EX.O3}]) """ + @spec put(t, t_param | [t_param], Statement.coercible_graph_name | boolean | nil) :: t def put(dataset, statements, graph_context \\ false) def put(%RDF.Dataset{} = dataset, {subject, predicate, objects}, false), @@ -299,6 +307,7 @@ defmodule RDF.Dataset do are deleted. If you want to delete only datasets with matching names, you can use `RDF.Data.delete/2`. """ + @spec delete(t, t_param | [t_param], Statement.coercible_graph_name | boolean | nil) :: t def delete(dataset, statements, graph_context \\ false) def delete(%RDF.Dataset{} = dataset, statements, graph_context) when is_list(statements) do @@ -362,6 +371,7 @@ defmodule RDF.Dataset do @doc """ Deletes the given graph. """ + @spec delete_graph(t, Statement.graph_name | [Statement.graph_name] | nil) :: t def delete_graph(graph, graph_names) def delete_graph(%RDF.Dataset{} = dataset, graph_names) when is_list(graph_names) do @@ -379,6 +389,7 @@ defmodule RDF.Dataset do @doc """ Deletes the default graph. """ + @spec delete_default_graph(t) :: t def delete_default_graph(%RDF.Dataset{} = graph), do: delete_graph(graph, nil) @@ -399,6 +410,7 @@ defmodule RDF.Dataset do :error """ @impl Access + @spec fetch(t, Statement.graph_name | nil) :: {:ok, Graph.t} | :error def fetch(%RDF.Dataset{graphs: graphs}, graph_name) do Access.fetch(graphs, coerce_graph_name(graph_name)) end @@ -421,6 +433,7 @@ defmodule RDF.Dataset do iex> RDF.Dataset.get(dataset, EX.Foo, :bar) :bar """ + @spec get(t, Statement.graph_name | nil, Graph.t | nil) :: Graph.t | nil def get(%RDF.Dataset{} = dataset, graph_name, default \\ nil) do case fetch(dataset, graph_name) do {:ok, value} -> value @@ -431,12 +444,14 @@ defmodule RDF.Dataset do @doc """ The graph with given name. """ + @spec graph(t, Statement.graph_name | nil) :: Graph.t def graph(%RDF.Dataset{graphs: graphs}, graph_name), do: Map.get(graphs, coerce_graph_name(graph_name)) @doc """ The default graph of a `RDF.Dataset`. """ + @spec default_graph(t) :: Graph.t def default_graph(%RDF.Dataset{graphs: graphs}), do: Map.get(graphs, nil, Graph.new) @@ -444,6 +459,7 @@ defmodule RDF.Dataset do @doc """ The set of all graphs. """ + @spec graphs(t) :: [Graph.t] def graphs(%RDF.Dataset{graphs: graphs}), do: Map.values(graphs) @@ -470,6 +486,7 @@ defmodule RDF.Dataset do {RDF.Graph.new({EX.S, EX.P, EX.O}, name: EX.Graph), RDF.Dataset.new({EX.S, EX.P, EX.NEW, EX.Graph})} """ @impl Access + @spec get_and_update(t, Statement.graph_name | nil, update_graph_fun) :: {Graph.t, t_param} def get_and_update(%RDF.Dataset{} = dataset, graph_name, fun) do with graph_context = coerce_graph_name(graph_name) do case fun.(get(dataset, graph_context)) do @@ -487,6 +504,7 @@ defmodule RDF.Dataset do @doc """ Pops an arbitrary statement from a `RDF.Dataset`. """ + @spec pop(t) :: {Statement.t | nil, t} def pop(dataset) def pop(%RDF.Dataset{graphs: graphs} = dataset) @@ -520,6 +538,7 @@ defmodule RDF.Dataset do {nil, dataset} """ @impl Access + @spec pop(t, Statement.coercible_graph_name) :: {Statement.t | nil, t} def pop(%RDF.Dataset{name: name, graphs: graphs} = dataset, graph_name) do case Access.pop(graphs, coerce_graph_name(graph_name)) do {nil, _} -> @@ -543,6 +562,7 @@ defmodule RDF.Dataset do ...> RDF.Dataset.statement_count 3 """ + @spec statement_count(t) :: non_neg_integer def statement_count(%RDF.Dataset{graphs: graphs}) do Enum.reduce graphs, 0, fn ({_, graph}, count) -> count + Graph.triple_count(graph) @@ -641,6 +661,7 @@ defmodule RDF.Dataset do {RDF.iri(EX.S1), RDF.iri(EX.p2), RDF.iri(EX.O3)}, {RDF.iri(EX.S2), RDF.iri(EX.p2), RDF.iri(EX.O2)}] """ + @spec statements(t) :: [Statement.t] def statements(%RDF.Dataset{graphs: graphs}) do Enum.reduce graphs, [], fn ({_, graph}, all_statements) -> statements = Graph.triples(graph) @@ -665,6 +686,7 @@ defmodule RDF.Dataset do ...> RDF.Dataset.include?(dataset, {EX.S1, EX.p1, EX.O1, EX.Graph}) true """ + @spec include?(t, Statement.t, Statement.coercible_graph_name | nil) :: boolean def include?(dataset, statement, graph_context \\ nil) def include?(%RDF.Dataset{graphs: graphs}, triple = {_, _, _}, graph_context) do @@ -691,6 +713,7 @@ defmodule RDF.Dataset do iex> RDF.Dataset.new([{EX.S1, EX.p1, EX.O1}]) |> RDF.Dataset.describes?(EX.S2) false """ + @spec describes?(t, Statement.t, Statement.coercible_graph_name | nil) :: boolean def describes?(%RDF.Dataset{graphs: graphs}, subject, graph_context \\ nil) do with graph_context = coerce_graph_name(graph_context) do if graph = graphs[graph_context] do @@ -713,6 +736,7 @@ defmodule RDF.Dataset do ...> RDF.Dataset.who_describes(dataset, EX.S1) [nil, RDF.iri(EX.Graph1)] """ + @spec who_describes(t, Statement.coercible_subject) :: [Graph.t] def who_describes(%RDF.Dataset{graphs: graphs}, subject) do with subject = coerce_subject(subject) do graphs @@ -775,6 +799,7 @@ defmodule RDF.Dataset do } """ + @spec values(t, Statement.term_mapping) :: map def values(dataset, mapping \\ &RDF.Statement.default_term_mapping/1) def values(%RDF.Dataset{graphs: graphs}, mapping) do @@ -790,6 +815,7 @@ defmodule RDF.Dataset do Two `RDF.Dataset`s are considered to be equal if they contain the same triples and have the same name. """ + @spec equal?(t | any, t | any) :: boolean def equal?(dataset1, dataset2) def equal?(%RDF.Dataset{} = dataset1, %RDF.Dataset{} = dataset2) do diff --git a/lib/rdf/datatype.ex b/lib/rdf/datatype.ex index 0984748..22508f9 100644 --- a/lib/rdf/datatype.ex +++ b/lib/rdf/datatype.ex @@ -6,23 +6,25 @@ defmodule RDF.Datatype do the validation, conversion and canonicalization of typed `RDF.Literal`s. """ - alias RDF.Literal + alias RDF.{IRI, Literal} alias RDF.Datatype.NS.XSD + @type t :: module + @doc """ The IRI of the datatype. """ - @callback id :: RDF.IRI.t + @callback id :: IRI.t @doc """ Produces the lexical form of a `RDF.Literal`. """ - @callback lexical(literal :: RDF.Literal.t) :: any + @callback lexical(Literal.t) :: any @doc """ Produces the lexical form of a value. """ - @callback canonical_lexical(any) :: binary + @callback canonical_lexical(any) :: String.t | nil @doc """ Produces the lexical form of an invalid value of a typed Literal. @@ -30,12 +32,12 @@ defmodule RDF.Datatype do The default implementation of the `_using__` macro just returns `to_string` representation of the value. """ - @callback invalid_lexical(any) :: binary + @callback invalid_lexical(any) :: String.t | nil @doc """ Produces the canonical form of a `RDF.Literal`. """ - @callback canonical(RDF.Literal.t) :: RDF.Literal.t + @callback canonical(Literal.t) :: Literal.t | nil @doc """ Converts a value into a proper native value. @@ -58,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(RDF.Literal.t) :: RDF.Literal.t + @callback cast(Literal.t) :: 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 :: RDF.Literal.t) :: boolean + @callback valid?(literal :: Literal.t) :: boolean | nil @doc """ Checks if the value of two `RDF.Literal`s of this datatype are equal. @@ -76,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 :: RDF.Literal.t, literal2 :: RDF.Literal.t) :: boolean | nil + @callback equal_value?(literal1 :: Literal.t, literal2 :: Literal.t) :: boolean | nil @doc """ Compares two `RDF.Literal`s. @@ -92,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 :: RDF.Literal.t, literal2 :: RDF.Literal.t) :: :lt | :gt | :eq | :indeterminate | nil + @callback compare(literal1 :: Literal.t, literal2 :: Literal.t) :: :lt | :gt | :eq | :indeterminate | nil @lang_string RDF.iri("http://www.w3.org/1999/02/22-rdf-syntax-ns#langString") @@ -113,21 +115,25 @@ defmodule RDF.Datatype do @doc """ The mapping of IRIs of datatypes to their `RDF.Datatype`. """ + @spec mapping :: %{IRI.t => t} def mapping, do: @mapping @doc """ The IRIs of all datatypes with a `RDF.Datatype` defined. """ + @spec ids :: [IRI.t] def ids, do: Map.keys(@mapping) @doc """ All defined `RDF.Datatype` modules. """ + @spec modules :: [t] def modules, do: Map.values(@mapping) @doc """ Returns the `RDF.Datatype` for a directly datatype IRI or the datatype IRI of a `RDF.Literal`. """ + @spec get(Literal.t | IRI.t) :: t def get(%Literal{datatype: id}), do: get(id) def get(id), do: @mapping[id] diff --git a/lib/rdf/datatypes/numeric.ex b/lib/rdf/datatypes/numeric.ex index 8f57e49..6aabcb8 100644 --- a/lib/rdf/datatypes/numeric.ex +++ b/lib/rdf/datatypes/numeric.ex @@ -35,11 +35,13 @@ defmodule RDF.Numeric do @doc """ The list of all numeric datatypes. """ + @dialyzer {:nowarn_function, types: 0} def types(), do: MapSet.to_list(@types) @doc """ Returns if a given datatype is a numeric datatype. """ + @dialyzer {:nowarn_function, type?: 1} def type?(type), do: MapSet.member?(@types, type) @doc """ diff --git a/lib/rdf/description.ex b/lib/rdf/description.ex index b60a3d5..272d948 100644 --- a/lib/rdf/description.ex +++ b/lib/rdf/description.ex @@ -13,9 +13,17 @@ defmodule RDF.Description do @behaviour Access import RDF.Statement + alias RDF.{Statement, Triple} @type predications :: %{Statement.predicate => %{Statement.object => nil}} + @type statements :: + {Statement.coercible_predicate, + Statement.coercible_object | [Statement.coercible_predicate]} + | Statement.t + | predications + | t + @type t :: %__MODULE__{ subject: Statement.subject, predications: predications @@ -30,21 +38,22 @@ defmodule RDF.Description do When given a list of statements, the first one must contain a subject. """ - @spec new(RDF.Statement.coercible_subject) :: RDF.Description.t + @spec new(Statement.coercible_subject | statements | [statements]) :: t def new(subject) def new({subject, predicate, object}), do: new(subject) |> add(predicate, object) def new([statement | more_statements]), do: new(statement) |> add(more_statements) - def new(%RDF.Description{} = description), + def new(%__MODULE__{} = description), do: description def new(subject), - do: %RDF.Description{subject: coerce_subject(subject)} + do: %__MODULE__{subject: coerce_subject(subject)} @doc """ Creates a new `RDF.Description` about the given subject with optional initial statements. """ + @spec new(Statement.coercible_subject, statements | [statements]) :: t def new(subject, {predicate, objects}), do: new(subject) |> add(predicate, objects) def new(subject, statements) when is_list(statements), @@ -57,8 +66,13 @@ defmodule RDF.Description do @doc """ Creates a new `RDF.Description` about the given subject with optional initial statements. """ + @spec new( + Statement.coercible_subject | statements | [statements], + Statement.coercible_predicate, + Statement.coercible_object | [Statement.coercible_object] + ) :: t def new(%RDF.Description{} = description, predicate, objects), - do: RDF.Description.add(description, predicate, objects) + do: add(description, predicate, objects) def new(subject, predicate, objects), do: new(subject) |> add(predicate, objects) @@ -73,6 +87,11 @@ defmodule RDF.Description do iex> RDF.Description.add(RDF.Description.new({EX.S, EX.P, EX.O1}), EX.P, [EX.O2, EX.O3]) RDF.Description.new([{EX.S, EX.P, EX.O1}, {EX.S, EX.P, EX.O2}, {EX.S, EX.P, EX.O3}]) """ + @spec add( + t, + Statement.coercible_predicate, + Statement.coercible_object | [Statement.coercible_object] + ) :: t def add(description, predicate, objects) def add(description, predicate, objects) when is_list(objects) do @@ -101,6 +120,7 @@ defmodule RDF.Description do are added. As opposed to that `RDF.Data.merge/2` will produce a `RDF.Graph` containing both descriptions. """ + @spec add(t, statements | [statements]) :: t def add(description, statements) def add(description, {predicate, object}), @@ -145,6 +165,11 @@ defmodule RDF.Description do iex> RDF.Description.put(RDF.Description.new({EX.S, EX.P1, EX.O1}), EX.P2, EX.O2) RDF.Description.new([{EX.S, EX.P1, EX.O1}, {EX.S, EX.P2, EX.O2}]) """ + @spec put( + t, + Statement.coercible_predicate, + Statement.coercible_object | [Statement.coercible_object] + ) :: t def put(description, predicate, objects) def put(%RDF.Description{subject: subject, predications: predications}, @@ -176,6 +201,7 @@ defmodule RDF.Description do ...> RDF.Description.put(%{EX.P2 => [EX.O3, EX.O4]}) RDF.Description.new([{EX.S, EX.P1, EX.O1}, {EX.S, EX.P2, EX.O3}, {EX.S, EX.P2, EX.O4}]) """ + @spec put(t, statements | [statements]) :: t def put(description, statements) def put(%RDF.Description{} = description, {predicate, object}), @@ -224,6 +250,11 @@ defmodule RDF.Description do @doc """ Deletes statements from a `RDF.Description`. """ + @spec delete( + t, + Statement.coercible_predicate, + Statement.coercible_object | [Statement.coercible_object] + ) :: t def delete(description, predicate, objects) def delete(description, predicate, objects) when is_list(objects) do @@ -261,6 +292,7 @@ defmodule RDF.Description do are deleted. If you want to delete only a matching description subject, you can use `RDF.Data.delete/2`. """ + @spec delete(t, statements | [statements]) :: t def delete(description, statements) def delete(desc = %RDF.Description{}, {predicate, object}), @@ -297,6 +329,7 @@ defmodule RDF.Description do @doc """ Deletes all statements with the given properties. """ + @spec delete_predicates(t, Statement.coercible_predicate | [Statement.coercible_predicate]) :: t def delete_predicates(description, properties) def delete_predicates(%RDF.Description{} = description, properties) when is_list(properties) do @@ -328,6 +361,7 @@ defmodule RDF.Description do :error """ @impl Access + @spec fetch(t, Statement.coercible_predicate) :: {:ok, [Statement.object]} | :error def fetch(%RDF.Description{predications: predications}, predicate) do with {:ok, objects} <- Access.fetch(predications, coerce_predicate(predicate)) do {:ok, Map.keys(objects)} @@ -348,6 +382,7 @@ defmodule RDF.Description do iex> RDF.Description.get(RDF.Description.new(EX.S), EX.foo, :bar) :bar """ + @spec get(t, Statement.coercible_predicate, any) :: [Statement.object] | any def get(description = %RDF.Description{}, predicate, default \\ nil) do case fetch(description, predicate) do {:ok, value} -> value @@ -367,6 +402,7 @@ defmodule RDF.Description do iex> RDF.Description.first(RDF.Description.new(EX.S), EX.foo) nil """ + @spec first(t, Statement.coercible_predicate) :: Statement.object | nil def first(description = %RDF.Description{}, predicate) do description |> get(predicate, []) @@ -395,6 +431,12 @@ defmodule RDF.Description do RDF.Description.new({EX.S, EX.p, EX.O}) """ + @spec update( + t, + Statement.coercible_predicate, + Statement.coercible_object | nil, + ([Statement.Object] -> [Statement.Object]) + ) :: t def update(description = %RDF.Description{}, predicate, initial \\ nil, fun) do predicate = coerce_predicate(predicate) @@ -444,6 +486,11 @@ defmodule RDF.Description do {[RDF.iri(EX.O1)], RDF.Description.new({EX.S, EX.P2, EX.O2})} """ @impl Access + @spec get_and_update( + t, + Statement.coercible_predicate, + ([Statement.Object] -> {[Statement.Object], t} | :pop) + ) :: {[Statement.Object], t} def get_and_update(description = %RDF.Description{}, predicate, fun) do with triple_predicate = coerce_predicate(predicate) do case fun.(get(description, triple_predicate)) do @@ -458,6 +505,7 @@ defmodule RDF.Description do @doc """ Pops an arbitrary triple from a `RDF.Description`. """ + @spec pop(t) :: {Triple.t | [Statement.Object] | nil, t} def pop(description) def pop(description = %RDF.Description{predications: predications}) @@ -511,6 +559,7 @@ defmodule RDF.Description do ...> RDF.Description.predicates MapSet.new([EX.p1, EX.p2]) """ + @spec predicates(t) :: MapSet.t def predicates(%RDF.Description{predications: predications}), do: predications |> Map.keys |> MapSet.new @@ -530,12 +579,14 @@ defmodule RDF.Description do ...> ]) |> RDF.Description.objects MapSet.new([RDF.iri(EX.O1), RDF.iri(EX.O2), RDF.bnode(:bnode)]) """ + @spec objects(t) :: MapSet.t def objects(%RDF.Description{} = description), do: objects(description, &RDF.resource?/1) @doc """ The set of all resources used in the objects within a `RDF.Description` satisfying the given filter criterion. """ + @spec objects(t, (Statement.object -> boolean)) :: MapSet.t def objects(%RDF.Description{predications: predications}, filter_fn) do Enum.reduce predications, MapSet.new, fn ({_, objects}, acc) -> objects @@ -560,6 +611,7 @@ defmodule RDF.Description do ...> ]) |> RDF.Description.resources MapSet.new([RDF.iri(EX.O1), RDF.iri(EX.O2), RDF.bnode(:bnode), EX.p1, EX.p2, EX.p3]) """ + @spec resources(t) :: MapSet.t def resources(description) do description |> objects @@ -569,6 +621,7 @@ defmodule RDF.Description do @doc """ The list of all triples within a `RDF.Description`. """ + @spec triples(t) :: keyword def triples(description = %RDF.Description{}), do: Enum.to_list(description) defdelegate statements(description), to: RDF.Description, as: :triples @@ -577,6 +630,7 @@ defmodule RDF.Description do @doc """ Returns the number of statements of a `RDF.Description`. """ + @spec count(t) :: non_neg_integer def count(%RDF.Description{predications: predications}) do Enum.reduce predications, 0, fn ({_, objects}, count) -> count + Enum.count(objects) end @@ -586,6 +640,7 @@ defmodule RDF.Description do @doc """ Checks if the given statement exists within a `RDF.Description`. """ + @spec include?(t, statements) :: boolean def include?(description, statement) def include?(%RDF.Description{predications: predications}, @@ -617,6 +672,7 @@ defmodule RDF.Description do iex> RDF.Description.new(EX.S1, EX.p1, EX.O1) |> RDF.Description.describes?(EX.S2) false """ + @spec describes?(t, Statement.subject) :: boolean def describes?(%RDF.Description{subject: subject}, other_subject) do with other_subject = coerce_subject(other_subject) do subject == other_subject @@ -656,6 +712,7 @@ defmodule RDF.Description do %{p: ["Foo"]} """ + @spec values(t, Statement.term_mapping) :: map def values(description, mapping \\ &RDF.Statement.default_term_mapping/1) def values(%RDF.Description{predications: predications}, mapping) do @@ -674,6 +731,7 @@ defmodule RDF.Description do If `nil` is passed, the description is left untouched. """ + @spec take(t, [Statement.coercible_predicate] | nil) :: t def take(description, predicates) def take(%RDF.Description{} = description, nil), do: description @@ -688,6 +746,7 @@ defmodule RDF.Description do Two `RDF.Description`s are considered to be equal if they contain the same triples. """ + @spec equal?(t, t) :: boolean def equal?(description1, description2) def equal?(%RDF.Description{} = description1, %RDF.Description{} = description2) do diff --git a/lib/rdf/diff.ex b/lib/rdf/diff.ex index ac8657f..f903189 100644 --- a/lib/rdf/diff.ex +++ b/lib/rdf/diff.ex @@ -9,8 +9,8 @@ defmodule RDF.Diff do alias RDF.{Description, Graph} @type t :: %__MODULE__{ - additions: RDF.Graph.t, - deletions: RDF.Graph.t + additions: Graph.t, + deletions: Graph.t } defstruct [:additions, :deletions] @@ -23,6 +23,7 @@ defmodule RDF.Diff do `additions` and `deletions` keywords. The statements for the additions and deletions can be provided in any form supported by the `RDF.Graph.new/1` function. """ + @spec new(keyword) :: t def new(diff \\ []) do %__MODULE__{ additions: Keyword.get(diff, :additions) |> coerce_graph(), @@ -58,6 +59,8 @@ defmodule RDF.Diff do deletions: RDF.graph({EX.S1, EX.p1, EX.O1}) } """ + @dialyzer {:nowarn_function, diff: 2} + @spec diff(Description.t | Graph.t, Description.t | Graph.t) :: t def diff(original_rdf_data, new_rdf_data) def diff(%Description{} = description, description), do: new() @@ -149,6 +152,7 @@ defmodule RDF.Diff do The diffs are merged by adding up the `additions` and `deletions` of both diffs respectively. """ + @spec merge(t, t) :: t def merge(%__MODULE__{} = diff1, %__MODULE__{} = diff2) do new( additions: Graph.add(diff1.additions, diff2.additions), @@ -161,6 +165,7 @@ defmodule RDF.Diff do A `RDF.Diff` is empty, if its `additions` and `deletions` graphs are empty. """ + @spec empty?(t) :: boolean def empty?(%__MODULE__{} = diff) do Enum.empty?(diff.additions) and Enum.empty?(diff.deletions) end @@ -174,6 +179,7 @@ defmodule RDF.Diff do The result of an application is always a `RDF.Graph`, even if a `RDF.Description` is given and the additions from the diff are all about the subject of this description. """ + @spec apply(t, Description.t | Graph.t) :: Graph.t def apply(diff, rdf_data) def apply(%__MODULE__{} = diff, %Graph{} = graph) do diff --git a/lib/rdf/graph.ex b/lib/rdf/graph.ex index fd0f394..1ea0696 100644 --- a/lib/rdf/graph.ex +++ b/lib/rdf/graph.ex @@ -13,22 +13,31 @@ defmodule RDF.Graph do @behaviour Access - alias RDF.Description import RDF.Statement + alias RDF.{Description, IRI, PrefixMap, Statement} + + @type graph_description :: %{IRI.t => Description.t} @type t :: %__MODULE__{ - name: RDF.IRI.t | nil, + name: IRI.t | nil, descriptions: graph_description, - prefixes: RDF.PrefixMap.t | nil, - base_iri: RDF.IRI | nil + prefixes: PrefixMap.t | nil, + base_iri: IRI.t | nil } + @type t_param :: Statement.t | Description.t | t + + @type update_description_fun :: (Description.t -> Description.t) + + @type get_and_update_description_fun :: (Description.t -> {Description.t, t_param} | :pop) + defstruct name: nil, descriptions: %{}, prefixes: nil, base_iri: nil @doc """ Creates an empty unnamed `RDF.Graph`. """ + @spec new :: t def new, do: %RDF.Graph{} @doc """ @@ -46,6 +55,7 @@ defmodule RDF.Graph do RDF.Graph.new(name: EX.GraphName) """ + @spec new(t_param | [t_param] | keyword) :: t def new(data_or_options) def new(data_or_options) @@ -88,6 +98,7 @@ defmodule RDF.Graph do RDF.Graph.new({EX.S, EX.p, EX.O}, name: EX.GraphName, base_iri: EX.base) """ + @spec new(t_param | [t_param], keyword) :: t def new(data, options) def new(%RDF.Graph{} = graph, options) do @@ -107,6 +118,12 @@ defmodule RDF.Graph do See `new/2` for available arguments. """ + @spec new( + Statement.coercible_subject, + Statement.coercible_predicate, + Statement.coercible_object | [Statement.coercible_object], + keyword + ) :: t def new(subject, predicate, objects, options \\ []), do: new([], options) |> add(subject, predicate, objects) @@ -118,6 +135,7 @@ defmodule RDF.Graph do another graph, as this function keeps graph name name, base IRI and default prefixes as they are and just removes the triples. """ + @spec clear(t) :: t def clear(%RDF.Graph{} = graph) do %RDF.Graph{graph | descriptions: %{}} end @@ -126,6 +144,12 @@ defmodule RDF.Graph do @doc """ Adds triples to a `RDF.Graph`. """ + @spec add( + t, + Statement.coercible_subject, + Statement.coercible_predicate, + Statement.coercible_object | [Statement.coercible_object] + ) :: t def add(%RDF.Graph{} = graph, subject, predicate, objects), do: add(graph, {subject, predicate, objects}) @@ -141,6 +165,7 @@ defmodule RDF.Graph do prefixes of this graph will be added. In case of conflicting prefix mappings the original prefix from `graph` will be kept. """ + @spec add(t, t_param | [t_param]) :: t def add(graph, triples) def add(%RDF.Graph{} = graph, {subject, _, _} = statement), @@ -196,6 +221,7 @@ defmodule RDF.Graph do RDF.Graph.new([{EX.S1, EX.P1, EX.O1}, {EX.S1, EX.P2, EX.O3}, {EX.S2, EX.P2, EX.O3}]) """ + @spec put(t, t_param | [t_param]) :: t def put(graph, statements) def put(%RDF.Graph{} = graph, {subject, _, _} = statement), @@ -233,6 +259,7 @@ defmodule RDF.Graph do @doc """ Add statements to a `RDF.Graph`, overwriting all statements with the same subject and predicate. """ + @spec put(t, Statement.coercible_subject, Description.statements | [Description.statements]) :: t def put(graph, subject, predications) def put(%RDF.Graph{descriptions: descriptions} = graph, subject, predications) @@ -273,6 +300,12 @@ defmodule RDF.Graph do RDF.Graph.new([{EX.S, EX.P1, EX.O1}, {EX.S, EX.P2, EX.O2}]) """ + @spec put( + t, + Statement.coercible_subject, + Statement.coercible_predicate, + Statement.coercible_object | [Statement.coercible_object] + ) :: t def put(%RDF.Graph{} = graph, subject, predicate, objects), do: put(graph, {subject, predicate, objects}) @@ -280,6 +313,12 @@ defmodule RDF.Graph do @doc """ Deletes statements from a `RDF.Graph`. """ + @spec delete( + t, + Statement.coercible_subject, + Statement.coercible_predicate, + Statement.coercible_object | [Statement.coercible_object] + ) :: t def delete(graph, subject, predicate, object), do: delete(graph, {subject, predicate, object}) @@ -292,6 +331,7 @@ defmodule RDF.Graph do use `RDF.Data.delete/2`. """ + @spec delete(t, t_param | [t_param]) :: t def delete(graph, triples) def delete(%RDF.Graph{} = graph, {subject, _, _} = triple), @@ -337,6 +377,10 @@ defmodule RDF.Graph do @doc """ Deletes all statements with the given subjects. """ + @spec delete_subjects( + t, + Statement.coercible_subject | [Statement.coercible_subject] + ) :: t def delete_subjects(graph, subjects) def delete_subjects(%RDF.Graph{} = graph, subjects) when is_list(subjects) do @@ -382,6 +426,12 @@ defmodule RDF.Graph do RDF.Graph.new([{EX.S, EX.p, EX.O}]) """ + @spec update( + t, + Statement.coercible_subject, + Description.statements | [Description.statements] | nil, + update_description_fun + ) :: t def update(graph = %RDF.Graph{}, subject, initial \\ nil, fun) do subject = coerce_subject(subject) @@ -424,6 +474,7 @@ defmodule RDF.Graph do """ @impl Access + @spec fetch(t, Statement.coercible_subject) :: {:ok, Description.t} | :error def fetch(%RDF.Graph{descriptions: descriptions}, subject) do Access.fetch(descriptions, coerce_subject(subject)) end @@ -444,6 +495,7 @@ defmodule RDF.Graph do :bar """ + @spec get(t, Statement.coercible_subject, Description.t | nil) :: Description.t | nil def get(%RDF.Graph{} = graph, subject, default \\ nil) do case fetch(graph, subject) do {:ok, value} -> value @@ -454,12 +506,14 @@ defmodule RDF.Graph do @doc """ The `RDF.Description` of the given subject. """ + @spec description(t, Statement.coercible_subject) :: Description.t | nil def description(%RDF.Graph{descriptions: descriptions}, subject), do: Map.get(descriptions, coerce_subject(subject)) @doc """ All `RDF.Description`s within a `RDF.Graph`. """ + @spec descriptions(t) :: [Description.t] def descriptions(%RDF.Graph{descriptions: descriptions}), do: Map.values(descriptions) @@ -488,6 +542,8 @@ defmodule RDF.Graph do """ @impl Access + @spec get_and_update(t, Statement.coercible_subject, get_and_update_description_fun) :: + {Description.t, t_param} def get_and_update(%RDF.Graph{} = graph, subject, fun) do with subject = coerce_subject(subject) do case fun.(get(graph, subject)) do @@ -505,6 +561,7 @@ defmodule RDF.Graph do @doc """ Pops an arbitrary triple from a `RDF.Graph`. """ + @spec pop(t) :: {Statement.t | nil, t} def pop(graph) def pop(%RDF.Graph{descriptions: descriptions} = graph) @@ -536,6 +593,7 @@ defmodule RDF.Graph do """ @impl Access + @spec pop(t, Statement.coercible_subject) :: {Description.t | nil, t} def pop(%RDF.Graph{descriptions: descriptions} = graph, subject) do case Access.pop(descriptions, coerce_subject(subject)) do {nil, _} -> @@ -559,6 +617,7 @@ defmodule RDF.Graph do 2 """ + @spec subject_count(t) :: non_neg_integer def subject_count(%RDF.Graph{descriptions: descriptions}), do: Enum.count(descriptions) @@ -575,6 +634,7 @@ defmodule RDF.Graph do 3 """ + @spec triple_count(t) :: non_neg_integer def triple_count(%RDF.Graph{descriptions: descriptions}) do Enum.reduce descriptions, 0, fn ({_subject, description}, count) -> count + Description.count(description) @@ -676,7 +736,8 @@ defmodule RDF.Graph do {RDF.iri(EX.S1), RDF.iri(EX.p2), RDF.iri(EX.O3)}, {RDF.iri(EX.S2), RDF.iri(EX.p2), RDF.iri(EX.O2)}] """ - def triples(graph = %RDF.Graph{}), do: Enum.to_list(graph) + @spec triples(t) :: [Statement.t] + def triples(%RDF.Graph{} = graph), do: Enum.to_list(graph) defdelegate statements(graph), to: RDF.Graph, as: :triples @@ -684,6 +745,7 @@ defmodule RDF.Graph do @doc """ Checks if the given statement exists within a `RDF.Graph`. """ + @spec include?(t, Statement.t) :: boolean def include?(%RDF.Graph{descriptions: descriptions}, triple = {subject, _, _}) do with subject = coerce_subject(subject), @@ -704,6 +766,7 @@ defmodule RDF.Graph do iex> RDF.Graph.new([{EX.S1, EX.p1, EX.O1}]) |> RDF.Graph.describes?(EX.S2) false """ + @spec describes?(t, Statement.coercible_subject) :: boolean def describes?(%RDF.Graph{descriptions: descriptions}, subject) do with subject = coerce_subject(subject) do Map.has_key?(descriptions, subject) @@ -753,6 +816,7 @@ defmodule RDF.Graph do } """ + @spec values(t, Statement.term_mapping) :: map def values(graph, mapping \\ &RDF.Statement.default_term_mapping/1) def values(%RDF.Graph{descriptions: descriptions}, mapping) do @@ -770,6 +834,7 @@ defmodule RDF.Graph do If `nil` is passed as the `subjects`, the subjects will not be limited. """ + @spec take(t, [Statement.coercible_subject] | nil, [Statement.coercible_predicate] | nil) :: t def take(graph, subjects, properties \\ nil) def take(%RDF.Graph{} = graph, nil, nil), do: graph @@ -795,6 +860,7 @@ defmodule RDF.Graph do Two `RDF.Graph`s are considered to be equal if they contain the same triples and have the same name. The prefixes of the graph are irrelevant for equality. """ + @spec equal?(t | any, t | any) :: boolean def equal?(graph1, graph2) def equal?(%RDF.Graph{} = graph1, %RDF.Graph{} = graph2) do @@ -814,6 +880,11 @@ defmodule RDF.Graph do the new one. This behaviour can be customized by providing a `conflict_resolver` function. See `RDF.PrefixMap.merge/3` for more on that. """ + @spec add_prefixes( + t, + PrefixMap.t | map | keyword | nil, + PrefixMap.conflict_resolver | nil + ) :: t def add_prefixes(graph, prefixes, conflict_resolver \\ nil) def add_prefixes(%RDF.Graph{} = graph, nil, _), do: graph @@ -838,6 +909,7 @@ defmodule RDF.Graph do The `prefixes` can be a single prefix or a list of prefixes. Prefixes not in prefixes of the graph are simply ignored. """ + @spec delete_prefixes(t, PrefixMap.t) :: t def delete_prefixes(graph, prefixes) def delete_prefixes(%RDF.Graph{prefixes: nil} = graph, _), do: graph @@ -849,6 +921,7 @@ defmodule RDF.Graph do @doc """ Clears all prefixes of the given `graph`. """ + @spec clear_prefixes(t) :: t def clear_prefixes(%RDF.Graph{} = graph) do %RDF.Graph{graph | prefixes: nil} end @@ -858,6 +931,7 @@ defmodule RDF.Graph do The `base_iri` can be given as anything accepted by `RDF.IRI.coerce_base/1`. """ + @spec set_base_iri(t, IRI.t | nil) :: t def set_base_iri(graph, base_iri) def set_base_iri(%RDF.Graph{} = graph, nil) do @@ -871,6 +945,7 @@ defmodule RDF.Graph do @doc """ Clears the base IRI of the given `graph`. """ + @spec clear_base_iri(t) :: t def clear_base_iri(%RDF.Graph{} = graph) do %RDF.Graph{graph | base_iri: nil} end @@ -878,6 +953,7 @@ defmodule RDF.Graph do @doc """ Clears the base IRI and all prefixes of the given `graph`. """ + @spec clear_metadata(t) :: t def clear_metadata(%RDF.Graph{} = graph) do graph |> clear_base_iri() diff --git a/lib/rdf/iri.ex b/lib/rdf/iri.ex index abbeebf..49100d6 100644 --- a/lib/rdf/iri.ex +++ b/lib/rdf/iri.ex @@ -16,12 +16,14 @@ defmodule RDF.IRI do see """ - alias RDF.Namespace + alias RDF.{Datatype, Namespace} @type t :: %__MODULE__{ - value: binary + value: String.t } + @type t_param :: String.t | Datatype.t | URI.t | t + @enforce_keys [:value] defstruct [:value] @@ -45,6 +47,7 @@ defmodule RDF.IRI do @doc """ Creates a `RDF.IRI`. """ + @spec new(t_param) :: t def new(iri) def new(iri) when is_binary(iri), do: %RDF.IRI{value: iri} def new(qname) when is_atom(qname) and qname not in [nil, true, false], @@ -59,6 +62,7 @@ defmodule RDF.IRI do see `valid?/1` """ + @spec new!(t_param) :: t def new!(iri) def new!(iri) when is_binary(iri), do: iri |> valid!() |> new() def new!(qname) when is_atom(qname) and qname not in [nil, true, false], @@ -73,6 +77,7 @@ defmodule RDF.IRI do As opposed to `new/1` this also accepts bare `RDF.Vocabulary.Namespace` modules and uses the base IRI from their definition. """ + @spec coerce_base(t_param) :: t def coerce_base(base_iri) def coerce_base(module) when is_atom(module) do @@ -98,6 +103,7 @@ defmodule RDF.IRI do iex> RDF.IRI.valid!("not an iri") ** (RDF.IRI.InvalidError) Invalid IRI: "not an iri" """ + @spec valid!(t_param) :: t_param def valid!(iri) do if not valid?(iri), do: raise RDF.IRI.InvalidError, "Invalid IRI: #{inspect iri}" iri @@ -116,6 +122,7 @@ defmodule RDF.IRI do iex> RDF.IRI.valid?("not an iri") false """ + @spec valid?(t_param) :: boolean def valid?(iri), do: absolute?(iri) # TODO: Provide a more elaborate validation @@ -125,6 +132,7 @@ defmodule RDF.IRI do 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. """ + @spec absolute?(any) :: boolean def absolute?(iri) def absolute?(value) when is_binary(value), do: not is_nil(scheme(value)) @@ -151,6 +159,7 @@ defmodule RDF.IRI do If the given is not an absolute IRI `nil` is returned. """ + @spec absolute(t_param, t_param) :: t | nil def absolute(iri, base) do cond do absolute?(iri) -> new(iri) @@ -166,6 +175,7 @@ defmodule RDF.IRI do This function merges two IRIs as per [RFC 3986, section 5.2](https://tools.ietf.org/html/rfc3986#section-5.2). """ + @spec merge(t_param, t_param) :: t def merge(base, rel) do base |> parse() @@ -186,6 +196,7 @@ defmodule RDF.IRI do iex> RDF.IRI.scheme("not an iri") nil """ + @spec scheme(t_param) :: String.t | nil def scheme(iri) def scheme(%RDF.IRI{value: value}), do: scheme(value) def scheme(%URI{scheme: scheme}), do: scheme @@ -200,6 +211,7 @@ defmodule RDF.IRI do @doc """ Parses an IRI into its components and returns them as an `URI` struct. """ + @spec parse(t_param) :: URI.t def parse(iri) def parse(iri) when is_binary(iri), do: URI.parse(iri) def parse(qname) when is_atom(qname) and qname not in [nil, true, false], @@ -215,6 +227,7 @@ defmodule RDF.IRI do see """ + @spec equal_value?(t | RDF.Literal.t, t | RDF.Literal.t) :: boolean | nil def equal_value?(left, right) def equal_value?(%RDF.IRI{value: left}, %RDF.IRI{value: right}), @@ -247,6 +260,7 @@ defmodule RDF.IRI do "http://example.com/#Foo" """ + @spec to_string(t | Datatype.t) :: String.t def to_string(iri) def to_string(%RDF.IRI{value: value}), diff --git a/lib/rdf/list.ex b/lib/rdf/list.ex index bdd3bce..1ec3a74 100644 --- a/lib/rdf/list.ex +++ b/lib/rdf/list.ex @@ -7,11 +7,11 @@ defmodule RDF.List do - """ - alias RDF.{Graph, Description, IRI, BlankNode} + alias RDF.{BlankNode, Description, Graph, IRI} @type t :: %__MODULE__{ - head: RDF.IRI.t, - graph: RDF.Graph.t + head: IRI.t, + graph: Graph.t } @enforce_keys [:head] @@ -31,6 +31,7 @@ defmodule RDF.List do - does not contain cycles, i.e. `rdf:rest` statements don't refer to preceding list nodes """ + @spec new(IRI.t_param, Graph.t) :: t def new(head, graph) def new(head, graph) when is_atom(head) and head not in ~w[true false nil]a, @@ -70,6 +71,7 @@ defmodule RDF.List do the head node of the empty list is always `RDF.nil`. """ + @spec from(Enumerable.t, keyword) :: t def from(list, opts \\ []) do with head = Keyword.get(opts, :head, RDF.bnode), graph = Keyword.get(opts, :graph, RDF.graph), @@ -118,6 +120,7 @@ defmodule RDF.List do Nested lists are converted recursively. """ + @spec values(t) :: Enumerable.t def values(%RDF.List{graph: graph} = list) do Enum.map list, fn node_description -> value = Description.first(node_description, RDF.first) @@ -135,6 +138,7 @@ defmodule RDF.List do @doc """ The RDF nodes constituting a `RDF.List` as an Elixir list. """ + @spec nodes(t) :: [BlankNode.t] def nodes(%RDF.List{} = list) do Enum.map list, fn node_description -> node_description.subject end end @@ -143,16 +147,18 @@ defmodule RDF.List do @doc """ Checks if a list is the empty list. """ + @spec empty?(t) :: boolean def empty?(%RDF.List{head: @rdf_nil}), do: true - def empty?(_), do: false + def empty?(%RDF.List{}), do: false @doc """ Checks if the given list consists of list nodes which are all blank nodes. """ + @spec valid?(t) :: boolean def valid?(%RDF.List{head: @rdf_nil}), do: true - def valid?(list) do + def valid?(%RDF.List{} = list) do Enum.all? list, fn node_description -> RDF.bnode?(node_description.subject) end @@ -168,6 +174,7 @@ defmodule RDF.List do Note: This function doesn't indicate if the list is valid. See `new/2` and `valid?/2` for validations. """ + @spec node?(any, Graph.t) :: boolean def node?(list_node, graph) def node?(@rdf_nil, _), diff --git a/lib/rdf/literal.ex b/lib/rdf/literal.ex index 48f343c..6341a7e 100644 --- a/lib/rdf/literal.ex +++ b/lib/rdf/literal.ex @@ -4,23 +4,26 @@ defmodule RDF.Literal do """ alias RDF.Datatype.NS.XSD + alias RDF.IRI @type literal_value :: - binary - | boolean + boolean | integer | float + | String.t | Decimal.t | Date.t | Time.t | DateTime.t | NaiveDateTime.t + | {Date.t, String.t} + | :nan @type t :: %__MODULE__{ value: literal_value, - datatype: RDF.IRI.t, - uncanonical_lexical: binary | nil, - language: binary | nil + datatype: IRI.t, + uncanonical_lexical: String.t | nil, + language: String.t | nil } defstruct [:value, :datatype, :uncanonical_lexical, :language] @@ -56,6 +59,7 @@ defmodule RDF.Literal do %RDF.Literal{value: 42, datatype: XSD.integer} """ + @spec new(literal_value | t) :: t def new(value) def new(%RDF.Literal{} = literal), do: literal @@ -79,6 +83,7 @@ defmodule RDF.Literal do @doc """ Creates a new `RDF.Literal` with the given datatype or language tag. """ + @spec new(literal_value | t, map | keyword) :: t def new(value, opts) def new(value, opts) when is_list(opts), @@ -134,6 +139,7 @@ defmodule RDF.Literal do ** (RDF.Literal.InvalidError) invalid RDF.Literal: %RDF.Literal{value: "foo", datatype: ~I, language: nil} """ + @spec new!(literal_value | t, map | keyword) :: t def new!(value, opts \\ %{}) do with %RDF.Literal{} = literal <- new(value, opts) do if valid?(literal) do @@ -150,6 +156,7 @@ defmodule RDF.Literal do @doc """ Returns the lexical representation of the given literal according to its datatype. """ + @spec lexical(t) :: String.t def lexical(%RDF.Literal{value: value, uncanonical_lexical: nil, datatype: id} = literal) do case RDF.Datatype.get(id) do nil -> to_string(value) @@ -162,6 +169,7 @@ defmodule RDF.Literal do @doc """ Returns the given literal in its canonical lexical representation. """ + @spec canonical(t) :: t def canonical(%RDF.Literal{uncanonical_lexical: nil} = literal), do: literal def canonical(%RDF.Literal{datatype: id} = literal) do case RDF.Datatype.get(id) do @@ -174,13 +182,15 @@ defmodule RDF.Literal do @doc """ Returns if the given literal is in its canonical lexical representation. """ + @spec canonical?(t) :: boolean def canonical?(%RDF.Literal{uncanonical_lexical: nil}), do: true - def canonical?(_), do: false + def canonical?(%RDF.Literal{} = _), do: false @doc """ Returns if the value of the given literal is a valid according to its datatype. """ + @spec valid?(t) :: boolean def valid?(%RDF.Literal{datatype: id} = literal) do case RDF.Datatype.get(id) do nil -> true @@ -196,8 +206,9 @@ defmodule RDF.Literal do see """ + @spec simple?(t) :: boolean def simple?(%RDF.Literal{datatype: @xsd_string}), do: true - def simple?(_), do: false + def simple?(%RDF.Literal{} = _), do: false @doc """ @@ -205,8 +216,9 @@ defmodule RDF.Literal do see """ + @spec has_language?(t) :: boolean def has_language?(%RDF.Literal{datatype: @lang_string}), do: true - def has_language?(_), do: false + def has_language?(%RDF.Literal{} = _), do: false @doc """ @@ -216,6 +228,7 @@ defmodule RDF.Literal do see """ + @spec has_datatype?(t) :: boolean def has_datatype?(literal) do not plain?(literal) and not has_language?(literal) end @@ -229,10 +242,12 @@ defmodule RDF.Literal do see """ + @spec plain?(t) :: boolean def plain?(%RDF.Literal{datatype: datatype}) when datatype in @plain_types, do: true - def plain?(_), do: false + def plain?(%RDF.Literal{} = _), do: false + @spec typed?(t) :: boolean def typed?(literal), do: not plain?(literal) @@ -245,6 +260,7 @@ defmodule RDF.Literal do see """ + @spec equal_value?(t | IRI.t | any, t | IRI.t | any) :: boolean | nil def equal_value?(left, right) def equal_value?(%RDF.Literal{datatype: id1} = literal1, %RDF.Literal{datatype: id2} = literal2) do @@ -282,6 +298,7 @@ defmodule RDF.Literal do Returns `nil` when the given arguments are not comparable datatypes. """ + @spec less_than?(t | any, t | any) :: boolean | nil def less_than?(literal1, literal2) do case compare(literal1, literal2) do :lt -> true @@ -296,6 +313,7 @@ defmodule RDF.Literal do Returns `nil` when the given arguments are not comparable datatypes. """ + @spec greater_than?(t | any, t | any) :: boolean | nil def greater_than?(literal1, literal2) do case compare(literal1, literal2) do :gt -> true @@ -316,6 +334,7 @@ defmodule RDF.Literal do Returns `nil` when the given arguments are not comparable datatypes. """ + @spec compare(t | any, t | any) :: :eq | :lt | :gt | nil def compare(left, right) def compare(%RDF.Literal{datatype: id1} = literal1, %RDF.Literal{datatype: id2} = literal2) do @@ -347,6 +366,7 @@ defmodule RDF.Literal do see """ + @spec matches?(t | String.t, t | String.t, t | String.t) :: boolean def matches?(value, pattern, flags \\ "") do string = to_string(value) case xpath_pattern(pattern, flags) do @@ -361,12 +381,13 @@ defmodule RDF.Literal do |> String.downcase() |> String.contains?(String.downcase(pattern)) - _ -> - raise "Invalid XQuery regex pattern or flags" + # _ -> + # raise "Invalid XQuery regex pattern or flags" end end @doc false + @spec xpath_pattern(t | String.t, t | String.t) :: {:regex, Regex.t} | {:q | :qi, String.t} def xpath_pattern(pattern, flags) def xpath_pattern(%RDF.Literal{datatype: @xsd_string} = pattern, flags), @@ -395,6 +416,7 @@ defmodule RDF.Literal do end @doc false + @spec convert_utf_escaping(String.t) :: String.t def convert_utf_escaping(string) do require Integer diff --git a/lib/rdf/namespace.ex b/lib/rdf/namespace.ex index 9b9a0fd..24f0bd1 100644 --- a/lib/rdf/namespace.ex +++ b/lib/rdf/namespace.ex @@ -7,10 +7,12 @@ defmodule RDF.Namespace do namespaces for JSON-LD contexts. """ + alias RDF.{Datatype, IRI} + @doc """ Resolves a term to a `RDF.IRI`. """ - @callback __resolve_term__(atom) :: RDF.IRI.t + @callback __resolve_term__(atom) :: IRI.t @doc """ All terms of a `RDF.Namespace`. @@ -25,9 +27,10 @@ defmodule RDF.Namespace do delegates to remaining part of the term to `__resolve_term__/1` of this determined namespace. """ + @spec resolve_term(IRI.t | Datatype.t) :: IRI.t def resolve_term(expr) - def resolve_term(%RDF.IRI{} = iri), do: iri + def resolve_term(%IRI{} = iri), do: iri def resolve_term(namespaced_term) when is_atom(namespaced_term) do namespaced_term diff --git a/lib/rdf/prefix_map.ex b/lib/rdf/prefix_map.ex index 067a220..67014ae 100644 --- a/lib/rdf/prefix_map.ex +++ b/lib/rdf/prefix_map.ex @@ -7,8 +7,15 @@ defmodule RDF.PrefixMap do alias RDF.IRI + @type prefix :: atom | String.t + @type namespace :: atom | String.t | IRI.t + + @type prefix_map :: %{prefix => namespace} + + @type conflict_resolver :: (prefix, namespace, namespace -> namespace) + @type t :: %__MODULE__{ - map: map + map: prefix_map } defstruct map: %{} @@ -17,7 +24,8 @@ defmodule RDF.PrefixMap do @doc """ Creates an empty `RDF.PrefixMap`. """ - def new(), do: %__MODULE__{} + @spec new :: t + def new, do: %__MODULE__{} @doc """ Creates a new `RDF.PrefixMap`. @@ -26,6 +34,7 @@ defmodule RDF.PrefixMap do The keys for the prefixes can be given as atoms or strings and will be normalized to atoms. The namespaces can be given as `RDF.IRI`s or strings and will be normalized to `RDF.IRI`s. """ + @spec new(t | map | keyword) :: t def new(map) def new(%__MODULE__{} = prefix_map), do: prefix_map @@ -54,6 +63,7 @@ defmodule RDF.PrefixMap do Unless a mapping of the given prefix to a different namespace already exists, an ok tuple is returned, other an error tuple. """ + @spec add(t, prefix, namespace) :: {:ok, t} | {:error, String.t} def add(prefix_map, prefix, namespace) def add(%__MODULE__{map: map}, prefix, %IRI{} = namespace) when is_atom(prefix) do @@ -73,6 +83,7 @@ defmodule RDF.PrefixMap do @doc """ Adds a prefix mapping to the given `RDF.PrefixMap` and raises an exception in error cases. """ + @spec add!(t, prefix, namespace) :: t def add!(prefix_map, prefix, namespace) do with {:ok, new_prefix_map} <- add(prefix_map, prefix, namespace) do new_prefix_map @@ -94,6 +105,7 @@ defmodule RDF.PrefixMap do See also `merge/3` which allows you to resolve conflicts with a function. """ + @spec merge(t, t | map | keyword) :: {:ok, t} | {:error, [atom | String.t]} def merge(prefix_map1, prefix_map2) def merge(%__MODULE__{map: map1}, %__MODULE__{map: map2}) do @@ -133,6 +145,7 @@ defmodule RDF.PrefixMap do If everything could be merged, an `:ok` tuple is returned. """ + @spec merge(t, t | map | keyword, conflict_resolver | nil) :: {:ok, t} | {:error, [atom | String.t]} def merge(prefix_map1, prefix_map2, conflict_resolver) def merge(%__MODULE__{map: map1}, %__MODULE__{map: map2}, conflict_resolver) @@ -185,20 +198,22 @@ defmodule RDF.PrefixMap do See `merge/2` and `merge/3` for more information on merging prefix maps. """ + @spec merge!(t, t | map | keyword, conflict_resolver | nil) :: t def merge!(prefix_map1, prefix_map2, conflict_resolver \\ nil) do with {:ok, new_prefix_map} <- merge(prefix_map1, prefix_map2, conflict_resolver) do new_prefix_map else {:error, conflicts} -> - raise "conflicting prefix mappings: #{ - conflicts |> Stream.map(&inspect/1) |> Enum.join(", ") - }" + conflicts = conflicts |> Stream.map(&inspect/1) |> Enum.join(", ") + + raise "conflicting prefix mappings: #{conflicts}" end end @doc """ Deletes a prefix mapping from the given `RDF.PrefixMap`. """ + @spec delete(t, prefix) :: t def delete(prefix_map, prefix) def delete(%__MODULE__{map: map}, prefix) when is_atom(prefix) do @@ -214,6 +229,7 @@ defmodule RDF.PrefixMap do If `prefixes` contains prefixes that are not in `prefix_map`, they're simply ignored. """ + @spec drop(t, [prefix]) :: t def drop(prefix_map, prefixes) def drop(%__MODULE__{map: map}, prefixes) do @@ -234,6 +250,7 @@ defmodule RDF.PrefixMap do Returns `nil`, when the given `prefix` is not present in `prefix_map`. """ + @spec namespace(t, prefix) :: namespace | nil def namespace(prefix_map, prefix) def namespace(%__MODULE__{map: map}, prefix) when is_atom(prefix) do @@ -249,6 +266,7 @@ defmodule RDF.PrefixMap do Returns `nil`, when the given `namespace` is not present in `prefix_map`. """ + @spec prefix(t, namespace) :: prefix | nil def prefix(prefix_map, namespace) def prefix(%__MODULE__{map: map}, %IRI{} = namespace) do @@ -262,6 +280,7 @@ defmodule RDF.PrefixMap do @doc """ Returns whether the given prefix exists in the given `RDF.PrefixMap`. """ + @spec has_prefix?(t, prefix) :: boolean def has_prefix?(prefix_map, prefix) def has_prefix?(%__MODULE__{map: map}, prefix) when is_atom(prefix) do @@ -275,6 +294,7 @@ defmodule RDF.PrefixMap do @doc """ Returns all prefixes from the given `RDF.PrefixMap`. """ + @spec prefixes(t) :: [prefix] def prefixes(%__MODULE__{map: map}) do Map.keys(map) end @@ -282,6 +302,7 @@ defmodule RDF.PrefixMap do @doc """ Returns all namespaces from the given `RDF.PrefixMap`. """ + @spec namespaces(t) :: [namespace] def namespaces(%__MODULE__{map: map}) do Map.values(map) end diff --git a/lib/rdf/quad.ex b/lib/rdf/quad.ex index 68f2bbd..5e84d64 100644 --- a/lib/rdf/quad.ex +++ b/lib/rdf/quad.ex @@ -6,10 +6,16 @@ defmodule RDF.Quad do RDF values for subject, predicate, object and a graph context. """ - alias RDF.Statement + alias RDF.{Literal, Statement} @type t :: {Statement.subject, Statement.predicate, Statement.object, Statement.graph_name} + @type coercible_t :: + {Statement.coercible_subject, Statement.coercible_predicate, + Statement.coercible_object, Statement.coercible_graph_name} + + @type t_values :: {String.t, String.t, Literal.literal_value, String.t} + @doc """ Creates a `RDF.Quad` with proper RDF values. @@ -25,6 +31,12 @@ defmodule RDF.Quad do iex> RDF.Quad.new(EX.S, EX.p, 42, EX.Graph) {RDF.iri("http://example.com/S"), RDF.iri("http://example.com/p"), RDF.literal(42), RDF.iri("http://example.com/Graph")} """ + @spec new( + Statement.coercible_subject, + Statement.coercible_predicate, + Statement.coercible_object, + Statement.coercible_graph_name + ) :: t def new(subject, predicate, object, graph_context) do { Statement.coerce_subject(subject), @@ -48,6 +60,7 @@ defmodule RDF.Quad do iex> RDF.Quad.new {EX.S, EX.p, 42, EX.Graph} {RDF.iri("http://example.com/S"), RDF.iri("http://example.com/p"), RDF.literal(42), RDF.iri("http://example.com/Graph")} """ + @spec new(coercible_t) :: t def new({subject, predicate, object, graph_context}), do: new(subject, predicate, object, graph_context) @@ -81,6 +94,7 @@ defmodule RDF.Quad do {:S, :p, 42, ~I} """ + @spec values(t | any, Statement.term_mapping) :: t_values | nil def values(quad, mapping \\ &Statement.default_term_mapping/1) def values({subject, predicate, object, graph_context}, mapping) do @@ -105,6 +119,7 @@ defmodule RDF.Quad do position only IRIs and blank nodes allowed, while on the predicate and graph context position only IRIs allowed. The object position can be any RDF term. """ + @spec valid?(t | any) :: boolean def valid?(tuple) def valid?({_, _, _, _} = quad), do: Statement.valid?(quad) def valid?(_), do: false diff --git a/lib/rdf/serialization/encoder.ex b/lib/rdf/serialization/encoder.ex index 4b5e02f..93aefe2 100644 --- a/lib/rdf/serialization/encoder.ex +++ b/lib/rdf/serialization/encoder.ex @@ -11,7 +11,8 @@ defmodule RDF.Serialization.Encoder do It returns an `{:ok, string}` tuple, with `string` being the serialized `RDF.Graph` or `RDF.Dataset`, or `{:error, reason}` if an error occurs. """ - @callback encode(RDF.Graph.t | RDF.Dataset.t, keyword) :: keyword(String.t) + @callback encode(RDF.Graph.t | RDF.Dataset.t, keyword) :: + {:ok, String.t} | {:error, any} @doc """ Encodes a `RDF.Graph` or `RDF.Dataset`. diff --git a/lib/rdf/serializations/turtle_encoder.ex b/lib/rdf/serializations/turtle_encoder.ex index 5887e10..fa92a4e 100644 --- a/lib/rdf/serializations/turtle_encoder.ex +++ b/lib/rdf/serializations/turtle_encoder.ex @@ -175,6 +175,7 @@ defmodule RDF.Turtle.Encoder do |> Enum.join(" ;" <> newline_indent(nesting)) end + @dialyzer {:nowarn_function, order_predications: 1} defp order_predications(predications) do sorted_predications = @predicate_order @@ -223,6 +224,7 @@ defmodule RDF.Turtle.Encoder do end end + @dialyzer {:nowarn_function, list_subject_description: 1} defp list_subject_description(description) do with description = Description.delete_predicates(description, [RDF.first, RDF.rest]) do if Enum.count(description.predications) == 0 do diff --git a/lib/rdf/serializations/turtle_encoder_state.ex b/lib/rdf/serializations/turtle_encoder_state.ex index eec36cf..90e608d 100644 --- a/lib/rdf/serializations/turtle_encoder_state.ex +++ b/lib/rdf/serializations/turtle_encoder_state.ex @@ -82,6 +82,7 @@ defmodule RDF.Turtle.Encoder.State do @list_properties MapSet.new([RDF.first, RDF.rest]) + @dialyzer {:nowarn_function, to_list?: 2} defp to_list?(%Description{} = description, 1) do Description.count(description) == 2 and Description.predicates(description) |> MapSet.equal?(@list_properties) diff --git a/lib/rdf/statement.ex b/lib/rdf/statement.ex index cf5ff95..8fb4a6b 100644 --- a/lib/rdf/statement.ex +++ b/lib/rdf/statement.ex @@ -5,18 +5,23 @@ defmodule RDF.Statement do A RDF statement is either a `RDF.Triple` or a `RDF.Quad`. """ - alias RDF.{Triple, Quad, IRI, BlankNode, Literal} + alias RDF.{BlankNode, IRI, Literal, Quad, Term, Triple} @type subject :: IRI.t | BlankNode.t - @type predicate :: IRI.t + @type predicate :: IRI.t | BlankNode.t @type object :: IRI.t | BlankNode.t | Literal.t @type graph_name :: IRI.t | BlankNode.t @type coercible_subject :: subject | atom | String.t @type coercible_predicate :: predicate | atom | String.t - @type coercible_object :: object | atom | String.t # TODO: all basic Elixir types coercible to Literals + @type coercible_object :: object | Literal.literal_value @type coercible_graph_name :: graph_name | atom | String.t + @type qualified_term :: {atom, Term.t | nil} + @type term_mapping :: (qualified_term -> Literal.literal_value | nil) + + @type t :: Triple.t | Quad.t + @doc """ Creates a `RDF.Statement` tuple with proper RDF values. @@ -30,11 +35,14 @@ defmodule RDF.Statement do iex> RDF.Statement.coerce {"http://example.com/S", "http://example.com/p", 42, "http://example.com/Graph"} {~I, ~I, RDF.literal(42), ~I} """ + @spec coerce(Triple.coercible_t) :: Triple.t + @spec coerce(Quad.coercible_t) :: Quad.t def coerce(statement) def coerce({_, _, _} = triple), do: Triple.new(triple) def coerce({_, _, _, _} = quad), do: Quad.new(quad) @doc false + @spec coerce_subject(coercible_subject) :: subject def coerce_subject(iri) def coerce_subject(iri = %IRI{}), do: iri def coerce_subject(bnode = %BlankNode{}), do: bnode @@ -43,6 +51,7 @@ defmodule RDF.Statement do def coerce_subject(arg), do: raise RDF.Triple.InvalidSubjectError, subject: arg @doc false + @spec coerce_predicate(coercible_predicate) :: predicate def coerce_predicate(iri) def coerce_predicate(iri = %IRI{}), do: iri # Note: Although, RDF does not allow blank nodes for properties, JSON-LD allows @@ -53,6 +62,7 @@ defmodule RDF.Statement do def coerce_predicate(arg), do: raise RDF.Triple.InvalidPredicateError, predicate: arg @doc false + @spec coerce_object(coercible_object) :: object def coerce_object(iri) def coerce_object(iri = %IRI{}), do: iri def coerce_object(literal = %Literal{}), do: literal @@ -62,6 +72,7 @@ defmodule RDF.Statement do def coerce_object(arg), do: Literal.new(arg) @doc false + @spec coerce_graph_name(coercible_graph_name) :: graph_name def coerce_graph_name(iri) def coerce_graph_name(nil), do: nil def coerce_graph_name(iri = %IRI{}), do: iri @@ -105,12 +116,14 @@ defmodule RDF.Statement do {"S", :p, 42, ~I} """ + @spec values(t | any, term_mapping) :: Triple.t_values | Quad.t_values | nil def values(statement, mapping \\ &default_term_mapping/1) def values({_, _, _} = triple, mapping), do: RDF.Triple.values(triple, mapping) def values({_, _, _, _} = quad, mapping), do: RDF.Quad.values(quad, mapping) def values(_, _), do: nil @doc false + @spec default_term_mapping(qualified_term) :: Literal.literal_value | nil def default_term_mapping(qualified_term) def default_term_mapping({:graph_name, nil}), do: nil def default_term_mapping({_, term}), do: RDF.Term.value(term) @@ -123,6 +136,7 @@ defmodule RDF.Statement do position only IRIs and blank nodes allowed, while on the predicate and graph context position only IRIs allowed. The object position can be any RDF term. """ + @spec valid?(Triple.t | Quad.t | any) :: boolean def valid?(tuple) def valid?({subject, predicate, object}) do @@ -136,18 +150,22 @@ defmodule RDF.Statement do def valid?(_), do: false + @spec valid_subject?(subject | any) :: boolean def valid_subject?(%IRI{}), do: true def valid_subject?(%BlankNode{}), do: true def valid_subject?(_), do: false + @spec valid_predicate?(predicate | any) :: boolean def valid_predicate?(%IRI{}), do: true def valid_predicate?(_), do: false + @spec valid_object?(object | any) :: boolean def valid_object?(%IRI{}), do: true def valid_object?(%BlankNode{}), do: true def valid_object?(%Literal{}), do: true def valid_object?(_), do: false + @spec valid_graph_name?(graph_name | any) :: boolean def valid_graph_name?(%IRI{}), do: true def valid_graph_name?(_), do: false diff --git a/lib/rdf/term.ex b/lib/rdf/term.ex index 8a2083d..923f0c6 100644 --- a/lib/rdf/term.ex +++ b/lib/rdf/term.ex @@ -109,6 +109,8 @@ defimpl RDF.Term, for: RDF.BlankNode do end defimpl RDF.Term, for: Reference do + @dialyzer {:nowarn_function, equal_value?: 2} + @dialyzer {:nowarn_function, coerce: 1} def equal?(term1, term2), do: term1 == term2 def equal_value?(term1, term2), do: RDF.Term.equal_value?(coerce(term1), term2) def coerce(term), do: RDF.BlankNode.new(term) diff --git a/lib/rdf/triple.ex b/lib/rdf/triple.ex index 5070b36..a070562 100644 --- a/lib/rdf/triple.ex +++ b/lib/rdf/triple.ex @@ -6,10 +6,16 @@ defmodule RDF.Triple do RDF values for subject, predicate and object. """ - alias RDF.Statement + alias RDF.{Literal, Statement} @type t :: {Statement.subject, Statement.predicate, Statement.object} + @type coercible_t :: + {Statement.coercible_subject, Statement.coercible_predicate, + Statement.coercible_object} + + @type t_values :: {String.t, String.t, Literal.literal_value} + @doc """ Creates a `RDF.Triple` with proper RDF values. @@ -25,6 +31,11 @@ defmodule RDF.Triple do iex> RDF.Triple.new(EX.S, EX.p, 42) {RDF.iri("http://example.com/S"), RDF.iri("http://example.com/p"), RDF.literal(42)} """ + @spec new( + Statement.coercible_subject, + Statement.coercible_predicate, + Statement.coercible_object + ) :: t def new(subject, predicate, object) do { Statement.coerce_subject(subject), @@ -47,6 +58,7 @@ defmodule RDF.Triple do iex> RDF.Triple.new {EX.S, EX.p, 42} {RDF.iri("http://example.com/S"), RDF.iri("http://example.com/p"), RDF.literal(42)} """ + @spec new(coercible_t) :: t def new({subject, predicate, object}), do: new(subject, predicate, object) @@ -75,6 +87,7 @@ defmodule RDF.Triple do {"S", "p", 42} """ + @spec values(t | any, Statement.term_mapping) :: t_values | nil def values(triple, mapping \\ &Statement.default_term_mapping/1) def values({subject, predicate, object}, mapping) do @@ -98,6 +111,7 @@ defmodule RDF.Triple do position only IRIs and blank nodes allowed, while on the predicate position only IRIs allowed. The object position can be any RDF term. """ + @spec valid?(t | any) :: boolean def valid?(tuple) def valid?({_, _, _} = triple), do: Statement.valid?(triple) def valid?(_), do: false diff --git a/lib/rdf/utils/resource_classifier.ex b/lib/rdf/utils/resource_classifier.ex index 4e1fa11..cfa3cb4 100644 --- a/lib/rdf/utils/resource_classifier.ex +++ b/lib/rdf/utils/resource_classifier.ex @@ -53,6 +53,7 @@ defmodule RDF.Utils.ResourceClassifier do |> Enum.map(&RDF.iri/1) |> MapSet.new + @dialyzer {:nowarn_function, property_by_rdf_type?: 1} defp property_by_rdf_type?(nil), do: nil defp property_by_rdf_type?(types) do not ( diff --git a/lib/rdf/vocabulary_namespace.ex b/lib/rdf/vocabulary_namespace.ex index 619e494..8ff5464 100644 --- a/lib/rdf/vocabulary_namespace.ex +++ b/lib/rdf/vocabulary_namespace.ex @@ -9,6 +9,7 @@ defmodule RDF.Vocabulary.Namespace do the `RDF.NS` module. """ + alias RDF.Datatype alias RDF.Utils.ResourceClassifier @vocabs_dir "priv/vocabs" @@ -295,6 +296,7 @@ defmodule RDF.Vocabulary.Namespace do terms end + @dialyzer {:nowarn_function, valid_term?: 1} defp valid_term?(term) do not MapSet.member?(@invalid_terms, term) end @@ -567,12 +569,14 @@ defmodule RDF.Vocabulary.Namespace do defp vocab_term?(_), do: false @doc false + @spec term_to_iri(String.t, String.t | atom) :: RDF.IRI.t def term_to_iri(base_iri, term) when is_atom(term), do: term_to_iri(base_iri, Atom.to_string(term)) def term_to_iri(base_iri, term), do: RDF.iri(base_iri <> term) @doc false + @spec vocabulary_namespace?(Datatype.t) :: boolean def vocabulary_namespace?(name) do Code.ensure_compiled?(name) && function_exported?(name, :__base_iri__, 0) end