Instantly share code, notes, and snippets.

Embed
What would you like to do?
Hydra Connect RDF Workshop Walkthrough
# lib/models/agent.rb
class Agent < ActiveTriples::Resource
configure base_uri: 'http://example.org/agent/', type: 'http://example.org/ns/Agent'
property :name, predicate: RDF::FOAF.name
end
# lib/models/cho.rb
class CHO < ActiveTriples::Resource
configure base_uri: 'http://example.org/resource/', type: 'http://example.org/ns/CHO'
property :title, predicate: RDF::DC.title
property :creator, predicate: RDF::DC.creator
property :date, predicate: RDF::DC.date
property :location, predicate: RDF::DC.spatial
en
# lib/models/datastream.rb
class CHODatastream < ActiveFedora::NtriplesRDFDatastream
property :title, predicate: RDF::DC.title
property :creator, predicate: RDF::DC.creator
property :date, predicate: RDF::DC.date
property :location, predicate: RDF::DC.spatial
end
# lib/models/collection.rb
class Collection < ActiveTriples::Resource
configure base_uri: 'http://example.org/collection/', type: 'http://example.org/ns/Collection'
property :name, predicate: RDF::DC.title
property :members, predicate: RDF::DC.hasPart
end
# lib/models/my_object.rb
class MyObject < ActiveFedora::Base
has_metadata 'descMetadata', type: CHODatastream
has_attributes :title, :creator, :date, :location, datastream: 'descMetadata', multiple: true
end
# lib/models/place.rb
class Place < ActiveTriples::Resource
configure base_uri: 'http://sws.geonames.org/', type: 'http://www.geonames.org/ontology#Feature'
property :name, predicate: RDF::URI('http://www.geonames.org/ontology#name')
property :lat, predicate: RDF::GEO.lat
property :long, predicate: RDF::GEO.long
property :parentFeature, predicate: RDF::URI('http://www.geonames.org/ontology#parentFeature'), class_name: 'Place'
property :parentCountry, predicate: RDF::URI('http://www.geonames.org/ontology#parentCountry'), class_name: 'Place'
end
# My First Resource
# ==================
#
# $ mkdir rdf-tutorial
# $ cd rdf-tutorial
#
# $ echo "source 'http://rubygems.org'" > Gemfile
# $ echo "gem 'active-fedora', '~> 7.1.1'" >> Gemfile
#
# $ bundle install
#
# $ mkdir -p lib/models
#
# create cho.rb as below in lib/models/
class CHO < ActiveTriples::Resource
property :title, predicate: RDF::DC.title
end
#
# $ bundle console
require './lib/models/cho'
cho = CHO.new('http://example.org/resources/1')
cho.title = 'my cultural heritage object'
cho.dump :ntriples
# => "<http://example.org/resources/1> <http://purl.org/dc/terms/title> \"my cultural heritage object\" .\n"
# your Resource object is a an RDF::Graph scoped specifically to your data. You can operate on it directly,
# adding statements with `<<`, `query`, and `delete`, call other Graph methods, and use `dump` to serialize
# in any RDF format.
require 'linkeddata'
cho.dump :ttl
# => "\n<http://example.org/resources/1> <http://purl.org/dc/terms/title> \"my cultural heritage object\" .\n"
#
# Adding a base_uri
# -----------------
# exit your ruby console and
# edit lib/models/cho.rb as follows:
class CHO < ActiveTriples::Resource
configure base_uri: 'http://example.org/resource'
property :title, predicate: RDF::DC.title
end
# $ bundle console
require './lib/models/cho'
cho = CHO.new('1')
cho.rdf_subject
# => #<RDF::URI:0x3f8326f14224 URI:http://example.org/resource/1>
#
# Adding an RDF::type
# -------------------
# exit your ruby console and
# edit lib/models/cho.rb as follows:
class CHO < ActiveTriples::Resource
configure base_uri: 'http://example.org/resource', type: 'http://example.org/ns/CHO'
property :title, predicate: RDF::DC.title
end
# $ bundle console
require './lib/models/cho'
cho = CHO.new('1')
cho.type
# => [#<RDF::URI:0x3f8326f8ae60 URI:http://example.org/ns/CHO>]
cho.dump :ntriples
# => "<http://example.org/resource/1> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://example.org/ns/CHO> .\n"
#
# Adding additional properties
# ----------------------------
# exit your ruby console and
# edit lib/models/cho.rb as follows:
class CHO < ActiveTriples::Resource
configure base_uri: 'http://example.org/resource/', type: 'http://example.org/ns/CHO'
property :title, predicate: RDF::DC.title
property :creator, predicate: RDF::DC.creator
property :date, predicate: RDF::DC.date
end
# $ bundle console
require './lib/models/cho'
cho = CHO.new('1')
cho.title = 'My Resource'
cho.creator = 'Me'
cho.date = DateTime.now
puts cho.dump :ntriples
# <http://example.org/resource/1> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://example.org/ns/CHO> .
# <http://example.org/resource/1> <http://purl.org/dc/terms/title> "My Resource" .
# <http://example.org/resource/1> <http://purl.org/dc/terms/creator> "Me" .
# <http://example.org/resource/1> <http://purl.org/dc/terms/date> "2014-09-11T14:34:28-07:00"^^<http://www.w3.org/2001/XMLSchema#dateTime> .
# => nil
# Note that the `date` property is encoded as a typed literal.
# When typed data is passed to a property, ActiveTriples serialises it correctly
# and returns the appropriate datatype when accessed. This is handled through
# RDF.rb's Literal class.
#
# For more about typed literals in RDF.rb, see: http://rdf.greggkellogg.net/yard/RDF/Literal.html
# For more about data types in RDF in general, see: http://www.w3.org/TR/rdf11-concepts/#section-Graph-Literal
cho.date
# => [Thu, 11 Sep 2014 14:34:28 -0700]
#
# Defining a Domain Model
# =======================
#
# First Relationship
# ------------------
# create models.rb as below in lib/
require 'linkeddata'
Dir["./lib/models/*.rb"].each {|file| require file }
# create collection.rb as below in lib/models/
class Collection < ActiveTriples::Resource
configure base_uri: 'http://example.org/collection/', type: 'http://example.org/ns/Collection'
property :name, predicate: RDF::DC.title
property :members, predicate: RDF::DC.hasPart
end
# $ bundle console
require './lib/models'
coll = Collection.new('1')
coll.name = 'Hydra Connect Photos'
coll.members = CHO.new('abc')
puts coll.dump :ttl
# <http://example.org/collection/1> a <http://example.org/ns/Collection>;
# <http://purl.org/dc/terms/title> "Hydra Connect Photos";
# <http://purl.org/dc/terms/hasPart> <http://example.org/resource/abc> .
#
# <http://example.org/resource/abc> a <http://example.org/ns/CHO> .
# => nil
coll.members
# => [#<CHO:0x3f9f7120ba84(default)>
coll.members.first.title = 'Rock & Roll Hall of Fame'
puts coll.dump :ttl
# <http://example.org/collection/1> a <http://example.org/ns/Collection>;
# <http://purl.org/dc/terms/title> "Hydra Connect Photos";
# <http://purl.org/dc/terms/hasPart> <http://example.org/resource/abc> .
# <http://example.org/resource/abc> a <http://example.org/ns/CHO>;
# <http://purl.org/dc/terms/title> "Rock & Roll Hall of Fame" .
# => nil
# Resource objects loaded from the graph will build in the class associated with
# their rdf:type. But what happens if no type is given?
coll.members << ActiveTriples::Resource.new('http://example.org/noType/1')
coll.members
# => [#<CHO:0x3f9f7120ba84(default)>, #<ActiveTriples::Resource:0x3f9f710314d4(default)>]
#
# Expanding the Domain Model with Agents and Places
# -------------------------------------------------
# create agent.rb as below in lib/models/
class Agent < ActiveTriples::Resource
configure base_uri: 'http://example.org/agent/', type: 'http://example.org/ns/Agent'
property :name, predicate: RDF::FOAF.name
end
# create place.rb as below in lib/models/
class Place < ActiveTriples::Resource
configure base_uri: 'http://example.org/place/', type: 'http://example.org/ns/Place'
property :name, predicate: RDF::URI('http://www.geonames.org/ontology#name')
property :lat, predicate: RDF::GEO.lat
property :long, predicate: RDF::GEO.long
end
# update lib/models/cho.rb as below:
class CHO < ActiveTriples::Resource
configure base_uri: 'http://example.org/resource/', type: 'http://example.org/ns/CHO'
property :title, predicate: RDF::DC.title
property :creator, predicate: RDF::DC.creator
property :date, predicate: RDF::DC.date
property :location, predicate: RDF::DC.spatial
end
# $ bundle console
require './lib/models'
cho = CHO.new('1')
cho.title = 'RDF Tutorial'
cho.date = Date.today
tom = Agent.new('tj')
tom.name = 'Tom'
karen = Agent.new('ke')
karen.name = 'Karen'
place = Place.new('cleveland')
place.name = 'Cleveland'
cho.location = place
cho.creator = [tom, karen]
puts cho.dump :ttl
# <http://example.org/resource/1> a <http://example.org/ns/CHO>;
# <http://purl.org/dc/terms/title> "RDF Tutorial";
# <http://purl.org/dc/terms/creator> <http://example.org/agent/tj>,
# <http://example.org/agent/ke>;
# <http://purl.org/dc/terms/date> "2014-09-29"^^<http://www.w3.org/2001/XMLSchema#date>;
# <http://purl.org/dc/terms/spatial> <http://example.org/place/cleveland> .
#
# <http://example.org/agent/ke> a <http://example.org/ns/Agent>;
# <http://xmlns.com/foaf/0.1/name> "Karen" .
#
# <http://example.org/agent/tj> a <http://example.org/ns/Agent>;
# <http://xmlns.com/foaf/0.1/name> "Tom" .
# <http://example.org/place/cleveland> a <http://example.org/ns/Place>;
# <http://www.geonames.org/ontology#name> "Cleveland" .
# => nil
#
# Using External Data
# -------------------
# update place.rb as follows
class Place < ActiveTriples::Resource
configure base_uri: 'http://sws.geonames.org/', type: 'http://example.org/ns/Place'
property :name, predicate: RDF::URI('http://www.geonames.org/ontology#name')
property :lat, predicate: RDF::GEO.lat
property :long, predicate: RDF::GEO.long
property :parentFeature, predicate: RDF::URI('http://www.geonames.org/ontology#parentFeature')
property :parentCountry, predicate: RDF::URI('http://www.geonames.org/ontology#parentCountry')
end
# $ bundle console
require './lib/models'
cw = Place.new('5149374/')
cw.fetch
cw.name
# => ["Case Western Reserve University"]
cw.lat
# => ["41.5045"]
cw.long
# => ["-81.59707"]
cw.parentFeature.first.fetch
cw.parentFeature.first.name
# => ["Cuyahoga County"]
cw.parentFeature.first.parentFeature.first.fetch
# => #<Place:0x3fdfd736ef9c(default)>
cw.parentFeature.first.parentFeature.first.name
# => ["Ohio"]
# ...and so on.
#
# Closing the Gap to Fedora
# =========================
# Everything so far has been persisting only to memory.
# ActiveTriples::Resources persist to an in-memory RDF::Repository
# by default, and can be configured to persist to a persistent
# Repository instance, but for most Hydra use cases, we want to associate
# a graph with an Fedora object and serialize it to a datastream.
# We can achieve this with ActiveFedora::RDFDatastream (and subclasses).
#
# Create a Datastream and Object
# -------------------------------
# in lib/models/datastream.rb
class CHODatastream < ActiveFedora::NtriplesRDFDatastream
property :title, predicate: RDF::DC.title
property :creator, predicate: RDF::DC.creator
property :date, predicate: RDF::DC.date
property :location, predicate: RDF::DC.spatial
end
# in lib/models/my_object.rb
class MyObject < ActiveFedora::Base
has_metadata 'descMetadata', type: CHODatastream
has_attributes :title, :creator, :date, :location, datastream: 'descMetadata', multiple: true
end
# $ bundle console
obj = MyObject.new
obj.descMetadata.title = 'my object'
obj.descMetadata.creator = Agent.new('tj')
puts obj.descMetadata.content
# _:g69927657577780 <http://purl.org/dc/terms/title> "my object" .
# _:g69927657577780 <http://purl.org/dc/terms/creator> <http://example.org/agent/tj> .
# <http://example.org/agent/tj> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://example.org/ns/Agent> .
# => nil
#
# Extras & Asides
# ================
#
# Check Out the DPLA MAP
# ----------------------
# DPLA is drafting it's Metadata Application Profile v4.0
# we have been prototyping the new model in ActiveTriples
# as we go. The prototype validates compliance with the model
# and uses the LinkedVocabs gem to implement controlled
# vocabularies.
# See: https://github.com/dpla/dpla_map/tree/map-4.0
# $ git clone git@github.com:dpla/dpla_map.git
# $ cd dpla_map
# $ git checkout map-4.0
# $ bundle install
# $ bundle console
sr = DPLA::SourceResource.new('123')
sr.valid # => false
sr.errors
# => #<ActiveModel::Errors:0x007f95b7a9aef8
# @base=#<DPLA::SourceResource:0x3fcadbdadf78(default)>,
# @messages={:rights=>["can't be blank"], :title=>["can't be blank"]}>
sr.title = 'my resource'
sr.rights = 'cc by-sa'
sr.valid? # => true
sr.dctype = 'IMG'
sr.valid? # => false
sr.errors
# => #<ActiveModel::Errors:0x007f95b82306b8
# @base=#<DPLA::SourceResource:0x3fcadbf7e550(default)>,
# @messages={:base=>["value `IMG for `dctype` property is not a term in a controlled vocabulary dcmitype"]}>
sr.dctype = DPLA::Controlled::DCMIType.new('Image')
sr.valid? # => true
# also look at our factories:
# https://github.com/dpla/dpla_map/blob/map-4.0/spec/factories.rb
#
# What is a Blank node/bnode/RDF::Node?
# -------------------------------------
# These are all terms for a Resource that is not identified
# by a URI. ActiveTriples creates bnodes for Resources that aren't
# passed a URI (or stub) at initialization.
cho = CHO.new
cho.rdf_subject
# => #<RDF::Node:0x3f98d5bb1a04(_:g69925653387780)>
# Blank nodes are identified by a UUID, which won't be stable from
# load to load/serialization to serialization. It identifies the
# resource only the context of a specific graph expression.
#
# ActiveTriples will let you set a URI for a bnode, but only once!
cho.set_subject!('abc')
cho.rdf_subject
# => #<RDF::URI:0x3f98d5ba4160 URI:http://example.org/resource/abc>
cho.set_subject!('123')
# RuntimeError: Refusing update URI when one is already assigned!
#
# Look Out: Lists are Unordered!
# ------------------------------
coll = Collection.new('1')
10.times do
coll.members << CHO.new
end
coll.members[4].rdf_subject
# => #<RDF::Node:0x3f98d544be54(_:g69925645631060)>
puts coll.dump :ttl
# <http://example.org/collection/1> a <http://example.org/ns/Collection>;
# <http://purl.org/dc/terms/title> "Hydra Connect Photos";
# <http://purl.org/dc/terms/hasPart> [ a <http://example.org/ns/CHO>], [ a <http://example.org/ns/CHO>], [ a <http://example.org/ns/CHO>], [ a <http://example.org/ns/CHO>], [ a <http://example.org/ns/CHO>], [ a <http://example.org/ns/CHO>], [ a <http://example.org/ns/CHO>], [ a <http://example.org/ns/CHO>], [ a <http://example.org/ns/CHO>], [ a <http://example.org/ns/CHO>] .
# => nil
# Though these `coll.members` returns a list, that list is unordered;
# it is a reflection on the underlying graph, which assigns no order
# to its elements.
#
# The order your objects load depends on where you serialize them and
# how RDF.rb reads them back.
coll.members[4].title = 'key 4'
coll.reload! # The object 'key 4' could be anywhere in the list!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment