# Description and Graph DSL

RDF.ex comes with a declarative DSL to encode full RDF graphs in Elixir, which allows to serialize RDF graphs with the full power of Elixir and compile-time checks.

It consists of two building-blocks (which can also be used independently of each other):

  • builder functions for compact RDF.Descriptions
  • a general builder function for the encoding of RDF.Graphs

# Description builder

The functions for the properties on a RDF.Vocabulary.Namespace module, which return the RDF.IRI of the property (see here for an introduction to RDF.Vocabulary.Namespaces), are also available in a description builder variant, that accepts a 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 pass the objects as a list:

RDF.type(EX.Foo, [EX.Bar, EX.Baz])
EX.foo(EX.Bar, [42, EX.Baz])

The produced statements are returned by this function as a RDF.Description structure. Since the first argument of these property functions also accept an RDF.Description for the subject (just using its subject as the subject for newly generated triple), the calls of these functions can be nested easily. In combination with Elixirs pipe operators this leads to a way to describe RDF resources which resembles Turtle (opens new window):

EX.Foo
|> RDF.type(EX.Bar)
|> EX.baz([1, 2, 3])

This will produce this RDF.Description:

#RDF.Description<subject: ~I<http://example.com/Foo>
  <http://example.com/Foo>
      a <http://example.com/Bar> ;
      <http://example.com/baz> 1, 2, 3 .
>

# Graph builder

Full RDF graphs can be build with the RDF.Graph.build/2 macro. It uses a do block in which you can write down RDF triples in any form supported by RDF.ex (including RDF.Descriptions with the description DSL) or Elixir expressions which return any of these forms. These triples will be added to the created RDF.Graph the macro returns.

As usual, you'll have to require RDF.Graph to be able to use the macro or just use RDF.

use RDF
alias NS.EX

RDF.Graph.build do
  {EX.S, EX.P, EX.O}

  EX.S2
  |> EX.p1(EX.O2)
  |> EX.p2([1, 2, 3])
  
  %{
    EX.S3 => %{
      EX.p1() => EX.O1,
      EX.p2() => [EX.O2, EX.O3]
    }
  }
end

This will return the following RDF.Graph:

#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.org/S>
      <http://example.org/P> <http://example.org/O> .

  <http://example.org/S2>
      <http://example.org/p1> <http://example.org/O2> ;
      <http://example.org/p2> 1, 2, 3 .

  <http://example.org/S3>
      <http://example.org/p1> <http://example.org/O1> ;
      <http://example.org/p2> <http://example.org/O2>, <http://example.org/O3> .
>

Apart from the vocabulary namespaces aliased in the scope of the block, you can use the RDF, RDFS and OWL vocabulary namespaces inside of the build block without an explicit alias. Also, RDF sigils are available without an explicit import RDF.Sigils. Similar to the a keyword in Turtle, an a alias for the RDF.type function can be used.

require RDF.Graph
alias NS.EX

