# Ids
In the last chapter about the API we saw that we'll have to provide a URI (or blank node) identifier when building a new resource with Grax.build/2
. Minting an identifier for a new resource is a non-trivial step as these identifiers must be unique and (in most cases) persistent, i.e. should not change over the course of the lifetime of the resource. So, it would be good if the identifier creation logic wouldn't be scattered around the code base wherever we are building new resources. For this purpose, we can define the identifier creation logic in one central place with Grax id specs.
A Grax id spec is a module which uses Grax.Id.Spec
macros to the define the overall identifier namespace structure and the specific rules for the creation of identifiers for the Grax schemas of our application.
This chapter explains the definition and use of those Grax id specs and enable you to implement common identifier patterns (opens new window).
# Namespaces
A Grax id spec module first has to use Grax.Id.Spec
to make the necessary macros available. With that, you can start to lay out the structure of the namespace the identifiers with the namespace/1
macro. It takes a string with a fragment of a URI and a do
block which consists of nested namespace
calls or definitions of id schemas, which we'll discuss later. The outermost namespace
call must be an absolute URI and can be given also a namespace vocabulary term instead of a string, while the nested namespaces just define a fragment string which will be concatenated to the parent namespace.
defmodule Example.IdSpec do
use Grax.Id.Spec
namespace "http://example.com/" do
namespace "foo/" do
end
namespace "bar#" do
end
end
end
This example defines three namespaces in which we can put our id schemas:
http://example.com/
http://example.com/foo/
http://example.com/bar#
You can also define an optional prefix for a namespace with the prefix
keyword argument on a namespace
call.
defmodule Example.IdSpec do
use Grax.Id.Spec
namespace "http://example.com/" do
namespace "foo/", prefix: :foo do
end
namespace "bar#", prefix: :bar do
end
end
end
The Grax id spec module provides a prefix_map/0
function which returns a RDF.PrefixMap
you can pass to the RDF serialization functions (see this section in the RDF.ex guide for more on this).
iex> Example.IdSpec.prefix_map()
%RDF.PrefixMap{
map: %{
bar: ~I<http://example.com/bar#>,
foo: ~I<http://example.com/foo/>
}
}
Unless you provide other prefixes on Grax.to_rdf/1
it will use this prefix map automatically to add them to the created graph, so that they in turn will be used on serialization.
The URI of one single namespace can be defined as the base URI by using the base
macro instead of the namespace
.
defmodule Example.IdSpec do
use Grax.Id.Spec
namespace "http://example.com/" do
base "foo/", prefix: :foo do
end
namespace "bar#", prefix: :bar do
end
end
end
The base_iri/0
function of the Grax id spec module will return the RDF.IRI
of this namespace.
iex> Example.IdSpec.base_iri()
~I<http://example.com/foo/>
As we will see later, namespaces can also act as containers for shared arguments of its id schemas, but let's introduce id schemas properly first.
# Id schemas
An id schema for a Grax schema can be defined in the most generic way with the id_schema/2
macro inside a namespace
block. It expects an URI template according to RFC 6570 (opens new window) as the first argument and something to specify on which Grax schemas this id schema should be applied, most of the time by specifying the schema (or multiple schemas as a list) directly with the schema
keyword argument. You can use the properties of the Grax schema as parameters in the URI template.
Let's say we have a Book
Grax schema defined with an :isbn
property we'd like to use as the basis for its ids. We can define an id schema for the Book
schema like this:
defmodule Example.IdSpec do
use Grax.Id.Spec
namespace "http://example.com/" do
id_schema "books/{isbn}", schema: Book
end
end
As we've said, most of the time the Grax schema on which the id schema should be applied is given directly. For this form, the id
macro provides a more succinct but semantically equivalent form. So, the following is an equivalent definition of the same id schema:
defmodule Example.IdSpec do
use Grax.Id.Spec
namespace "http://example.com/" do
id Book, "books/{isbn}"
end
end
For cases, where the template consists solely of a template parameter with a property from the schema, an even shorter form is supported by the id
macro, where the property is written in the typical dot syntax after the schema. In our book example however, we don't have this simple template form, but we can get it simple into that form, by introducing a separate sub namespace, which might have been a good idea in the first place, since it allows us to define a prefix for this namespace.
defmodule Example.IdSpec do
use Grax.Id.Spec
namespace "http://example.com/" do
namespace "books/", prefix: :book do
id Book.isbn
end
end
end
Again, this id schema will produce the same ids as the previous forms.
Before we look at more techniques to define id schemas, let's look at how we can use id schemas.
# Using id schemas
With the id schema we've created in the previous section a new book can now be build without having to provide an id:
iex> Book.build!(
...> title: "Exploring Graphs with Elixir",
...> isbn: "1680508407")
%Book{
__id__: ~I<http://example.com/books/1680508407>,
title: "Exploring Graphs with Elixir",
isbn: "1680508407"
}
This also makes it particularly easy to build nested graph structures, where we can provide linked resources just as a map (as long as everything is included to produce the ids). Let's suppose we also have an :author
link property in our Book
schema which links to an Author
schema with an id schema, which for the sake of simplicity is based on the name (although this is not a good idea in terms of uniqueness). This allows us to build a our book like this:
iex> Book.build!(
...> title: "Exploring Graphs with Elixir",
...> isbn: "1680508407")
...> author: %{first_name: "Tony", last_name: "Hammond"})
%Book{
__id__: ~I<http://example.com/books/1680508407>,
title: "Exploring Graphs with Elixir",
isbn: "1680508407",
author: [%Author{
__id__: ~I<http://example.com/authors/Tony_Hammond>,
first_name: "Tony",
last_name: "Hammond"
}]
}
# Connecting id schemas with Grax schemas
But how are Grax schemas and Grax id specs actually connected? How does Grax know in which id spec to search for an id schema?
The most direct option is to specify the Grax.Id.Spec
module on a Grax schema with the id_spec
keyword argument.
defmodule Book do
use Grax.Schema, id_spec: Example.IdSpec
alias NS.SchemaOrg
schema SchemaOrg.Book do
property :title, SchemaOrg.name, type: :string, required: true
property :isbn, SchemaOrg.isbn, type: :string, required: true
link :author, SchemaOrg.author, type: list_of(Author)
end
end
However, this won't be needed most of the time, since an application will usually have just one id spec for all Grax schemas. This id spec module can be configured with the :grax_id_spec
key under the configuration of your application. So, for an application with the name :my_app
this configuration would look like this:
import Config
config :my_app,
grax_id_spec: Example.IdSpec
For all Grax schemas defined in the :my_app
application the id spec module specified like this will be search for an id schema (unless another id spec module is specified directly with id_spec
keyword directly in the Grax schema).
For an id schema which directly specifies the Grax schema(s) on which it should be applied, finding the id spec is everything that's needed. The id schema with the matching Grax schema must be simply looked up. On a build
call the Grax schema is directly available (either the one passed to Grax.build/2
or the module on which we call the build/1
function). When the id generation is used to build nested Grax schemas the schema specified as the type of the link property is used for the lookup. For polymorphic properties however the schema is not determined, therefore the automatic building of nested Grax schemas is not supported on polymorphic links and you'll have to build and create the identifier for these manually.
# Custom schema selectors
Sometimes however we want to define an id schema for whole groups of classes. For example, think of Wikidata URIs, where we have just a few kinds of identifiers. In scenarios like these we don't want to have to enumerate all Grax schemas on an id schema. We can define a custom selector function instead with the :selector
keyword argument on the id_schema
macro (the id
macro works with directly given Grax schemas only). The function will be called with the Grax schema module and the map of key-value pairs which were passed on the build
function call. It is expected to return a boolean which signals if the id schema on this Grax schema with these values should be applied.
defmodule Example.IdSpec do
use Grax.Id.Spec
namespace "http://example.com/" do
id_schema "books/{isbn}", selector: :example_selector
end
def example_selector(Book, initial_values) do
IO.inspect(initial_values)
true
end
def example_selector(_, _), do: false
end
In this example we return true
for all Book
schema requests, making it essentially behave exactly like our previous examples. We just also output the second argument to demonstrate what else we have available to determine an id schema.
iex> Book.build!(
...> title: "Exploring Graphs with Elixir",
...> isbn: "1680508407")
%{isbn: "1680508407", title: "Exploring Graphs with Elixir"}
%Book{
__id__: ~I<http://example.com/books/1680508407>,
title: "Exploring Graphs with Elixir",
isbn: "1680508407"
}
# Custom variables
Obviously, we will not always have a property available to use in our templates. Either we need to process an existing property value or there are template parameters which are completely independent of the properties of a Grax schema. For these situations we can define a variable processing or generating function and associate it with our id schema via the var_mapping
keyword argument. This function will receive a map of all properties and their values and is expected to return an :ok
tuple with an updated map which should be used to resolve the variable in the URI template.
Let's say we want to use the slugified title our our Post
schema from the previous chapters.
defmodule Example.IdSpec do
use Grax.Id.Spec
namespace "http://example.com/" do
id Post, "posts/{slug}", var_mapping: :slugify_title
end
def slugify_title(%{title: title} = vars) do
{:ok,
Map.put(vars, :slug,
title
|> to_string()
|> String.downcase()
|> String.replace(" ", "-")
)
}
end
def slugify_title(_) do
{:error, "missing :title value for URI generation"}
end
end
The passed variables map will also include a special :__schema__
value with Grax schema for which the id is requested. This can be very useful when the id schema is used for multiple Grax schemas. You can then pattern match on the :__schema__
field and provide different variable processing logic for the different Grax schemas.
# Auto-incremented counters
If you want to base the ids of a schema on an auto-incremented counter, you can do so by defining a counter on your id schema with the counter
keyword argument, which will define a persistent counter with the given name. Every time a new instance of a schema is created with the build
functions this counter will be automatically increased. You can include the value of the counter in your URI templates with the counter
variable.
defmodule Example.IdSpec do
use Grax.Id.Spec
namespace "http://example.com/" do
id Post, "posts/{counter}", counter: :posts
end
end
end
iex> Post.build(title: "Foo")
%Post{
__id__: ~I<http://example.com/posts/1>,
author: nil,
content: nil,
title: "Foo"
}
iex> Post.build(title: "Bar")
%Post{
__id__: ~I<http://example.com/posts/2>,
author: nil,
content: nil,
title: "Bar"
}
The abbreviated dot operator syntax when the template consists of a variable only is also supported for the counter variable:
defmodule Example.IdSpec do
use Grax.Id.Spec
namespace "http://example.com/" do
namespace "posts/", prefix: :post do
id Post.counter, counter: :posts
end
end
end
You can also share the same counter between different schemas just by using the same name on the counter
keyword argument, for example to define Wikidata-like id schemas with counters for different categories of classes.
By default the counters are persisted in DETS tables, each in dedicated files with the name of the counter accordingly. The path where these are stored can be configured with the counter_dir
option in your Mix config (default is the current directory):
use Mix.Config
config :grax, counter_dir: "example/counters/"
When you want to use a completely different way to manage and store your counters, you can define your own by implementing the Grax.Id.Counter.Adapter
behaviour. The default adapter we've been using above is the Grax.Id.Counter.Dets
implementation. An alternative implementation which simple stores a counter in a text file is available with Grax.Id.Counter.TextFile
. This implementation however shouldn't be used in production. It just serves as a reference implementation and is only useful in tests or very simple scenarios.
Another adapter can be set on an individual id schema with the counter_adapter
keyword argument.
defmodule Example.IdSpec do
use Grax.Id.Spec
namespace "http://example.com/" do
namespace "posts/", prefix: :post do
id Post.counter, counter: :posts, counter_adapter: Grax.Id.Counter.TextFile
end
end
end
An adapter can also be set as the default counter adapter with the counter_adapter
keyword either on a namespace or for a whole id spec on the use Grax.Id.Spec
call.
defmodule Example.IdSpec do
use Grax.Id.Spec, counter_adapter: Grax.Id.Counter.TextFile
namespace "http://example.com/", counter_adapter: Grax.Id.Counter.Dets do
namespace "posts/", prefix: :post do
id Post.counter, counter: :posts
end
end
end
# Hash ids
If you want to base your identifiers on cryptographic hashes, you can use the hash
macro from the Grax.Id.Hash
extension. It works as a replacement for the id
macro and provides an additional hash
variable for use in the template given with the :template
keyword argument. The property from which the hash is computed is given with the :data
keyword argument and the hashing algorithm must be specified with the :algorithm
argument. The names of the algorithms are passed down to Erlang hash function, so please refer to the Erlang documentation (opens new window) of your version, for which ones are available.
Let's use our Post
schema from the last chapters as an example again.
defmodule Example.IdSpec do
use Grax.Id.Spec
import Grax.Id.Hash
namespace "http://example.com/" do
hash Post, template: "posts/{hash}", data: :content, algorithm: :sha256
end
end
There are various ways this can be shortened. First, we can introduce again an additional namespace for posts/
, so that the template
would become "{hash}"
, which we can omit, since it is the default template used in the hash
macro.
defmodule Example.IdSpec do
use Grax.Id.Spec
import Grax.Id.Hash
namespace "http://example.com/" do
namespace "foo/" do
hash Post, data: :content, algorithm: :sha256
end
end
end
Next, instead of providing the property with the data for the hashing input with the :data
keyword argument, it can be given with the dot operator after the schema.
defmodule Example.IdSpec do
use Grax.Id.Spec
import Grax.Id.Hash
namespace "http://example.com/" do
namespace "foo/" do
hash Post.content, algorithm: :sha256
end
end
end
If all hash identifiers inside of a namespace should use the same algorithm we can also specify the hash algorithm on the respective namespace with the :hash_algorithm
keyword argument.
defmodule Example.IdSpec do
use Grax.Id.Spec
import Grax.Id.Hash
namespace "http://example.com/", hash_algorithm: :sha256 do
namespace "foo/" do
hash Post.content
end
end
end
When the same hash algorithm should be used throughout the whole id spec, the :hash_algorithm
can also specified on the use Grax.Id.Spec
.
defmodule Example.IdSpec do
use Grax.Id.Spec,
hash_algorithm: :sha256
import Grax.Id.Hash
namespace "http://example.com/" do
namespace "foo/" do
hash Post.content
end
end
end
All these outer scope :hash_algorithm
specifications can be overwritten in inner scopes including the id schema itself.
# UUIDs
The Grax.Id.UUID
extension provides macros for defining UUID id schemas. The most generic one is the uuid
macros and has the following keyword arguments:
:version
: the version of the UUIDs to be generated; it can be 1, 3, 4 or 5 and is required:format
: the format of the UUIDs to be generated; it can be:default
(which also is the default if not specified) or:hex
):namespace
: the namespace to be used for name-based UUIDs of version 3 and 5; it can be:url
,:dns
,:oid
, ,:x500
,:nil
or another UUID as a string and is required when:version
is 3 or 5:name_var
: the name of the property of the Grax schema whose value should be used as the basis for the name for which the UUID will be generated; required when:version
is 3 or 5
The generated is available with the uuid
variable for the template in the :template
keyword argument.
We're using our schemas from the last chapters as an example again to demonstrate this.
defmodule Example.IdSpec do
use Grax.Id.Spec
import Grax.Id.UUID
@custom_uuid_namespace UUID.uuid5(:dns, "example.domain.com")
namespace "http://example.com/" do
uuid User, template: "{uuid}", version: 4, format: :hex
uuid Comment,
template: "{uuid}",
version: 5,
namespace: @custom_uuid_namespace,
name_var: :content,
format: :hex
end
end
We can apply all of the techniques we saw in the previous sections to make everything more compact.
- We can omit the template when it just consists solely of the
uuid
variable. - For the name-based UUIDs in version 3 and 5 we can provide the property for the
:name_var
with the dot operator after the schema. - We can move shared arguments to the namespace by using the equivalent keyword argument just prefixed with the extension name.
- There are also dedicated macros for the different versions available, which make the
:version
argument implicit.
defmodule Example.IdSpec do
use Grax.Id.Spec
import Grax.Id.UUID
@custom_uuid_namespace UUID.uuid5(:dns, "example.domain.com")
namespace "http://example.com/", uuid_format: :hex do
uuid4 User
uuid5 Comment.content, namespace: @custom_uuid_namespace
end
end
# URNs
Grax id schemas for URN identifiers can be defined by using any of the macros for the definition of id schemas, but putting them in special URN namespaces. These namespaces can be defined with the urn
macro and get a symbol for the namespace identifier instead of a path fragment. The colons before and after the namespace identifier are added automatically, so you won't have to add them in the template.
defmodule UrnIds do
use Grax.Id.Spec
import Grax.Id.{UUID, Hash}
urn :example do
id User, "{name}"
end
urn :uuid do
uuid4 Post.content
end
urn :sha1, algorithm: :sha do
hash Post.content
end
end
Note, that in the case of the UUID URNs the URN format will be automatically selected. This format is only available in the URN namespaces.
# Blank nodes
Grax schemas which should have blank nodes as identifiers can be declared with the blank_node
macro in an id spec. It just takes a single Grax schema or a list of Grax schemas as the only argument
defmodule UrnIds do
use Grax.Id.Spec
blank_node Address
end
The generated blank nodes ids will be underscored UUIDs.
iex> Address.build!(
...> street: "Marienstraße 11",
...> city: "Berlin")
%Address{
__id__: ~B<_07456224d1074c87a1152f75319593ef>,
street: "Marienstraße 11",
city: "Berlin"
}
← API