# API
As we've said, this is an early version, which can map RDF graphs only. But even the mapping to RDF graphs is limited. You can only map to and from RDF.ex graphs directly, not to graphs in triple stores via SPARQL. Also there are no querying capabilities. So, you'll have to provide the RDF.ex graphs by yourself and you'll get back an RDF.ex graph. You can use the serializing capabilities of RDF.ex or the SPARQL client to read and write the RDF.ex graph.
The API for working with the Grax.Schema
structs and the instances of these structs is available on the top-level Grax
module and can be applied polymorphically on Grax.Schema
structs (with two exceptions).
In the following we will use the example from the last chapter to show the API in action. Here it is again. The example data:
graph =
"""
@prefix : <http://example.com/> .
@prefix schema: <https://schema.org/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
:User1
a schema:Person, :PremiumUser ;
schema:name "Jane" ;
schema:email "jane@example.com", "jane@work.com" ;
foaf:age 30 .
:Post1
schema:author :User1 ;
schema:name "Lorem" ;
schema:articleBody "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Provident, nihil, dignissimos. Nesciunt aut totam eius. Magnam quaerat modi vel sed, ipsam atque rem, eos vero ducimus beatae harum explicabo labore!" .
"""
|> RDF.Turtle.read_string!()
The example Grax.Schema
structs we've developed in the last chapter:
defmodule User do
use Grax.Schema
alias NS.{SchemaOrg, FOAF, EX}
schema SchemaOrg.Person do
property name: SchemaOrg.name, type: :string, required: true
property emails: SchemaOrg.email, type: list_of(:string), required: true
property age: FOAF.age, type: :integer
property customer_type: RDF.type,
from_rdf: :customer_type_from_rdf,
to_rdf: :customer_type_to_rdf
field :password
link friends: FOAF.friend, type: list_of(User)
link posts: -SchemaOrg.author, type: list_of(Post)
end
def customer_type_from_rdf(types, _description, _graph) do
{:ok, if(RDF.iri(EX.PremiumUser) in types, do: :premium_user)}
end
def customer_type_to_rdf(:premium_user, _user), do: {:ok, EX.PremiumUser}
def customer_type_to_rdf(_, _), do: {:ok, nil}
end
defmodule Post do
use Grax.Schema
alias NS.SchemaOrg
schema do
property title: SchemaOrg.name(), type: :string
property content: SchemaOrg.articleBody(), type: :string
link author: SchemaOrg.author(), type: User
end
end
# Loading from RDF graphs
You can load a Grax.Schema
struct from the description in an RDF graph with the Grax.load/4
function, which expects
- an
RDF.Graph
with a description of this resource, - the identifier of the resource to be loaded,
- the
Grax.Schema
struct module and - optional arguments
iex> Grax.load(graph, EX.User1, User)
{:ok,
%User{
__id__: ~I<http://example.com/User1>,
__additional_statements__: %{
~I<http://www.w3.org/1999/02/22-rdf-syntax-ns#type> => %{
~I<https://schema.org/Person> => nil
}
},
age: 30,
customer_type: :premium_user,
email: ["jane@example.com", "jane@work.com"],
friends: [],
name: "Jane",
password: nil,
posts: [
%Post{
__id__: ~I<http://example.com/Post1>,
_additional_statements__: %{},
author: ~I<http://example.com/User1>,
content: "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Provident, nihil, dignissimos. Nesciunt aut totam eius. Magnam quaerat modi vel sed, ipsam atque rem, eos vero ducimus beatae harum explicabo labore!",
title: "Lorem"
}
]
}}
The Grax.Schema
modules also provide a dedicated load/3
function on which the schema is implicit. So, the following function call is equivalent to the previous call:
graph |> User.load(EX.User1)
It is also possible to omit the schema on the general Grax.load/3
function. In this case Grax trys to automatically detect the schema based on the rdf:type
of the loaded resource. That means the most specific schema with a class declaration matching one of the rdf:type
s will be selected. So, since our User
schema is associated with the Schema.org class used in our example data, we can also load this resource with:
graph |> Grax.load(EX.User1)
There are also bang variants of the both the general Grax.load/4
and the dedicated load/3
functions on the struct modules available, which return the result directly and fail in error cases as usual. But there's another difference between these load
variants. The non-bang variant by default performs validations of the data against the schema (described further below), while the non-bang variant does not perform this validation step by default. However, you can control this validation step with the optional :validate
option flag independently. Except for the different return type, the load
variants just differ in the default value of this :validate
option.
TIP
Loading values into the structs and performing the validation later is useful when you want to confront the user with invalid data, eg. in a HTML form for manual cleaning of the data.
When the source data contains statements about the subject with a property that is not part of the Grax schema, it will be stored in the map of the __additional_statements__
field of the Grax.Schema
struct, so the description of the subject won't lose any information when serializing an updated version back. See more on accessing the additional statements below
The links of a schema will be preloaded as configured in the schema specification. As described in the last chapter, currently only the depth-preloading strategy is implemented. And unless you've configured other preloading depths on the links or the schema, the default preloading depth of one is used, which means you'll get all data and link properties of the loaded resource, but from the linked resources just the data properties. The links of the linked resource won't be loaded.
You can overwrite the preloading options from the schema on a load
call with the :depth
option.
iex> User.load(graph, EX.User1, depth: 2)
{:ok,
%User{
__id__: ~I<http://example.com/User1>,
__additional_statements__: %{
~I<http://www.w3.org/1999/02/22-rdf-syntax-ns#type> => %{
~I<https://schema.org/Person> => nil
}
},
age: 30,
customer_type: :premium_user,
email: ["jane@example.com", "jane@work.com"],
friends: [],
name: "Jane",
password: nil,
posts: [
%Post{
__id__: ~I<http://example.com/Post1>,
__additional_statements__: %{},
author: %User{
__id__: ~I<http://example.com/User1>,
__additional_statements__: %{
~I<http://www.w3.org/1999/02/22-rdf-syntax-ns#type> => %{
~I<https://schema.org/Person> => nil
}
},
age: 30,
customer_type: :premium_user,
email: ["jane@example.com", "jane@work.com"],
friends: [],
name: "Jane",
password: nil,
posts: [~I<http://example.com/Post1>]
},
content: "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Provident, nihil, dignissimos. Nesciunt aut totam eius. Magnam quaerat modi vel sed, ipsam atque rem, eos vero ducimus beatae harum explicabo labore!",
title: "Lorem"
}
]
}}
# Explicit preloading
There still can be occasions when a manual preloading is needed:
- when the defaults, either from the schema or the
:depth
keyword onload/3
weren't sufficient - when a circle occurred during preloading, aborting the preloading further down
- or you simple discover later on, that you need further data
Instead of checking for RDF.IRI
or RDF.BlankNode
values on link properties to determine if a manual preloading is needed, you can use the Grax.preloaded?/2
function with a schema and a specific property or Grax.preloaded?/1
or with just the schema. The later will return true
only when all link properties are preloaded.
iex> User.load!(graph, EX.User1, depth: 0) |> Grax.preloaded?(:posts)
false
iex> User.load!(graph, EX.User1, depth: 0) |> Grax.preloaded?()
false
You can do a manual preload with the Grax.preload/3
function.
iex> user = User.load!(graph, EX.User1)
%User{
__id__: ~I<http://example.com/User1>,
__additional_statements__: %{
~I<http://www.w3.org/1999/02/22-rdf-syntax-ns#type> => %{
~I<https://schema.org/Person> => nil
}
},
age: 30,
customer_type: :premium_user,
email: ["jane@example.com", "jane@work.com"],
friends: [],
name: "Jane",
password: nil,
posts: [
%Post{
__id__: ~I<http://example.com/Post1>,
__additional_statements__: %{},
author: ~I<http://example.com/User1>,
content: "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Provident, nihil, dignissimos. Nesciunt aut totam eius. Magnam quaerat modi vel sed, ipsam atque rem, eos vero ducimus beatae harum explicabo labore!",
title: "Lorem"
}
]
}
iex(4)> Grax.preload(user, graph, depth: 2)
{:ok,
%User{
__id__: ~I<http://example.com/User1>,
__additional_statements__: %{
~I<http://www.w3.org/1999/02/22-rdf-syntax-ns#type> => %{
~I<https://schema.org/Person> => nil
}
},
age: 30,
customer_type: :premium_user,
email: ["jane@example.com", "jane@work.com"],
friends: [],
name: "Jane",
password: nil,
posts: [
%Post{
__id__: ~I<http://example.com/Post1>,
__additional_statements__: %{},
author: %Example.User{
__id__: ~I<http://example.com/User1>,
__additional_statements__: %{
~I<http://www.w3.org/1999/02/22-rdf-syntax-ns#type> => %{
~I<https://schema.org/Person> => nil
}
},
age: 30,
customer_type: :premium_user,
email: ["jane@example.com", "jane@work.com"],
name: "Jane",
password: nil,
posts: [~I<http://example.com/Post1>],
},
content: "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Provident, nihil, dignissimos. Nesciunt aut totam eius. Magnam quaerat modi vel sed, ipsam atque rem, eos vero ducimus beatae harum explicabo labore!",
title: "Lorem"
}
]
}}
Note, that this function essentially overwrites everything accept the data properties on the root node.
# Creation from scratch
When an RDF description of a resource does not exist yet, but should be created in the application, the build
functions can be used. Similar to the load
functions there is also a bang-variant and as it requires the Grax.Schema
module as the first argument too, there are are also dedicated functions of both build
functions without this argument on the Grax.Schema
modules available. The other required argument then remains the resource identifier.
iex> User.build!(EX.User2)
%User{
__id__: ~I<http://example.com/ex>,
__additional_statements__: %{
~I<http://www.w3.org/1999/02/22-rdf-syntax-ns#type> => %{
~I<https://schema.org/Person> => nil
}
},
age: nil,
customer_type: nil,
email: [],
friends: [],
name: nil,
password: nil,
posts: []
}
You can also pass initial values as a map or keyword list to each of the build functions:
iex> user = User.build!(EX.User2,
...> name: "John",
...> email: "john@example.com",
...> password: "secret")
%User{
__id__: ~I<http://example.com/User2>,
__additional_statements__: %{
~I<http://www.w3.org/1999/02/22-rdf-syntax-ns#type> => %{
~I<https://schema.org/Person> => nil
}
},
age: nil,
customer_type: nil,
email: ["john@example.com"],
friends: [],
name: "John",
password: "secret",
posts: []
}
Again, the non-bang variant performs validations, while the bang variant doesn't.
# Schema-conformant updates
You can set specific property values of Grax.Schema
structs with the Grax.put/3
function.
iex> Grax.put(user, :age, 42)
{:ok,
%User{
__id__: ~I<http://example.com/User2>,
__additional_statements__: %{
~I<http://www.w3.org/1999/02/22-rdf-syntax-ns#type> => %{
~I<https://schema.org/Person> => nil
}
},
age: 42,
customer_type: nil,
email: ["john@example.com"],
friends: [],
name: "John",
password: "secret",
posts: []
}}
All preexisting values of a property will be overwritten.
If the value doesn't match the type of the property specified in the schema an :error
with a detailed error struct is returned.
iex> Grax.put(user, :age, "old")
{:error,
%Grax.Schema.TypeError{
message: "value \"old\" does not match type RDF.XSD.Integer",
type: RDF.XSD.Integer,
value: "old"
}}
The non-bang variant doesn't perform validations. But even without that, you should always prefer the Grax.put!/3
function over Elixir's Map.put/3
function or other methods for updating structs, since it will behave schema-aware. For example, if you put a single value on a property with multiple possible values it will be put into a list:
iex> Grax.put!(user, :email, "john@doe.com")
%User{
__id__: ~I<http://example.com/User2>,
__additional_statements__: %{
~I<http://www.w3.org/1999/02/22-rdf-syntax-ns#type> => %{
~I<https://schema.org/Person> => nil
}
},
age: 42,
customer_type: nil,
email: ["john@doe.com"],
friends: [],
name: "John",
password: "secret",
posts: []
}
Nested structs can be set by providing a Grax.Schema
struct accordingly.
iex> Grax.put!(user, :posts, Post.build!(EX.Post2, title: "Foo"))
%User{
__id__: ~I<http://example.com/User2>,
__additional_statements__: %{
~I<http://www.w3.org/1999/02/22-rdf-syntax-ns#type> => %{
~I<https://schema.org/Person> => nil
}
},
age: 42,
customer_type: nil,
email: ["john@example.com"],
friends: [],
name: "John",
password: "secret",
posts: [
%Post{
__id__: ~I<http://example.com/Post2>,
__additional_statements__: %{},
author: nil,
content: nil,
title: "Foo"
}
]
}
You can also provide the properties and the values of a nested struct as a map. This however requires that you either provide the id of the nested resource in the __id__
field or that a Grax id schema is defined for the schema of the linked resource, so that the id can be generated automatically, which will be further discussed in the next chapter.
iex> Grax.put!(user, :posts, %{__id__: EX.Post2, title: "Foo"})
%User{
__id__: ~I<http://example.com/User2>,
__additional_statements__: %{
~I<http://www.w3.org/1999/02/22-rdf-syntax-ns#type> => %{
~I<https://schema.org/Person> => nil
}
},
age: 42,
customer_type: nil,
email: ["john@example.com"],
friends: [],
name: "John",
password: "secret",
posts: [
%Post{
__id__: ~I<http://example.com/Post2>,
__additional_statements__: %{},
author: nil,
content: nil,
title: "Foo"
}
]
}
It is also possible to put just the node identifier of a linked resource as a RDF.IRI
or RDF.BlankNode
.
iex> Grax.put!(user, :posts, EX.Post2)
%User{
__id__: ~I<http://example.com/User2>,
__additional_statements__: %{
~I<http://www.w3.org/1999/02/22-rdf-syntax-ns#type> => %{
~I<https://schema.org/Person> => nil
}
},
age: 42,
customer_type: nil,
email: ["john@example.com"],
friends: [],
name: "John",
password: "secret",
posts: [~I<http://example.com/Post2>]
}
The Grax.put/2
and Grax.put!/2
functions also allow to set multiple values at once with a map or keyword list.
iex> Grax.build!(user,
...> email: ["john@doe.com" | user.email],
...> age: user.age + 1)
%User{
__id__: ~I<http://example.com/User2>,
__additional_statements__: %{
~I<http://www.w3.org/1999/02/22-rdf-syntax-ns#type> => %{
~I<https://schema.org/Person> => nil
}
},
age: 43,
customer_type: nil,
email: ["john@doe.com", "john@example.com"],
friends: [],
name: "John",
password: "secret",
posts: []
}
# Validation against the schema
A Grax.Schema
struct can be validated with the Grax.validate/1
function which either returns the given Grax.Schema
struct unchanged in an :ok
tuple. Otherwise an :error
tuple with a Grax.ValidationError
containing a collection of all failed validations.
iex> User.build!(EX.User2,
...> name: ["John", "JD"],
...> age: "old")
...> |> Grax.validate()
{:error,
%Grax.ValidationError{
errors: [
name: %Grax.Schema.TypeError{
message: "value [\"John\", \"JD\"] does not match type RDF.XSD.String",
type: RDF.XSD.String,
value: ["John", "JD"]
},
email: %Grax.Schema.CardinalityError{
message: "[] does not match cardinality {:min, 1}",
cardinality: {:min, 1},
value: []
},
age: %Grax.Schema.TypeError{
message: "value \"old\" does not match type RDF.XSD.Integer",
type: RDF.XSD.Integer,
value: "old"
}
]
}}
# Mapping to RDF graphs
With the Grax.to_rdf/1
function finally, you can map a Grax.Schema
struct to a RDF.Graph
.
iex> Grax.to_rdf(user)
{:ok,
#RDF.Graph<name: nil
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
<http://example.com/Post2>
<https://schema.org/author> <http://example.com/User2> ;
<https://schema.org/name> "Foo" .
<http://example.com/User2>
a <https://schema.org/Person> ;
<https://schema.org/email> "john@doe.com", "john@example.com" ;
<https://schema.org/name> "John" ;
<http://xmlns.com/foaf/0.1/age> 43 .
>}
TIP
The options given to Grax.to_rdf/2
as the optional second argument are passed-through as options to the RDF.Graph.new/2
call used for the creation of the graph. This allows you to set the name of the graph, define some prefixes, a base URI etc.
# Additional statements
When we're loading data which contains statements about the subject with a property that is not part of the Grax schema, it will be stored in the map of the __additional_statements__
field of the Grax.Schema
struct.
The Grax.to_rdf/1
function will add these statements to the result.
This way no statements won't be lost when processing RDF descriptions with Grax.
Let's say the example RDF description of our user would contain an additional statement:
@prefix : <http://example.com/> .
@prefix schema: <https://schema.org/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
:User1
a schema:Person, :PremiumUser ;
schema:name "Jane" ;
schema:email "jane@example.com", "jane@work.com" ;
foaf:age 30 ;
rdfs:comment "a comment about our example user resource" .
Since we don't have specified a field for this property, the statement would be stored in the __additional_statements__
map.
iex> user = User.load!(graph, EX.User1)
%User{
__id__: ~I<http://example.com/User1>,
__additional_statements__: %{
~I<http://www.w3.org/1999/02/22-rdf-syntax-ns#type> => %{
~I<https://schema.org/Person> => nil
},
~I<http://www.w3.org/2000/01/rdf-schema#comment> => %{
~L"a comment about our example user resource" => nil
}
},
age: 30,
customer_type: :premium_user,
email: ["jane@example.com", "jane@work.com"],
friends: [],
name: "Jane",
password: nil,
posts: [
%Post{
__id__: ~I<http://example.com/Post1>,
__additional_statements__: %{},
author: ~I<http://example.com/User1>,
content: "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Provident, nihil, dignissimos. Nesciunt aut totam eius. Magnam quaerat modi vel sed, ipsam atque rem, eos vero ducimus beatae harum explicabo labore!",
title: "Lorem"
}
]
}
Due to the class declaration we have on our User
schema, a respective rdf:type
statement is also included.
Since all of the properties of importance for your application usually are defined on a Grax.Schema
, you usually don't care for the contents of this map.
However, if you want to access the additional statements, you can do so with the Grax.additional_statements/1
, Grax.add_additional_statements/2
, Grax.put_additional_statements/2
, Grax.delete_additional_statements/2
, Grax.delete_additional_predicates/2
and Grax.clear_additional_statements/1
functions.
iex> Grax.add_additional_statements(user, %{RDFS.comment() => "another comment"})
...> |> Grax.additional_statements()
#RDF.Description<subject: ~I<http://example.com/User1>
<http://example.com/User1>
a <https://schema.org/Person> ;
rdfs:comment "a comment about our example user resource", "another comment" .
>
The Grax.delete_additional_statements/2
functions deletes only the explicitly given predicate-object pairs. If you want to delete all statements with a given predicate, you can use Grax.delete_additional_predicates/2
.
Grax.delete_additional_predicates(user, [RDFS.comment()])
# Mapping between schemas
Sometimes you have multiple schemas for the same kind of resource, e.g. different domain models for the same entity in different parts of your app. The from/1
function on every Grax schema module allows you to load a Grax schema struct from another kind of Grax schema struct. The property values are then fetched from the property fields for the respective IRIs (and a potionally different name) or the additional statements.
Let's say, for example, we have this additional Grax schema for customers besides the User
model in our app:
defmodule Customer do
use Grax.Schema
alias NS.SchemaOrg
schema SchemaOrg.Person do
property full_name: SchemaOrg.name, type: :string, required: true
property email: SchemaOrg.email, type: :string, required: true
property address: SchemaOrg.address, type: :string, required: true
end
end
With a user
like this:
user =
"""
@prefix : <http://example.com/> .
@prefix schema: <https://schema.org/> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
:User1
a schema:Person, :PremiumUser ;
schema:name "John Doe" ;
schema:email "john@example.com" ;
schema:address "711-2880 Nulla St.\\nMankato Mississippi 96522" ;
foaf:age 30 .
"""
|> RDF.Turtle.read_string!()
|> User.load!(EX.User1)
we can could get a Customer
struct like this:
iex> Customer.from(user)
{:ok,
%Customer{
__additional_statements__: %{
~I<http://www.w3.org/1999/02/22-rdf-syntax-ns#type> => %{
~I<http://example.com/PremiumUser> => nil,
~I<https://schema.org/Person> => nil
},
~I<http://xmlns.com/foaf/0.1/age> => %{RDF.XSD.Integer.new(30) => nil}
},
__id__: ~I<http://example.com/User1>,
address: "711-2880 Nulla St.\nMankato Mississippi 96522",
email: "john@example.com",
full_name: "John Doe"
}}
Internally, this is done with a RDF mapping roundtrip via to_rdf/1
and load/2
, which means you can use custom mappings for your custom mapping logic.