Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial upload

  • Loading branch information...
commit ca68b1bfa10f853dbd30c351fda28dc57211aecb 0 parents
@serebryakov serebryakov authored
22 .gemspec
@@ -0,0 +1,22 @@
+Gem::Specification.new do |gem|
+
+ gem.version = File.read('VERSION').chomp
+ gem.date = File.mtime('VERSION').strftime('%Y-%m-%d')
+ gem.name = 'rdf-mapper'
+ gem.rubyforge_project = 'rdf-mapper'
+ gem.homepage = 'http://github.com/42cities/rdf-mapper/'
+ gem.summary = 'A Ruby ORM that is designed to play nicely with RDF data.'
+ gem.description = 'RDFMapper is a lightweight Ruby ORM that works with RDF data in a Rails-like fashion. Is supports XML, N-Triples, JSON formats, SPARQL and ActiveRecord as data sources.'
+ gem.authors = ['Alex Serebryakov']
+ gem.email = 'serebryakov@gmail.com'
+ gem.platform = Gem::Platform::RUBY
+ gem.files = %w(README.rdoc UNLICENSE VERSION) + Dir.glob('lib/**/*.rb')
+ gem.require_paths = %w(lib)
+ gem.has_rdoc = true
+ gem.add_development_dependency 'rspec', '>= 1.3.0'
+ gem.add_runtime_dependency 'rdf', '>= 0.1.1'
+ gem.add_runtime_dependency 'rdf-xml', '>= 0.0.1'
+ gem.add_runtime_dependency 'patron', '>= 0.4.6'
+ gem.post_install_message = nil
+
+end
7 .gitignore
@@ -0,0 +1,7 @@
+.DS_Store
+.tmp
+.yardoc
+pkg
+tmp
+test
+*.gem
189 README.rdoc
@@ -0,0 +1,189 @@
+= RDFMapper -- Object-relation mapping for RDF data
+
+RDFMapper is an ORM[http://en.wikipedia.org/wiki/Object-relational_mapping]
+written in Ruby that is designed to play nicely with RDF data.
+
+== Features
+
+- 100% Ruby code based on a slim & smart {RDF.rb}[http://rdf.rubyforge.org/] library
+- All the usual Rails methods: find, create, belongs_to, has_many -- you name it
+- Built with performance in mind: all objects are lazy-loaded by default
+- Supports REST, SPARQL and ActiveRecord as RDF data sources
+- Supports XML, N-Triples and JSON out of the box
+
+== Installation
+
+The prefered method of installing RDFMapper is through its gem file (requires
+RubyGems[http://rubygems.rubyforge.org/wiki/wiki.pl]):
+
+ % [sudo] gem install rdf-mapper
+
+The latest version of RDFMapper can be found at
+
+* http://github.com/42cities/rdf-mapper
+
+
+== Contribute
+
+Please note that RDFMapper in under heavy development right now, it's not yet
+production safe. Any contribution (bug tickets, code patches) is more than
+welcome. Email us at team@42cities.com or submit a ticket on
+GitHub[http://github.com/42cities/rdf-mapper/issues]
+
+
+= 5-minute crash course
+
+=== Idea behind RDF models
+
+Models in RDFMapper are essentially RDF nodes that have an ID and at least one triple
+with an rdf:type predicate. Consider the following example:
+
+ <http://example.org/people/237643> rdf:type <http://www.example.org/schema#Person>
+ <http://example.org/people/237643> example:name "John Smith"
+ <http://example.org/people/237643> example:age "27"^^xsd:integer
+
+This set of triples defines a node (with an ID of <http://example.org/people/237643>)
+that has three 'attributes': `example:name`, `example:age`, and `rdf:type`. Now `rdf:type`
+predicate tells us that there's a class (<http://www.example.org/schema#Person>)
+with more or less predefined behavior. And our node (<http://example.org/people/237643>)
+is an instance of that class. We could replicate the same logic in Ruby:
+
+ class Person
+ attr_accessor :id
+ attr_accessor :name
+ attr_accessor :age
+ end
+
+ person = Person.new
+ person.id = "http://example.org/people/237643"
+ person.name = "John Smith"
+ person.age = 27
+
+That's essentially what RDFMapper does. It accepts RDF triples (XML or N-triples),
+creates instances, assigns attributes and binds models together (via Rails-like
+belongs_to and has_many associations).
+
+
+=== Defining a model
+
+Before you start working with RDFMapper, you need to define at least one model. The only
+required setting is its namespace (think XML namespace) or type (think rdf:type). If you
+specify the namespace, it will be used by the model itself (to figure out its rdf:type)
+and by its attributes (to figure out RDF predicates).
+
+ class Person < RDFMapper::Model
+ namespace "http://example.org/#"
+ attribute :name, :type => :text
+ attribute :homepage, :type => :uri, :predicate => 'http://xmlns.com/foaf/0.1/homepage'
+ end
+
+ Person.namespace #=> #<RDF::Vocabulary(http://example.org/#)>
+ Person.type #=> #<RDF::URI(http://example.org/#Person)>
+
+ Person.name.type #=> #<RDF::URI(http://example.org/#name")>
+ Person.homepage.type #=> #<RDF::URI(http://xmlns.com/foaf/0.1/homepage)>
+
+For more information on {RDF::URI}[http://rdf.rubyforge.org/RDF/URI.html],
+{RDF::Vocabulary}[http://rdf.rubyforge.org/RDF/Vocabulary.html] and other
+classes within RDF namespace, refer to {RDF.rb documentation}[http://rdf.rubyforge.org/].
+
+
+=== Defining the data source
+
+By this moment you can work with RDFMapper models with no additional settings.
+However, if you want to load, save and search for your objects, you need to
+specify their data source. RDFMapper comes with 3 different flavors of data
+sources: REST, SPARQL and Rails.
+
+* {RDFMapper::Adapters::SPARQL SPARQL} [*read-only*] -- the standard for RDF data.
+ RDFMapper will query specified SPARQL server over HTTP using standard SPARQL
+ syntax. Currently it supports only a few functions (no subqueries, updates,
+ aggregates, etc.)
+
+* {RDFMapper::Adapters::REST REST} [*read-only*] -- good old HTTP-based data
+ storage. It assumes that an object's ID (which is an URI) is the place to
+ look when you want to get object's properties. For example, if an object has
+ an ID `http://example.org/people/237643`, RDFMapper will download data from
+ this address and parse any RDF triples it finds along the way.
+
+* {RDFMapper::Adapters::Rails Rails} [*read/write*] -- gets the data from an
+ ActiveRecord model (that is Rails model). This adapter assumes an RDFMapper
+ model has a 'mirror' ActiveRecord model with the same attributes and
+ associations.
+
+Assigning data source to a model is easy:
+
+ class Person < RDFMapper::Model
+ adapter :rails # There should be a `Person` class that subclasses ActiveRecord::Base
+ end
+
+ class Person < RDFMapper
+ adapter :rails, :class_name => 'Employee' # ActiveRecord::Base model is called `Employee`
+ end
+
+ class Person < RDFMapper
+ adapter :sparql, {
+ :server => 'http://some-sparql-server.com'
+ :headers => { 'API-Key' => '89d7sfd9sfs' }
+ }
+ end
+
+
+=== Searching
+
+If you search objects by an ID, it's up to the adapter (REST, SPARQL, or Rails) to
+decide what type of ID it requires (an URI, a database column or something else).
+Check out the documentation for each adapter to see how works:
+{RDFMapper::Adapters::SPARQL SPARQL}, {RDFMapper::Adapters::REST REST}, {RDFMapper::Adapters::Rails Rails}.
+
+ Person.all #=> #<PersonCollection:23784623>
+ Person.find('132987') #=> #<Person:217132856>
+ Person.find(:all, :conditions => { :name => 'John' }) #=> #<PersonCollection:32462387>
+
+Note, the objects above are not loaded. RDFMapper will load them once you
+access an attribute of a collection or an object. The following 3 objects are
+loaded instantly, since RDFMapper needs to figure out what their attributes are
+(in this case `nil?`, `name` and `length`).
+
+ Person.find('132987').nil? #=> false
+ Person.find('132987').name #=> "John"
+ Person.find(:all, :conditions => { :name => 'John' }).length #=> 3
+
+You should take extra care when dealing with lazy-loaded models, since
+exceptions may occur when a model is not found:
+
+ Person.find('132987') #=> #<Person:217132856>
+ Person.find('132987').name #=> NoMethodError: undefined method `name' for nil:NilClass
+
+Instead, you should first check if a model exists:
+
+ @person = Person.find('132987')
+ @person.name unless @person.nil?
+
+
+=== Working with attributes
+
+Attributes in RDFMapper work just as you would expect them to work with just one
+small exception. Since any attribute of a model is essentially an RDF triple, you
+can access attributes by their predicates as well:
+
+ class Person < RDFMapper::Model
+ namespace "http://example.org/#"
+ attribute :name, :type => :text
+ attribute :homepage, :type => :uri, :predicate => 'http://xmlns.com/foaf/0.1/homepage'
+ end
+
+ instance = Person.new
+ instance.name #=> "John Smith"
+ instance[:name] #=> "John Smith"
+ instance['http://example.org/#name'] #=> "John Smith"
+ instance.homepage #=> #<RDF::URI(http://johnsmith.com/")>
+ instance['http://xmlns.com/foaf/0.1/homepage'] #=> #<RDF::URI(http://johnsmith.com/")>
+
+
+That's pretty much all you need to know. Go try and let us know what you think!
+
+== License
+
+RDFMapper is free and unencumbered public domain software. For more information,
+see http://unlicense.org or the accompanying UNLICENSE file.
32 Rakefile
@@ -0,0 +1,32 @@
+require 'rake'
+require 'yard'
+require 'spec/rake/spectask'
+
+desc 'Default: run unit tests.'
+task :default => :test
+
+desc 'Test library.'
+Spec::Rake::SpecTask.new(:test) do |test|
+ test.spec_files = Dir.glob('test/**/*_spec.rb')
+ test.spec_opts << '--format specdoc'
+end
+
+desc 'Generate documentation.'
+YARD::Rake::YardocTask.new(:doc) do |t|
+ t.files = ['lib/**/*.rb']
+ t.options = ['--title=RDFMapper']
+end
+
+desc 'Build a gem package'
+task :build_gem do
+ system 'rm rdf-mapper-*.gem'
+ system 'gem build .gemspec'
+end
+
+desc 'Install gem locally'
+task :local_install do
+ system 'sudo gem install --local rdf-mapper-*.gem'
+end
+
+desc 'Build and install gem locally'
+task :install => [:build_gem, :local_install]
25 UNLICENSE
@@ -0,0 +1,25 @@
+This is free and unencumbered software released into the public domain.
+
+Anyone is free to copy, modify, publish, use, compile, sell, or
+distribute this software, either in source code form or as a compiled
+binary, for any purpose, commercial or non-commercial, and by any
+means.
+
+In jurisdictions that recognize copyright laws, the author or authors
+of this software dedicate any and all copyright interest in the
+software to the public domain. We make this dedication for the benefit
+of the public at large and to the detriment of our heirs and
+successors. We intend this dedication to be an overt act of
+relinquishment in perpetuity of all present and future rights to this
+software under copyright law.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+For more information, contact Alex Serebryakov [serebryakov@gmail.com]
+or visit <http://unlicense.org/>
1  VERSION
@@ -0,0 +1 @@
+0.0.1
83 lib/lib/adapters/base.rb
@@ -0,0 +1,83 @@
+module RDFMapper
+ module Adapters
+
+ autoload :Rails, 'lib/adapters/rails'
+ autoload :REST, 'lib/adapters/rest'
+ autoload :SPARQL, 'lib/adapters/sparql'
+
+ ##
+ # Instantiates and returns an instance of an adapter.
+ #
+ # @param [Symbol] name (:rails, :sparql, :rest)
+ # @param [Object] cls subclass of RDFMapper::Model
+ # @param [Hash] options options to pass on to the adapter constructor
+ #
+ # @return [Object] instance of an adapter
+ ##
+ def self.register(name, cls, options = {})
+ self[name].new(cls, options)
+ end
+
+ ##
+ # Returns adapter's class based on specified `name` (:rails, :sparql, :rest)
+ #
+ # @return [Object]
+ ##
+ def self.[](name)
+ case name
+ when :rails then Rails
+ when :sparql then SPARQL
+ when :rest then REST
+ else raise NameError, 'Adapter `%s` not recognized' % value.inspect
+ end
+ end
+
+ ##
+ # Parent class for all adapters. Contains default constructor method
+ # and interface methods that each adapter should override.
+ ##
+ class Base
+
+ ##
+ # All adapters implement Logger
+ ##
+ include RDFMapper::Logger
+
+ ##
+ # Adapter implementation should override this method
+ ##
+ def load(query)
+ raise NotImplementedError, 'Expected adapter to override `load`'
+ end
+
+ ##
+ # Adapter implementation should override this method
+ ##
+ def save(instance)
+ raise NotImplementedError, 'Expected adapter to override `save`'
+ end
+
+ ##
+ # Adapter implementation should override this method
+ ##
+ def reload(instance)
+ raise NotImplementedError, 'Expected adapter to override `save`'
+ end
+
+ ##
+ # Adapter implementation should override this method
+ ##
+ def update(instance)
+ raise NotImplementedError, 'Expected adapter to override `save`'
+ end
+
+ ##
+ # Adapter implementation should override this method
+ ##
+ def create(instance)
+ raise NotImplementedError, 'Expected adapter to override `save`'
+ end
+
+ end
+ end
+end
307 lib/lib/adapters/rails.rb
@@ -0,0 +1,307 @@
+module RDFMapper
+ module Adapters
+ ##
+ # [-]
+ ##
+ class Rails < Base
+
+ ##
+ # [-]
+ ##
+ def initialize(cls, options = {})
+ @rdf, @options = cls, options
+ @options[:skip] ||= []
+ @options[:substitute] ||= { }
+ @options[:substitute][:id] ||= :uid
+ end
+
+
+ ##
+ # [-]
+ ##
+ def load(query)
+ @rdf.associations.values.select do |assoc|
+ assoc.belongs_to?
+ end.map do |assoc|
+ assoc.name
+ end.reject do |name|
+ @options[:skip].include?(name)
+ end.each do |name|
+ query.include!(name)
+ end
+ Query.new(query, @options).find
+ end
+
+ ##
+ # [-]
+ ##
+ def save(instance)
+ if instance[:rails_id].nil?
+ obj = instance.class.find(instance.id.to_s).from(:rails)
+ instance[:rails_id] = obj.rails_id unless obj.nil?
+ end
+ if instance[:rails_id].nil?
+ create(instance)
+ else
+ update(instance)
+ end
+ end
+
+ ##
+ # [-]
+ ##
+ def reload(instance)
+ query = RDFMapper::Scope::Query.new(instance.class, :conditions => { :id => instance.id })
+ Query.new(query, @options).find.first
+ end
+
+ ##
+ # [-]
+ ##
+ def update(instance)
+ query = RDFMapper::Scope::Query.new(instance.class, :conditions => instance.attributes)
+ Query.new(query, @options).update
+ end
+
+ ##
+ # [-]
+ ##
+ def create(instance)
+ query = RDFMapper::Scope::Query.new(instance.class, :conditions => instance.attributes)
+ Query.new(query, @options).create
+ end
+
+
+ private
+
+ def check_for_rails_id(instance)
+ end
+
+ class Query
+
+ include RDFMapper::Logger
+
+ def initialize(query, options = {})
+ @query, @options = query, options
+ @rails = (@options[:class_name] || @query.cls.to_s.demodulize).constantize
+ setup_replacements
+ end
+
+ ##
+ # [-]
+ ##
+ def update
+ record = @rails.update(@query[:rails_id], save_options)
+ record_attributes(record)
+ end
+
+ ##
+ # [-]
+ ##
+ def create
+ record = @rails.create(save_options)
+ record_attributes(record)
+ end
+
+ ##
+ # [-]
+ ##
+ def find
+ @query.check(:rails_id)
+ #
+ debug 'Searching for %s with %s' % [@rails, @query.inspect]
+ debug 'Query: %s' % find_options.inspect
+ #
+ @rails.find(:all, find_options).map do |record|
+ record_attributes(record)
+ end
+ end
+
+
+ private
+
+ ##
+ # [-]
+ ##
+ def record_attributes(record)
+ record_id = [:id, :rails_id].map do |name|
+ [name, record_value(record, name)]
+ end
+ record_props = record_properties(record)
+ record_assoc = record_associations(record)
+ Hash[record_id + record_props + record_assoc]
+ end
+
+ ##
+ # [-]
+ ##
+ def save_options
+ Hash[@query.to_a.map do |condition|
+ name = @replace[condition.name]
+ [name, validate(condition.value)]
+ end.reject do |name, value|
+ name.nil? or value.nil?
+ end]
+ end
+
+ ##
+ # Substitutes names of those attributes specified in `options[:substitute]`
+ # and raises a runtime error for attributes that could not be found in the
+ # database. Returns an object which can then be used with ActiveRecord::Base.find
+ ##
+ def find_options #nodoc
+ { :conditions => SQL.new(@query, @replace).to_a,
+ :order => @query.order,
+ :limit => @query.limit,
+ :offset => @query.offset,
+ :include => @query.include
+ }.delete_if { |name, value| value.nil? }
+ end
+
+ ##
+ # [-]
+ ##
+ def setup_replacements #nodoc
+ @replace = default_replacements
+ @query.flatten.map do |condition|
+ # Original RDF name
+ rdf_name = condition.name
+ # Expected name in the DB
+ expected_name = @replace[rdf_name] || rdf_name
+ # Silently ignore attributes that are not in the DB
+ rails_name = activerecord_attribute?(expected_name)
+ @replace[rdf_name] = rails_name unless rails_name.nil?
+ end
+ end
+
+ ##
+ # [-]
+ ##
+ def default_replacements
+ @options[:substitute].merge({ :rails_id => :id })
+ end
+
+ ##
+ # [-]
+ ##
+ def record_properties(record) #nodoc
+ @query.cls.properties.keys.map do |name|
+ value = record_value(record, name)
+ [name, value]
+ end
+ end
+
+ ##
+ # [-]
+ ##
+ def record_associations(record) #nodoc
+ @query.include.map do |name|
+ value = record_value(record, name)
+ value = value.nil? ? nil : value[@replace[:id]]
+ [name, value]
+ end
+ end
+
+ ##
+ # [-]
+ ##
+ def record_value(record, rdf_name) #nodoc
+ name = default_replacements[rdf_name] || rdf_name
+ unless record.respond_to?(name)
+ nil
+ else
+ record.send(name)
+ end
+ end
+
+ ##
+ # [-]
+ ##
+ def activerecord_attribute?(name) #nodoc
+ activerecord_property?(name) || activerecord_association?(name)
+ end
+
+ ##
+ # [-]
+ ##
+ def activerecord_association?(name) #nodoc
+ reflection = @rails.reflections[name.to_sym]
+ if reflection.nil? or not reflection.belongs_to?
+ return nil
+ end
+ reflection.primary_key_name.to_sym
+ end
+
+ ##
+ # [-]
+ ##
+ def activerecord_property?(name) #nodoc
+ @rails.column_names.include?(name.to_s) ? name.to_sym : nil
+ end
+
+ class SQL
+
+ def initialize(query, replace)
+ @query, @replace = query, replace
+ @text, @values = [], []
+
+ @query.to_a.map do |condition|
+ if condition.kind_of?(query.class)
+ add_query(condition)
+ else
+ add_condition(condition)
+ end
+ end
+ end
+
+ def add_query(query)
+ child = SQL.new(query, @replace)
+ unless child.text.empty?
+ @text.push("(%s)" % child.text)
+ @values.push(*child.values)
+ end
+ end
+
+ def add_condition(condition)
+ name = @replace[condition.name]
+ if name.nil?
+ return nil
+ end
+ if condition.value.kind_of?(Array)
+ @text << "%s IN (?)" % name
+ else
+ @text << "%s %s ?" % [name, condition.eq]
+ end
+ @values << validate(condition.value)
+ end
+
+ def validate(value) #nodoc
+ if value.kind_of? Array
+ return value.map do |item|
+ validate(item)
+ end
+ end
+ if value.kind_of? RDFMapper::Model
+ value[:rails_id]
+ else
+ value
+ end
+ end
+
+ def text
+ @text.join(' %s ' % @query.modifier)
+ end
+
+ def values
+ @values
+ end
+
+ def to_a
+ [text] + values
+ end
+
+ end # SQL
+ end # Query
+ end # Rails
+ end # Adapters
+end # RDFMapper
45 lib/lib/adapters/rest.rb
@@ -0,0 +1,45 @@
+module RDFMapper
+ module Adapters
+ ##
+ # Not yet implemented
+ ##
+ class REST < Base
+
+ ##
+ # @todo. Not implemented
+ ##
+ def load(query)
+ raise NotImplementedError, 'REST adapter is not yet implemented'
+ end
+
+ ##
+ # @todo. Not implemented
+ ##
+ def save(instance)
+ raise NotImplementedError, 'REST adapter is not yet implemented'
+ end
+
+ ##
+ # @todo. Not implemented
+ ##
+ def reload(instance)
+ raise NotImplementedError, 'REST adapter is not yet implemented'
+ end
+
+ ##
+ # @todo. Not implemented
+ ##
+ def update(instance)
+ raise NotImplementedError, 'REST adapter is not yet implemented'
+ end
+
+ ##
+ # @todo. Not implemented
+ ##
+ def create(instance)
+ raise NotImplementedError, 'REST adapter is not yet implemented'
+ end
+
+ end # REST
+ end # Adapters
+end # RDFMapper
105 lib/lib/adapters/sparql.rb
@@ -0,0 +1,105 @@
+module RDFMapper
+ module Adapters
+ ##
+ # [-]
+ ##
+ class SPARQL < Base
+
+ ##
+ # [-]
+ ##
+ def initialize(cls, options = {})
+ @rdf, @options = cls, options
+ end
+
+ ##
+ # [-]
+ ##
+ def load(query)
+ Query.new(query, @options).find
+ end
+
+ class Query
+
+ include RDFMapper::Logger
+
+ ##
+ # [-]
+ ##
+ def initialize(query, options = {})
+ @query, @options = query, options
+ @rdf = @query.cls
+ end
+
+ ##
+ # [-]
+ ##
+ def find
+ describe
+ end
+
+
+ private
+
+ ##
+ # [-]
+ ##
+ def describe
+ data = sparql_writer(:describe) do |writer|
+ triples = @query.to_triples
+ writer.write_triples(triples)
+ # Target always comes first in first triple
+ primary = triples.first.first
+ writer.targets << primary
+ end
+ repository = {}
+ download(data).each_triple do |triple|
+ s, p, o = triple
+ repository[s.to_s] ||= {}
+ repository[s.to_s][p.to_s] ||= []
+ repository[s.to_s][p.to_s] << o
+ end
+ objects(repository)
+ end
+
+ ##
+ # [-]
+ ##
+ def objects(repository)
+ repository.select do |uri, atts|
+ atts.key?(RDF.type.to_s)
+ end.select do |uri, atts|
+ @rdf.type == atts[RDF.type.to_s].first
+ end.map do |uri, atts|
+ atts.delete(RDF.type.to_s)
+ atts[:id] = uri
+ atts
+ end
+ end
+
+ ##
+ # [-]
+ ##
+ def sparql_writer(type, &block)
+ RDF::Writer.for(:sparql).buffer({ :type => type }, &block)
+ end
+
+ ##
+ # [-]
+ ##
+ def download(request)
+ data = RDFMapper::HTTP.post(@options[:server], request)
+
+ if data =~ /\<sparql/
+ RDF::Reader.for(:sparql_results)
+ elsif data =~ /\<rdf:RDF/
+ RDF::Reader.for(:xml)
+ else
+ raise RuntimeError, 'Unknown content type'
+ end.new(data)
+ end
+
+ end # Query
+ end # SPARQL
+ end # Adapters
+end # RDFMapper
95 lib/lib/associations/base.rb
@@ -0,0 +1,95 @@
+module RDFMapper
+ module Associations
+
+ autoload :BelongsTo, 'lib/associations/belongs_to'
+ autoload :HasMany, 'lib/associations/has_many'
+ autoload :HasOne, 'lib/associations/has_one'
+ autoload :HasAndBelongs, 'lib/associations/has_and_belongs'
+
+ ##
+ # Base class for all association types. Contains default constructor.
+ ##
+ class Base
+
+ include RDFMapper::Logger
+
+ def initialize(instance, options = {})
+ @instance = instance
+ @association = options[:cls]
+ @options = options
+ end
+
+ ##
+ # [-]
+ ##
+ def replace(value)
+ raise NotImplementedError, 'Expected association to override `replace`'
+ end
+
+ ##
+ # [-]
+ ##
+ def object(force = false)
+ value.nil? and value.empty? if force
+ self
+ end
+
+ ##
+ # [-]
+ ##
+ def to_statements(options = {})
+ options[:skip] ||= []
+ if value.kind_of? Array
+ items = value
+ else
+ items = [value]
+ end
+ items.reject do |item|
+ options[:skip].include?(items)
+ end.map do |item|
+ node = if options[:full]
+ item.to_statements(:skip => @instance)
+ else
+ item.to_statements(:short => true)
+ end
+ node + [{
+ :subject => @instance.id,
+ :predicate => @options[:type],
+ :object => item.id
+ }]
+ end.flatten
+ end
+
+ ##
+ # Developer-friendly representation of the instance
+ #
+ # @return [String]
+ ##
+ def inspect #nodoc
+ value.inspect
+ end
+
+
+ private
+
+ ##
+ # [-]
+ ##
+ def value
+ raise NotImplementedError, 'Expected association to override `value`'
+ end
+
+ ##
+ # [-]
+ ##
+ def method_missing(symbol, *args, &block)
+ if value.respond_to? symbol
+ value.send(symbol, *args, &block)
+ else
+ raise RuntimeError, 'Undefined method `%s`' % symbol
+ end
+ end
+
+ end # Base
+ end # Associations
+end # RDFMapper
64 lib/lib/associations/belongs_to.rb
@@ -0,0 +1,64 @@
+module RDFMapper
+ module Associations
+ ##
+ # [-]
+ ##
+ class BelongsTo < Base
+
+ ##
+ # [-]
+ ##
+ def id
+ value.id
+ end
+
+ ##
+ # [-]
+ ##
+ def nil?
+ value.nil?
+ end
+
+ ##
+ # [-]
+ ##
+ def kind_of?(type)
+ value.kind_of?(type)
+ end
+
+ ##
+ # Replaces current association with a new object
+ ##
+ def replace(value)
+ if value.kind_of? String
+ @key = RDF::URI.new(value)
+ end
+ if value.kind_of? RDF::URI
+ @key = value
+ end
+ if value.kind_of? RDFMapper::Model
+ @value = value
+ else
+ nil
+ end
+ end
+
+
+ private
+
+ ##
+ # [-]
+ ##
+ def value
+ unless @value.nil?
+ return @value
+ end
+ if @key.nil?
+ return nil
+ end
+ replace(@association.find(@key))
+ end
+
+ end # BelongsTo
+ end # Associations
+end # RDFMapper
17 lib/lib/associations/has_and_belongs.rb
@@ -0,0 +1,17 @@
+module RDFMapper
+ module Associations
+ ##
+ # [-]
+ ##
+ class HasAndBelongs
+
+ ##
+ # [-]
+ ##
+ def initialize(instance, options = {})
+ raise NotImplementedError, 'has_and_belongs_to is not yet implemented'
+ end
+
+ end # HasAndBelongs
+ end # Associations
+end # RDFMapper
147 lib/lib/associations/has_many.rb
@@ -0,0 +1,147 @@
+module RDFMapper
+ module Associations
+ ##
+ # [-]
+ ##
+ class HasMany < Base
+
+ ##
+ # Replaces the collections content by deleting and adding
+ # objects as appropriate.
+ ##
+ def replace(objects)
+ new_objects = filter(objects.to_a)
+ return @value if new_objects.empty?
+
+ new_objects.each do |child|
+ self << child
+ end
+
+ @value ||= []
+ @value.each do |child|
+ delete(child) unless new_objects.include?(child)
+ end
+
+ @value
+ end
+
+ ##
+ # Adds one or more objects to the collection by setting their
+ # foreign keys to the collection's primary key.
+ ##
+ def <<(*objects)
+ objects.to_a.select do |child|
+ child.kind_of? RDFMapper::Model
+ end.each do |child|
+ unless include?(child)
+ child[reverse] = @instance
+ @value << child
+ end
+ end
+ self
+ end
+
+ alias_method :push, :<<
+
+ ##
+ # Removes one or more objects from the collection by removing
+ # the association between objects
+ ##
+ def delete(*objects)
+ objects.each do |child|
+ if include?(child)
+ child[reverse] = nil
+ @value.delete(child)
+ end
+ end
+ self
+ end
+
+ ##
+ # Removes every object from the collection.
+ ##
+ def clear
+ delete(@value)
+ end
+
+ ##
+ # Finds an associated object according to the same rules as
+ # RDFMapper::Model.find.
+ ##
+ def find
+ raise NotImplementedError, '`find` not yet implemented' # TODO
+ end
+
+ ##
+ # Returns one or more new objects of the collection type that
+ # have been instantiated with attributes and linked to this object
+ # through a foreign key, but have not yet been saved.
+ ##
+ def build
+ raise NotImplementedError, '`build` not yet implemented' # TODO
+ end
+
+ ##
+ # Returns a new object of the collection type that has been
+ # instantiated with attributes, linked to this object through
+ # a foreign key, and that has already been saved.
+ ##
+ def create
+ raise NotImplementedError, '`create` not yet implemented' # TODO
+ end
+
+ ##
+ # Returns true if a given object is present in the collection
+ ##
+ def include?(object)
+ @value ||= []
+ @value.include?(object)
+ end
+
+ ##
+ # [-]
+ ##
+ def to_a
+ value
+ end
+
+
+ private
+
+ def filter(objects)
+ objects.select do |child|
+ child.kind_of? RDFMapper::Model
+ end
+ end
+
+ ##
+ # [-]
+ ##
+ def value
+ unless @value.nil?
+ return @value
+ end
+ if @instance.id.nil?
+ return []
+ end
+ replace @association.find(:all, {
+ :conditions => { reverse => @instance },
+ :skip => [reverse]
+ })
+ end
+
+ ##
+ # [-]
+ ##
+ def reverse
+ @reverse ||= @association.has?(nil, @instance)
+ if @reverse.nil?
+ raise RuntimeError, 'Expected %s to belong to %s' % [@association, @instance.class]
+ else
+ @reverse.name
+ end
+ end
+
+ end # HasMany
+ end # Associations
+end # RDFMapper
17 lib/lib/associations/has_one.rb
@@ -0,0 +1,17 @@
+module RDFMapper
+ module Associations
+ ##
+ # [-]
+ ##
+ class HasOne
+
+ ##
+ # [-]
+ ##
+ def initialize(instance, options = {})
+ raise NotImplementedError, 'has_one is not yet implemented'
+ end
+
+ end # HasOne
+ end # Associations
+end # RDFMapper
59 lib/lib/model/association.rb
@@ -0,0 +1,59 @@
+module RDFMapper
+ class Model
+ class << self
+
+ ##
+ # Specifies a one-to-many association. The following methods for retrieval
+ # and query of collections of associated objects will be added:
+ #
+ # * collection(force_load = false) -- Returns an array of all the associated
+ # objects. An empty array is returned if none are found.
+ #
+ # * collection<<(object, ...) -- Adds one or more objects to the collection by
+ # setting their foreign keys to the collection‘s primary key.
+ #
+ # * collection.delete(object, ...) -- Removes one or more objects from the
+ # collection by removing the association between objects.
+ #
+ # * collection=objects -- Replaces the collections content by deleting and
+ # adding objects as appropriate.
+ #
+ # * collection.clear -- Removes every object from the collection.
+ #
+ # * collection.empty? -- Returns true if there are no associated objects.
+ #
+ # * collection.size -- Returns the number of associated objects.
+ #
+ # @param [Symbol] name name of the association
+ #
+ # @param [Symbol] options[:class_name] class name of the association. Use it
+ # only if that name can't be inferred from the association name
+ #
+ # @return [Object] instance of RDFMapper::Attribute
+ ##
+ def has_many(name, options = {})
+ attribute(name, options.merge(:association => :has_many))
+ end
+
+ ##
+ # Specifies a one-to-one association with another class. The following
+ # methods for retrieval and query of the associated object will be added:
+ #
+ # * association(force_reload = false) -- Returns the associated object.
+ # nil is returned if none is found.
+ #
+ # * association=(associate) -- Assigns the associate object.
+ #
+ # @param [Symbol] name name of the association
+ #
+ # @param [Symbol] options[:class_name] class name of the association. Use it
+ # only if that name can't be inferred from the association name
+ ##
+ def belongs_to(name, options = {})
+ attribute(name, options.merge(:association => :belongs_to))
+ end
+
+ end
+ end # Model
+end # RDFMapper
+
186 lib/lib/model/attribute.rb
@@ -0,0 +1,186 @@
+module RDFMapper
+ class Model
+ ##
+ # Contains configuration and convenience methods for model attributes.
+ # Instances of this class are assigned to classes (not instances!) of
+ # RDFMapper::Model.
+ ##
+ class Attribute
+
+ include RDFMapper::Logger
+
+ attr_reader :name
+
+ ##
+ # Constructor is called for each attribute of a model at the time
+ # the model is defined.
+ #
+ # @param [Object] cls class of the model
+ # @param [String] name name of the attribute
+ # @param [Hash] options options to pass on to the property / association constructor
+ #
+ # @return [self]
+ ##
+ def initialize(cls, name, options)
+ @cls = cls
+ @name = name
+ @options = options.dup
+ end
+
+ ##
+ # Checks if this attribute is a `belongs_to` association.
+ #
+ # @return [Boolean]
+ ##
+ def belongs_to?
+ not property? and not multiple?
+ end
+
+ ##
+ # Checks if this attribute is a property (i.e. not an association).
+ #
+ # @return [Boolean]
+ ##
+ def property?
+ @options[:association].nil?
+ end
+
+ ##
+ # Returns attribute's RDF predicate.
+ #
+ # @return [RDF::URI]
+ # @return [nil] if not specified and model has no namespace
+ ##
+ def type
+ # Type is 'cached': lookups based on namespace can be quite slow
+ @type ||= unless @options[:predicate].nil?
+ RDF::URI.new(@options[:predicate].to_s)
+ else
+ # Keep this weird comparison. RDF::Vocabulary doesn't recognize `nil?` or `==`
+ (nil == @cls.namespace) ? nil : @cls.namespace[@name]
+ end
+ end
+
+ ##
+ # Returns class of the associated model. Uses either the `:class_name`
+ # option or relies on association name. It follows ActiveRecord naming
+ # conventions(e.g. has_many should be plural, belongs_to - singular).
+ #
+ # @return [Object] a subclass of RDFMapper::Model
+ # @return [nil] if association is not found or this is a property attribute
+ ##
+ def model
+ # A bit of 'caching': lookups based on namespace can be quite slow
+ unless @model.nil?
+ return @model
+ end
+
+ # Should return nil if this is not an association
+ if property?
+ return nil
+ end
+
+ if @model = model_from_options || model_from_namespace
+ @model
+ else
+ raise RuntimeError, 'Could not find association model for %s (%s :%s)' % [@cls, @options[:association], @name]
+ end
+ end
+
+ ##
+ # Returns the class or an instance of a class associated with this
+ # attribute.
+
+ # When model instance is not specified, it will return Associations::HasMany,
+ # Associations::BelongsTo, Property, etc. Alternatively, when RDFMapper::Model
+ # is specified, it will return an instance of these classes.
+ #
+ # @param [Object] instance instance of RDFMapper::Model
+ # @return [Object] class of this attribute
+ # @return [Object] instance of this attribute
+ ##
+ def value(instance = nil)
+ if nil == instance
+ return attribute_type
+ end
+ attribute_type.new(instance, @options.merge({
+ :cls => model,
+ :type => type,
+ :name => name
+ }))
+ end
+
+ ##
+ # Checks if this attribute has the same predicate and / or value.
+ # Value is accepted both as an instance and a class.
+ ##
+ def matches?(predicate, value = nil)
+ if type.nil? # Always false if attribute predicate is not defined
+ return false
+ end
+ if value == nil # Checking predicates
+ return type.to_s == predicate.to_s
+ end
+ unless value.respond_to? :new # Converting instance to a class
+ value = value.class
+ end
+ if model.nil? # Value is not nil, but model is undefined
+ return false
+ end
+ if value.type != model.type # Value type and model type should match
+ return false
+ end
+ if predicate.nil? # Value and model types match
+ true
+ else
+ predicate == type
+ end
+ end
+
+
+ private
+
+ ##
+ # Returns attribute's class (e.g. Associations::HasMany, Property) based
+ # on the supplied `options[:association]`
+ ##
+ def attribute_type #nodoc
+ case @options[:association]
+ when :has_many then Associations::HasMany
+ when :belongs_to then Associations::BelongsTo
+ when :has_one then Association::HasOne
+ when :has_and_belongs then Association::HasAndBelongs
+ else Property
+ end
+ end
+
+ ##
+ # Derives the name of associated model from `options[:class_name]` and
+ # returns its class.
+ ##
+ def model_from_options #nodoc
+ @options[:class_name].nil? ? nil : @options[:class_name].constantize
+ end
+
+ ##
+ # Derives the name of associated model from association name and
+ # returns its class
+ ##
+ def model_from_namespace #nodoc
+ name = multiple? ? @name.to_s.classify : @name.to_s.pluralize.classify
+ # Keep this weird comparison. RDF::Vocabulary doesn't recognize `nil?` or `==`
+ (nil == @cls.namespace) ? nil : @cls[@cls.namespace[name]]
+ end
+
+ ##
+ # Checks if this attribute is an association with multiple objects
+ # (i.e. `has_many` or `has_and_belongs_to`). Used for deriving association
+ # name (plural / singular)
+ ##
+ def multiple? #nodoc
+ @options[:association] == :has_many || @options[:association] == :has_and_belongs
+ end
+
+ end # Attribute
+ end # Model
+end # RDFMapper
623 lib/lib/model/base.rb
@@ -0,0 +1,623 @@
+module RDFMapper
+
+ require 'lib/model/association'
+ require 'lib/model/output'
+ require 'lib/model/attribute'
+ require 'lib/model/property'
+
+ ##
+ # [-]
+ ##
+ class Model
+
+ class << self
+
+ alias_method :original_name, :name #nodoc
+
+ ##
+ # Sets or returns model's namespace. It is intended to operate as a shortcut:
+ # model and its attributes will calculate their RDF type and predicates
+ # automatically. The following two examples produce identical models:
+ #
+ # class Person < RDFMapper::Model
+ # namespace 'http://example.org/schema#'
+ # attribute :name
+ # attribute :age
+ # end
+ #
+ # class Person < RDFMapper::Model
+ # type 'http://example.org/schema#Person'
+ # attribute :name, :predicate => 'http://example.org/schema#name'
+ # attribute :age, :predicate => 'http://example.org/schema#age'
+ # end
+ #
+ # Person.type #=> 'http://xmlns.com/foaf/0.1/Person'
+ #
+ # @overload namespace(value)
+ # Sets model's namespace
+ # @param [RDF::Vocabulary, RDF::URI, String] value
+ #
+ # @overload namespace
+ # Returns model's namespace
+ # @param [nil]
+ #
+ # @see type
+ # @return [RDF::Vocabulary]
+ ##
+ def namespace(value = nil, options = {})
+ @ns = options[:name] || 'myrdf'
+ case value
+ when NilClass
+ @namespace
+ when RDF::Vocabulary
+ @namespace = value
+ else
+ @namespace = RDF::Vocabulary.new(value.to_s)
+ end
+ end
+
+ def ns
+ @ns || 'myrdf'
+ end
+
+ ##
+ # Sets or returns model's RDF type
+ #
+ # class Company < RDFMapper::Model
+ # type RDF::URI.new('http://example.org/schema#Company')
+ # end
+ #
+ # class Person < RDFMapper::Model
+ # type 'http://example.org/schema#Person'
+ # end
+ #
+ # Company.type #=> #<RDF::URI(http://example.org/schema#Company)>
+ # Person.type #=> #<RDF::URI(http://example.org/schema#Person)>
+ #
+ # @overload type(value)
+ # Sets model's RDF type
+ # @param [RDF::URI, String] value
+ #
+ # @overload type
+ # @param [nil]
+ #
+ # @see namespace
+ # @return [RDF::URI]
+ ##
+ def type(value = nil)
+ unless value.nil?
+ return @type = RDF::URI.new(value.to_s)
+ end
+ unless @type.nil?
+ return @type
+ end
+ (nil == namespace) == true ? nil : namespace[name]
+ end
+
+ ##
+ # Returns model's name without modules. Original class name is stored
+ # as 'original_name'
+ #
+ # module TestModule
+ # class Person < RDFMapper::Model; end
+ # end
+ #
+ # Person.name => 'Person'
+ # Person.original_name => 'TestModule::Person'
+ #
+ # @return [String]
+ ##
+ def name
+ original_name.split('::').last
+ end
+
+ ##
+ # Sets or returns model's connection adapter.
+ #
+ # @overload adapter(instance)
+ # Sets model's connection adapter
+ # @param [Symbol] name adapter name (`:rails`, `:rest` or `:sparql`)
+ # @param [Hash] options options to pass on to the adapter constructor
+ #
+ # @overload adapter
+ # Returns model's connection adapter
+ #
+ # @return [Object] an instance of RDFMapper adapter
+ ##
+ def adapter(name = nil, options = {})
+ return @adapter if name.nil?
+ @adapter = RDFMapper::Adapters.register(name, self, options)
+ end
+
+ ##
+ # Returns a model that subclassed {RDFMapper::Model} and has specified
+ # URI as its rdf:type
+ #
+ # class Person < RDFMapper::Model
+ # type 'http://example.org/schema#Person'
+ # end
+ #
+ # class Company < RDFMapper::Model
+ # namespace 'http://example.org/schema#'
+ # end
+ #
+ # RDFMapper::Model['http://example.org/schema#Person'] #=> Person
+ # RDFMapper::Model['http://example.org/schema#Company'] #=> Company
+ # RDFMapper::Model['http://unknown-url.com/'] #=> nil
+ #
+ # @param [String] URI
+ # @param [RDF::URI] URI
+ #
+ # @return [Object] an RDFMapper model
+ ##
+ def [](uri)
+ return nil if uri.nil?
+ @@subclasses.select do |model|
+ model.type.to_s == uri.to_s
+ end.first
+ end
+
+ ##
+ # Returns RDFMapper::Attribute that is assigned to the specified name.
+ # Accepts symbol, string, RDF::URI as a parameter. Value is optional
+ # and is used for associations.
+ #
+ # class Person < RDFMapper::Model
+ # namespace 'http://example.org/schema#'
+ # attribute :name, :type => :text
+ # has_many :contacts, :predicate => 'http://example.org/schema#has'
+ # has_many :friends, :predicate => 'http://example.org/schema#has'
+ # end
+ #
+ # Person.has?(:name) #=> #<RDFMapper::Model::Attribute>
+ # Person.has?('http://example.org/schema#name') #=> #<RDFMapper::Model::Attribute>
+ # Person.has?('http://example.org/schema#unknown') #=> nil
+ #
+ # Person.has?('http://example.org/schema#has', Contact) #=> #<RDFMapper::Model::Attribute>
+ # Person.has?('http://example.org/schema#has', Contact.new) #=> #<RDFMapper::Model::Attribute>
+ # Person.has?(nil, Contact) #=> #<RDFMapper::Model::Attribute>
+ #
+ # @param [Symbol, RDF::URI, String] name
+ # @param [Object] value
+ #
+ # @return [RDFMapper::Attribute]
+ # @return [nil] if attribute was not found
+ ##
+ def has?(name, value = nil)
+ if name.kind_of? String
+ return has?(RDF::URI.new(name), value)
+ end
+ if name.kind_of? Symbol
+ return attributes[name]
+ end
+ attributes.values.select do |att|
+ att.matches?(name, value)
+ end.first
+ end
+
+ ##
+ # Returns the association name for the supplied predicate and / or value
+ # @see has?
+ #
+ # @param [Symbol, RDF::URI, String] name
+ # @param [Object] value
+ #
+ # @return [Symbol]
+ # @return [nil] if attribute was not found
+ ##
+ def symbol(name, value = nil)
+ att = has?(name, value)
+ att.nil? ? nil : att.name
+ end
+
+ ##
+ # Returns a hash of all attributes with their names as keys and
+ # RDFMapper::Attribute instances as values.
+ #
+ # @return [Hash]
+ ##
+ def attributes
+ @attributes ||= {}
+ end
+
+ ##
+ # Returns a hash of all properties with their names as keys and
+ # RDFMapper::Attribute instances as values.
+ #
+ # @return [Hash]
+ ##
+ def properties
+ Hash[attributes.select { |name, att| att.property? }]
+ end
+
+ ##
+ # Returns a hash of all associations with their names as keys and
+ # RDFMapper::Attribute instances as values.
+ #
+ # @return [Hash]
+ ##
+ def associations
+ Hash[attributes.reject { |name, att| att.property? }]
+ end
+
+ ##
+ # Defines an attribute within a model.
+ #
+ # @param [Symbol] name attribute name
+ # @param [Symbol] options[:type] attribute type (:text, :uri, :integer, :float)
+ # @param [RDF::URI, String] options[:predicate] RDF predicate
+ #
+ # @return [Object] instance of RDFMapper::Model::Attribute
+ ##
+ def attribute(name, options = {})
+ attributes[name.to_sym] = Attribute.new(self, name.to_sym, options)
+ class_eval <<-EOF
+ def #{name}(*args, &block)
+ get_attribute(:#{name}, *args, &block)
+ end
+ def #{name}=(value)
+ set_attribute(:#{name}, value)
+ end
+ EOF
+ end
+
+ ##
+ # Creates an object and saves it via the assigned adapter.
+ # The resulting object is returned whether the object was saved
+ # successfully to the database or not.
+ #
+ # @param [Hash] attributes attributes of the new object
+ # @param [RDF::URI, String] id object's ID
+ #
+ # @return [Object] instance of RDFMapper::Model
+ # @return [nil] if save was unsuccessful
+ ##
+ def create(attributes, id)
+ new(attributes).save(id)
+ end
+
+ ##
+ # Find operates similarly to Rails' ActiveRecord::Base.find function. It has
+ # the same four retrieval approaches:
+ #
+ # * Find by id -- This can either be a specific id, a list of ids, or
+ # an array of ids ([5, 6, 10]).
+ #
+ # * Find first -- This will return the first record matched by the
+ # options used. These options can either be specific conditions or
+ # merely an order. If no record can be matched, `nil` is returned.
+ # Use Model.find(:first, *args) or its shortcut Model.first(*args).
+ #
+ # * Find last - This will return the last record matched by the options
+ # used. These options can either be specific conditions or merely an
+ # order. If no record can be matched, `nil` is returned. Use
+ # Model.find(:last, *args) or its shortcut Model.last(*args).
+ #
+ # * Find all - This will return all the records matched by the options
+ # used. If no records are found, an empty array is returned. Use
+ # Model.find(:all, *args) or its shortcut Model.all(*args).
+ ##
+ def find(*args)
+ options = args.last.is_a?(::Hash) ? args.pop : {}
+ case args.first
+ when :first then find_every(options.merge(:limit => 1)).first
+ when :last then find_every(options).last
+ when :all then find_every(options)
+ else find_from_ids(args, options)
+ end
+ end
+
+ ##
+ # Either finds or creates an object with the specified ID.
+ #
+ # @param [RDF::URI, String] id object's ID
+ # @param [Hash] attributes attributes of the new object
+ #
+ # @return [Object] instance of RDFMapper::Model
+ # @return [nil] if save was unsuccessful
+ ##
+ def find_or_create(id, attributes = {})
+ instance = find(id)
+ instance.nil? ? create(id, attributes) : instance
+ end
+
+ ##
+ # A convenience wrapper for find(:first, *args). You can pass in
+ # all the same arguments to this method as you can to find(:first).
+ #
+ # @see find
+ ##
+ def first(*args)
+ find(:first, *args)
+ end
+
+ ##
+ # A convenience wrapper for find(:last, *args). You can pass in
+ # all the same arguments to this method as you can to find(:last).
+ #
+ # @see find
+ ##
+ def last(*args)
+ find(:last, *args)
+ end
+
+ ##
+ # This is an alias for find(:all). You can pass in all the same
+ # arguments to this method as you can to find(:all).
+ #
+ # @see find
+ ##
+ def all(*args)
+ find(:all, *args)
+ end
+
+
+ private
+
+ ##
+ # Returns an Array of instances that match specified conditions.
+ # Note that they are not loaded until they are accessed (lazy loading)
+ ##
+ def find_every(options) #nodoc
+ RDFMapper::Scope::Collection.new(self, options)
+ end
+
+ ##
+ # Returns instances with specified IDs. Depending on the number
+ # of IDs it returns either an Array or a single instance (or nil
+ # if nothing was found)
+ ##
+ def find_from_ids(ids, options) #nodoc
+ unless ids.kind_of?(Array)
+ ids = [ids]
+ end
+ options[:conditions] ||= { }
+ options[:conditions][:id] = ids
+ result = find_every(options)
+ case ids.size
+ when 0 then []
+ when 1 then result.first
+ else result
+ end
+ end
+
+ ##
+ # Keeps track of all models that subclass RDFMapper::Model
+ ##
+ def inherited(subclass) #nodoc
+ @@subclasses ||= []
+ @@subclasses << subclass
+ end
+
+ end
+
+ include RDFMapper::Logger
+
+ ##
+ # Creates a new instance of a model with specified attributes.
+ # Note that attributes include properties as well as associations.
+ # It also accepts URIs in addition to symbols:
+ #
+ # class Company << RDFMapper::Model
+ # namespace 'http://myschema.com/#'
+ # has_many :people
+ # end
+ #
+ # class Person << RDFMapper::Model
+ # namespace 'http://myschema.com/#'
+ # attribute :name, :type => text
+ # belongs_to :company, :predicate => 'http://myschema.com/#employer'
+ # end
+ #
+ # The following two examples create identical models:
+ #
+ # Person.new(:name => 'John')
+ # Person.new('http://myschema.com/#name' => 'John')
+ #
+ # And so do the following two examples:
+ #
+ # @company = Company.new(:name => 'MyCo Inc.')
+ #
+ # Person.new(:company => @company)
+ # Person.new('http://myschema.com/#employer' => @company)
+ #
+ # @param [Hash] attributes attributes of the new object
+ # @return [Object] instance of RDFMapper::Model
+ ##
+ def initialize(attributes = {})
+ @arbitrary = {}
+ @attributes = {}
+ @id = nil
+
+ self.class.attributes.map do |name, att|
+ @attributes[name] = att.value(self)
+ end
+
+ self.attributes = attributes
+ yield self if block_given?
+ end
+
+ ##
+ # Returns objects's unique ID.
+ #
+ # @return [RDF::URI] object's ID
+ ##
+ def id(*args)
+ @id.nil? ? nil : @id.dup
+ end
+
+ ##
+ # Compares instances based on their IDs.
+ #
+ # @return [Boolean]
+ ##
+ def ==(other)
+ (other.nil? or other.id.nil?) ? false : (id == other.id)
+ end
+
+ alias_method :eql?, :==
+ alias_method :equal?, :==
+
+ ##
+ # Returns the value of the attribute identified by `name` after it
+ # has been typecast (for example, "2004-12-12" is cast to a date
+ # object, like Date.new(2004, 12, 12)). (Alias for the private
+ # get_attribute method).
+ #
+ # @param [Symbol, String, RDF::URI] name attribute name or predicate URI
+ # @return [Object] instance of a property or an association
+ ##
+ def [](name)
+ unless name.kind_of? Symbol
+ name = self.class.symbol(name)
+ end
+ name.nil? ? nil : get_attribute(name)
+ end
+
+ ##
+ # Updates the attribute identified by `name` with the specified
+ # value. (Alias for the private set_attribute method).
+ #
+ #
+ # @param [Symbol, String, RDF::URI] name attribute name or predicate URI
+ # @param [Object] value new value of the attribute
+ #
+ # @return [Object] instance of a property or an association
+ ##
+ def []=(name, value)
+ unless name.kind_of? Symbol
+ name = self.class.symbol(name)
+ end
+ name.nil? ? nil : set_attribute(name, value)
+ end
+
+ ##
+ # Returns a hash of all the attributes with their names as keys and
+ # the attributes' values as values.
+ #
+ # @return [Hash] all attributes of an instance (name => value)
+ ##
+ def attributes(*args)
+ @attributes.merge(@arbitrary)
+ end
+
+ ##
+ # Allows you to set all the attributes at once by passing in a hash
+ # with keys matching attribute names or RDF predicates.
+ #
+ # @param [Hash] attributes object's new attributes
+ # @return [Hash] hash of all attributes (name => value)
+ ##
+ def attributes=(hash)
+ return unless hash.kind_of? Hash
+ hash.nil? ? nil : hash.each { |name, value| self[name] = value }
+ end
+
+ ##
+ # Checks whether the model originated from or was saved to
+ # a data source (in other word, whether it has RDF ID).
+ #
+ # @return [Boolean]
+ ##
+ def new?
+ id.nil?
+ end
+
+ alias_method :new_record?, :==
+
+ ##
+ # Saves the instance. If the model is new, a record gets created
+ # via the specified adapter (ID must be supplied in this case),
+ # otherwise the existing record gets updated.
+ #
+ # @param [RDF::URI, String] id object's ID
+ # @return [Object] self
+ # @return [nil] if save was unsuccessful
+ ##
+ def save(id = nil)
+ # Raise error if adapter is unspecified
+ check_for_adapter
+
+ if new? and id.nil?
+ raise RuntimeError, 'Save failed. ID must be specified'
+ end
+ if new?
+ self.id = id
+ end
+ self.attributes = self.class.adapter.save(self)
+ self
+ end
+
+ ##
+ # [-]
+ ##
+ def reload
+ # Raise error if adapter is unspecified
+ check_for_adapter
+
+ if id.nil?
+ raise RuntimeError, 'Reload failed. Model has no ID'
+ end
+
+ self.attributes = self.class.adapter.reload(self)
+ self
+ end
+
+ ##
+ # Developer-friendly representation of the instance.
+ #
+ # @return [String]
+ ##
+ def inspect #nodoc
+ "#<%s:%s>" % [self.class, object_id]
+ end
+
+
+ private
+
+ ##
+ # Raises an error if adapter is undefined.
+ ##
+ def check_for_adapter #nodoc
+ if self.class.adapter.nil?
+ raise RuntimeError, 'Save failed. Model adapter is undefined'
+ end
+ end
+
+ ##
+ # Sets ID of this object (must be RDF::URI or a String).
+ ##
+ def id=(value) #nodoc
+ @id = RDF::URI.new(value.to_s)
+ end
+
+ ##
+ # Returns the value of an attribute identified by `name` after it
+ # has been typecast (for example, "2004-12-12" is cast to a date
+ # object, like Date.new(2004, 12, 12)).
+ ##
+ def get_attribute(name, *args, &block) #nodoc
+ if @attributes.key?(name)
+ @attributes[name].object(*args, &block)
+ else
+ @arbitrary[name]
+ end
+ end
+
+ ##
+ # Updates the attribute identified by `name` with the specified value.
+ ##
+ def set_attribute(name, value) #nodoc
+ if name == :id
+ return nil
+ end
+ if @attributes.key?(name)
+ @attributes[name].replace(value)
+ else
+ @arbitrary[name] = value
+ end
+ end
+
+ end # Model
+end # RDFMapper
+
70 lib/lib/model/output.rb
@@ -0,0 +1,70 @@
+module RDFMapper
+ class Model
+
+ ##
+ # RDF XML representaion of the instance.
+ #
+ # @todo. Not implemented
+ #
+ # @param [Hash] options [TODO]
+ # @return [String]
+ ##
+ def to_xml(options = {})
+ RDF::Writer.for(:xml).buffer({ :declaration => false }) do |writer|
+ if self.class.namespace
+ writer.namespace!(self.class.namespace, self.class.ns)
+ end
+ to_triples.each do |triple|
+ writer << triple
+ end
+ end
+ end
+
+ ##
+ # [-]
+ ##
+ def to_triples(options = {})
+ to_statements(options).map do |statement|
+ [ statement[:subject], statement[:predicate], statement[:object] ]
+ end
+ end
+
+ ##
+ # options[:short] - class declaration only
+ # options[:full] - include associations
+ ##
+ def to_statements(options = {})
+ if options[:full]
+ atts = attribute_statements(options)
+ elsif options[:short]
+ return type_statement
+ else
+ atts = attribute_statements
+ end
+ type_statement + atts
+ end
+
+ private
+
+ ##
+ # [-]
+ ##
+ def attribute_statements(options = {})
+ @attributes.map do |name, att|
+ att.to_statements(options)
+ end.flatten.compact
+ end
+
+ def associations_statements
+
+ end
+
+ def type_statement
+ [{ :subject => id,
+ :predicate => RDF.type,
+ :object => self.class.type }]
+ end
+
+ end # Model
+end # RDFMapper
+
78 lib/lib/model/property.rb
@@ -0,0 +1,78 @@
+module RDFMapper
+ class Model
+ class Property
+
+ def initialize(instance, options = {}, *args)
+ @instance = instance
+ @options = options
+ @value = nil
+ @new = true
+ end
+
+ ##
+ # Checks if property has default (nil) value.
+ #
+ # @params [Boolean]
+ ##
+ def new?
+ @new
+ end
+
+ ##
+ # Assigns a new value to the property.
+ #
+ # @param [Object] value
+ # @return [Object]
+ ##
+ def replace(value)
+ @new = false
+ if value.kind_of? Array
+ value = value.first
+ end
+ if value.kind_of? RDF::Literal
+ value = value.object
+ end
+ @value = case @options[:type]
+ when :text then value.to_s
+ when :integer then value.to_i
+ when :float then value.to_f
+ when :uri then RDF::URI.new(value.to_s)
+ else value.to_s
+ end
+ end
+
+ ##
+ # [-]
+ ##
+ def object(*args)
+ @value
+ end
+
+ ##
+ # [-]
+ ##
+ def to_statements(options = {})
+ { :subject => @instance.id,
+ :predicate => @options[:type],
+ :object => RDF::Literal.new(@value) }
+ end
+
+ ##
+ # Developer-friendly representation of the instance
+ #
+ # @return [String]
+ ##
+ def inspect #nodoc
+ @value.inspect
+ end
+
+ ##
+ # [-]
+ ##
+ def method_missing(symbol, *args, &block)
+ @value.send(symbol, *args, &block)
+ end
+
+ end # Property
+ end # Model
+end # RDFMapper
165 lib/lib/scope/collection.rb
@@ -0,0 +1,165 @@
+module RDFMapper
+ module Scope
+ ##
+ # This class contains collections of models. It is primarily used in
+ # search queries (find(:all) queries will yield instances of this
+ # class) and associations.
+ #
+ # It implements most commonly used Array and Enumerable methods.
+ ##
+ class Collection
+
+ attr_reader :loader #Temporary
+
+ def initialize(cls, options)
+ @loader = Loader.new(cls, options)
+ @models = []
+ @cls = cls
+ end
+
+ ##
+ # Set data adapter for the query and return self. This will override
+ # the default model adapter. It is intended to be used as a chain method:
+ #
+ # Person.find(:all).from(:rest) #=> #<PersonCollection:217132856>
+ # Person.find(:all).from(:rest).length #=> 10
+ #
+ # @param [Symbol] adapter (:rails, :sparql, :rest)
+ # @param [Hash] options options to pass on to the adapter constructor
+ #
+ # @return [self]
+ ##
+ def from(adapter, options = {})
+ @loader.from(adapter, options)
+ self
+ end
+
+ ##
+ # Returns first object of the collection. Note that the
+ # object is not yet loaded at this point.
+ #
+ # @return [Object]
+ ##
+ def first
+ at(0)
+ end
+
+ ##
+ # Returns first object of the collection. Note that the
+ # object is not yet loaded at this point.
+ #
+ # @return [Object]
+ ##
+ def last
+ at(-1)
+ end
+
+ ##
+ # Returns the object at `index`. Note that the object is
+ # not yet loaded at this point.
+ #
+ # @param [Integer] index
+ # @return [Object]
+ ##
+ def [](index)
+ at(index)
+ end
+
+ ##
+ # Returns the object at `index`. Note that the object is
+ # not yet loaded at this point.
+ #
+ # @param [Integer] index
+ # @return [Object]
+ ##
+ def at(index)
+ @models[index] ||= @loader.get(index)
+ end
+
+ ##
+ # Returns true if collection has no objects.
+ #
+ # @param [Boolean]
+ ##
+ def empty?
+ length == 0
+ end
+
+ ##
+ # Returns the number of objects in a collection.
+ #
+ # @return [Integer]
+ ##
+ def length
+ @loader.length
+ end
+
+ ##
+ # Calls block once for each object in a collection, passing
+ # that element as a parameter.
+ #
+ # @yield [Object]
+ # @return [self]
+ ##
+ def each(&block)
+ items.each { |x| block.call(x) }
+ self
+ end
+
+ ##
+ # Invokes block once for each object in a collection. Creates
+ # a new array containing the values returned by the block
+ #
+ # @yield [Object]
+ # @return [Array]
+ ##
+ def map(&block)
+ items.map { |x| block.call(x) }
+ end
+
+ ##
+ # Returns true if collection contains specified object.
+ #
+ # @param [Object] object instance of RDFMapper::Model
+ # @return [Boolean]
+ ##
+ def exists?(object)
+ items.include?(object)
+ end
+
+ ##
+ # Converts collection into Array.
+ #
+ # @return [Array]
+ ##
+ def to_a
+ items
+ end
+
+ alias_method :size, :length
+ alias_method :collect, :map
+ alias_method :slice, :[]
+ alias_method :include?, :exists?
+
+ ##
+ # Developer-friendly representation of the instance
+ #
+ # @return [String]
+ ##
+ def inspect #nodoc
+ "#<%sCollection:%s>" % [@cls, object_id]
+ end
+
+
+ private
+
+ ##
+ # Loads entire collection (if needed) and returns all objects.
+ ##
+ def items #nodoc
+ (0..length-1).map { |x| at(x) }
+ end
+
+ end # Collection
+ end # Scope
+end # RDFMapper
132 lib/lib/scope/condition.rb
@@ -0,0 +1,132 @@
+module RDFMapper
+ module Scope
+ ##
+ # [-]
+ ##
+ class Condition
+
+ attr_reader :eq
+
+ def initialize(cls, att, value, eq = '=')
+ @cls, @att, @value, @eq = cls, att, value, eq
+ end
+
+ ##
+ # [-]
+ ##
+ def name
+ if @att == :id
+ return @att
+ end
+ unless att = @cls.has?(@att)
+ raise RuntimeError, 'Undefined attribute %s for %s' % [@att, @cls]
+ else
+ att.name
+ end
+ end
+
+ ##
+ # [-]
+ ##
+ def value(required = [])
+ association? ? association(required) : literal
+ end
+
+ alias_method :check, :value
+
+ ##
+ # [-]
+ ##
+ def to_triples(subject)
+ to_statements(subject).map do |statement|
+ [ statement[:subject], statement[:predicate], statement[:object] ]
+ end
+ end
+
+ ##
+ # [-]
+ ##
+ def to_statements(subject)
+ if association?
+ object = association.id
+ rdf_type = association.to_triples(:short => true)
+ else
+ object = RDF::Query::Variable.new
+ object.bind(value, eq)
+ rdf_type = []
+ end
+ rdf_type + [{
+ :subject => subject,
+ :predicate => @cls.has?(@att).type,
+ :object => object
+ }]
+ end
+
+ ##
+ # Developer-friendly representation of the instance.
+ #
+ # @return [String]
+ ##
+ def inspect #nodoc
+ "#<Condition:(%s%s%s)>" % [name, eq, value]
+ end
+
+
+ private
+
+ ##
+ # [-]
+ ##
+ def association(required = [], value = nil) #nodoc
+ if value.nil?
+ value = @value
+ end
+ if value.kind_of? Array or value.kind_of? RDFMapper::Scope::Collection
+ return value.map do |item|
+ association(required, item)
+ end
+ end
+ unless value.kind_of? RDFMapper::Model
+ att = @cls.associations[name]
+ value = att.model.find(value.to_s)
+ end
+ required.each do |att|
+ if value[att].nil?
+ raise RuntimeError, 'Expected %s to have %s' % [value, att] if value.reload[att].nil?
+ end
+ end
+ value
+ end
+
+ ##
+ # [-]
+ ##
+ def literal(value = nil) #nodoc
+ if value.nil?
+ value = @value
+ end
+ if value.kind_of? Array
+ return value.map do |item|
+ literal(item)
+ end
+ end
+ if value.kind_of? RDF::Literal
+ value.object
+ elsif value.kind_of? RDF::URI
+ value.to_s
+ else
+ value
+ end
+ end
+
+ ##
+ # Return the association name that has `name_or_key` as foreign key or name.
+ ##
+ def association? #nodoc
+ att = @cls.associations[name]
+ not (att.nil? or att.property?)
+ end
+
+ end # Condition
+ end # Scope
+end # RDFMapper
111 lib/lib/scope/loader.rb
@@ -0,0 +1,111 @@
+module RDFMapper
+ module Scope
+ ##
+ # Loader is responsible for loading and updating model attributes. An instance
+ # of Loader is assigned to each search query and association.
+ ##
+ class Loader
+
+ def initialize(cls, options = {})
+ @conditions = Query.new(cls, options)
+ @objects = []
+ @cls = cls
+ end
+
+ ##
+ # Checks if model ID is specified within conditions. Returns
+ # RDF::URI if found, nil otherwise.
+ #
+ # @return [RDF::URI]
+ # @return [nil]
+ ##
+ def has_id?
+ @conditions[:id].nil? ? nil : RDF::URI.new(@conditions[:id])
+ end
+
+ ##
+ # Sets data adapter for this loader. This will override default model adapter.
+ #
+ # @param [Symbol] adapter (:rails, :sparql, :rest)
+ # @param [Hash] options options to pass on to the adapter constructor
+ # @return [Object] adapter instance
+ ##
+ def from(adapter, options = {})
+ @adapter = RDFMapper::Adapters.register(adapter, @cls, options)
+ end
+
+ ##
+ # Returns the number of loaded objects.
+ #
+ # @return [Integer]
+ ##
+ def length
+ load.length
+ end
+
+ ##
+ # Creates a new 'scoped' instance of RDFMapper::Model.
+ #
+ # @param [Integer] index
+ # @return [Object]
+ ##
+ def get(index)
+ instance = @cls.new(@objects[index])
+ RDFMapper::Scope.apply(instance, self, index)
+ end
+
+ ##
+ # Updates an existing 'scoped' instance of RDFMapper::Model
+ # (sets ID and attributes).
+ #
+ # @param [Integer] index
+ # @param [Object] instance
+ # @return [Object]
+ ##
+ def update(index, instance = nil) #nodoc
+ atts = load[index]
+ if atts.nil?
+ return instance.send(:nil!)
+ end
+ instance.send(:id=, atts[:id])
+ instance.attributes = atts
+ instance
+ end
+
+ ##
+ # Developer-friendly representation of the instance
+ #
+ # @return [String]
+ ##
+ def inspect #nodoc
+ "#<%sLoader:%s>" % [@cls, object_id]
+ end
+
+ private
+
+ ##
+ # Returns adapter class to be used when loading. It's either the
+ # default model adapter or the one explicitly specified via `from`.
+ ##
+ def adapter #nodoc
+ @adapter || @cls.adapter
+ end
+
+ ##
+ # Loads and returns objects. Objects are 'cached' and not reloaded
+ # upon subsequent requests.
+ ##
+ def load #nodoc
+ if @loaded
+ return @objects
+ end
+ if adapter.nil?
+ raise RuntimeError, "No adapter specified for %s" % @cls
+ end
+ @loaded = true
+ @objects = adapter.load(@conditions)
+ end
+
+ end # Loader
+ end # Scope
+end # RDFMapper
129 lib/lib/scope/model.rb
@@ -0,0 +1,129 @@
+module RDFMapper
+ module Scope
+
+ require 'lib/scope/loader'
+ require 'lib/scope/collection'
+ require 'lib/scope/query'
+ require 'lib/scope/condition'
+
+
+ def self.apply(instance, loader, index)
+ instance.extend(Model)
+ instance.send(:scoped!, loader, index)
+ end
+
+ ##
+ # Extension of RDFMapper::Model that implements lazy-loading. All models
+ # in collections and search queries will implement this module by default.
+ ##
+ module Model
+
+ ##
+ # Returns instance's unique ID (as RDF::URI).
+ ##
+ def id
+ super || @loader.has_id? || load.id
+ end
+
+ ##
+ # Set data adapter for the query and return self. This will override
+ # the default model adapter. It is intended to be used as a chain method:
+ #
+ # Person.find(:first).from(:rails) #=> #<Person:217132856>
+ # Person.find(:first).from(:rails).name #=> 'John'
+ #
+ # @param [Symbol] adapter (:rails, :sparql, :rest)
+ # @param [Hash] options options to pass on to the adapter constructor
+ #
+ # @return [self]
+ ##
+ def from(adapter, options = {})
+ @loader.from(adapter, options)
+ self
+ end
+
+ ##
+ # In addition to the original method, preloads the model.
+ ##
+ def [](name)
+ super || (@loaded ? nil : load[name])
+ end
+
+ ##
+ # Returns a hash of all the attributes with their names as keys and
+ # the attributes' values as values. If model is unloaded, it will
+ # preload it before returning its attributes.
+ #
+ # @return [Hash] all attributes of an instance (name => value)
+ ##
+ def attributes
+ check_for_nil_error
+ @loaded ? super : load.attributes
+ end
+
+ ##
+ # Checks if the instance is `nil`. Will load the instance to find it out.
+ #
+ # @return [Boolean]
+ ##
+ def nil?
+ @nil or (not @loaded and load.nil?)
+ end
+
+ ##
+ # Developer-friendly representation of the instance.
+ #
+ # @return [String]
+ ##
+ def inspect #nodoc
+ "#<Scoped|%s:%s>" % [self.class, object_id]
+ end
+