RDF.Graph.build do
  {~I<http://example.com/Demo>, EX.P, ~L<literal>en}

  EX.Foo
  |> a(OWL.Class)
  |> RDFS.description("example")
end

In order to not pollute the context of the build caller with these auto-aliases and -imports, the build block is isolated under the hood from the caller context. But this also means variables from the caller context are not available in the build block. If you want to use them in build block, you'll have to rebind the variables in a keyword list as the first argument of the build call.

use RDF

foo = 42

RDF.Graph.build foo: foo do
  {EX.S, EX.p(), foo}
end

The same applies to imports and requires from the caller context. You'll have to re-import or re-require them. Aliases from the caller context, however, are all available because they are re-aliased automatically in the build block.

use RDF
alias EX
import Something

RDF.Graph.build do
  # Something is not imported here and needs to be 
  # re-imported again to be available
  import Something

  # EX is available  
  {EX.S, something(), EX.O}
end

In fact, with Elixir versions >= 1.17, aliases inside of the build block are no longer supported, i.e. don't have the desired effect. However, instead of aliasing vocabulary namespaces in the surrounding module, there is a better solution anyway: you can also declare them inside of the build block with a @prefix definition. This will not only create an alias for the vocabulary namespace in the build block, but adds it as a prefix to the created RDF.Graph. By default, it will use the downcased and underscored name of the vocabulary namespace module (resp. the last segment of its fully qualified name), but you can also define a custom prefix by providing it as the key in a keyword tuple after the @prefix, instead of just defining the vocabulary namespace module.

require RDF.Graph

RDF.Graph.build do
  @prefix NS.FOAF
  @prefix rel: NS.Rel

  ~I<http://example.org/#green-goblin>
    |> a(FOAF.Person)    
    |> Rel.enemyOf(~I<http://example.org/#spiderman>)
    |> FOAF.name("Green Goblin")

  ~I<http://example.org/#spiderman>
    |> a(FOAF.Person)
    |> Rel.enemyOf(~I<http://example.org/#green-goblin>)
    |> FOAF.name(["Spiderman", ~L"Человек-паук"ru])
end

This will build the following RDF.Graph:

#RDF.Graph<name: nil
  @prefix foaf: <http://xmlns.com/foaf/0.1/> .
  @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
  @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
  @prefix rel: <http://www.perceive.net/schemas/relationship/> .
  @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

  <http://example.org/#green-goblin>
      a foaf:Person ;
      rel:enemyOf <http://example.org/#spiderman> ;
      foaf:name "Green Goblin" .

  <http://example.org/#spiderman>
      a foaf:Person ;
      rel:enemyOf <http://example.org/#green-goblin> ;
      foaf:name "Человек-паук"@ru, "Spiderman" .
>

When you want to use terms from a URI namespace for which you don't have RDF.Vocabulary.Namespace defined in your application, you can define an ad-hoc namespace in your build block with a @prefix definition and a string with the URI namespace. In that case you must also provide the prefix to be used. The name for the generated ad-hoc vocabulary namespace will be the upper-cased version of the prefix.

RDF.Graph.build do
  @prefix ex: "http://example.com/ad-hoc/"

  Ex.S |> Ex.p(Ex.O)
end

This will result in the following RDF.Graph:

#RDF.Graph<name: nil
  @prefix ex: <http://example.com/ad-hoc/> .
  @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#> .

  ex:S
      ex:p ex:O .
>

WARNING

Unfortunately, for Elixir versions < 1.13 you might encounter undefined-function warnings for uses of lower-cased terms from ad-hoc namespaces defined with such @prefix definitions.

The base URI of the RDF.Graph can be specified with a @base declaration and the URI as a string, IRI sigil or a vocabulary namespace. With a @base declaration in place, all IRI sigils in the build block with relative URIs, will be automatically resolved to absolute URIs against the specified base URI.

require RDF.Graph

RDF.Graph.build do
  @base EX
  @prefix FOAF
  @prefix Rel
  
  ~I<#green-goblin>
    |> a(FOAF.Person)    
    |> Rel.enemyOf(~I<#spiderman>)
    |> FOAF.name("Green Goblin")

  ~I<#spiderman>
    |> a(FOAF.Person)
    |> Rel.enemyOf(~I<#green-goblin>)
    |> FOAF.name(["Spiderman", ~L"Человек-паук"ru])
end

Now, we're building this RDF.Graph:

#RDF.Graph<name: nil
  @base <http://example.org/> .

  @prefix foaf: <http://xmlns.com/foaf/0.1/> .
  @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
  @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
  @prefix rel: <http://www.perceive.net/schemas/relationship/> .
  @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

  <#green-goblin>
      a foaf:Person ;
      rel:enemyOf <#spiderman> ;
      foaf:name "Green Goblin" .

  <#spiderman>
      a foaf:Person ;
      rel:enemyOf <#green-goblin> ;
      foaf:name "Человек-паук"@ru, "Spiderman" .
>

So far, all examples have shown fixed triple structures only, but the build blocks can include any Elixir expression, as long it returns RDF data in any of the forms supported by RDF.ex.

RDF.Graph.build file: file, args: args, rank: rank do
  @base "http://chess.example.com/"
  ExampleModule.function_returning_rdf(args)

  Enum.map(1..100, &{EX.S, EX.p(), &1})

  for file <- ?a..?h, rank <- 1..8 do
    ~i<##{file}#{rank}>
    |> a(Chess.Square)
  end
end

If an expression evaluates to nil or :ok, it will be excluded automatically. This enables the use of conditionals and the use of the logger in your build blocks:

RDF.Graph.build args: args do
  # the nil result in the negative case is ignored
  if someCondition?(args) do
    ExampleModule.function_returning_rdf(args)
  end 

  if good_data?(args) do
    ExampleModule.function_returning_more_rdf(args)
  else
    # the :ok value returned by Logger.warn/1 is ignored
    Logger.warn("Bad data was ignored")
  end 
end

Assignments in build blocks are essentially declarations, as their results are also ignored from the inclusion in the produced graph.

RDF.Graph.build do
  reusable_value = 42
 
  EX.S1 |> EX.p(reusable_value)
  EX.S2 |> EX.p(reusable_value)
end

So, if you assign some RDF data to a variable and want to include it in the graph, you have to evaluate the assigned variable explicitly.

RDF.Graph.build do
  triple = EX.S |> EX.p(EX.O)
  triple
end

The fact that assignments are not added to the graph, allows you to use them to ignore an expression in a build block, which does return something you don't want to include in the built RDF graph.

RDF.Graph.build args: args do
  _ = function_with_side_effects()

  ExampleModule.function_returning_rdf(args)
end

Note that the assignments are still pattern matches, so they can be used as validation guards.

RDF.Graph.build args: args do
  {:ok, _} = function_with_side_effects()

  ExampleModule.function_returning_rdf(args)
end

Finally, you can also exclude the result of an expression from inclusion in the built RDF graph more explicitly, by prepending it with the exclude function.

RDF.Graph.build args: args do
  exclude function_with_side_effects()

  ExampleModule.function_returning_rdf(args)
end

The RDF.Graph.build/2 function also accepts all options available on RDF.Graph.new/2 as the second argument, which it will use to create the initial RDF.Graph to which the triples in the build block are added. The @prefixes and @base URI declared within the build block will overwrite the ones from :prefixes and :base keyword options.

use RDF
alias NS.EX

opts = [
  name: EX.GraphName, 
  base_iri: "http://base_iri/A",
  prefixes: [
    ex: EX.old(), 
    other: EX.used()
  ],
  init: {EX.Foo, EX.Bar, EX.Baz}
]

RDF.Graph.build [], opts do
  @base "http://base_iri/B"
  @prefix ex: EX

  EX.S |> EX.p(EX.O)
end

This will build this graph:

#RDF.Graph<name: ~I<http://example.org/GraphName>
  @base <http://base_iri/B> .

  @prefix ex: <http://example.org/> .
  @prefix other: <http://example.org/used> .

  ex:Foo
      ex:Bar ex:Baz .

  ex:S
      ex:p ex:O .
>

TIP

Providing an existing graph on the :init opt is the shortest way to use build for adding statements to an existing graph.