wiki:RubyLayer
Last modified 3 years ago Last modified on 02/06/09 14:07:16

Redleaf Ruby Interface

This is a page to capture ideas for how to make a idiomatic Ruby interface to RDF. We're probably going to incorporate some of the great ideas in Redleaf's predecessors and contemporaries as well as ideas from other smart people posted to various blogs and mailing lists.

Inspiration

Here are a few of the places we'll be looking:

Example Usage

Graph

This example is a translation of a Python Rdflib example:

require 'redleaf'

FOAF = Redleaf::Namespace( "http://xmlns.com/foaf/0.1/" )

graph = Redleaf::Graph.new
graph.load( "http://bigasterisk.com/foaf.rdf" )
graph.load( "http://www.w3.org/People/Berners-Lee/card.rdf" )
graph.load( "http://danbri.livejournal.com/data/foaf" ) 

# Create foaf:name triples for each foaf:member_name (the attribute LiveJournal uses for the
# member's full name)
graph[ nil, FOAF[:member_name], nil ].each do |stmt|
    graph << [ stmt.subject, FOAF[:name], stmt.object ]
end

sparql = %(
  SELECT ?aname ?bname
  WHERE {
    ?a foaf:knows ?b .
    ?a foaf:name ?aname .
    ?b foaf:name ?bname .
  }
)

graph.query( sparql, :foaf => FOAF ) do |row|
    puts "%s knows %s" % [ row[:aname], row[:bname] ]
end

# Output:
#   Timothy Berners-Lee knows Edd Dumbill
#   Timothy Berners-Lee knows Jennifer Golbeck
#   Timothy Berners-Lee knows Nicholas Gibbins
#   Timothy Berners-Lee knows Nigel Shadbolt
#   Dan Brickley knows binzac
#   Timothy Berners-Lee knows Eric Miller
#   Drew Perttula knows David McClosky
#   Timothy Berners-Lee knows Dan Connolly
#   ...

Interacting with a Store

require 'redleaf'
require 'redleaf/store'

# By default a Graph just uses a Memory-backed Store
graph = Redleaf::Graph.new
graph.store
# ==> #<Redleaf::MemoryStore:0x5f97e0>

# Migrate the graph to a BDB-backed Hashes store
graph.store = Redleaf::BDBStore.new( 'db01', :dir => '/path/to/dbenv', :new => true )

# Load a graph from a store (?)
store = Redleaf::BDBStore.new( 'db01', :dir => '/path/to/dbenv' )
graph = store.graph

Interacting with Parsers

require 'redleaf'
require 'redleaf/parser'

uri = URI.parse( 'http://www.w3.org/2000/10/rdf-tests/rdfcore/ntriples/test.nt' )

# By default, just try to guess what we're parsing and parse it:
statements = Redleaf::Parser.parse( uri )

# Or get the guessed parser class, instantiate it, and parse the URI explicitly
parser_class = Redleaf::Parser.guess_type( uri )
# => Redleaf::NTriplesParser
parser = parser_class.new
parser.parse( uri ) do |stmt|
    puts stmt
end

# Or don't guess if you know what's being parsed:
parser = Redleaf::NTriplesParser.new
statements = parser.parse( uri )

# Handle parse errors:
parser = Redleaf::RDFXMLParser.new
statements = begin
    parser.parse( uri )
rescue Redleaf::ParseError => err
    $stderr.puts "Parse error while parsing '%s': %s" % [ uri, err.message ]
    []
end

Query Interface

SELECT-type queries (bindings results):

# Select the titles of albums by Metallica created after 1980:
sparql = <<-END_OF_QUERY
PREFIX mo:       <http://purl.org/ontology/mo/>
PREFIX dcterms:  <http://purl.org/dc/terms/>
SELECT ?title
WHERE 
{
    ?album a mo:Record;
           dc:creator <http://zitgist.com/music/artist/65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab>;
           dcterms:created ?creation_date;
           dc:title ?title.
     FILTER ( xsd:dateTime(?creation_date) > "1981-01-01 00:00:00"^^xsd:dateTime ) .
}
END_OF_QUERY

