config | ||
lib | ||
priv/vocabs | ||
src | ||
test | ||
.editorconfig | ||
.gitignore | ||
CHANGELOG.md | ||
CODE_OF_CONDUCT.md | ||
CONTRIBUTING.md | ||
LICENSE.md | ||
mix.exs | ||
mix.lock | ||
README.md |
RDF.ex
An implementation of the RDF data model in Elixir.
Features
- aims to be fully compatible with the RDF 1.1 specification; any incompatibility is considered a bug
- pure Elixir implementation
- no dependencies
- in-memory data structures for RDF descriptions, RDF graphs and RDF datasets
- support for RDF vocabularies via Elixir modules for safe, i.e. compile-time checked and concise usage of the URIs of vocabularies, resembling QNames
- XML schema datatypes for RDF literals (not yet all supported)
- sigils for the most common types of nodes, i.e. URIs, literals and blank nodes
- a description DSL resembling Turtle in Elixir
- foundation for RDF serialization readers and writers
- implementations for the N-Triples and N-Quads serialization formats
- other formats will be available as dedicated separate Hex packages
- currently only JSON-LD is available via the JSON-LD.ex package
Installation
RDF.ex can be installed as a Hex package as usual:
- Add
rdf
to your list of dependencies inmix.exs
:
def deps do
[{:rdf, "~> 0.1.0"}]
end
- Ensure
rdf
is started before your application:
def application do
[applications: [:rdf]]
end
Usage
The RDF standard defines a graph data model for distributed information on the web. A RDF graph is a set of statements aka RDF triples consisting of three nodes:
- a subject node with an URI or a blank node,
- a predicate node with the URI of a RDF property,
- an object node with an URI, a blank node or a RDF literal value.
Let's see how the different types of nodes are represented with RDF.ex in Elixir.
URIs
Although the RDF standards speaks of IRIs, an internationalized generalization of URIs, RDF.ex currently supports only URIs. They are represented with Elixirs builtin URI
struct. It's a pragmatic, temporary decision, which will likely be subject to changes, in favour of a more dedicated representation of IRIs specialised for its usage within RDF data. See this issue for progress on this matter.
The RDF
module defines a handy constructor function RDF.uri/1
:
RDF.uri("http://www.example.com/foo")
Besides being a little shorter than URI.parse
and better import
able, it will provide a gentlier migration to the mentioned, more optimized URI-representation in RDF.ex.
An URI can also be created with the ~I
sigil:
~I<http://www.example.com/foo>
But there's an even shorter notation for URI literals.
Vocabularies
RDF.ex supports modules which represent RDF vocabularies as RDF.Vocabulary.Namespace
s. It comes with predefined modules for some fundamental vocabularies defined in the RDF.NS
module.
Furthermore, the rdf_vocab package
contains predefined RDF.Vocabulary.Namespace
s for the most popular vocabularies.
These RDF.Vocabulary.Namespace
s (a special case of a RDF.Namespace
) allow for something similar to QNames in XML: an atom or function qualified with a RDF.Vocabulary.Namespace
can be resolved to an URI.
There are two types of terms in a RDF.Vocabulary.Namespace
which are
resolved differently:
- Capitalized terms are by standard Elixir semantics module names, i.e.
atoms. At all places in RDF.ex where an URI is expected, you can use atoms
qualified with a
RDF.Namespace
instead. If you want to resolve them manually, you can pass aRDF.Namespace
qualified atom toRDF.uri
. - Lowercased terms for RDF properties are represented as functions on a
RDF.Vocabulary.Namespace
module and return the URI directly, but sinceRDF.uri
can also handle URIs directly, you can safely and consistently use it with lowercased terms too.
iex> import RDF, only: [uri: 1]
iex> alias RDF.NS.{RDFS}
iex> RDFS.Class
RDF.NS.RDFS.Class
iex> uri(RDFS.Class)
%URI{authority: "www.w3.org", fragment: "Class", host: "www.w3.org",
path: "/2000/01/rdf-schema", port: 80, query: nil, scheme: "http",
userinfo: nil}
iex> RDFS.subClassOf
%URI{authority: "www.w3.org", fragment: "subClassOf", host: "www.w3.org",
path: "/2000/01/rdf-schema", port: 80, query: nil, scheme: "http",
userinfo: nil}
iex> uri(RDFS.subClassOf)
%URI{authority: "www.w3.org", fragment: "subClassOf", host: "www.w3.org",
path: "/2000/01/rdf-schema", port: 80, query: nil, scheme: "http",
userinfo: nil}
As this example shows, the namespace modules can be easily alias
ed. When required, they can be also aliased to a completely different name. Since the RDF
vocabulary namespace in RDF.NS.RDF
can't be aliased (it would clash with the top-level RDF
module), all of its elements can be accessed directly from the RDF
module (without an alias).
iex> import RDF, only: [uri: 1]
iex> RDF.type
%URI{authority: "www.w3.org", fragment: "type", host: "www.w3.org",
path: "/1999/02/22-rdf-syntax-ns", port: 80, query: nil, scheme: "http",
userinfo: nil}
iex> uri(RDF.Property)
%URI{authority: "www.w3.org", fragment: "Property", host: "www.w3.org",
path: "/1999/02/22-rdf-syntax-ns", port: 80, query: nil, scheme: "http",
userinfo: nil}
This way of expressing URIs has the additional benefit, that the existence of the referenced URI is checked at compile time, i.e. whenever a term is used that is not part of the resp. vocabulary an error is raised by the Elixir compiler (unless the vocabulary namespace is non-strict; see below).
For terms not adhering to the capitalization rules (lowercase properties, capitalized non-properties) or containing characters not allowed within atoms, these namespace define aliases accordingly. If unsure, you can have a look at the documentation or the vocabulary namespace definition.
Description DSL
The functions for the properties on a vocabulary namespace module, are also available in a description builder variant, which accepts subject and objects as arguments.
RDF.type(EX.Foo, EX.Bar)
If you want to state multiple statements with the same subject and predicate, you can either pass the objects as a list or as additional arguments, if there are not more than five of them:
RDF.type(EX.Foo, EX.Bar, EX.Baz)
EX.foo(EX.Bar, [1, 2, 3, 4, 5, 6])
In combination with Elixirs pipe operators this leads to a description DSL resembling Turtle:
EX.Foo
|> RDF.type(EX.Bar)
|> EX.baz(1, 2, 3)
The produced statements are returned by this function as a RDF.Description
structure which will be described below.
Defining vocabulary namespaces
There are two basic ways to define a namespace for a vocabulary:
- You can define all terms manually.
- You can extract the terms from existing RDF data for URIs of resources under the specified base URI.
It's recommended to introduce a dedicated module for the defined namespaces. In this module you'll use RDF.Vocabulary.Namespace
and define your vocabulary namespaces with the defvocab
macro.
A vocabulary namespace with manually defined terms can be defined in this way like that:
defmodule YourApp.NS do
use RDF.Vocabulary.Namespace
defvocab EX,
base_uri: "http://www.example.com/ns/",
terms: ~w[Foo bar]
end
The base_uri
argument with the URI prefix of all the terms in the defined
vocabulary is required and expects a valid URI ending with either a "/"
or
a "#"
. Terms will be checked for invalid characters at compile-time and will raise a compiler error. This handling of invalid characters can be modified with the invalid_characters
options, which is set to :fail
by default. By setting it to :warn
only warnings will be raised or it can be turned off completely with :ignore
.
A vocabulary namespace with extracted terms can be by defined either providing RDF data directly with the data
option or from serialized RDF data file in the priv/vocabs
directory:
defmodule YourApp.NS do
use RDF.Vocabulary.Namespace
defvocab EX,
base_uri: "http://www.example.com/ns/",
file: "your_vocabulary.nt"
end
Currently only NTriple and NQuad files are supported at this place.
During compilation the terms will be validated and checked for proper capitalisation by analysing the schema description of the resp. resource in the given data.
This validation behaviour can be modified with the case_violations
options, which is by default set to :warn
. By setting it explicitly to :fail
errors will be raised during compilation or it can be turned off with :ignore
.
Invalid characters or violations of capitalization rules can be fixed by defining aliases for these terms with the alias
option and a keyword list:
defmodule YourApp.NS do
use RDF.Vocabulary.Namespace
defvocab EX,
base_uri: "http://www.example.com/ns/",
file: "your_vocabulary.nt"
alias: [example_term: "example-term"]
end
The :ignore
option allows to ignore terms:
defmodule YourApp.NS do
use RDF.Vocabulary.Namespace
defvocab EX,
base_uri: "http://www.example.com/ns/",
file: "your_vocabulary.nt",
ignore: ~w[Foo bar]
end
Though strictly discouraged, a vocabulary namespace can be defined as non-strict with the strict
option set to false
. A non-strict vocabulary doesn't require any terms to be defined (although they can). A term is resolved dynamically at runtime by concatenation of the term and the base URI of the resp. namespace module:
defmodule YourApp.NS do
use RDF.Vocabulary.Namespace
defvocab EX,
base_uri: "http://www.example.com/ns/",
terms: [],
strict: false
end
iex> import RDF, only: [uri: 1]
iex> alias YourApp.NS.{EX}
iex> uri(EX.Foo)
%URI{authority: "www.example.com", fragment: nil, host: "www.example.com",
path: "/ns/Foo", port: 80, query: nil, scheme: "http", userinfo: nil}
iex> EX.bar
%URI{authority: "www.example.com", fragment: nil, host: "www.example.com",
path: "/ns/bar", port: 80, query: nil, scheme: "http", userinfo: nil}
iex> EX.Foo |> EX.bar(EX.Baz)
#RDF.Description{subject: ~I<http://www.example.com/ns/Foo>
~I<http://www.example.com/ns/bar>
~I<http://www.example.com/ns/Baz>}
Blank nodes
Blank nodes are nodes of an RDF graph without an URI. They are always local to that graph and mostly used as helper nodes.
They can be created with RDF.BlankNode.new
or its alias function RDF.bnode
. You can either pass an atom, string, integer or Erlang reference with a custom local identifier or call it without any arguments, which will create a local identifier automatically.
RDF.bnode(:foo)
RDF.bnode(42)
RDF.bnode
You can also use the ~B
sigil to create a blank node with a custom name:
import RDF.Sigils
~B<foo>
Literals
Literals are used for values such as strings, numbers, and dates. They can be untyped, languaged-tagged or typed. In general they are created with the RDF.Literal.new
constructor function or its alias function RDF.literal
:
RDF.Literal.new("foo")
RDF.literal("foo")
The actual value can be accessed via the value
struct field:
RDF.literal("foo").value
An untyped literal can also be created with the ~L
sigil:
import RDF.Sigils
~L"foo"
A language-tagged literal can be created by providing the language
option with a BCP47-conform language or by adding the language as a modifier to the ~L
sigil:
RDF.literal("foo", language: "en")
import RDF.Sigils
~L"foo"en
Note: Only languages without subtags are supported as modifiers of the ~L
sigil, i.e. if you want to use en-US
as a language tag, you would have to use RDF.literal
or RDF.Literal.new
.
A typed literal can be created by providing the datatype
option with an URI of a datatype. Most of the time this will be an XML schema datatype:
RDF.literal("42", datatype: XSD.integer)
It is also possible to create a typed literal by using a native Elixir non-string value, for which the following datatype mapping will be applied:
Elixir datatype | XSD datatype |
---|---|
boolean |
xsd:boolean |
integer |
xsd:integer |
float |
xsd:double |
Time |
xsd:time |
Date |
xsd:date |
DateTime |
xsd:dateTime |
NaiveDateTime |
xsd:dateTime |
So the former example literal can be created equivalently like this:
RDF.literal(42)
For all of these known datatypes the value
struct field contains the native Elixir value representation according to this mapping. When a known XSD datatype is specified, the given value will be converted automatically if needed and possible.
iex> RDF.literal(42, datatype: XSD.double).value
42.0
For all of these supported XSD datatypes there're RDF.Datatype
s available that allow the creation of RDF.Literal
s with the respective datatype:
iex> RDF.Double.new("0042").value
42.0
iex> RDF.Double.new(42).value
42.0
The RDF.Literal.valid?/1
function checks if a given literal is valid according to the XML schema datatype specification.
iex> RDF.Literal.valid? RDF.Integer.new("42")
true
iex> RDF.Literal.valid? RDF.Integer.new("foo")
false
A RDF literal is bound to the lexical form of the initially given value. This lexical representation can be retrieved with the RDF.Literal.lexical/1
function:
iex> RDF.Literal.lexical RDF.Integer.new("0042")
"0042"
iex> RDF.Literal.lexical RDF.Integer.new(42)
"42"
Although two literals might have the same value, they are not equal if they don't have the same lexical form:
iex> RDF.Integer.new("0042").value == RDF.Integer.new("42").value
true
iex> RDF.Integer.new("0042") == RDF.Integer.new("42")
false
The RDF.Literal.canonical/1
function returns the given literal with its canonical lexical form according its datatype:
iex> RDF.Integer.new("0042") |> RDF.Literal.canonical |> RDF.Literal.lexical
"42"
iex> RDF.Literal.canonical(RDF.Integer.new("0042")) ==
RDF.Literal.canonical(RDF.Integer.new("42"))
true
Note: Although you can create any XSD datatype by using the resp. URI with the datatype
option of RDF.Literal.new
, not all of them support the validation and conversion behaviour of RDF.Literal
s and the value
field simply contains the initially given value unvalidated and unconverted.
Statements
RDF statements are generally represented in RDF.ex as native Elixir tuples, either as 3-element tuples for triples or a 4-element tuples for quads.
The RDF.Triple
and RDF.Quad
modules both provide a function new
for such tuples, which convert the elements to proper nodes when possible or raise an error when such a conversion is not possible. In particular these functions also resolve qualified terms from a vocabulary namespace. They can also be called with the delegator functions RDF.triple
and RDF.quad
.
iex> RDF.triple(EX.S, EX.p, 1)
{~I<http://example.com/S>, ~I<http://example.com/p>, RDF.Integer.new(1)}
iex> RDF.triple {EX.S, EX.p, 1}
{~I<http://example.com/S>, ~I<http://example.com/p>, RDF.Integer.new(1)}
iex> RDF.quad(EX.S, EX.p, 1, EX.Graph)
{~I<http://example.com/S>, ~I<http://example.com/p>, RDF.Integer.new(1),
~I<http://example.com/Graph>}
iex> RDF.triple {EX.S, 1, EX.O}
** (RDF.Triple.InvalidPredicateError) '1' is not a valid predicate of a RDF.Triple
(rdf) lib/rdf/statement.ex:53: RDF.Statement.convert_predicate/1
(rdf) lib/rdf/triple.ex:26: RDF.Triple.new/3
If you want to explicitly create a Quad in the default graph context, you can use nil
as the graph name. The nil
value is used consistently as the name of the default graph within RDF.ex.
iex> RDF.quad(EX.S, EX.p, 1, nil)
{~I<http://example.com/S>, ~I<http://example.com/p>, RDF.Integer.new(1), nil}
RDF data structures
RDF.ex provides various data structures for collections of statements:
RDF.Description
: a collection of triples about the same subjectRDF.Graph
: a named collection of statementsRDF.Dataset
: a named collection of graphs, i.e. a collection of statements from different graphs; it may have multiple named graphs and at most one unnamed ("default") graph
All of these structures have similar sets of functions and implement Elixirs Enumerable
protocol, Elixirs Access
behaviour and the RDF.Data
protocol of RDF.ex.
The new
function of these data structures create new instances of the struct and optionally initialize them with initial statements. RDF.Description.new
requires at least an URI or blank node for the subject, while RDF.Graph.new
and RDF.Dataset.new
take an optional URI for the name of the graph or dataset.
empty_description = RDF.Description.new(EX.Subject)
empty_unnamed_graph = RDF.Graph.new
empty_named_graph = RDF.Graph.new(EX.Graph)
empty_unnamed_dataset = RDF.Dataset.new
empty_named_dataset = RDF.Dataset.new(EX.Dataset)
As you can see, qualified terms from a vocabulary namespace can be given instead of an URI and will be resolved automatically. This applies to all of the functions discussed below.
The new
functions can be called more shortly with the resp. delegator functions RDF.description
, RDF.graph
and RDF.dataset
.
The new
functions also take optional initial data, which can be provided in various forms. Basically it takes the given data and hands it to the add
function with the newly created struct.
Adding statements
So let's look at these various forms of data the add
function can handle.
Firstly, they can handle single statements:
description |> RDF.Description.add {EX.S, EX.p, EX.O}
graph |> RDF.Graph.add {EX.S, EX.p, EX.O}
dataset |> RDF.Dataset.add {EX.S, EX.p, EX.O, EX.Graph}
When the subject of a statement doesn't match the subject of the description, RDF.Description.add
ignores it and is a no-op.
RDF.Description.add
also accepts a property-value pair as a tuple.
RDF.Description.new(EX.S, {EX.p, EX.O1})
|> RDF.Description.add {EX.p, EX.O2}
In general, the object position of a statement can be a list of values, which will be interpreted as multiple statements with the same subject and predicate. So the former could be written more shortly:
RDF.Description.new(EX.S, {EX.p, [EX.O1, EX.O2]})
Multiple statements with different subject and/or predicate can be given as a list of statements, where everything said before on single statements applies to the individual statements of these lists:
description |> RDF.Description.add [{EX.p1, EX.O}, {EX.p2, [EX.O1, EX.O2]}
graph |> RDF.Graph.add [{EX.S1, EX.p1, EX.o1}, {EX.S2, EX.p2, EX.o2}]
dataset |> RDF.Dataset.add [{EX.S, EX.p, EX.o}, {EX.S, EX.p, EX.o, EX.Graph}
A RDF.Description
can be added to any of the three data structures:
input = RDF.Description.new(EX.S, {EX.p, EX.O1})
description |> RDF.Description.add input
graph |> RDF.Graph.add input
dataset |> RDF.Dataset.add input
Note that, unlike mismatches in the subjects of directly given statements, RDF.Description.add
ignores the subject of a given RDF.Description
and just adds the property-value pairs of the given description, because this is a common use case when merging the descriptions of differently named resources (eg. because they are linked via owl:sameAs
).
RDF.Graph.add
and RDF.Dataset.add
can also add other graphs and RDF.Dataset.add
can add the contents of another dataset.
RDF.Dataset.add
is also special, in that it allows to overwrite the explicit or implicit graph context of the input data and redirect the input into another graph. For example, the following examples all add the given statements to the EX.Other
graph:
RDF.Dataset.new
|> RDF.Dataset.add({EX.S, EX.p, EX.O}, EX.Other)
|> RDF.Dataset.add[{EX.S, EX.p, EX.O1, nil}, {EX.S, EX.p, EX.O2, EX.Graph}], EX.Other)
|> RDF.Dataset.add(RDF.Graph.new(EX.Graph, {EX.S, EX.p, EX.O3}), EX.Other)
Unlike the add
function, which always returns the same data structure as the data structure to which the addition happens, which possible means ignoring some input statements (eg. when the subject of a statement doesn't match the description subject) or reinterpreting some parts of the input statement (eg. ignoring the subject of another description), the merge
function of the RDF.Data
protocol implemented by all three data structures will always add all of the input and possibly creates another type of data structure. For example, merging two RDF.Description
s with different subjects results in a RDF.Graph
. Or adding a quad to a RDF.Graph
with a different name than the quad’s graph context results in a RDF.Dataset
.
RDF.Description.new(EX.S1, {EX.p, EX.O})
|> RDF.Data.merge(RDF.Description.new(EX.S2, {EX.p, EX.O})) # returns an unnamed RDF.Graph
|> RDF.Data.merge(RDF.Graph.new(EX.Graph, {EX.S2, EX.p, EX.O2})) # returns a RDF.Dataset
Statements added with put
overwrite all existing statements with the same subject and predicate.
iex> RDF.Graph.new({EX.S1, EX.p, EX.O1}) |> RDF.Graph.put({EX.S1, EX.p, EX.O2})
#RDF.Graph{name: nil
~I<http://example.com/S1>
~I<http://example.com/p>
~I<http://example.com/O2>}
It is available on all three data structures and can handle all of the input data types as their add
counterpart.
Accessing the content of RDF data structures
All three RDF data structures implement the Enumerable
protocol over the set of contained statements. As a set of triples in the case of RDF.Description
and RDF.Graph
and as a set of quads in case of RDF.Dataset
. This means you can use all Enum
functions over the contained statements as tuples.
RDF.Description.new(EX.S1, {EX.p, [EX.O1, EX.O2]})
|> Enum.each(&IO.inspect/1)
The RDF.Data
protocol offers various functions to access the contents of RDF data structures:
RDF.Data.subjects/1
returns the set of all subject resources.RDF.Data.predicates/1
returns the set of all used properties.RDF.Data.objects/1
returns the set of all resources on the object position of statements. Note: Literals not included.RDF.Data.resources/1
returns the set of all used resources at any position in the contained RDF statements.RDF.Data.description/2
returns all statements from a data structure about the given resource as aRDF.Description
. It will be empty if no such statements exist. On aRDF.Dataset
it will aggregate the statements about the resource from all graphs.RDF.Data.statements/1
returns a list of all contained RDF statements.
The get
functions return individual elements of a RDF data structure:
RDF.Description.get
returns the list of all object values for a given property.RDF.Graph.get
returns theRDF.Description
for a given subject resource.RDF.Dataset.get
returns theRDF.Graph
with the given graph name.
All of these get
functions return nil
or the optionally given default value, when the given element can not be found.
iex> RDF.Description.new(EX.S1, {EX.p, [EX.O1, EX.O2]})
...> |> RDF.Description.get(EX.p)
[~I<http://example.com/O1>, ~I<http://example.com/O2>]
iex> RDF.Graph.new({EX.S1, EX.p, [EX.O1, EX.O2]})
...> |> RDF.Graph.get(EX.p2, :not_found)
:not_found
Since all three RDF data structures implement the Access
behaviour, you can also use data[key]
syntax, which basically just calls the resp. get
function.
iex> description[EX.p]
[~I<http://example.com/O1>, ~I<http://example.com/O2>]
iex> graph[EX.p2]
nil
Also, the familiar fetch
function of the Access
behaviour, as a variant of get
which returns ok
tuples, is available on all RDF data structures.
iex> RDF.Description.new(EX.S1, {EX.p, [EX.O1, EX.O2]})
...> |> RDF.Description.fetch(EX.p)
{:ok, [~I<http://example.com/O1>, ~I<http://example.com/O2>]}
iex> RDF.Graph.new({EX.S1, EX.p, [EX.O1, EX.O2]})
...> |> RDF.Graph.fetch(EX.p2)
:error
RDF.Dataset
also provides the following functions to access individual graphs:
RDF.Dataset.graphs
returns the list of all the graphs of the datasetRDF.Dataset.default_graph
returns the default graph of the datasetRDF.Dataset.graph
returns the graph of the dataset with the given name
Deleting statements
Statements can be deleted in two slightly different ways. One way is to use the delete
function of the resp. data structure. It accepts all the supported ways for specifying collections of statements supported by the resp. add
counterparts and removes the found triples.
iex> RDF.Description.new(EX.S1, {EX.p, [EX.O1, EX.O2]})
...> |> RDF.Description.delete({EX.S1, EX.p, EX.O1})
#RDF.Description{subject: ~I<http://example.com/S1>
~I<http://example.com/p>
~I<http://example.com/O2>}
Another way to delete statements is the delete
function of the RDF.Data
protocol. The only difference to delete
functions on the data structures directly is how it handles the deletion of a RDF.Description
from another RDF.Description
or RDF.Graph
from another RDF.Graph
. While the dedicated RDF data structure function ignores the description subject or graph name and removes the statements even when they don't match, RDF.Data.delete
only deletes when the description’s subject resp. graph name matches.
iex> RDF.Description.new(EX.S1, {EX.p, [EX.O1, EX.O2]})
...> |> RDF.Description.delete(RDF.Description.new(EX.S2, {EX.p, EX.O1}))
#RDF.Description{subject: ~I<http://example.com/S1>
~I<http://example.com/p>
~I<http://example.com/O2>}
iex> RDF.Description.new(EX.S1, {EX.p, [EX.O1, EX.O2]})
...> |> RDF.Data.delete(RDF.Description.new(EX.S2, {EX.p, EX.O1}))
#RDF.Description{subject: ~I<http://example.com/S1>
~I<http://example.com/p>
~I<http://example.com/O1>
~I<http://example.com/O2>}
Beyond that, there is
RDF.Description.delete_predicates
which deletes all statements with the given property from aRDF.Description
,RDF.Graph.delete_subjects
which deletes all statements with the given subject resource from aRDF.Graph
,RDF.Dataset.delete_graph
which deletes all graphs with the given graph name from aRDF.Dataset
andRDF.Dataset.delete_default_graph
which deletes the default graph of aRDF.Dataset
.
Serializations
RDF graphs and datasets can be read and written to files or strings in a RDF serialization format using the read_file
, read_string
and write_file
, write_string
functions of the resp. RDF.Serialization
module.
{:ok, graph} = RDF.NTriples.read_file("/path/to/some_file.nt")
{:ok, nquad_string} = RDF.NQuads.write_string(graph)
All of the read and write functions are also available in bang variants which will fail in error cases.
The RDF.ex package only comes with implementations of the N-Triples and N-Quads serialization formats. Other formats are and should be implemented in separate Hex packages. Currently only JSON-LD is available with the JSON-LD.ex package.
Getting help
Development
There's still much to do for a complete RDF ecosystem for Elixir, which means there are plenty of opportunities for you to contribute. Here are some suggestions:
- more serialization formats
- missing XSD datatypes
- more sophisticated query capabilities and full SPARQL support (in the style of Ecto queries)
RDF.Repo
abstraction for RDF triple stores (in the style of Ecto Repos)- improve documentation
Contributing
see CONTRIBUTING for details.
License and Copyright
(c) 2017 Marcel Otto. MIT Licensed, see LICENSE for details.