Skip to content

Commit

Permalink
FIX: Default minter not receiving parameters. Improve YARD doc. Add t…
Browse files Browse the repository at this point in the history
…ests (all pass). Write README.
  • Loading branch information
elrayle committed Nov 25, 2014
1 parent 48d702f commit 716fb4f
Show file tree
Hide file tree
Showing 4 changed files with 312 additions and 178 deletions.
203 changes: 67 additions & 136 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,177 +1,108 @@
Description
-----------
# ActiveTriples::LocalName

[![Build Status](https://travis-ci.org/no-reply/ActiveTriples.png?branch=master)](https://travis-ci.org/no-reply/ActiveTriples)

An ActiveModel-like interface for RDF data. Models graphs as Resources with property/attribute configuration, accessors, and other methods to support Linked Data in a Ruby/Rails enviornment.
Provides utilities for working with local names under the [ActiveTriples](https://github.com/ActiveTriples/ActiveTriples)
framework. Includes a default implementation of a local name minter.

This library was extracted from work on [ActiveFedora](https://github.com/projecthydra/active_fedora). It is closely related to (and borrows some syntax from) [Spira](https://github.com/ruby-rdf/spira), but does some important things differently.

Installation
------------
## Installation

Add `gem "active-triples"` to your Gemfile and run `bundle`.
Add this line to your application's Gemfile:

Or install manually with `gem install active-triples`.
gem 'active_triples-local_name'

Defining Resource Models
------------------------
And then execute:

The core class of ActiveTriples is ActiveTriples::Resource. You can subclass this to create ActiveModel-like classes that represent a node in an RDF graph, and its surrounding statements. Resources implement all the functionality of an RDF::Graph. You can manipulate them by adding or deleting statements, query, serialize, and load arbitrary RDF.
$ bundle install

Or install it yourself as:

```ruby
class Thing < ActiveTriples::Resource
configure :type => RDF::OWL.Thing, :base_uri => 'http://example.org/things#'
property :title, :predicate => RDF::DC.title
property :description, :predicate => RDF::DC.description
end

obj = Thing.new('123')
obj.title = 'Resource'
obj.description = 'A resource.'
obj.dump :ntriples # => "<http://example.org/things#123> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Thing> .\n<http://example.org/things#123> <http://purl.org/dc/terms/title> \"Resource\" .\n<http://example.org/things#123> <http://purl.org/dc/terms/description> \"A resource.\" .\n"
```
URI and bnode values are built out as Resources when accessed, and a model class can be configured on individual properties.
$ gem install active_triples-local_name

```ruby
Thing.property :creator, :predicate => RDF::DC.creator, :class_name => 'Person'

class Person < ActiveTriples::Resource
configure :type => RDF::FOAF.Person, :base_uri => 'http://example.org/people#'
property :name, :predicate => RDF::FOAF.name
end
## Usage

obj_2 = Thing.new('2')
obj_2.creator = Person.new
obj_2.creator
# => [#<Person:0x3fbe84ac9234(default)>]
**Utilities**

obj_2.creator.first.name = 'Herman Melville'
obj_2.dump :ntriples # => "<http://example.org/things#2> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Thing> .\n<http://example.org/things#2> <http://purl.org/dc/terms/creator> _:g70263220218800 .\n_:g70263220218800 <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://xmlns.com/foaf/0.1/Person> .\n_:g70263220218800 <http://xmlns.com/foaf/0.1/name> \"Herman Melville\" .\n"
```
* Local Name Minter
* Others may be added in the future.

Open Model
-----------

A Resource lets you handle data as a graph, independent of whether it is defined in the model. This is important for working in a Linked Data context, where you will want access to data you may not have known about when your models were written.
### Local Name Minter

Common prep code for all examples:
```ruby
related = Thing.new

related << RDF::Statement(related, RDF::DC.relation, obj)
related << RDF::Statement(related, RDF::DC.subject, 'ActiveTriples')

related.query(:subject => related, :predicate => RDF::DC.relation).each_statement {|s,p,o| puts o}
# => http://example.org/things#123
related.query(:subject => subject, :predicate => RDF::DC.relation).each_statement {|s,p,o| puts o}
# => http://example.org/things#123
```
require 'active_triples'
require 'active_triples/local_name'

Any operation you can run against an RDF::Graph works with Resources, too. Or you can use generic setters and getters with URI predicates:

```ruby
related.set_value(RDF::DC.relation, obj)
related.set_value(RDF::DC.subject, 'ActiveTriples')
# create an in-memory repository for ad-hoc testing
ActiveTriples::Repositories.add_repository :default, RDF::Repository.new

related.get_values(RDF::DC.relation) # => [#<Thing:0x3f949c6a2294(default)>]
related.get_values(RDF::DC.subject) # => ["ActiveTriples"]
# create a DummyResource for ad-hoc testing
# NOTE: local name minter requires resource class to have base_uri configured
class DummyResourceWithBaseURI < ActiveTriples::Resource
configure :base_uri => "http://example.org",
:type => RDF::URI("http://example.org/SomeClass"),
:repository => :default
end
```

Some convienience methods provide support for handling data from web sources:
* `fetch` loads data from the Resource's #rdf_subject URI
* `rdf_label` queries across common (& configured) label fields and returning the best match

#### Example: Using default minter
```ruby
require 'linkeddata' # to support various serializations

osu = ActiveTriples::Resource.new 'http://dbpedia.org/resource/Oregon_State_University'
osu.fetch

osu.rdf_label => => ["Oregon State University", "Oregon State University", "Université d'État de l'Oregon", "Oregon State University", "Oregon State University", "オレゴン州立大学", "Universidad Estatal de Oregón", "Oregon State University", "俄勒岡州立大學", "Universidade do Estado do Oregon"]
# create a new resource with a minted local name using default minter
localname = ActiveTriples::LocalName::Minter.generate_local_name(
DummyResourceWithBaseURI, 10, {:prefix=>'d'})
# => something like -- "http://example.org/d59beebc5-5238-4aad-bf92-f63fbbd8faaa"
```

Typed Data
-----------
Parameter NOTES:
* for_class = DummyResourceWithBaseURI - resource class must have base_uri configured
* max_tries = 10 - If using default minter, it should easily find an available local name in 10 tries.
If your minter algorithm gets lots of clashes with existing URIs and max_tries is set high, you may
run into performance issues.
* minter_args (optional) = {:prefix=>'d'} - The default minter takes a single hash argument. You can
define minters that take no arguments, multiple arguments, or a multiple item hash argument.
* minter_block = nil - When minter_block is not passed in, the default minter algorithm, which produces
a UUID, will be used. Best practice is to start local names with an alpha character. UUIDs generate
with either an alpha or numeric as the first character. Passing in a prefix of 'd' forces the local
name to start with the character 'd'.

Typed literals are handled natively through Ruby types and [RDF::Literal](https://github.com/ruby-rdf/rdf/tree/develop/lib/rdf/model/literal). There is no need to register a specific type for a property, simply pass the setter the appropriate typed data. See the examples in the RDF::Literal documentation for futher information about supported datatypes.

```ruby
Thing.property :date, :predicate => RDF::DC.date

my_thing = Thing.new
my_thing.date = Date.today

puts my_thing.dump :ntriples
# _:g70072864570340 <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Thing> .
# _:g70072864570340 <http://purl.org/dc/terms/date> "2014-06-19Z"^^<http://www.w3.org/2001/XMLSchema#date> .
```

Data is cast back to the appropriate class when it is accessed.
#### Example: Passing in a block as minter

```ruby
my_thing.date
# => [Thu, 19 Jun 2014]
```

Note that you can mix types on a single property.

```ruby
my_thing.date << DateTime.now
my_thing.date << "circa 2014"
my_thing.date
# => [Thu, 19 Jun 2014, Thu, 19 Jun 2014 11:39:21 -0700, "circa 2014"]

puts my_thing.dump :ntriples
# _:g70072864570340 <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Thing> .
# _:g70072864570340 <http://purl.org/dc/terms/date> "2014-06-19Z"^^<http://www.w3.org/2001/XMLSchema#date> .
# _:g70072864570340 <http://purl.org/dc/terms/date> "2014-06-19T11:39:21-07:00"^^<http://www.w3.org/2001/XMLSchema#dateTime> .
# _:g70072864570340 <http://purl.org/dc/terms/date> "circa 2014" .
# create a new resource with a minted local name using passed in block
localname = ActiveTriples::LocalName::Minter.generate_local_name(
DummyResourceWithBaseURI,10,'d') do |prefix|
prefix ||= ""
local_name = SecureRandom.uuid
local_name = prefix + "_blockproc_" + local_name if prefix && prefix.is_a?(String)
local_name
end
# => something like -- "http://example.org/d_blockproc_59beebc5-5238-4aad-bf92-f63fbbd8faaa"
```

Repositories and Persistence
-----------------------------

Resources can persist to various databases and triplestores though integration with [RDF::Repository](http://rubydoc.info/github/ruby-rdf/rdf/RDF/Repository).

#### Example: Passing in a method as minter
```ruby
# Registers in-memory repositories. Other implementations of
# RDF::Repository support persistence to (e.g.) triplestores & NoSQL
# databases.
ActiveTriples::Repositories.add_repository :default, RDF::Repository.new
ActiveTriples::Repositories.add_repository :people, RDF::Repository.new

class Person < ActiveTriples::Resource
configure :type => RDF::FOAF.Person, :base_uri => 'http://example.org/people#', :repository => :people
property :name, :predicate => RDF::FOAF.name
end

class Thing < ActiveTriples::Resource
configure :type => RDF::OWL.Thing, :base_uri => 'http://example.org/things#', :repository => :default
property :title, :predicate => RDF::DC.title
property :description, :predicate => RDF::DC.description
property :creator, :predicate => RDF::DC.creator, :class_name => 'Person'
# minter method
def uuid_minter( *options )
prefix = options[0][:prefix] if ! options.empty? && options[0].is_a?(Hash) && options[0].key?(:prefix)
local_name = SecureRandom.uuid
local_name = prefix + "_method_" + local_name if prefix && prefix.is_a?(String)
local_name
end

t = Thing.new('1')
t.title = 'A Thing'
t.creator = Person.new('1')
t.persisted? # => false
t.creator.first.name = 'Tove'
t.persist!

ActiveTriples::Repositories.repositories[:default].dump :ntriples
# => "<http://example.org/things#1> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://www.w3.org/2002/07/owl#Thing> .\n<http://example.org/things#1> <http://purl.org/dc/terms/title> \"A Thing\" .\n<http://example.org/things#1> <http://purl.org/dc/terms/creator> <http://example.org/people#1> .\n"
# create a new resource with a minted local name using a minter method
localname = ActiveTriples::LocalName::Minter.generate_local_name(
DummyResourceWithBaseURI,10,{:prefix=>"d"}) {|args| uuid_minter(args)}
# => something like -- "http://example.org/d_method_59beebc5-5238-4aad-bf92-f63fbbd8faaa"
```

t.creator.first.persisted? # => false
t.creator.first.persist!
See more examples in spec/active_triples/local_name/minter_spec.rb.

ActiveTriples::Repositories.repositories[:people].dump :ntriples
# => "<http://example.org/people#1> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://xmlns.com/foaf/0.1/Person> .\n<http://example.org/people#1> <http://xmlns.com/foaf/0.1/name> \"Tove\" .\n"
```

Contributing
-------------
## Contributing

Please observe the following guidelines:

Expand Down
File renamed without changes.
61 changes: 36 additions & 25 deletions lib/active_triples/local_name/minter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,56 +7,67 @@ module LocalName
class Minter

##
# Generate a random ID that does not already exist in the
# triplestore.
# Generate a random ID that does not already exist in the triplestore.
#
# @param [Class, #read] resource_class: The ID will be minted for
# an object of this class, conforming to the configurations
# defined in the class' resource model.
# @param [Function, #read] minter_func: funtion to use to mint
# the new ID. If not specified, the default minter function
# will be used to generate an UUID.
# @param [Hash, #read] minter_args: The arguments to be passed
# through to the minter block, if specified.
# @
# @param [Class] the class inheriting from <tt>ActiveTriples::Reource</tt> whose configuration
# is used to generate the full URI for testing for uniqueness of the generated local name
# @param [Integer] the maximum number of attempts to make a unique local name
# @yieldparam the arguments to pass to the minter block (optional)
# @yield the function to use to mint the local name. If not specified, the
# +default_minter+ function will be used to generate an UUID.
# @yieldreturn [String] the generated local name to be tested for uniqueness
#
# @return [String] the generated id
# @return [String] the generated local name
#
# @raise [Exception] if an available ID is not found in
# the maximum allowed tries.
# @raise [ArgumentError] if maximum allowed tries is less than 0
# @raise [ArgumentError] if for_class does not inherit from ActiveTriples::Resources
# @raise [ArgumentError] if minter_block is not a block (does not respond to call)
# @raise [Exception] if for_class does not have base_uri configured
# @raise [Exception] if an available local name is not found in the maximum allowed tries.
#
# @TODO This is inefficient if max_tries is large. Could try
# multi-threading. When using the default_minter included
# in this class, it is unlikely to be a problem and should
# find an ID within the first few attempts.
def self.generate_local_name(for_class, max_tries=10, *minter_args, &minter_block)
raise ArgumentError, 'Argument max_tries must be >= 1 if passed in' if max_tries <= 0
raise ArgumentError, 'Argument for_class must be of type class' unless for_class.class == Class
raise 'Requires base_uri to be defined in for_class.' unless for_class.base_uri

# raise ArgumentError, 'Invalid minter_block.' unless minter_block.respond_to?(:call)
raise ArgumentError, 'Invalid minter_block.' if minter_block && !minter_block.respond_to?(:call)
# raise ArgumentError, 'Invalid minter_block.' if minter_block && !minter_block.kind_of?(Proc)
minter_block ||= proc { default_minter }
raise ArgumentError, 'Argument for_class must inherit from ActiveTriples::Resource' unless for_class < ActiveTriples::Resource
raise 'Requires base_uri to be defined in for_class.' unless for_class.base_uri

raise ArgumentError, 'Invalid minter_block.' if minter_block && !minter_block.respond_to?(:call)
minter_block = proc { |args| default_minter(args) } unless minter_block

found = true
test_id = nil
(1).upto(max_tries) do
test_id = minter_block.call *minter_args
test_id = minter_block.call( *minter_args )
found = for_class.id_persisted?(test_id)
break unless found
end
raise 'Available ID not found. Exceeded maximum tries.' if found
test_id
end


##
# Default minter used by generate_id.
# @param [Hash] options - not used by this minter
#
# @param [Hash] options the options to use while generating the local name
# @option options [String] :prefix the prefix to put before the generated local name (optional)
#
# @note Because <tt>:prefix</tt> is optional, errors in type for <tt>:prefix</tt> are ignored. Any additional
# parameters beyond <tt>:prefix</tt> are also ignored.
#
# @note Best practice is to begin localnames with an alpha character. UUIDs can generate with an alpha or
# numeric as the first character. Pass in an alpha character as <tt>:prefix</tt> to enforce this best
# practice.
#
# @return [String] a uuid
def self.default_minter( *args )
SecureRandom.uuid
def self.default_minter( *options )
prefix = options[0][:prefix] if ! options.empty? && options[0].is_a?(Hash) && options[0].key?(:prefix)
local_name = SecureRandom.uuid
local_name = prefix + local_name if prefix && prefix.is_a?(String)
local_name
end
end
end
Expand Down
Loading

0 comments on commit 716fb4f

Please sign in to comment.