# Run a query against the graph
result = graph.query( sparql )
# => #<Redleaf::BindingsQueryResult:0x52a008>

# See what variable bindings the result has
names = result.bindings
# => [ :title ]

result.first.title
# => "...And Justice For All"

CONSTRUCT and DESCRIBE-type queries (graph results):

graph << 
    [ :_a, FOAF[:name], "Alice" ] <<
    [ :_a, FOAF[:mbox], URI.parse('mailto:alice@example.org')]

sparql = <<-END_OF_QUERY
PREFIX foaf:    <http://xmlns.com/foaf/0.1/>
PREFIX vcard:   <http://www.w3.org/2001/vcard-rdf/3.0#>
CONSTRUCT   { <http://example.org/person#Alice> vcard:FN ?name }
WHERE       { ?x foaf:name ?name }
END_OF_QUERY

result = graph.query( sparql ) 
# => #<Redleaf::GraphQueryResult:0x5272a4>

# The result contains a new graph with the matched statements
new_graph = result.graph

ASK-type queries (boolean results):

graph <<
    [ :_a, FOAF[:name],       "Alice" ] <<
    [ :_a, FOAF[:homepage],   URI('http://work.example.org/alice/') ] <<
    [ :_b, FOAF[:name],       "Bob" ] <<
    [ :_b, FOAF[:mbox],       URI('mailto:bob@work.example') ]

sparql = <<-END_OF_QUERY
PREFIX foaf:    <http://xmlns.com/foaf/0.1/>
ASK  { ?x foaf:name  "Alice" }
END_OF_QUERY

res = graph.query( sparql )
# => #<Redleaf::BooleanQueryResult:0x30e4f4>

# The 'value' of the result is either `true` or `false`
res.value
# => true

# Or you use a predicate method
res.is_true?
# => true
res.is_false?
# => false

Future Redleaf::Graph Features

Turtle-like Node Appending

Turtle supports a syntax like this for describing complex graphs of triples:

@prefix rdf:     <http://www.w3.org/1999/02/22-rdf-syntax-ns#>.
@prefix xsd:     <http://www.w3.org/2001/XMLSchema#>.
@prefix exterms: <hhttp://www.example.org/terms/>.

:item10245 
   exterm:models     "Overnighter"^^xsd:string;
   exterm:sleeps     "2"^^xsd:integer;
   exterm:weight     "2.4"^^xsd:decimal;
   exterm:packedSize "784"^^xsd:integer.

Since the #<< operator method already supports something based on N3/Turtle, I'd like to eventually support this syntax from Ruby, too. This would append the nodes described above to the graph:

graph << {
  :item10245 => {
    exterm[:models]     => "Overnighter",
    exterm[:sleeps]     => 2,
    exterm[:weight]     => 2.4,
    exterm[:packedSize] => 784,
  }
}

This would mean that Redleaf::Graph#<< would have to support both Arrays and Hashes, but I think it's well worth it.

Redland Features Not In Redleaf

These are a list of features that Redland's model class has that Redleaf::Graph is missing at the moment, mostly because I don't feel like I understand them well enough to do the translation of the functionality into Ruby idiom.

Here's what I'm thinking currently:

Transactions

graph = Redleaf::Graph.new

URLS = [...]

# Either load all of the URLs or none of them
graph.transaction do
    URLS.each do |url|
        graph.load( url )
    end
end

Contexts

Using the ideas in David's notes on the subject, something like:

graph = Redleaf::Graph.new
dajobe_foaf = URI.parse( 'http://www.dajobe.org/foaf.rdf' )

# Get a delegating proxy (Redleaf::Graph::Context) for the receiving Redleaf::Graph
graph_context = graph.context( dajobe_foaf )

# Append some new nodes in the specified context
graph.append_with_context( dajobe_foaf, *statements )

# Or via a proxy, whose #append calls its graph's #append_with_context:
graph.context( dajobe_foaf ).append( *statements )

# Also via a proxy, whose #append calls its graph's #append_with_context:
graph[ dajobe_foaf ].append( *statements )

Further Reading