Skip to content

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
You can’t perform that action at this time.