├── README ├── VERSION ├── .gitignore ├── AUTHORS ├── .yardopts ├── lib ├── spira │ ├── base.rb │ ├── types │ │ ├── native.rb │ │ ├── uri.rb │ │ ├── integer.rb │ │ ├── string.rb │ │ ├── float.rb │ │ ├── any.rb │ │ ├── boolean.rb │ │ └── decimal.rb │ ├── version.rb │ ├── exceptions.rb │ ├── types.rb │ ├── resource │ │ ├── validations.rb │ │ ├── dsl.rb │ │ ├── class_methods.rb │ │ └── instance_methods.rb │ ├── yard.rb │ ├── type.rb │ ├── errors.rb │ └── resource.rb └── spira.rb ├── spec ├── fixtures │ ├── bob.nt │ ├── relations.nt │ ├── non_model_data.nt │ ├── types.nt │ └── has_many.nt ├── spec_helper.rb ├── types │ ├── integer.spec │ ├── uri.spec │ ├── string.spec │ ├── decimal.spec │ ├── float.spec │ ├── any.spec │ └── boolean.spec ├── nodes.spec ├── type_classes.spec ├── errors.spec ├── enumerable.spec ├── non_model_data.spec ├── dirty.spec ├── base_uri.spec ├── vocabulary.spec ├── validations.spec ├── property_types.spec ├── instantiation.spec ├── repository.spec ├── has_many.spec ├── basic.spec ├── loading.spec ├── rdf_types.spec ├── relations.spec ├── hooks.spec ├── inheritance.spec └── update.spec ├── UNLICENSE ├── spira.gemspec ├── Rakefile ├── CHANGES.md └── README.md /README: -------------------------------------------------------------------------------- 1 | README.md -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.0.11 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | pkg 3 | coverage 4 | .yardoc 5 | doc 6 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | * Ben Lavender 2 | * Arto Bendiken 3 | * Nicholas J Humfrey 4 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --title "Spira: Breathing life into linked data" 2 | --output-dir doc/yard 3 | --protected 4 | --no-private 5 | --hide-void-return 6 | --markup markdown 7 | --readme README.md 8 | - 9 | AUTHORS 10 | UNLICENSE 11 | VERSION 12 | CHANGES.md 13 | -------------------------------------------------------------------------------- /lib/spira/base.rb: -------------------------------------------------------------------------------- 1 | module Spira 2 | 3 | ## 4 | # Spira::Base does nothing but include Spira::Resource, if it's more your 5 | # style to do inheritance than module inclusion. 6 | # 7 | # @see Spira::Resource 8 | class Base 9 | include Spira::Resource 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/fixtures/bob.nt: -------------------------------------------------------------------------------- 1 | "15"^^ . 2 | "Bob Smith" . 3 | 4 | "15.00"^^ . 5 | "Bad Age" . 6 | -------------------------------------------------------------------------------- /lib/spira/types/native.rb: -------------------------------------------------------------------------------- 1 | module Spira::Types 2 | 3 | ## 4 | # This type is a native type, doing no conversion to Ruby types. The naked 5 | # RDF::Value (URI, Node, Literal, etc) will be used, and no deserialization 6 | # is done. 7 | # 8 | # @see Spira::Type 9 | class Native 10 | 11 | include Spira::Type 12 | 13 | def self.unserialize(value) 14 | value 15 | end 16 | 17 | def self.serialize(value) 18 | value 19 | end 20 | 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/spira/version.rb: -------------------------------------------------------------------------------- 1 | module Spira 2 | module VERSION 3 | MAJOR = 0 4 | MINOR = 0 5 | TINY = 11 6 | EXTRA = nil 7 | 8 | STRING = [MAJOR, MINOR, TINY].join('.') 9 | STRING << ".#{EXTRA}" if EXTRA 10 | 11 | ## 12 | # @return [String] 13 | def self.to_s() STRING end 14 | 15 | ## 16 | # @return [String] 17 | def self.to_str() STRING end 18 | 19 | ## 20 | # @return [Array(Integer, Integer, Integer)] 21 | def self.to_a() [MAJOR, MINOR, TINY] end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/spira/types/uri.rb: -------------------------------------------------------------------------------- 1 | module Spira::Types 2 | 3 | ## 4 | # This type takes RDF Resource objects and provides RDF::URI objects for the 5 | # ruby representation. 6 | # 7 | # @see Spira::Type 8 | # @see http://rdf.rubyforge.org/RDF/URI.html 9 | class URI 10 | 11 | include Spira::Type 12 | 13 | def self.unserialize(value) 14 | RDF::URI(value) 15 | end 16 | 17 | def self.serialize(value) 18 | RDF::URI(value) 19 | end 20 | 21 | register_alias :uri 22 | register_alias RDF::URI 23 | 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $:.unshift(File.join(File.dirname(__FILE__),'..','lib')) 2 | $:.unshift(File.join(File.dirname(__FILE__),'fixtures','')) 3 | #$:.unshift(File.join(File.dirname(__FILE__),'..','..','rdf-spec','lib')) 4 | #$:.unshift(File.join(File.dirname(__FILE__),'..','..','rdf','lib')) 5 | #$:.unshift(File.join(File.dirname(__FILE__),'..','..','rdf-isomorphic','lib')) 6 | require 'spira' 7 | require 'rdf/spec/enumerable' 8 | require 'rdf/spec' 9 | require 'rdf/isomorphic' 10 | 11 | def fixture(filename) 12 | File.join(File.dirname(__FILE__),'fixtures',filename) 13 | end 14 | 15 | -------------------------------------------------------------------------------- /lib/spira/exceptions.rb: -------------------------------------------------------------------------------- 1 | module Spira 2 | 3 | ## 4 | # For cases when a method is called which requires a `type` method be 5 | # declared on a Spira class. 6 | class NoTypeError < StandardError ; end 7 | 8 | ## 9 | # For cases when a projection fails a validation check 10 | class ValidationError < StandardError ; end 11 | 12 | ## 13 | # For cases in which a repository is required but none has been given 14 | class NoRepositoryError < StandardError ; end 15 | 16 | ## 17 | # For errors in the DSL, such as invalid predicates 18 | class ResourceDeclarationError < StandardError ; end 19 | end 20 | -------------------------------------------------------------------------------- /lib/spira/types/integer.rb: -------------------------------------------------------------------------------- 1 | module Spira::Types 2 | 3 | ## 4 | # A {Spira::Type} for integer values. Values will be associated with the 5 | # `XSD.integer` type. 6 | # 7 | # A {Spira::Resource} property can reference this type as 8 | # `Spira::Types::Integer`, `Integer`, or `XSD.integer`. 9 | # 10 | # @see Spira::Type 11 | # @see http://rdf.rubyforge.org/RDF/Literal.html 12 | class Integer 13 | 14 | include Spira::Type 15 | 16 | def self.unserialize(value) 17 | value.object 18 | end 19 | 20 | def self.serialize(value) 21 | RDF::Literal.new(value) 22 | end 23 | 24 | register_alias XSD.integer 25 | 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/spira/types/string.rb: -------------------------------------------------------------------------------- 1 | module Spira::Types 2 | 3 | ## 4 | # A {Spira::Type} for string values. Values will be associated with the 5 | # `XSD.string` type with no language code. 6 | # 7 | # A {Spira::Resource} property can reference this type as 8 | # `Spira::Types::String`, `String`, or `XSD.string`. 9 | # 10 | # @see Spira::Type 11 | # @see http://rdf.rubyforge.org/RDF/Literal.html 12 | class String 13 | 14 | include Spira::Type 15 | 16 | def self.unserialize(value) 17 | value.object.to_s 18 | end 19 | 20 | def self.serialize(value) 21 | RDF::Literal.new(value.to_s) 22 | end 23 | 24 | register_alias XSD.string 25 | 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/fixtures/relations.nt: -------------------------------------------------------------------------------- 1 | "Nirvana" . 2 | 3 | 4 | 5 | "Nevermind" . 6 | . 7 | 8 | "In Utero" . 9 | . 10 | -------------------------------------------------------------------------------- /lib/spira/types/float.rb: -------------------------------------------------------------------------------- 1 | module Spira::Types 2 | 3 | ## 4 | # A {Spira::Type} for Float values. Values will be associated with the 5 | # `XSD.double` type. 6 | # 7 | # A {Spira::Resource} property can reference this type as 8 | # `Spira::Types::Float`, `Float`, `XSD.double`, or `XSD.float`. 9 | # 10 | # @see Spira::Type 11 | # @see http://rdf.rubyforge.org/RDF/Literal.html 12 | class Float 13 | 14 | include Spira::Type 15 | 16 | def self.unserialize(value) 17 | value.object.to_f 18 | end 19 | 20 | def self.serialize(value) 21 | RDF::Literal.new(value.to_f, :datatype => XSD.double) 22 | end 23 | 24 | register_alias XSD.float 25 | register_alias XSD.double 26 | 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/spira/types/any.rb: -------------------------------------------------------------------------------- 1 | module Spira::Types 2 | 3 | ## 4 | # This class does its best to serialize or unserialize RDF values into Ruby 5 | # values and vice versa using RDF.rb's built-in helpers for `RDF::Literal`s. 6 | # Its behavior is defined as 'What `RDF::Literal` does' for a given value. 7 | # 8 | # @see Spira::Type 9 | # @see http://rdf.rubyforge.org/RDF/Literal.html 10 | class Any 11 | 12 | include Spira::Type 13 | 14 | def self.unserialize(value) 15 | value.respond_to?(:object) ? value.object : value 16 | end 17 | 18 | def self.serialize(value) 19 | raise TypeError, "Spira::Types::Any cannot serialize collections" if value.is_a?(Array) 20 | value.is_a?(RDF::Value) ? value : RDF::Literal.new(value) 21 | end 22 | 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/types/integer.spec: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/../spec_helper' 2 | 3 | describe Spira::Types::Integer do 4 | 5 | context "when serializing" do 6 | it "should serialize integers to XSD integers" do 7 | serialized = Spira::Types::Integer.serialize(5) 8 | serialized.should be_a RDF::Literal 9 | serialized.should have_datatype 10 | serialized.datatype.should == RDF::XSD.integer 11 | serialized.should == RDF::Literal.new(5) 12 | end 13 | end 14 | 15 | context "when unserializing" do 16 | it "should unserialize XSD integers to integers" do 17 | value = Spira::Types::Integer.unserialize(RDF::Literal.new(5, :datatype => RDF::XSD.integer)) 18 | value.should be_a Fixnum 19 | value.should == 5 20 | end 21 | end 22 | 23 | 24 | end 25 | 26 | -------------------------------------------------------------------------------- /lib/spira/types/boolean.rb: -------------------------------------------------------------------------------- 1 | module Spira::Types 2 | 3 | ## 4 | # A {Spira::Type} for Boolean values. Values will be expressed as booleans 5 | # and packaged as `XSD.boolean` `RDF::Literal`s. 6 | # 7 | # A {Spira::Resource} property can reference this type as 8 | # `Spira::Types::Boolean`, `Boolean`, or `XSD.boolean`. 9 | # 10 | # @see Spira::Type 11 | # @see http://rdf.rubyforge.org/RDF/Literal.html 12 | class Boolean 13 | 14 | include Spira::Type 15 | 16 | def self.unserialize(value) 17 | value.object == true 18 | end 19 | 20 | def self.serialize(value) 21 | if value 22 | RDF::Literal.new(true, :datatype => XSD.boolean) 23 | else 24 | RDF::Literal.new(false, :datatype => XSD.boolean) 25 | end 26 | end 27 | 28 | register_alias XSD.boolean 29 | 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/spira/types.rb: -------------------------------------------------------------------------------- 1 | module Spira 2 | 3 | ## 4 | # Spira::Types is a set of default Spira::Type classes. 5 | # 6 | # @see Spira::Type 7 | # @see Spira::Types::Integer 8 | # @see Spira::Types::Boolean 9 | # @see Spira::Types::String 10 | # @see Spira::Types::Float 11 | # @see Spira::Types::Any 12 | module Types 13 | 14 | # No autoloading here--the associations to XSD types are made by the 15 | # classes themselves, so we need to explicitly require them or XSD types 16 | # will show up as not found. 17 | require 'spira/types/integer' 18 | require 'spira/types/boolean' 19 | require 'spira/types/any' 20 | require 'spira/types/string' 21 | require 'spira/types/float' 22 | require 'spira/types/uri' 23 | require 'spira/types/decimal' 24 | require 'spira/types/native' 25 | 26 | 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/spira/types/decimal.rb: -------------------------------------------------------------------------------- 1 | require 'bigdecimal' 2 | 3 | module Spira::Types 4 | 5 | ## 6 | # A {Spira::Type} for integer values. Values will be associated with the 7 | # `XSD.integer` type. 8 | # 9 | # A {Spira::Resource} property can reference this type as 10 | # `Spira::Types::Integer`, `Integer`, or `XSD.integer`. 11 | # 12 | # @see Spira::Type 13 | # @see http://rdf.rubyforge.org/RDF/Literal.html 14 | class Decimal 15 | include Spira::Type 16 | 17 | def self.unserialize(value) 18 | object = value.object 19 | object.is_a?(BigDecimal) ? object : BigDecimal.new(object.to_s) 20 | end 21 | 22 | def self.serialize(value) 23 | RDF::Literal.new(value.is_a?(BigDecimal) ? value.to_s('F') : value.to_s, :datatype => XSD.decimal) 24 | end 25 | 26 | register_alias XSD.decimal 27 | 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/fixtures/non_model_data.nt: -------------------------------------------------------------------------------- 1 | "a name" . 2 | "another name" . 3 | "15"^^ . 4 | "Not in the model" . 5 | 6 | "a name" . 7 | "another name" . 8 | "15"^^ . 9 | "20"^^ . 10 | -------------------------------------------------------------------------------- /spec/fixtures/types.nt: -------------------------------------------------------------------------------- 1 | . 2 | . 3 | "sedan" . 4 | 5 | "sports car" . 6 | 7 | . 8 | "minivan" . 9 | 10 | . 11 | "full-sized van" . 12 | 13 | . 14 | "some other van" . 15 | -------------------------------------------------------------------------------- /spec/types/uri.spec: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/../spec_helper' 2 | 3 | describe Spira::Types::URI do 4 | 5 | before :each do 6 | @uri = RDF::URI('http://example.org/example') 7 | end 8 | 9 | context "when serializing" do 10 | it "should serialize URIs to URIs" do 11 | serialized = Spira::Types::URI.serialize(@uri) 12 | serialized.should be_a RDF::URI 13 | serialized.should == @uri 14 | end 15 | 16 | it "should serialize non-URIs to URIs based on the URI constructor" do 17 | serialized = Spira::Types::URI.serialize("test") 18 | serialized.should be_a RDF::URI 19 | serialized.should == RDF::URI('test') 20 | end 21 | 22 | end 23 | 24 | context "when unserializing" do 25 | it "should unserialize URIs to themselves" do 26 | value = Spira::Types::URI.unserialize(@uri) 27 | value.should be_a RDF::URI 28 | value.should == @uri 29 | end 30 | 31 | it "should unserialize non-URIs to URIs based on the URI constructor" do 32 | value = Spira::Types::URI.unserialize("test") 33 | value.should be_a RDF::URI 34 | value.should == RDF::URI('test') 35 | end 36 | end 37 | 38 | 39 | end 40 | 41 | -------------------------------------------------------------------------------- /spec/types/string.spec: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/../spec_helper' 2 | 3 | describe Spira::Types::String do 4 | 5 | context "when serializing" do 6 | it "should serialize strings to XSD strings" do 7 | serialized = Spira::Types::String.serialize("a string") 8 | serialized.should be_a RDF::Literal 9 | serialized.should == RDF::Literal.new("a string") 10 | end 11 | 12 | it "should serialize other types to XSD strings" do 13 | serialized = Spira::Types::String.serialize(5) 14 | serialized.should be_a RDF::Literal 15 | serialized.should == RDF::Literal.new("5") 16 | end 17 | end 18 | 19 | context "when unserializing" do 20 | it "should unserialize XSD strings to strings" do 21 | value = Spira::Types::String.unserialize(RDF::Literal.new("a string", :datatype => RDF::XSD.string)) 22 | value.should be_a String 23 | value.should == "a string" 24 | end 25 | 26 | it "should unserialize anything else to a string" do 27 | value = Spira::Types::String.unserialize(RDF::Literal.new(5, :datatype => RDF::XSD.integer)) 28 | value.should be_a String 29 | value.should == "5" 30 | end 31 | end 32 | 33 | 34 | end 35 | 36 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /spec/types/decimal.spec: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/../spec_helper' 2 | 3 | describe Spira::Types::Decimal do 4 | 5 | context "when serializing" do 6 | it "should serialize decimals to XSD decimals" do 7 | serialized = Spira::Types::Decimal.serialize(5.15) 8 | serialized.should be_a RDF::Literal 9 | serialized.should have_datatype 10 | serialized.datatype.should == RDF::XSD.decimal 11 | serialized.should == RDF::Literal.new(5.15, :datatype => RDF::XSD.decimal) 12 | end 13 | end 14 | 15 | context "when unserializing" do 16 | it "should unserialize XSD decimals to BigDecimals" do 17 | value = Spira::Types::Decimal.unserialize(RDF::Literal.new(5.15, :datatype => RDF::XSD.decimal)) 18 | value.should be_a BigDecimal 19 | value.should == BigDecimal.new('5.15') 20 | end 21 | end 22 | 23 | # BigDecimal has a silly default to_s, which this test makes sure we are avoiding 24 | context "when round tripping" do 25 | it "should serialize to the original value after unserializing" do 26 | literal = RDF::Literal.new(5.15, :datatype => RDF::XSD.decimal) 27 | unserialized = Spira::Types::Decimal.unserialize(literal) 28 | serialized = Spira::Types::Decimal.serialize(unserialized) 29 | serialized.should == literal 30 | end 31 | end 32 | 33 | end 34 | 35 | -------------------------------------------------------------------------------- /spec/types/float.spec: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/../spec_helper' 2 | 3 | describe Spira::Types::Float do 4 | 5 | context "when serializing" do 6 | it "should serialize floats to XSD floats" do 7 | serialized = Spira::Types::Float.serialize(5.0) 8 | serialized.should be_a RDF::Literal 9 | serialized.should have_datatype 10 | serialized.datatype.should == RDF::XSD.double 11 | serialized.should == RDF::Literal.new(5.0) 12 | end 13 | 14 | it "should serialize integers to XSD floats" do 15 | serialized = Spira::Types::Float.serialize(5) 16 | serialized.should be_a RDF::Literal 17 | serialized.should have_datatype 18 | serialized.datatype.should == RDF::XSD.double 19 | serialized.should == RDF::Literal.new(5.0) 20 | end 21 | 22 | end 23 | 24 | context "when unserializing" do 25 | it "should unserialize XSD floats to floats" do 26 | value = Spira::Types::Float.unserialize(RDF::Literal.new(5, :datatype => RDF::XSD.float)) 27 | value.should be_a Float 28 | value.should == 5.0 29 | end 30 | 31 | it "should unserialize XSD doubles to floats" do 32 | value = Spira::Types::Float.unserialize(RDF::Literal.new(5, :datatype => RDF::XSD.double)) 33 | value.should be_a Float 34 | value.should == 5.0 35 | end 36 | end 37 | 38 | 39 | end 40 | 41 | -------------------------------------------------------------------------------- /spec/types/any.spec: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/../spec_helper' 2 | 3 | describe Spira::Types::Any do 4 | 5 | before :all do 6 | @uri = RDF::URI('http://example.org') 7 | end 8 | 9 | # this spec is going to be necessarily loose. The 'Any' type is defined to 10 | # use RDF.rb's automatic RDF Literal boxing and unboxing, which may or may 11 | # not change between verions. 12 | # 13 | context "when serializing" do 14 | it "should serialize literals to RDF Literals" do 15 | serialized = Spira::Types::Any.serialize(15) 16 | serialized.should be_a RDF::Literal 17 | serialized = Spira::Types::Any.serialize("test") 18 | serialized.should be_a RDF::Literal 19 | end 20 | 21 | it "should keep RDF::URIs as URIs" do 22 | Spira::Types::Any.serialize(@uri).should == @uri 23 | end 24 | 25 | it "should fail to serialize collections" do 26 | lambda { Spira::Types::Any.serialize([]) }.should raise_error TypeError 27 | end 28 | end 29 | 30 | context "when unserializing" do 31 | it "should unserialize to ruby types" do 32 | value = Spira::Types::Any.unserialize(RDF::Literal.new(5, :datatype => RDF::XSD.integer)) 33 | value.should == 5 34 | value = Spira::Types::Any.unserialize(RDF::Literal.new("a string")) 35 | value.should == "a string" 36 | end 37 | 38 | it "should unserialize URIs to URIs" do 39 | Spira::Types::Any.unserialize(@uri).should == @uri 40 | end 41 | end 42 | 43 | 44 | end 45 | 46 | -------------------------------------------------------------------------------- /spec/fixtures/has_many.nt: -------------------------------------------------------------------------------- 1 | "Test post 1" . 2 | "some content" . 3 | 4 | 5 | 6 | 7 | . 8 | "test comment 1" . 9 | "some comment content" . 10 | "1"^^ . 11 | "3"^^ . 12 | "5"^^ . 13 | . 14 | 15 | 16 | . 17 | "test comment 2" . 18 | "some comment content" . 19 | -------------------------------------------------------------------------------- /spec/types/boolean.spec: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/../spec_helper' 2 | 3 | describe Spira::Types::Boolean do 4 | 5 | context "when serializing" do 6 | it "should serialize booleans to XSD booleans" do 7 | serialized = Spira::Types::Boolean.serialize(true) 8 | serialized.should be_a RDF::Literal 9 | serialized.should have_datatype 10 | serialized.datatype.should == RDF::XSD.boolean 11 | serialized.should == RDF::Literal.new(true) 12 | end 13 | 14 | it "should serialize true-equivalents to XSD booleans" do 15 | serialized = Spira::Types::Boolean.serialize(15) 16 | serialized.should be_a RDF::Literal 17 | serialized.should have_datatype 18 | serialized.datatype.should == RDF::XSD.boolean 19 | serialized.should == RDF::Literal.new(true) 20 | end 21 | 22 | it "should serialize false-equivalents to XSD booleans" do 23 | serialized = Spira::Types::Boolean.serialize(nil) 24 | serialized.should be_a RDF::Literal 25 | serialized.should have_datatype 26 | serialized.datatype.should == RDF::XSD.boolean 27 | serialized.should == RDF::Literal.new(false) 28 | end 29 | end 30 | 31 | context "when unserializing" do 32 | it "should unserialize XSD booleans to booleans" do 33 | value = Spira::Types::Integer.unserialize(RDF::Literal.new(true, :datatype => RDF::XSD.boolean)) 34 | value.should equal true 35 | value = Spira::Types::Integer.unserialize(RDF::Literal.new(false, :datatype => RDF::XSD.boolean)) 36 | value.should equal false 37 | end 38 | end 39 | 40 | 41 | end 42 | 43 | -------------------------------------------------------------------------------- /lib/spira/resource/validations.rb: -------------------------------------------------------------------------------- 1 | module Spira 2 | module Resource 3 | 4 | ## 5 | # Instance methods relating to validations for a Spira resource. This 6 | # includes the default assertions. 7 | # 8 | # @see Spira::Resource::InstanceMethods 9 | # @see Spira::Resource::ClassMethods 10 | # @see Spira::Resource::DSL 11 | module Validations 12 | 13 | ## 14 | # Assert a fact about this instance. If the given expression is false, 15 | # an error will be noted. 16 | # 17 | # @example Assert that a title is correct 18 | # assert(title == 'xyz', :title, 'bad title') 19 | # @param [Any] boolean The expression to evaluate 20 | # @param [Symbol] property The property or has_many to mark as incorrect on failure 21 | # @param [String] message The message to record if this assertion fails 22 | # @return [Void] 23 | def assert(boolean, property, message) 24 | errors.add(property, message) unless boolean 25 | end 26 | 27 | ## 28 | # A default helper assertion. Asserts that a given property is set. 29 | # 30 | # @param [Symbol] name The property to check 31 | # @return [Void] 32 | def assert_set(name) 33 | assert(!(self.send(name).nil?), name, "#{name.to_s} cannot be nil") 34 | end 35 | 36 | ## 37 | # A default helper assertion. Asserts that a given property is numeric. 38 | # 39 | # @param [Symbol] name The property to check 40 | # @return [Void] 41 | def assert_numeric(name) 42 | assert(self.send(name).is_a?(Numeric), name, "#{name.to_s} must be numeric (was #{self.send(name)})") 43 | end 44 | 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spira.gemspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby -rubygems 2 | # -*- encoding: utf-8 -*- 3 | 4 | GEMSPEC = Gem::Specification.new do |gem| 5 | gem.version = File.read('VERSION').chomp 6 | gem.date = File.mtime('VERSION').strftime('%Y-%m-%d') 7 | 8 | gem.name = 'spira' 9 | gem.homepage = 'http://spira.rubyforge.org' 10 | gem.license = 'Public Domain' if gem.respond_to?(:license=) 11 | gem.summary = 'A framework for using the information in RDF.rb repositories as model objects.' 12 | gem.description = 'Spira is a framework for using the information in RDF.rb repositories as model objects.' 13 | gem.rubyforge_project = 'spira' 14 | 15 | gem.authors = ['Ben Lavender'] 16 | gem.email = 'blavender@gmail.com' 17 | 18 | gem.platform = Gem::Platform::RUBY 19 | gem.files = %w(CHANGES.md AUTHORS README UNLICENSE VERSION) + Dir.glob('lib/**/*.rb') 20 | gem.bindir = %w(bin) 21 | gem.executables = %w() 22 | gem.default_executable = gem.executables.first 23 | gem.require_paths = %w(lib) 24 | gem.extensions = %w() 25 | gem.test_files = %w() 26 | gem.has_rdoc = false 27 | gem.has_yardoc = true 28 | 29 | gem.required_ruby_version = '>= 1.8.2' 30 | gem.requirements = [] 31 | gem.add_development_dependency 'rdf-spec', '>= 0.2.2' 32 | gem.add_development_dependency 'rspec', '>= 1.3.0' 33 | gem.add_development_dependency 'yard' , '>= 0.5.3' 34 | gem.add_runtime_dependency 'rdf', '>= 0.2.3' 35 | gem.add_runtime_dependency 'rdf-isomorphic', '>= 0.3.0' 36 | gem.add_runtime_dependency 'promise', '>= 0.3.0' 37 | gem.post_install_message = nil 38 | end 39 | -------------------------------------------------------------------------------- /spec/nodes.spec: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/spec_helper' 2 | 3 | # Behaviors relating to BNodes vs URIs 4 | 5 | describe 'Spira resources' do 6 | 7 | before :all do 8 | class ::NodeTest 9 | include Spira::Resource 10 | property :name, :predicate => FOAF.name 11 | end 12 | end 13 | 14 | before :each do 15 | Spira.clear_repositories! 16 | Spira.add_repository(:default, RDF::Repository.new) 17 | end 18 | 19 | context "when instatiated from URIs" do 20 | before :each do 21 | @uri = RDF::URI('http://example.org/bob') 22 | @test = @uri.as(NodeTest) 23 | end 24 | 25 | it "should respond to :to_uri" do 26 | @test.should respond_to :to_uri 27 | end 28 | 29 | it "should not respond to :to_node" do 30 | @test.should_not respond_to :to_node 31 | end 32 | 33 | it "should not be a node" do 34 | @test.node?.should be_false 35 | end 36 | 37 | it "should return the subject URI for :to_uri" do 38 | @test.to_uri.should == @uri 39 | end 40 | 41 | it "should raise a NoMethodError for :to_node" do 42 | lambda { @test.to_node }.should raise_error NoMethodError 43 | end 44 | end 45 | 46 | context "when instantiated from Nodes" do 47 | before :each do 48 | @node = RDF::Node.new 49 | @test = @node.as(NodeTest) 50 | end 51 | 52 | it "should not respond to :to_uri" do 53 | @test.should_not respond_to :to_uri 54 | end 55 | 56 | it "should respond to :to_node" do 57 | @test.should respond_to :to_node 58 | end 59 | 60 | it "should not be a node" do 61 | @test.node?.should be_true 62 | end 63 | 64 | it "should return the subject URI for :to_node" do 65 | @test.to_node.should == @node 66 | end 67 | 68 | it "should raise a NoMethodError for :to_uri" do 69 | lambda { @test.to_uri }.should raise_error NoMethodError 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /spec/type_classes.spec: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/spec_helper' 2 | 3 | 4 | describe 'types' do 5 | 6 | context "when declaring a new type" do 7 | before :all do 8 | class ::TypeA 9 | include Spira::Type 10 | end 11 | 12 | class ::TypeB 13 | include Spira::Type 14 | register_alias :typeb_alias 15 | end 16 | 17 | class ::TypeC 18 | include Spira::Type 19 | 20 | def self.serialize(value) 21 | value.to_s 22 | end 23 | 24 | def self.unserialize(value) 25 | value.to_i 26 | end 27 | end 28 | end 29 | 30 | it "should find itself registered as a type with spira" do 31 | Spira.types[TypeA].should == TypeA 32 | end 33 | 34 | it "should have a class method to serialize" do 35 | TypeA.should respond_to :serialize 36 | end 37 | 38 | it "should have a class method to unserialize" do 39 | TypeA.should respond_to :unserialize 40 | end 41 | 42 | it "should allow itself to be aliased" do 43 | TypeA.register_alias(:typea_alias) 44 | Spira.types[:typea_alias].should == TypeA 45 | end 46 | 47 | it "should allow aliases in the DSL" do 48 | Spira.types[:typeb_alias].should == TypeB 49 | end 50 | 51 | it "should allow a self.serialize function" do 52 | TypeC.serialize(5).should == "5" 53 | end 54 | 55 | it "should allow a self.unserialize function" do 56 | TypeC.unserialize("5").should == 5 57 | end 58 | 59 | context "working with RDF vocabularies" do 60 | before :all do 61 | class ::TypeWithRDF 62 | include Spira::Type 63 | register_alias DC.title 64 | end 65 | end 66 | 67 | it "should have the RDF namespace included for default vocabularies" do 68 | Spira.types[::RDF::DC.title].should == TypeWithRDF 69 | end 70 | end 71 | 72 | end 73 | 74 | 75 | end 76 | 77 | -------------------------------------------------------------------------------- /lib/spira/yard.rb: -------------------------------------------------------------------------------- 1 | # A small YARD handler for processing Spira properties. 2 | # 3 | # This handler processes {property} and {has_many} calls inside classes and 4 | # transforms them into attribute definitions. Document them as you would 5 | # `attr_accessor`. 6 | # 7 | # @example Using it from a Rakefile 8 | # require 'yard' 9 | # require 'spira/yard' 10 | # 11 | # YARD::Rake::YardocTask.new do |yard| 12 | # # Set YARD options. 13 | # end 14 | class SpiraPropertyHandler < YARD::Handlers::Ruby::Base 15 | namespace_only 16 | handles method_call(:property), method_call(:has_many) 17 | 18 | def process 19 | name = statement.parameters.first.jump(:tstring_content, :ident).source 20 | namespace.attributes[scope][name] ||= SymbolHash[:read => nil, :write => nil] 21 | namespace.attributes[scope][name][:read] = MethodObject.new(namespace, name, scope) do |o| 22 | o.docstring = if statement.comments.to_s.empty? 23 | "Returns the value of the `#{name}` property." 24 | else 25 | statement.comments 26 | end 27 | if statement.method_name(true) == :has_many 28 | o.docstring.add_tag(YARD::Tags::Tag.new(:return, 'something', ['Set'])) 29 | end 30 | end 31 | register namespace.attributes[scope][name][:read] 32 | 33 | if statement.method_name(true) != :has_many 34 | namespace.attributes[scope][name][:write] = MethodObject.new(namespace, "#{name}=", scope) do |o| 35 | o.docstring = if statement.comments.to_s.empty? 36 | "Sets the value of the `#{name}` property." 37 | else 38 | statement.comments 39 | end 40 | o.docstring.add_tag(YARD::Tags::Tag.new(:param, 'the value to set the `#{name}` property to', ['RDF::Value'], 'value')) unless o.docstring.tag(:param) 41 | end 42 | register namespace.attributes[scope][name][:write] 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/errors.spec: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/spec_helper' 2 | 3 | describe Spira::Errors do 4 | 5 | before :all do 6 | end 7 | 8 | context "when instantiating" do 9 | 10 | it "should be instantiable" do 11 | Spira::Errors.new.should_not be_nil 12 | end 13 | 14 | it "should not have any errors to start" do 15 | Spira::Errors.new.should be_empty 16 | end 17 | 18 | end 19 | 20 | context "when adding errors" do 21 | before :each do 22 | @errors = Spira::Errors.new 23 | end 24 | 25 | it "should allow adding errors" do 26 | lambda {@errors.add(:field, "cannot be null")}.should_not raise_error 27 | end 28 | end 29 | 30 | context "when retrieving errors" do 31 | before :each do 32 | @errors = Spira::Errors.new 33 | @errors.add(:field, "cannot be null") 34 | end 35 | 36 | it "should not be empty" do 37 | @errors.should_not be_empty 38 | end 39 | 40 | it "have errors for a field with errors" do 41 | @errors.any_for?(:field).should be_true 42 | end 43 | 44 | it "should not have errors for a field without errors" do 45 | @errors.any_for?(:other).should be_false 46 | end 47 | 48 | it "should have the correct error name for a given error" do 49 | @errors.for(:field).should == ["cannot be null"] 50 | end 51 | 52 | it "should provide a list of all errors" do 53 | @errors.should respond_to :each 54 | @errors.each.should == ["field cannot be null"] 55 | end 56 | 57 | end 58 | 59 | context "when clearing errors" do 60 | before :each do 61 | @errors = Spira::Errors.new 62 | @errors.add(:field, "cannot be null") 63 | end 64 | 65 | it "should respond to :clear" do 66 | @errors.should respond_to :clear 67 | end 68 | 69 | it "should clear errors on #clear" do 70 | @errors.any_for?(:field).should be_true 71 | @errors.clear 72 | @errors.should be_empty 73 | end 74 | 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /spec/enumerable.spec: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/spec_helper' 2 | 3 | # Tests in terms of RDF::Enumerable, and interaction with other enumerables 4 | 5 | describe Spira::Resource do 6 | 7 | context "as an RDF::Enumerable" do 8 | 9 | before :all do 10 | require 'rdf/ntriples' 11 | Spira.add_repository(:default, ::RDF::Repository) 12 | 13 | class ::EnumerableSpec 14 | include Spira::Resource 15 | 16 | base_uri "http://example.org/example/people" 17 | 18 | property :name, :predicate => RDFS.label 19 | property :age, :predicate => FOAF.age, :type => Integer 20 | end 21 | end 22 | 23 | before :each do 24 | @uri = RDF::URI('http://example.org/example/people/bob') 25 | @person = EnumerableSpec.for @uri 26 | @enumerable_repository = RDF::Repository.new 27 | @enumerable_repository << RDF::Statement.new(@uri, RDF::FOAF.age, 15) 28 | @enumerable_repository << RDF::Statement.new(@uri, RDF::RDFS.label, "Bob Smith") 29 | @statements = @enumerable_repository 30 | @person.name = "Bob Smith" 31 | @person.age = 15 32 | @enumerable = @person 33 | end 34 | 35 | context "when running the rdf-spec RDF::Enumerable shared groups" do 36 | 37 | it_should_behave_like RDF_Enumerable 38 | 39 | end 40 | 41 | context "when comparing with other RDF::Enumerables" do 42 | 43 | it "should be equal if they are completely the same" do 44 | @enumerable.should == @enumerable_repository 45 | end 46 | 47 | # This one is a tough call. Are two resources really equal if one is a 48 | # subset of the other? No. But Spira is supposed to let you access 49 | # existing data, and that means you might have data which has properties 50 | # a model class doesn't know about. 51 | # 52 | # Spira will default, then, to calling itself equal to an enumerable 53 | # which has the same uri and all the same properties, and then some. 54 | it "should be equal if the resource is a subgraph of the repository" do 55 | pending "Awaiting subgraph implementation in rdf_isomorphic" 56 | end 57 | 58 | it "should allow other enumerables to be isomorphic to a resource" do 59 | @enumerable_repository.should be_isomorphic_with @enumerable 60 | end 61 | 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'spec' 3 | require 'spec/rake/spectask' 4 | 5 | desc 'Run specs' 6 | task 'spec' do 7 | Spec::Rake::SpecTask.new("spec") do |t| 8 | t.spec_files = FileList["spec/**/*.spec","spec/*.rb"] 9 | t.rcov = false 10 | t.spec_opts = ["-c"] 11 | end 12 | end 13 | 14 | desc 'Run specs with backtrace' 15 | task 'tracespec' do 16 | Spec::Rake::SpecTask.new("tracespec") do |t| 17 | t.spec_files = FileList["spec/**/*.spec", "spec/*.rb"] 18 | t.rcov = false 19 | t.spec_opts = ["-bcfn"] 20 | end 21 | end 22 | 23 | desc 'Run coverage' 24 | task 'coverage' do 25 | Spec::Rake::SpecTask.new("coverage") do |t| 26 | t.spec_files = FileList["spec/**/*.spec","spec/*.rb"] 27 | t.rcov = true 28 | t.spec_opts = ["-c"] 29 | end 30 | end 31 | 32 | 33 | 34 | desc "Open an irb session with everything loaded, including test fixtures" 35 | task :console do 36 | sh "irb -rubygems -I lib -r spira -I spec/fixtures -r person -r event -r cds -r cars -r posts -I spec -r spec_helper -r loading" 37 | end 38 | 39 | task :default => [:spec] 40 | 41 | desc "Create yardocs according to .yardopts file" 42 | task :yardoc do 43 | `yardoc` 44 | end 45 | 46 | desc "Add analytics tracking information to yardocs" 47 | task :addanalytics do 48 | tracking_code = < 50 | 51 | var _gaq = _gaq || []; 52 | _gaq.push(['_setAccount', 'UA-3784741-3']); 53 | _gaq.push(['_trackPageview']); 54 | 55 | (function() { 56 | var ga = document.createElement('script'); ga.type = 'text\/javascript'; ga.async = true; 57 | ga.src = ('https:' == document.location.protocol ? 'https:\/\/ssl' : 'http:\/\/www') + '.google-analytics.com\/ga.js'; 58 | var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); 59 | })(); 60 | 61 | 62 | EOC 63 | files = Dir.glob('./doc/yard/**/*.html').reject { |file| %w{class_list file_list frames.html _index.html method_list}.any? { |skipfile| file.include?(skipfile) }} 64 | files.each do |file| 65 | contents = File.read(file) 66 | writer = File.open(file, 'w') 67 | writer.write(contents.gsub(/\<\/body\>/, tracking_code + "")) 68 | writer.flush 69 | end 70 | end 71 | 72 | desc "Upload docs to rubyforge" 73 | task :uploadyardocs => [:yardoc, :addanalytics] do 74 | `rsync -av doc/yard/* bhuga@rubyforge.org:/var/www/gforge-projects/spira` 75 | end 76 | -------------------------------------------------------------------------------- /lib/spira/type.rb: -------------------------------------------------------------------------------- 1 | module Spira 2 | 3 | ## 4 | # Spira::Type can be included by classes to create new property types for 5 | # Spira. These types are responsible for serialization a Ruby value into an 6 | # `RDF::Value`, and deserialization of an `RDF::Value` into a Ruby value. 7 | # 8 | # A simple example: 9 | # 10 | # class Integer 11 | # 12 | # include Spira::Type 13 | # 14 | # def self.unserialize(value) 15 | # value.object 16 | # end 17 | # 18 | # def self.serialize(value) 19 | # RDF::Literal.new(value) 20 | # end 21 | # 22 | # register_alias XSD.integer 23 | # end 24 | # 25 | # This example will serialize and deserialize integers. It's included with 26 | # Spira by default. It allows either of the following forms to declare an 27 | # integer property on a Spira resource: 28 | # 29 | # property :age, :predicate => FOAF.age, :type => Integer 30 | # property :age, :predicate => FOAF.age, :type => XSD.integer 31 | # 32 | # `Spira::Type`s include the RDF namespace and thus have all of the base RDF 33 | # vocabularies available to them without the `RDF::` prefix. 34 | # 35 | # @see http://rdf.rubyforge.org/RDF/Value.html 36 | # @see Spira::Resource 37 | module Type 38 | 39 | ## 40 | # Make the DSL available to a child class. 41 | # 42 | # @private 43 | def self.included(child) 44 | child.extend(ClassMethods) 45 | Spira.type_alias(child,child) 46 | end 47 | 48 | include RDF 49 | 50 | module ClassMethods 51 | 52 | ## 53 | # Register an alias that this type can be referred to as, such as an RDF 54 | # URI. The alias can be any object, symbol, or constant. 55 | # 56 | # @param [Any] identifier The new alias in property declarations for this class 57 | # @return [Void] 58 | def register_alias(any) 59 | Spira.type_alias(any, self) 60 | end 61 | 62 | ## 63 | # Serialize a given value to RDF. 64 | # 65 | # @param [Any] value The Ruby value to be serialized 66 | # @return [RDF::Value] The RDF form of this value 67 | def serialize(value) 68 | value 69 | end 70 | 71 | ## 72 | # Unserialize a given RDF value to Ruby 73 | # 74 | # @param [RDF::Value] value The RDF form of this value 75 | # @return [Any] The Ruby form of this value 76 | def unserialize(value) 77 | value 78 | end 79 | end 80 | 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/spira/errors.rb: -------------------------------------------------------------------------------- 1 | module Spira 2 | 3 | ## 4 | # Spira::Errors represents a collection of validation errors for a Spira 5 | # resource. It tracks a list of errors for each field. 6 | # 7 | # This class does not perform validations. It only tracks the results of 8 | # them. 9 | class Errors 10 | 11 | ## 12 | # Creates a new Spira::Errors 13 | # 14 | # @return [Spira::Errors] 15 | def initialize 16 | @errors = {} 17 | end 18 | 19 | ## 20 | # Returns true if there are no errors, false otherwise 21 | # 22 | # @return [true, false] 23 | def empty? 24 | @errors.all? do |field, errors| errors.empty? end 25 | end 26 | 27 | ## 28 | # Returns true if there are errors, false otherwise 29 | # 30 | # @return [true, false] 31 | def any? 32 | !empty? 33 | end 34 | 35 | ## 36 | # Returns true if the given property or list has any errors 37 | # 38 | # @param [Symbol] name The name of the property or list 39 | # @return [true, false] 40 | def any_for?(property) 41 | !(@errors[property].nil?) && !(@errors[property].empty?) 42 | end 43 | 44 | ## 45 | # Add an error to a given property or list 46 | # 47 | # @example Add an error to a property 48 | # errors.add(:username, "cannot be nil") 49 | # @param [Symbol] property The property or list to add the error to 50 | # @param [String] problem The error 51 | # @return [Void] 52 | def add(property, problem) 53 | @errors[property] ||= [] 54 | @errors[property].push problem 55 | end 56 | 57 | ## 58 | # The list of errors for a given property or list 59 | # 60 | # @example Get the errors for the `:username` field 61 | # errors.add(:username, "cannot be nil") 62 | # errors.for(:username) #=> ["cannot be nil"] 63 | # @param [Symbol] property The property or list to check 64 | # @return [Array] The list of errors 65 | def for(property) 66 | @errors[property] 67 | end 68 | 69 | ## 70 | # Clear all errors 71 | # 72 | # @return [Void] 73 | def clear 74 | @errors = {} 75 | end 76 | 77 | ## 78 | # Return all errors as strings 79 | # 80 | # @example Get all errors 81 | # errors.add(:username, "cannot be nil") 82 | # errors.each #=> ["username cannot be nil"] 83 | # @return [Array] 84 | def each 85 | @errors.map do |property, problems| 86 | problems.map do |problem| 87 | property.to_s + " " + problem 88 | end 89 | end.flatten 90 | end 91 | 92 | 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /spec/non_model_data.spec: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/spec_helper' 2 | 3 | describe 'Resources with data not associated with a model' do 4 | 5 | before :all do 6 | require 'rdf/ntriples' 7 | class ::ExtraDataTest 8 | include Spira::Resource 9 | base_uri "http://example.org/example" 10 | 11 | property :property, :predicate => FOAF.age, :type => Integer 12 | 13 | has_many :list, :predicate => RDFS.label 14 | end 15 | @filename = fixture('non_model_data.nt') 16 | end 17 | 18 | before :each do 19 | @extra_repo = RDF::Repository.load(@filename) 20 | Spira.add_repository(:default, @extra_repo) 21 | end 22 | 23 | context "when multiple objects exist for a property" do 24 | before :each do 25 | @example2 = ExtraDataTest.for('example2') 26 | @uri = @example2.uri 27 | end 28 | 29 | it "should not raise an error to load a model with multiple instances of a property predicate" do 30 | lambda { @example = ExtraDataTest.for('example2') }.should_not raise_error 31 | end 32 | 33 | it "should treat the property as a single property" do 34 | @example2.property.should be_a Fixnum 35 | end 36 | 37 | it "should load one of the available property examples as the property" do 38 | [15,20].should include @example2.property 39 | end 40 | 41 | end 42 | 43 | context "when enumerating statements" do 44 | before :each do 45 | @example1 = ExtraDataTest.for('example1') 46 | end 47 | 48 | it "unspecified model information should appear in the enumeration when using #data" do 49 | @example1.data.should have_predicate RDF::FOAF.name 50 | end 51 | end 52 | 53 | context "when deleting" do 54 | before :each do 55 | @example1 = ExtraDataTest.for('example1') 56 | @uri = @example1.uri 57 | end 58 | 59 | it "should not delete non-model data on Resource#!destroy" do 60 | @example1.destroy! 61 | @extra_repo.query(:subject => @uri, :predicate => RDF::FOAF.name).count.should == 1 62 | end 63 | 64 | end 65 | 66 | context "when updating" do 67 | before :each do 68 | @example1 = ExtraDataTest.for('example1') 69 | @uri = @example1.uri 70 | end 71 | 72 | it "should save model data" do 73 | @example1.property = 17 74 | @example1.save! 75 | @extra_repo.query(:subject => @uri, :predicate => RDF::FOAF.age).count.should == 1 76 | @extra_repo.first_value(:subject => @uri, :predicate => RDF::FOAF.age).to_i.should == 17 77 | end 78 | 79 | it "should not affect non-model data" do 80 | @example1.property = 17 81 | @example1.save! 82 | @extra_repo.query(:subject => @uri, :predicate => RDF::FOAF.name).count.should == 1 83 | @extra_repo.first_value(:subject => @uri, :predicate => RDF::FOAF.name).should == "Not in the model" 84 | end 85 | end 86 | 87 | end 88 | -------------------------------------------------------------------------------- /lib/spira/resource.rb: -------------------------------------------------------------------------------- 1 | module Spira 2 | 3 | ## 4 | # Spira::Resource is the main interface to Spira. Classes and modules 5 | # include Spira::Resource to create projections of RDF data as a class. For 6 | # an overview, see the {file:README}. 7 | # 8 | # Projections are a mapping of RDF predicates to fields. 9 | # 10 | # class Person 11 | # include Spira::Resource 12 | # 13 | # property :name, :predicate => FOAF.name 14 | # property :age, :predicate => FOAF.age, :type => Integer 15 | # end 16 | # 17 | # RDF::URI('http://example.org/people/bob').as(Person) #=> <#Person @uri=http://example.org/people/bob> 18 | # 19 | # Spira resources include the RDF namespace, and can thus reference all of 20 | # the default RDF.rb vocabularies without the RDF:: prefix: 21 | # 22 | # property :name, :predicate => FOAF.name 23 | # 24 | # The Spira::Resource documentation is broken into several parts, vaguely 25 | # related by functionality: 26 | # * {Spira::Resource::DSL} contains methods used during the declaration of a class or module 27 | # * {Spira::Resource::ClassMethods} contains class methods for use by declared classes 28 | # * {Spira::Resource::InstanceMethods} contains methods for use by instances of Spira resource classes 29 | # * {Spira::Resource::Validations} contains some default validation functions 30 | # 31 | # @see Spira::Resource::DSL 32 | # @see Spira::Resource::ClassMethods 33 | # @see Spira::Resource::InstanceMethods 34 | # @see Spira::Resource::Validations 35 | module Resource 36 | 37 | autoload :DSL, 'spira/resource/dsl' 38 | autoload :ClassMethods, 'spira/resource/class_methods' 39 | autoload :InstanceMethods, 'spira/resource/instance_methods' 40 | autoload :Validations, 'spira/resource/validations' 41 | 42 | ## 43 | # When a child class includes Spira::Resource, this does the magic to make 44 | # it a Spira resource. 45 | # 46 | # @private 47 | def self.included(child) 48 | # Don't do inclusion work twice. Checking for the properties accessor is 49 | # a proxy for a proper check to see if this is a resource already. Ruby 50 | # has already extended the child class' ancestors to include 51 | # Spira::Resource by the time we get here. 52 | # FIXME: Find a 'more correct' check. 53 | unless child.respond_to?(:properties) 54 | child.extend DSL 55 | child.extend ClassMethods 56 | child.instance_eval do 57 | class << self 58 | attr_accessor :properties, :lists 59 | end 60 | @properties = {} 61 | @lists = {} 62 | end 63 | end 64 | end 65 | 66 | # This lets including classes reference vocabularies without the RDF:: prefix 67 | include Spira::Types 68 | include ::RDF 69 | include InstanceMethods 70 | 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /spec/dirty.spec: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/spec_helper' 2 | 3 | 4 | describe Spira do 5 | 6 | 7 | before :all do 8 | class ::DirtyTest 9 | include Spira::Resource 10 | property :name, :predicate => RDFS.label 11 | property :age, :predicate => FOAF.age, :type => Integer 12 | has_many :items, :predicate => RDFS.seeAlso 13 | end 14 | end 15 | 16 | before :each do 17 | @repo = RDF::Repository.new 18 | @uri = RDF::URI("http://example.org/example/people/alice") 19 | @repo << RDF::Statement.new(@uri, RDF::RDFS.label, "Alice") 20 | @repo << RDF::Statement.new(@uri, RDF::FOAF.age, 15) 21 | @repo << RDF::Statement.new(@uri, RDF::RDFS.seeAlso, "A Literal") 22 | @repo << RDF::Statement.new(@uri, RDF::RDFS.seeAlso, "Another Literal") 23 | Spira.add_repository!(:default, @repo) 24 | end 25 | 26 | context "when tracking dirty attributes" do 27 | 28 | before :each do 29 | @test = DirtyTest.for(@uri) 30 | end 31 | 32 | it "should not mark the projetion as dirty initially" do 33 | @test.dirty?.should be_false 34 | end 35 | 36 | context "that are properties" do 37 | 38 | it "should not mark attributes as dirty when loading" do 39 | @test.dirty?(:name).should be_false 40 | @test.dirty?(:age).should be_false 41 | end 42 | 43 | it "should mark the projection as dirty if an attribute is dirty" do 44 | @test.name = "Steve" 45 | @test.dirty?.should be_true 46 | end 47 | 48 | it "should mark attributes as dirty when changed" do 49 | @test.name = "Steve" 50 | @test.dirty?(:name).should be_true 51 | @test.dirty?(:age).should be_false 52 | end 53 | 54 | it "should mark attributes as dirty when providing them as arguments" do 55 | test = DirtyTest.for(@uri, :name => "Steve") 56 | test.dirty?(:name).should be_true 57 | test.dirty?(:age).should be_false 58 | end 59 | end 60 | 61 | context "that are lists" do 62 | it "should not mark attributes as dirty when loading" do 63 | @test.dirty?(:items).should be_false 64 | end 65 | 66 | it "should mark the projection as dirty if an attribute is dirty" do 67 | @test.items = ["Steve"] 68 | @test.dirty?.should be_true 69 | end 70 | 71 | it "should mark attributes as dirty when changed" do 72 | @test.items = ["Steve"] 73 | @test.dirty?(:items).should be_true 74 | @test.dirty?(:age).should be_false 75 | end 76 | 77 | it "should mark attributes as dirty when providing them as arguments" do 78 | test = DirtyTest.for(@uri, :items => ["Steve"]) 79 | test.dirty?(:items).should be_true 80 | test.dirty?(:age).should be_false 81 | end 82 | 83 | it "should mark attributes as dirty when updated" do 84 | @test.items << "Steve" 85 | @test.dirty?(:items).should be_true 86 | end 87 | 88 | end 89 | end 90 | 91 | end 92 | -------------------------------------------------------------------------------- /spec/base_uri.spec: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/spec_helper' 2 | 3 | describe 'Default URIs' do 4 | 5 | before :each do 6 | Spira.add_repository(:default, ::RDF::Repository) 7 | end 8 | 9 | context "classes with a base URI" do 10 | 11 | before :all do 12 | class ::BaseURITest < Spira::Base 13 | base_uri "http://example.org/example" 14 | property :name, :predicate => RDFS.label 15 | end 16 | 17 | class ::HashBaseURITest < Spira::Base 18 | base_uri "http://example.org/example#" 19 | property :name, :predicate => RDFS.label 20 | end 21 | end 22 | 23 | it "have a base URI method" do 24 | BaseURITest.should respond_to :base_uri 25 | end 26 | 27 | it "have a correct base URI" do 28 | BaseURITest.base_uri.should == "http://example.org/example" 29 | end 30 | 31 | it "provide an id_for method" do 32 | BaseURITest.should respond_to :id_for 33 | end 34 | 35 | it "provide a uri based on the base URI for string arguments" do 36 | BaseURITest.id_for('bob').should == RDF::URI.new('http://example.org/example/bob') 37 | end 38 | 39 | it "use the string form of an absolute URI as an absolute URI" do 40 | uri = 'http://example.org/example/bob' 41 | BaseURITest.id_for(uri).should == RDF::URI.new(uri) 42 | end 43 | 44 | it "allow any type to be used as a URI fragment, via to_s" do 45 | uri = 'http://example.org/example/5' 46 | BaseURITest.id_for(5).should == RDF::URI.new(uri) 47 | end 48 | 49 | it "allow appending fragment RDF::URIs to base_uris" do 50 | BaseURITest.for(RDF::URI('test')).subject.to_s.should == 'http://example.org/example/test' 51 | end 52 | 53 | it "do not raise an exception to project with a relative URI" do 54 | lambda {x = BaseURITest.for 'bob'}.should_not raise_error 55 | end 56 | 57 | it "return an absolute, correct RDF::URI from #uri when created with a relative uri" do 58 | test = BaseURITest.for('bob') 59 | test.uri.should be_a RDF::URI 60 | test.uri.to_s.should == "http://example.org/example/bob" 61 | end 62 | 63 | it "save objects created with a relative URI as absolute URIs" do 64 | test = BaseURITest.for('bob') 65 | test.name = 'test' 66 | test.save! 67 | saved = BaseURITest.for('bob') 68 | saved.name.should == 'test' 69 | end 70 | 71 | it "do not append a / if the base URI ends with a #" do 72 | HashBaseURITest.id_for('bob').should == RDF::URI.new('http://example.org/example#bob') 73 | end 74 | end 75 | 76 | context "classes without a base URI" do 77 | before :all do 78 | class ::NoBaseURITest < Spira::Base 79 | property :name, :predicate => RDFS.label 80 | end 81 | end 82 | 83 | it "have a base URI method" do 84 | NoBaseURITest.should respond_to :base_uri 85 | end 86 | 87 | it "provide a id_for method" do 88 | NoBaseURITest.should respond_to :id_for 89 | end 90 | 91 | it "have a nil base_uri" do 92 | NoBaseURITest.base_uri.should be_nil 93 | end 94 | 95 | it "raise an ArgumentError when projected with a relative URI" do 96 | lambda { x = NoBaseURITest.id_for('bob')}.should raise_error ArgumentError 97 | end 98 | end 99 | 100 | end 101 | -------------------------------------------------------------------------------- /spec/vocabulary.spec: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/spec_helper' 2 | # testing out default vocabularies 3 | 4 | describe 'default vocabularies' do 5 | 6 | before :all do 7 | @bubble_repo = RDF::Repository.new 8 | Spira.add_repository(:default, @bubble_repo) 9 | 10 | end 11 | 12 | before :each do 13 | @vocabulary = RDF::URI.new('http://example.org/vocabulary/') 14 | end 15 | 16 | context "defining classes" do 17 | it "should allow a property without a predicate if there is a default vocabulary" do 18 | lambda { 19 | class VocabTestX 20 | include Spira::Resource 21 | default_vocabulary RDF::URI.new('http://example.org/vocabulary/') 22 | property :test 23 | end 24 | }.should_not raise_error 25 | end 26 | 27 | it "should raise a ResourceDeclarationError to set a property without a default vocabulary" do 28 | lambda { 29 | class VocabTestY 30 | include Spira::Resource 31 | property :test 32 | end 33 | }.should raise_error Spira::ResourceDeclarationError 34 | end 35 | 36 | it "should raise a ResourceDelcarationError to set a predicate without a default vocabulary that is not an RDF::URI" do 37 | lambda { 38 | class VocabTestY 39 | include Spira::Resource 40 | property :test, :predicate => "http://example.org/test" 41 | end 42 | }.should raise_error Spira::ResourceDeclarationError 43 | end 44 | end 45 | 46 | context "using classes with a default vocabulary" do 47 | 48 | before :all do 49 | class ::Bubble 50 | include Spira::Resource 51 | 52 | default_vocabulary RDF::URI.new 'http://example.org/vocab/' 53 | 54 | base_uri "http://example.org/bubbles/" 55 | property :year, :type => Integer 56 | property :name 57 | property :title, :predicate => DC.title, :type => String 58 | end 59 | class ::DefaultVocabVocab < ::RDF::Vocabulary('http://example.org/test#') ; end 60 | class ::HashVocabTest 61 | include Spira::Resource 62 | default_vocabulary DefaultVocabVocab 63 | base_uri "http://example.org/testing/" 64 | property :name 65 | end 66 | end 67 | 68 | before :each do 69 | @year = RDF::URI.new 'http://example.org/vocab/year' 70 | @name = RDF::URI.new 'http://example.org/vocab/name' 71 | end 72 | 73 | it "should do non-default sets and gets normally" do 74 | bubble = Bubble.for 'tulips' 75 | bubble.year = 1500 76 | bubble.title = "Holland tulip" 77 | bubble.save! 78 | 79 | bubble.title.should == "Holland tulip" 80 | bubble.should have_predicate RDF::DC.title 81 | end 82 | it "should create a predicate for a given property" do 83 | bubble = Bubble.for 'dotcom' 84 | bubble.year = 2000 85 | bubble.name = 'Dot-com boom' 86 | 87 | bubble.save! 88 | bubble.should have_predicate @year 89 | bubble.should have_predicate @name 90 | end 91 | 92 | context "that ends in a hash seperator" do 93 | before :each do 94 | @name = RDF::URI("http://example.org/test#name") 95 | end 96 | 97 | it "should correctly not append a slash" do 98 | test = HashVocabTest.for('test1') 99 | test.name = "test1" 100 | test.save! 101 | test.should have_predicate @name 102 | end 103 | 104 | end 105 | end 106 | 107 | 108 | 109 | end 110 | -------------------------------------------------------------------------------- /spec/validations.spec: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/spec_helper' 2 | 3 | describe 'validations' do 4 | 5 | before :all do 6 | class ::Bank 7 | 8 | include Spira::Resource 9 | 10 | default_vocabulary URI.new('http://example.org/banks/vocab') 11 | 12 | property :title, :predicate => RDFS.label 13 | property :balance, :type => Integer 14 | 15 | validate :validate_bank 16 | 17 | def validate_bank 18 | assert_set :title 19 | assert_numeric :balance 20 | end 21 | 22 | end 23 | Spira.add_repository(:default, RDF::Repository.new) 24 | end 25 | 26 | context "when validating" do 27 | it "should not save an invalid model" do 28 | bank = Bank.for RDF::URI.new('http://example.org/banks/bank1') 29 | lambda { bank.save! }.should raise_error Spira::ValidationError 30 | end 31 | 32 | it "should save a valid model" do 33 | bank = Bank.for RDF::URI.new('http://example.org/banks/bank1') 34 | bank.title = "A bank" 35 | bank.balance = 1000 36 | end 37 | 38 | end 39 | 40 | context "included validations" do 41 | context "provides a working assert" do 42 | 43 | before :all do 44 | class ::V1 45 | include Spira::Resource 46 | property :title, :predicate => DC.title 47 | validate :title_is_bad 48 | def title_is_bad 49 | assert(title == 'xyz', :title, 'bad title') 50 | end 51 | end 52 | end 53 | 54 | before :each do 55 | @v1 = V1.for RDF::URI.new('http://example.org/v1/first') 56 | end 57 | 58 | it "should fail when false" do 59 | @v1.title = 'abc' 60 | lambda { @v1.save! }.should raise_error Spira::ValidationError 61 | end 62 | 63 | it "should pass when true" do 64 | @v1.title = 'xyz' 65 | lambda { @v1.save! }.should_not raise_error Spira::ValidationError 66 | end 67 | 68 | end 69 | 70 | context "provides a working assert_set" do 71 | before :all do 72 | class ::V2 73 | include Spira::Resource 74 | property :title, :predicate => DC.title 75 | validate :title_is_set 76 | def title_is_set 77 | assert_set(:title) 78 | end 79 | end 80 | end 81 | 82 | before :each do 83 | @v2 = V2.for RDF::URI.new('http://example.org/v2/first') 84 | end 85 | 86 | it "should fail when nil" do 87 | lambda { @v2.save! }.should raise_error Spira::ValidationError 88 | end 89 | 90 | it "should pass when set" do 91 | @v2.title = 'xyz' 92 | lambda { @v2.save! }.should_not raise_error Spira::ValidationError 93 | end 94 | 95 | end 96 | 97 | context "provides a working assert_numeric" do 98 | before :all do 99 | class ::V3 100 | include Spira::Resource 101 | property :title, :predicate => DC.title, :type => Integer 102 | validate :title_is_numeric 103 | def title_is_numeric 104 | assert_numeric(:title) 105 | end 106 | end 107 | end 108 | 109 | before :each do 110 | @v3 = V3.for RDF::URI.new('http://example.org/v3/first') 111 | end 112 | 113 | it "should fail when nil" do 114 | lambda { @v3.save! }.should raise_error Spira::ValidationError 115 | end 116 | 117 | it "should fail when non-numeric" do 118 | @v3.title = 'xyz' 119 | lambda { @v3.save! }.should raise_error Spira::ValidationError 120 | end 121 | 122 | it "should pass when numeric" do 123 | @v3.title = 15 124 | lambda { @v3.save! }.should_not raise_error Spira::ValidationError 125 | end 126 | 127 | end 128 | 129 | end 130 | 131 | end 132 | -------------------------------------------------------------------------------- /spec/property_types.spec: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/spec_helper' 2 | 3 | describe 'types for properties' do 4 | 5 | before :all do 6 | end 7 | 8 | 9 | context "when declaring type classes" do 10 | it "should raise a type error to use a type that has not been declared" do 11 | lambda { 12 | class ::PropTypeA 13 | include Spira::Resource 14 | default_vocabulary RDF::URI.new('http://example.org/vocab') 15 | base_uri RDF::URI.new('http://example.org/props') 16 | 17 | property :test, :type => XSD.non_existent_type 18 | end 19 | }.should raise_error TypeError 20 | end 21 | 22 | it "should not raise a type error to use a symbol type, even if the class has not been declared yet" do 23 | lambda { 24 | class ::PropTypeB 25 | include Spira::Resource 26 | default_vocabulary RDF::URI.new('http://example.org/vocab') 27 | base_uri RDF::URI.new('http://example.org/props') 28 | 29 | property :test, :type => :non_existent_type 30 | end 31 | }.should_not raise_error TypeError 32 | end 33 | 34 | it "should not raise an error to use an included XSD type aliased to a Spira type" do 35 | lambda { 36 | class ::PropTypeD 37 | include Spira::Resource 38 | default_vocabulary RDF::URI.new('http://example.org/vocab') 39 | base_uri RDF::URI.new('http://example.org/props') 40 | 41 | property :test, :type => XSD.string 42 | end 43 | }.should_not raise_error TypeError 44 | end 45 | 46 | it "should not raise an error to use an included Spira type" do 47 | lambda { 48 | class ::PropTypeC 49 | include Spira::Resource 50 | default_vocabulary RDF::URI.new('http://example.org/vocab') 51 | base_uri RDF::URI.new('http://example.org/props') 52 | 53 | property :test, :type => String 54 | end 55 | }.should_not raise_error TypeError 56 | end 57 | 58 | end 59 | 60 | # These tests are to make sure that type declarations and mappings work 61 | # correctly. For specific type boxing/unboxing, see the types/ folder. 62 | context 'when declaring types for properties' do 63 | 64 | before :all do 65 | 66 | @property_types_repo = RDF::Repository.new 67 | Spira.add_repository(:default, @property_types_repo) 68 | 69 | class ::TestType 70 | include Spira::Type 71 | 72 | def self.serialize(value) 73 | RDF::Literal.new(value, :datatype => XSD.test_type) 74 | end 75 | 76 | def self.unserialize(value) 77 | value.value 78 | end 79 | 80 | register_alias XSD.test_type 81 | end 82 | 83 | class ::PropTest 84 | 85 | include Spira::Resource 86 | 87 | default_vocabulary RDF::URI.new('http://example.org/vocab') 88 | base_uri RDF::URI.new('http://example.org/props') 89 | 90 | property :test, :type => TestType 91 | property :xsd_test, :type => XSD.test_type 92 | end 93 | end 94 | 95 | before :each do 96 | @resource = PropTest.for 'test' 97 | end 98 | 99 | it "uses the given serialize function" do 100 | @resource.test = "a string" 101 | @resource.should have_object RDF::Literal.new("a string", :datatype => RDF::XSD.test_type) 102 | end 103 | 104 | it "uses the given unserialize function" do 105 | @resource.test = "a string" 106 | @resource.save! 107 | @resource.test.should == "a string" 108 | @resource.test.should == PropTest.for('test').test 109 | end 110 | 111 | it "correctly associates a URI datatype alias to the correct class" do 112 | Spira.types[RDF::XSD.test_type].should == TestType 113 | PropTest.properties[:xsd_test][:type].should == TestType 114 | end 115 | 116 | end 117 | 118 | 119 | end 120 | -------------------------------------------------------------------------------- /spec/instantiation.spec: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/spec_helper' 2 | 3 | describe Spira do 4 | 5 | context "when instantiating" do 6 | 7 | before :all do 8 | class ::InstantiationTest 9 | include Spira::Resource 10 | 11 | property :name, :predicate => FOAF.name 12 | 13 | end 14 | end 15 | 16 | context "when instantiating from a URI" do 17 | before :each do 18 | @uri = RDF::URI('http://example.org/example') 19 | Spira.add_repository(:default, RDF::Repository.new) 20 | @repo = Spira.repository(:default) 21 | end 22 | 23 | it "should add the 'as' method to RDF::URI" do 24 | @uri.should respond_to :as 25 | end 26 | 27 | it "should allow instantiation from a URI using RDF::URI#as" do 28 | @uri.as(InstantiationTest).should be_a InstantiationTest 29 | end 30 | 31 | it "should yield the new instance to a block given to #as" do 32 | test = @uri.as(InstantiationTest) do |test| 33 | test.name = "test name" 34 | end 35 | test.name.should == "test name" 36 | @repo.should have_statement(RDF::Statement.new(@uri, RDF::FOAF.name, "test name")) 37 | end 38 | 39 | it "should allow instantiation from a resource class using #for" do 40 | InstantiationTest.for(@uri).should be_a InstantiationTest 41 | end 42 | 43 | it "should yield the new instance to a block given to #for" do 44 | test = InstantiationTest.for(@uri) do |test| 45 | test.name = "test name" 46 | end 47 | test.name.should == "test name" 48 | @repo.should have_statement(RDF::Statement.new(@uri, RDF::FOAF.name, "test name")) 49 | end 50 | 51 | it "should allow instantiation from a URI with attributes given" do 52 | test = @uri.as(InstantiationTest, :name => "a name") 53 | test.name.should == "a name" 54 | end 55 | 56 | it "should know if a URI does not exist" do 57 | InstantiationTest.for(@uri).exists?.should be_false 58 | InstantiationTest.for(@uri).exist?.should be_false 59 | end 60 | 61 | it "should know if a URI exists" do 62 | InstantiationTest.repository << RDF::Statement.new(@uri, RDF::FOAF.name, 'test') 63 | InstantiationTest.for(@uri).exists?.should be_true 64 | InstantiationTest.for(@uri).exist?.should be_true 65 | end 66 | 67 | it "should allow the use of #[] as an alias to #for" do 68 | InstantiationTest.repository << RDF::Statement.new(@uri, RDF::FOAF.name, 'test') 69 | InstantiationTest[@uri].exists?.should be_true 70 | end 71 | end 72 | 73 | context "when instantiating from a BNode" do 74 | before :each do 75 | @node = RDF::Node.new 76 | Spira.add_repository(:default, RDF::Repository.new) 77 | @repo = Spira.repository(:default) 78 | end 79 | 80 | it "should add the 'as' method to RDF::" do 81 | @node.should respond_to :as 82 | end 83 | 84 | it "should allow instantiation from a Node using RDF::Node#as" do 85 | @node.as(InstantiationTest).should be_a InstantiationTest 86 | end 87 | 88 | it "should allow instantiation from a resource class using #for" do 89 | InstantiationTest.for(@node).should be_a InstantiationTest 90 | end 91 | 92 | it "should allow instantiation from a Node with attributes given" do 93 | test = @node.as(InstantiationTest, :name => "a name") 94 | test.name.should == "a name" 95 | end 96 | 97 | it "should allow the use of #[] as an alias to #for" do 98 | InstantiationTest[@node].should be_a InstantiationTest 99 | end 100 | end 101 | 102 | context "when creating without an identifier" do 103 | before :each do 104 | Spira.add_repository(:default, RDF::Repository.new) 105 | @repo = Spira.repository(:default) 106 | end 107 | 108 | it "should create an instance with a new Node identifier" do 109 | test = InstantiationTest.new 110 | test.subject.should be_a RDF::Node 111 | test.uri.should be_nil 112 | end 113 | 114 | it "should yield the new instance to a block given to #new" do 115 | test = InstantiationTest.new do |test| 116 | test.name = "test name" 117 | end 118 | test.name.should == "test name" 119 | @repo.should have_statement(RDF::Statement.new(test.subject, RDF::FOAF.name, "test name")) 120 | end 121 | end 122 | 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /lib/spira.rb: -------------------------------------------------------------------------------- 1 | require 'rdf' 2 | require 'promise' 3 | require 'spira/exceptions' 4 | 5 | ## 6 | # Spira is a framework for building projections of RDF data into Ruby classes. 7 | # It is built on top of RDF.rb. 8 | # 9 | # @see http://rdf.rubyforge.org 10 | # @see http://github.com/bhuga/spira 11 | # @see Spira::Resource 12 | module Spira 13 | 14 | ## 15 | # The list of repositories available for Spira resources 16 | # 17 | # @see http://rdf.rubyforge.org/RDF/Repository.html 18 | # @return [Hash{Symbol => RDF::Repository}] 19 | # @private 20 | def repositories 21 | settings[:repositories] ||= {} 22 | end 23 | module_function :repositories 24 | 25 | ## 26 | # The list of all property types available for Spira resources 27 | # 28 | # @see Spira::Types 29 | # @return [Hash{Symbol => Spira::Type}] 30 | def types 31 | settings[:types] ||= {} 32 | end 33 | module_function :types 34 | 35 | ## 36 | # A thread-local hash for storing settings. Used by Resource classes. 37 | # 38 | # @see Spira::Resource 39 | # @see Spira.repositories 40 | # @see Spira.types 41 | def settings 42 | Thread.current[:spira] ||= {} 43 | end 44 | module_function :settings 45 | 46 | ## 47 | # Add a repository to Spira's list of repositories. 48 | # 49 | # @overload add_repository(name, repo) 50 | # @param [Symbol] name The name of this repository 51 | # @param [RDF::Repository] repo An RDF::Repository 52 | # @overload add_repository(name, klass, *args) 53 | # @param [Symbol] name The name of this repository 54 | # @param [RDF::Repository, Class] repo A Class that inherits from RDF::Repository 55 | # @param [*Object] The list of arguments to instantiate the class 56 | # @example Adding an ntriples file as a repository 57 | # Spira.add_repository(:default, RDF::Repository.load('http://datagraph.org/jhacker/foaf.nt')) 58 | # @example Adding an empty repository to be instantiated on use 59 | # Spira.add_repository(:default, RDF::Repository) 60 | # @return [Void] 61 | # @see RDF::Repository 62 | def add_repository(name, klass, *args) 63 | repositories[name] = case klass 64 | when Class 65 | promise { klass.new(*args) } 66 | else 67 | klass 68 | end 69 | if (name == :default) && settings[:repositories][name].nil? 70 | warn "WARNING: Adding nil default repository" 71 | end 72 | end 73 | alias_method :add_repository!, :add_repository 74 | module_function :add_repository, :add_repository! 75 | 76 | ## 77 | # The RDF::Repository for the named repository 78 | # 79 | # @param [Symbol] name The name of the repository 80 | # @return [RDF::Repository] 81 | # @see RDF::Repository 82 | def repository(name) 83 | repositories[name] 84 | end 85 | module_function :repository 86 | 87 | ## 88 | # Clear all repositories from Spira's knowledge. Use it if you want, but 89 | # it's really here for testing. 90 | # 91 | # @return [Void] 92 | # @private 93 | def clear_repositories! 94 | settings[:repositories] = {} 95 | end 96 | module_function :clear_repositories! 97 | 98 | ## 99 | # Alias a property type to another. This allows a range of options to be 100 | # specified for a property type which all reference one Spira::Type 101 | # 102 | # @param [Any] new The new symbol or reference 103 | # @param [Any] original The type the new symbol should refer to 104 | # @return [Void] 105 | # @private 106 | def type_alias(new, original) 107 | types[new] = original 108 | end 109 | module_function :type_alias 110 | 111 | autoload :Resource, 'spira/resource' 112 | autoload :Base, 'spira/base' 113 | autoload :Type, 'spira/type' 114 | autoload :Types, 'spira/types' 115 | autoload :Errors, 'spira/errors' 116 | autoload :VERSION, 'spira/version' 117 | 118 | end 119 | 120 | module RDF 121 | class URI 122 | ## 123 | # Create a projection of this URI as the given Spira::Resource class. 124 | # Equivalent to `klass.for(self, *args)` 125 | # 126 | # @example Instantiating a URI as a Spira Resource 127 | # RDF::URI('http://example.org/person/bob').as(Person) 128 | # @param [Class] klass 129 | # @param [*Any] args Any arguments to pass to klass.for 130 | # @yield [self] Executes a given block and calls `#save!` 131 | # @yieldparam [self] self The newly created instance 132 | # @return [Klass] An instance of klass 133 | def as(klass, *args, &block) 134 | raise ArgumentError, "#{klass} is not a Spira resource" unless klass.is_a?(Class) && klass.ancestors.include?(Spira::Resource) 135 | klass.for(self, *args, &block) 136 | end 137 | end 138 | 139 | class Node 140 | ## 141 | # Create a projection of this Node as the given Spira::Resource class. 142 | # Equivalent to `klass.for(self, *args)` 143 | # 144 | # @example Instantiating a blank node as a Spira Resource 145 | # RDF::Node.new.as(Person) 146 | # @param [Class] klass 147 | # @param [*Any] args Any arguments to pass to klass.for 148 | # @yield [self] Executes a given block and calls `#save!` 149 | # @yieldparam [self] self The newly created instance 150 | # @return [Klass] An instance of klass 151 | def as(klass, *args) 152 | raise ArgumentError, "#{klass} is not a Spira resource" unless klass.is_a?(Class) && klass.ancestors.include?(Spira::Resource) 153 | klass.for(self, *args) 154 | end 155 | end 156 | end 157 | -------------------------------------------------------------------------------- /spec/repository.spec: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/spec_helper' 2 | 3 | # Fixture to test :default repository loading 4 | 5 | describe Spira do 6 | 7 | context "when registering repositories" do 8 | 9 | before :all do 10 | @repo = RDF::Repository.new 11 | class ::Event 12 | include Spira::Resource 13 | property :name, :predicate => DC.title 14 | end 15 | 16 | class ::Stadium 17 | include Spira::Resource 18 | property :name, :predicate => DC.title 19 | default_source :stadium 20 | end 21 | end 22 | 23 | before :each do 24 | Spira.clear_repositories! 25 | end 26 | 27 | it "should construct a repository from a class name" do 28 | lambda {Spira.add_repository(:test_class_name, ::RDF::Repository)}.should_not raise_error 29 | Spira.repository(:test_class_name).should be_a ::RDF::Repository 30 | end 31 | 32 | it "should construct a repository from a class name and constructor arguments" do 33 | lambda {Spira.add_repository(:test_class_args, ::RDF::Repository, :uri => ::RDF::DC.title)}.should_not raise_error 34 | Spira.repository(:test_class_args).should be_a ::RDF::Repository 35 | Spira.repository(:test_class_args).uri.should == ::RDF::DC.title 36 | end 37 | 38 | it "should add a default repository for classes without one" do 39 | lambda {Spira.add_repository(:default, @repo)}.should_not raise_error 40 | Event.repository.should equal @repo 41 | end 42 | 43 | it "should allow updating of the default repository" do 44 | @new_repo = RDF::Repository.new 45 | Spira.add_repository(:default, @new_repo) 46 | Event.repository.should equal @new_repo 47 | end 48 | 49 | it "should allow clearing all repositories" do 50 | Spira.should respond_to :clear_repositories! 51 | Spira.add_repository(:test_clear, RDF::Repository.new) 52 | Spira.clear_repositories! 53 | Spira.repository(:test_clear).should be_nil 54 | end 55 | 56 | end 57 | 58 | context "classes using the default repository" do 59 | 60 | context "without a set repository" do 61 | before :each do 62 | Spira.clear_repositories! 63 | @event = Event.for(RDF::URI.new('http://example.org/events/this-one')) 64 | end 65 | 66 | it "should return nil for a repository which does not exist" do 67 | Event.repository.should == nil 68 | end 69 | 70 | it "should raise an error when accessing an attribute" do 71 | lambda { @event.name }.should raise_error Spira::NoRepositoryError 72 | end 73 | 74 | it "should raise an error to call instance#save!" do 75 | @event.name = "test" 76 | lambda { @event.save! }.should raise_error Spira::NoRepositoryError 77 | end 78 | 79 | it "should raise an error to call instance#destroy!" do 80 | lambda { @event.destroy! }.should raise_error Spira::NoRepositoryError 81 | end 82 | 83 | end 84 | 85 | context "with a set repository" do 86 | before :each do 87 | Spira.clear_repositories! 88 | @repo = RDF::Repository.new 89 | Spira.add_repository(:default, @repo) 90 | end 91 | 92 | it "should know their repository" do 93 | Event.repository.should equal @repo 94 | end 95 | 96 | it "should allow accessing an attribute" do 97 | event = RDF::URI('http://example.org/events/that-one').as(Event) 98 | lambda { event.name }.should_not raise_error 99 | end 100 | 101 | it "should allow calling instance#save!" do 102 | event = Event.for(RDF::URI.new('http://example.org/events/this-one')) 103 | lambda { event.save! }.should_not raise_error 104 | end 105 | end 106 | end 107 | 108 | context "classes using a named repository" do 109 | 110 | context "without a set repository" do 111 | before :each do 112 | Spira.clear_repositories! 113 | end 114 | 115 | it "should return nil for a repository which does not exist" do 116 | Stadium.repository.should == nil 117 | end 118 | 119 | it "should raise an error when accessing an attribute" do 120 | stadium = RDF::URI('http://example.org/stadiums/that-one').as(Stadium) 121 | lambda { stadium.name }.should raise_error Spira::NoRepositoryError 122 | end 123 | 124 | it "should raise an error to call instance#save!" do 125 | stadium = Stadium.for(RDF::URI.new('http://example.org/stadiums/this-one')) 126 | stadium.name = 'test' 127 | lambda { stadium.save! }.should raise_error Spira::NoRepositoryError 128 | end 129 | end 130 | 131 | context "with a set repository" do 132 | before :each do 133 | Spira.clear_repositories! 134 | @repo = RDF::Repository.new 135 | Spira.add_repository(:stadium, @repo) 136 | end 137 | 138 | it "should know their repository" do 139 | Stadium.repository.should equal @repo 140 | end 141 | 142 | it "should allow accessing an attribute" do 143 | stadium = RDF::URI('http://example.org/stadiums/that-one').as(Stadium) 144 | lambda { stadium.name }.should_not raise_error 145 | end 146 | 147 | it "should allow calling instance#save!" do 148 | stadium = Stadium.for(RDF::URI.new('http://example.org/stadiums/this-one')) 149 | lambda { stadium.save! }.should_not raise_error 150 | end 151 | end 152 | end 153 | end 154 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Changelog for Spira 2 | 3 | ## 0.0.11 4 | * Bumped the version dependency on rdf-isomorphic to 0.3.0 5 | * Added support for before_create, after_create, before_save, after_save, 6 | after_update, before_destroy and after_destroy hooks. 7 | * Switch RDF.rb dependency to >= instead of ~> 8 | 9 | ## 0.0.10 10 | * Use RDF::URI.intern on URIs generated via base URIs 11 | * Added a Spira::Types::Native, which will return the RDF::Value for a given 12 | predicate directly without any serialization or dserialization. 13 | 14 | ## 0.0.9 15 | * Fix a bug with Spira::Types::Any that prevented blank node objects 16 | * Added Spira::Resource#copy, #copy!, #copy_resource!, and #rename! 17 | * Fix a bug with fragment RDF::URI arguments that prevented correct URI 18 | construction 19 | * Added Spira::Resource.project(subject, attributes, &block), which creates a 20 | new instance without attempting to perform any base_uri logic on the given 21 | subject. This provides a supported API entry point for implementors to 22 | create their own domain-specific URI construction. 23 | * Updating a value to nil will now remove it from the repository on #save! 24 | * Tweaks to dirty tracking to correctly catch both changed and updated values. 25 | All tests pass for the first time with this change. 26 | * Change gemspec name to work with bundler 27 | 28 | ## 0.0.8 29 | * Remove type checking for repository addition. More power in return for 30 | slightly more difficult error messages. 31 | * Repositories added via klass, *arg are now only instantiated on first use 32 | instead of immediately. 33 | * RDF::URI#as, RDF::Node#as, Resource.for, and Resource#new can now all accept 34 | a block, which yields the new instance and saves it after the block. 35 | * Clarify error message when default repository is not setup 36 | * Added a weak-reference identity map for each instance. Any circular references in 37 | relations will now return the original object instead of querying for a new 38 | one. 39 | * Use a weak-reference identity map when iterating by class. 40 | * When serializing/unserializing, duck typing (:serialize, :unserialize) is now 41 | permitted. 42 | 43 | ## 0.0.7 44 | * Added Resource.[], an alias for Resource.for 45 | * Resource.each now correctly raises an exception when a repository isn't found 46 | 47 | ## 0.0.6 48 | * Added #exists?, which returns a boolean if an instance exists in 49 | the backing store. 50 | * Added #data, which returns an enumerator of all RDF data for a subject, not 51 | just model data. 52 | * #save! and #update! now return self for chaining 53 | * Implemented #update and #update!, which allow setting multiple properties 54 | at once 55 | * Existing values not matching a model's defined type will now be deleted on 56 | #save! 57 | * Saving resources will now only update dirty fields 58 | * Saving resources now removes all existing triples for a given predicate 59 | if the field was updated instead of only removing one. 60 | * Implemented and documented #destroy!, #destroy!(:subject), 61 | #destroy!(:object), and #destroy!(:completely). Removed #destroy_resource! 62 | * has_many collections are now Sets and not Arrays, more accurately reflecting 63 | RDF semantics. 64 | * The Any (default) property type will now work fine with URIs 65 | * Added ResourceDeclarationError to replace various errors that occur during 66 | invalid class declarations via the DSL. 67 | * Raise an error if a non-URI predicate is given in the DSL 68 | * Small updates for RDF.rb 0.2.0 69 | * Implemented dirty field tracking. Resource#dirty?(:name) will now report if 70 | a field has not been saved. 71 | 72 | ## 0.0.5 73 | * Relations can now find related classes in modules, either by absolute 74 | reference, or by class name if they are in the same namespace. 75 | * Fix a bug with default_vocabulary in which a '/' was appended to 76 | vocabularies ending in '#' 77 | * Fix a bug with the Decimal type where round-tripping was incorrect 78 | * Fix some error messages that were missing closing parentheses 79 | 80 | ## 0.0.4 81 | * Added a Decimal type 82 | * Small updates for RDF.rb 0.2.0 compatibility 83 | * Add a Spira::Base class that can be inherited from for users who prefer to 84 | inherit rather than include. 85 | * Resource#new returns to the public API as a way to create a resource with a 86 | new blank node subject. 87 | 88 | ## 0.0.3 89 | * Bumped promise dependency to 0.1.1 to fix a Ruby 1.9 warning 90 | * Rework error handling when a repository is not configured; this should 91 | always now raise a Spira::NoRepositoryError regardless of what operation 92 | was attempted, and the error message was improved as well. 93 | * A '/' is no longer appended to base URIs ending with a '#' 94 | * Resources can now take a BNode as a subject. Implemented #node?, #uri, 95 | #to_uri, #to_node, and #to_subject in support of this; see the yardocs for 96 | exact semantics. RDF::Node is monkey patched with #as, just like RDF::URI, 97 | for instantiation. Old code should not break, but if you want to add 98 | BNodes, you may be using #uri where you want to now be using #subject. 99 | 100 | ## 0.0.2 101 | * Implemented #each on resource classes, allowing classes with a defined RDF 102 | type to be enumerated 103 | * Fragment URIs are now used as strings, allowing i.e. Integers to be used as 104 | the final portion of a URI for classes with a base_uri defined. 105 | * Added an RDF::URI property type 106 | * Implemented #to_rdf and #to_uri for increased compatibility with the RDF.rb 107 | ecosystem 108 | 109 | ## 0.0.1 110 | * Initial release 111 | -------------------------------------------------------------------------------- /spec/has_many.spec: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/spec_helper' 2 | 3 | class Posts < RDF::Vocabulary('http://example.org/posts/predicates/') 4 | property :rating 5 | end 6 | 7 | describe "has_many" do 8 | 9 | before :all do 10 | require 'rdf/ntriples' 11 | class ::Post 12 | 13 | include Spira::Resource 14 | 15 | type URI.new('http://rdfs.org/sioc/types#Post') 16 | 17 | has_many :comments, :predicate => SIOC.has_reply, :type => :Comment 18 | property :title, :predicate => DC.title 19 | property :body, :predicate => SIOC.content 20 | 21 | end 22 | 23 | 24 | 25 | class ::Comment 26 | 27 | include Spira::Resource 28 | 29 | type URI.new('http://rdfs.org/sioc/types#Comment') 30 | 31 | property :post, :predicate => SIOC.reply_of, :type => :Post 32 | property :title, :predicate => DC.title 33 | property :body, :predicate => SIOC.content 34 | has_many :ratings, :predicate => Posts.rating, :type => Integer 35 | 36 | end 37 | end 38 | 39 | context "Comment class basics" do 40 | before :each do 41 | @posts_repository = RDF::Repository.load(fixture('has_many.nt')) 42 | Spira.add_repository(:default, @posts_repository) 43 | @uri = RDF::URI.new('http://example.org/comments/comment1') 44 | @empty_uri = RDF::URI.new('http://example.org/comments/comment0') 45 | @comment = Comment.for @uri 46 | @empty_comment = Comment.for @empty_uri 47 | end 48 | 49 | it "should have a ratings method" do 50 | @comment.should respond_to :ratings 51 | end 52 | 53 | it "should having a ratings= method" do 54 | @comment.should respond_to :ratings= 55 | end 56 | 57 | it "should support is_list?" do 58 | Comment.should respond_to :is_list? 59 | end 60 | 61 | it "should report that ratings are a list" do 62 | Comment.is_list?(:ratings).should == true 63 | end 64 | 65 | it "should report that bodies are not a list" do 66 | Comment.is_list?(:body).should == false 67 | end 68 | 69 | it "should return an empty array of ratings for comments with none" do 70 | @empty_comment.ratings.should == Set.new 71 | end 72 | 73 | it "should return a set of ratings for comments with some" do 74 | @comment.ratings.should be_a Set 75 | @comment.ratings.size.should == 3 76 | @comment.ratings.sort.should == [1,3,5] 77 | end 78 | 79 | it "should allow setting and saving non-array elements" do 80 | @comment.title = 'test' 81 | @comment.title.should == 'test' 82 | @comment.save! 83 | @comment.title.should == 'test' 84 | end 85 | 86 | it "should allow setting on array elements" do 87 | @comment.ratings = [1,2,4] 88 | @comment.save! 89 | @comment.ratings.sort.should == [1,2,4] 90 | end 91 | 92 | it "should allow saving array elements" do 93 | @comment.ratings = [1,2,4] 94 | @comment.ratings.sort.should == [1,2,4] 95 | @comment.save! 96 | @comment.ratings.sort.should == [1,2,4] 97 | @comment = Comment.for @uri 98 | @comment.ratings.sort.should == [1,2,4] 99 | end 100 | 101 | it "should allow appending to array elements" do 102 | @comment.ratings << 6 103 | @comment.ratings.sort.should == [1,3,5,6] 104 | @comment.save! 105 | @comment.ratings.sort.should == [1,3,5,6] 106 | end 107 | 108 | it "should allow saving of appended elements" do 109 | @comment.ratings << 6 110 | @comment.save! 111 | @comment = Comment.for @uri 112 | @comment.ratings.sort.should == [1,3,5,6] 113 | end 114 | end 115 | 116 | context "Post class basics" do 117 | before :all do 118 | @posts_repository = RDF::Repository.load(fixture('has_many.nt')) 119 | Spira.add_repository(:default, @posts_repository) 120 | end 121 | 122 | before :each do 123 | @uri = RDF::URI.new('http://example.org/posts/post1') 124 | @empty_uri = RDF::URI.new('http://example.org/posts/post0') 125 | @post = Post.for @uri 126 | @empty_post = Post.for @empty_uri 127 | @empty_comment_uri = RDF::URI.new('http://example.org/comments/comment0') 128 | @empty_comment = Comment.for @empty_comment_uri 129 | end 130 | 131 | it "should have a comments method" do 132 | @post.should respond_to :comments 133 | end 134 | 135 | it "should have a comments= method" do 136 | @post.should respond_to :comments= 137 | end 138 | 139 | it "should return an empty array from comments for an object with none" do 140 | @empty_post.comments.should == Set.new 141 | end 142 | 143 | it "should return an array of comments for an object with some" do 144 | @post.comments.size.should == 2 145 | @post.comments.each do |comment| 146 | comment.should be_a Comment 147 | end 148 | end 149 | 150 | it "should allow setting and saving non-array elements" do 151 | @post.title = "test post title" 152 | @post.save! 153 | @post.title.should == 'test post title' 154 | end 155 | 156 | it "should allow setting array elements" do 157 | @post.comments = (@post.comments + [@empty_comment]) 158 | @post.comments.size.should == 3 159 | @post.comments.should include @empty_comment 160 | end 161 | 162 | it "should allow saving array elements" do 163 | comments = @post.comments + [@empty_comment] 164 | @post.comments = (@post.comments + [@empty_comment]) 165 | @post.comments.size.should == 3 166 | @post.save! 167 | @post.comments.size.should == 3 168 | @post.comments.each do |comment| 169 | comments.should include comment 170 | end 171 | end 172 | end 173 | 174 | 175 | 176 | 177 | end 178 | -------------------------------------------------------------------------------- /spec/basic.spec: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/spec_helper' 2 | 3 | # Tests of basic functionality--getting, setting, creating, saving, when no 4 | # relations or anything fancy are involved. 5 | 6 | describe Spira do 7 | 8 | before :all do 9 | class ::Person 10 | include Spira::Resource 11 | base_uri "http://example.org/example/people" 12 | property :name, :predicate => RDFS.label 13 | property :age, :predicate => FOAF.age, :type => Integer 14 | end 15 | 16 | class Employee 17 | include Spira::Resource 18 | property :name, :predicate => RDFS.label 19 | property :age, :predicate => FOAF.age, :type => Integer 20 | end 21 | 22 | require 'rdf/ntriples' 23 | @person_repository = RDF::Repository.load(fixture('bob.nt')) 24 | Spira.add_repository(:default, @person_repository) 25 | end 26 | 27 | before :each do 28 | @person_repository = RDF::Repository.load(fixture('bob.nt')) 29 | Spira.add_repository(:default, @person_repository) 30 | end 31 | 32 | context "The person fixture" do 33 | 34 | it "should know its source" do 35 | Person.repository.should be_a RDF::Repository 36 | Person.repository.should equal @person_repository 37 | end 38 | 39 | context "when instantiating new URIs" do 40 | 41 | it "should offer a for method" do 42 | Person.should respond_to :for 43 | end 44 | 45 | it "should be able to create new instances for non-existing resources" do 46 | lambda { Person.for(RDF::URI.new('http://example.org/newperson')) }.should_not raise_error 47 | end 48 | 49 | it "should create Person instances" do 50 | Person.for(RDF::URI.new('http://example.org/newperson')).should be_a Person 51 | end 52 | 53 | context "with attributes given" do 54 | before :each do 55 | @alice = Person.for 'alice', :age => 30, :name => 'Alice' 56 | end 57 | 58 | it "should have properties if it had them as attributes on creation" do 59 | @alice.age.should == 30 60 | @alice.name.should == 'Alice' 61 | end 62 | 63 | it "should save updated properties" do 64 | @alice.age = 16 65 | @alice.age.should == 16 66 | end 67 | 68 | end 69 | end 70 | 71 | context "when instantiating existing URIs" do 72 | 73 | it "should return a Person for a non-existent URI" do 74 | Person.for('nobody').should be_a Person 75 | end 76 | 77 | it "should return an empty Person for a non-existent URI" do 78 | person = Person.for('nobody') 79 | person.age.should be_nil 80 | person.name.should be_nil 81 | end 82 | 83 | end 84 | 85 | context "with attributes given" do 86 | before :each do 87 | @alice = Person.for 'alice', :age => 30, :name => 'Alice' 88 | @bob = Person.for 'bob', :name => 'Bob Smith II' 89 | end 90 | 91 | it "should overwrite existing properties with given attributes" do 92 | @bob.name.should == "Bob Smith II" 93 | end 94 | 95 | it "should not overwrite existing properties which are not given" do 96 | @bob.age.should == 15 97 | end 98 | 99 | it "should allow property updating" do 100 | @bob.age = 16 101 | @bob.age.should == 16 102 | end 103 | end 104 | 105 | context "A newly-created person" do 106 | 107 | before :each do 108 | @person = Person.for 'http://example.org/example/people/alice' 109 | end 110 | 111 | context "in respect to some general methods" do 112 | it "should #uri" do 113 | @person.should respond_to :uri 114 | end 115 | 116 | it "should return a RDF::URI from #uri" do 117 | @person.uri.should be_a RDF::URI 118 | end 119 | 120 | it "should return the correct URI from #uri" do 121 | @person.uri.to_s.should == 'http://example.org/example/people/alice' 122 | end 123 | 124 | it "should support #to_uri" do 125 | @person.should respond_to :to_uri 126 | end 127 | 128 | it "should return the correct URI from #to_uri" do 129 | @person.to_uri.to_s.should == 'http://example.org/example/people/alice' 130 | end 131 | 132 | it "should support #to_rdf" do 133 | @person.should respond_to :to_rdf 134 | end 135 | 136 | it "should return an RDF::Enumerable for #to_rdf" do 137 | @person.to_rdf.should be_a RDF::Enumerable 138 | end 139 | end 140 | 141 | context "using getters and setters" do 142 | it "should have a name method" do 143 | @person.should respond_to :name 144 | end 145 | 146 | it "should have an age method" do 147 | @person.should respond_to :age 148 | end 149 | 150 | it "should return nil for unset properties" do 151 | @person.name.should == nil 152 | end 153 | 154 | it "should allow setting a name" do 155 | lambda { @person.name = "Bob Smith" }.should_not raise_error 156 | end 157 | 158 | it "should allow getting a name" do 159 | @person.name = "Bob Smith" 160 | @person.name.should == "Bob Smith" 161 | end 162 | 163 | it "should allow setting an age" do 164 | lambda { @person.age = 15 }.should_not raise_error 165 | end 166 | 167 | it "should allow getting an age" do 168 | @person.age = 15 169 | @person.age.should == 15 170 | end 171 | 172 | it "should correctly set more than one property" do 173 | @person.age = 15 174 | @person.name = "Bob Smith" 175 | @person.age.should == 15 176 | @person.name.should == "Bob Smith" 177 | end 178 | end 179 | end 180 | 181 | end 182 | end 183 | -------------------------------------------------------------------------------- /spec/loading.spec: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/spec_helper' 2 | 3 | describe Spira do 4 | 5 | before :all do 6 | class ::LoadTest 7 | include Spira::Resource 8 | type FOAF.load_type 9 | property :name, :predicate => FOAF.name 10 | property :label, :predicate => RDFS.label 11 | property :child, :predicate => FOAF.load_test, :type => 'LoadTest' 12 | end 13 | end 14 | 15 | context "when querying repositories" do 16 | 17 | before :each do 18 | @repo = RDF::Repository.new 19 | Spira.add_repository(:default, @repo) 20 | @uri = RDF::URI('http://example.org/example') 21 | end 22 | 23 | it "should not attempt to query on instantiation" do 24 | @repo.should_not_receive(:query) 25 | test = @uri.as(LoadTest) 26 | end 27 | 28 | it "should not attempt query on property setting" do 29 | @repo.should_not_receive(:query) 30 | test = @uri.as(LoadTest) 31 | test.name = "test" 32 | end 33 | 34 | it "should attempt to query on property getting" do 35 | @repo.should_receive(:query).once.and_return([]) 36 | test = @uri.as(LoadTest) 37 | name = test.name 38 | end 39 | 40 | it "should only query once for all properties" do 41 | @repo.should_receive(:query).once.and_return([]) 42 | test = @uri.as(LoadTest) 43 | name = test.name 44 | label = test.label 45 | end 46 | 47 | it "should support :reload" do 48 | test = @uri.as(LoadTest) 49 | test.should respond_to :reload 50 | end 51 | 52 | it "should not touch the repository to reload" do 53 | @repo.should_not_receive(:query) 54 | test = @uri.as(LoadTest) 55 | test.reload 56 | end 57 | 58 | it "should query the repository again after a reload" do 59 | @repo.should_receive(:query).twice.and_return([]) 60 | test = @uri.as(LoadTest) 61 | name = test.name 62 | test.reload 63 | name = test.name 64 | end 65 | 66 | context "for relations" do 67 | before :each do 68 | @child_uri = RDF::URI("http://example.org/example2") 69 | @repo << RDF::Statement.new(:subject => @uri, :predicate => RDF::FOAF.load_test, :object => @child_uri) 70 | @repo << RDF::Statement.new(:subject => @uri, :predicate => RDF::FOAF.name, :object => RDF::Literal.new("a name")) 71 | @repo << RDF::Statement.new(:subject => @uri, :predicate => RDF::RDFS.label, :object => RDF::Literal.new("a name")) 72 | # @uri and @child_uri now point at each other 73 | @repo << RDF::Statement.new(:subject => @child_uri, :predicate => RDF::FOAF.load_test, :object => @uri) 74 | @repo << RDF::Statement.new(:subject => @child_uri, :predicate => RDF::FOAF.load_test, :object => @uri) 75 | # Set up types for iteration 76 | @repo << RDF::Statement.new(:subject => @uri, :predicate => RDF.type, :object => RDF::FOAF.load_type) 77 | @repo << RDF::Statement.new(:subject => @child_uri, :predicate => RDF.type, :object => RDF::FOAF.load_type) 78 | # We need this copy to return from mocks, as the return value is itself queried inside spira, 79 | # confusing the count 80 | @statements = RDF::Repository.new 81 | @statements.insert(*@repo) 82 | end 83 | 84 | it "should not query the repository when loading a parent and not accessing a child" do 85 | @repo.should_receive(:query).once.and_return(@statements) 86 | test = @uri.as(LoadTest) 87 | name = test.name 88 | end 89 | 90 | it "should query the repository when loading a parent and accessing a field on a child" do 91 | @repo.should_receive(:query).twice.and_return(@statements, []) 92 | test = @uri.as(LoadTest) 93 | child = test.child.name 94 | end 95 | 96 | it "should not re-query to access a child twice" do 97 | @repo.should_receive(:query).twice.and_return(@statements, []) 98 | test = @uri.as(LoadTest) 99 | child = test.child.name 100 | child = test.child.name 101 | end 102 | 103 | it "should not re-query to access a child's parent from the child" do 104 | @repo.should_receive(:query).twice.and_return(@statements) 105 | test = @uri.as(LoadTest) 106 | test.child.child.name.should == "a name" 107 | test.child.child.name.should == "a name" 108 | test.child.child.name.should == "a name" 109 | end 110 | 111 | it "should re-query for children after a #reload" do 112 | @repo.should_receive(:query).exactly(4).times.and_return(@statements) 113 | test = @uri.as(LoadTest) 114 | test.child.child.name.should == "a name" 115 | test.child.name.should be_nil 116 | test.reload 117 | test.child.child.name.should == "a name" 118 | test.child.name.should be_nil 119 | end 120 | 121 | it "should not re-query to iterate by type twice" do 122 | # once to get the list of subjects, once for @uri, once for @child_uri, 123 | # and once for the list of subjects again 124 | @repo.should_receive(:query).exactly(4).times.and_return(@statements) 125 | # need to map to touch a property on each to make sure they actually 126 | # get loaded due to lazy evaluation 127 | LoadTest.each.map { |lt| lt.name }.size.should == 2 128 | LoadTest.each.map { |lt| lt.name }.size.should == 2 129 | end 130 | 131 | it "should not touch the repository to reload" do 132 | @repo.should_not_receive(:query) 133 | LoadTest.reload 134 | end 135 | 136 | it "should query the repository again after a reload" do 137 | # once for list of subjects, twice for items, once for list of subjects, twice for items 138 | @repo.should_receive(:query).exactly(6).times.and_return(@statements) 139 | LoadTest.each.map { |lt| lt.name }.size.should == 2 140 | LoadTest.reload 141 | LoadTest.each.map { |lt| lt.name }.size.should == 2 142 | end 143 | end 144 | end 145 | 146 | end 147 | -------------------------------------------------------------------------------- /spec/rdf_types.spec: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/spec_helper' 2 | # These classes are to test finding based on rdfs.type 3 | 4 | 5 | class Cars < RDF::Vocabulary('http://example.org/cars/') 6 | property :car 7 | property :van 8 | property :car1 9 | property :van 10 | property :station_wagon 11 | property :unrelated_type 12 | end 13 | 14 | describe 'models with a defined rdf type' do 15 | 16 | 17 | before :all do 18 | require 'rdf/ntriples' 19 | 20 | class ::Car 21 | include Spira::Resource 22 | type Cars.car 23 | property :name, :predicate => RDFS.label 24 | end 25 | 26 | class ::Van 27 | include Spira::Resource 28 | type Cars.van 29 | property :name, :predicate => RDFS.label 30 | end 31 | 32 | class ::Wagon 33 | include Spira::Resource 34 | property :name, :predicate => RDFS.label 35 | end 36 | end 37 | 38 | before :each do 39 | @types_repository = RDF::Repository.load(fixture('types.nt')) 40 | Spira.add_repository(:default, @types_repository) 41 | end 42 | 43 | context "when declaring types" do 44 | it "should raise an error when declaring a non-uri type" do 45 | lambda { 46 | class ::XYZ 47 | include Spira::Resource 48 | type 'a string, for example' 49 | end 50 | }.should raise_error TypeError 51 | end 52 | 53 | it "should provide a class method which returns the type" do 54 | Car.should respond_to :type 55 | end 56 | 57 | it "should return the correct type" do 58 | Car.type.should == Cars.car 59 | end 60 | 61 | it "should return nil if no type is declared" do 62 | Wagon.type.should == nil 63 | end 64 | end 65 | 66 | context "When finding by types" do 67 | it "should find 1 car" do 68 | Car.count.should == 1 69 | end 70 | 71 | it "should find 3 vans" do 72 | Van.count.should == 3 73 | end 74 | 75 | end 76 | 77 | context "when creating" do 78 | 79 | before :each do 80 | @car = Car.for RDF::URI.new('http://example.org/cars/newcar') 81 | end 82 | 83 | it "should have a type on creation" do 84 | @car.type.should == Car.type 85 | end 86 | 87 | it "should include a type statement on dump" do 88 | @car.query(:predicate => RDF.type).count.should == 1 89 | @car.query(:predicate => RDF.type).first.object.should == Car.type 90 | @car.query(:predicate => RDF.type).first.subject.should == @car.uri 91 | end 92 | 93 | it "should raise a type error when receiving a type attribute option on instantiation" do 94 | lambda { Car.for RDF::URI.new('http://example.org/cars/newcar2'), :type => Cars.van }.should raise_error TypeError 95 | end 96 | 97 | end 98 | 99 | context "when loading" do 100 | before :each do 101 | @car1 = Car.for Cars.car1 102 | @car2 = Car.for Cars.car2 103 | end 104 | 105 | it "should have a type" do 106 | @car1.type.should == Car.type 107 | end 108 | 109 | it "should have a type when loading a resource without one in the data store" do 110 | @car2.type.should == Car.type 111 | end 112 | end 113 | 114 | context "when saving" do 115 | before :each do 116 | @car2 = Car.for Cars.car2 117 | end 118 | 119 | it "should save a type for resources which don't have one in the data store" do 120 | @car2.save! 121 | @types_repository.query(:subject => Cars.car2, :predicate => RDF.type, :object => Cars.car).count.should == 1 122 | end 123 | 124 | it "should save a type for newly-created resources which in the data store" do 125 | car3 = Car.for(Cars.car3) 126 | car3.save! 127 | @types_repository.query(:subject => Cars.car3, :predicate => RDF.type, :object => Cars.car).count.should == 1 128 | end 129 | end 130 | 131 | context "When getting/setting" do 132 | before :each do 133 | @car = Car.for Cars.car1 134 | @car.nil?.should_not be_true 135 | end 136 | 137 | it "should allow setting other properties" do 138 | @car.name = "prius" 139 | @car.save! 140 | @car.type.should == Cars.car 141 | @car.name.should == "prius" 142 | end 143 | 144 | it "should raise an exception when trying to change the type" do 145 | lambda {@car.type = Cars.van}.should raise_error TypeError 146 | end 147 | 148 | it "should maintain all triples related to this object on save" do 149 | original_triples = @types_repository.query(:subject => Cars.car1) 150 | @car.name = 'testing123' 151 | @car.save! 152 | @types_repository.query(:subject => Cars.car1).count.should == original_triples.size 153 | end 154 | end 155 | 156 | context "when counting" do 157 | it "should provide a count method for resources with types" do 158 | Car.count.should == 1 159 | end 160 | 161 | it "should increase the count when items are saved" do 162 | Car.for(Cars.toyota).save! 163 | Car.count.should == 2 164 | end 165 | 166 | it "should decrease the count when items are destroyed" do 167 | Car.for(Cars.car1).destroy! 168 | Car.count.should == 0 169 | end 170 | 171 | it "should raise a Spira::NoTypeError to call #count for models without types" do 172 | lambda { Wagon.count }.should raise_error Spira::NoTypeError 173 | end 174 | end 175 | 176 | context "when enumerating" do 177 | it "should provide an each method for resources with types" do 178 | Van.each.to_a.size.should == 3 179 | end 180 | 181 | it "should raise a Spira::NoTypeError to call #each for models without types" do 182 | lambda { Wagon.each }.should raise_error Spira::NoTypeError 183 | end 184 | 185 | it "should return an enumerator if no block is given" do 186 | Van.each.should be_a RDF::Enumerator 187 | end 188 | 189 | it "should execute a block if one is given" do 190 | vans = [] 191 | Van.each do |resource| 192 | vans << resource 193 | end 194 | [Cars.van1, Cars.van2, Cars.van3].each do |uri| 195 | vans.any? { |van| van.uri == uri }.should be_true 196 | end 197 | end 198 | end 199 | 200 | end 201 | -------------------------------------------------------------------------------- /spec/relations.spec: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/spec_helper' 2 | 3 | describe "Spira resources" do 4 | 5 | before :all do 6 | 7 | class ::CDs < RDF::Vocabulary('http://example.org/') 8 | property :artist 9 | property :cds 10 | property :artists 11 | property :has_cd 12 | end 13 | 14 | class ::CD 15 | include Spira::Resource 16 | base_uri CDs.cds 17 | property :name, :predicate => DC.title, :type => String 18 | property :artist, :predicate => CDs.artist, :type => 'Artist' 19 | end 20 | 21 | class ::Artist 22 | include Spira::Resource 23 | base_uri CDs.artists 24 | property :name, :predicate => DC.title, :type => String 25 | has_many :cds, :predicate => CDs.has_cd, :type => :CD 26 | end 27 | end 28 | 29 | context "when referencing relationships" do 30 | context "in the root namespace" do 31 | before :all do 32 | class ::RootNSTest 33 | include Spira::Resource 34 | property :name, :predicate => DC.title, :type => 'RootNSTest' 35 | end 36 | Spira.add_repository!(:default, RDF::Repository.new) 37 | end 38 | 39 | it "should find a class based on the string version of the name" do 40 | test = RootNSTest.new 41 | subject = test.subject 42 | test.name = RootNSTest.new 43 | test.name.save! 44 | test.save! 45 | 46 | test = subject.as(RootNSTest) 47 | test.name.should be_a RootNSTest 48 | end 49 | end 50 | 51 | context "in the same namespace" do 52 | before :all do 53 | module ::NSTest 54 | class X 55 | include Spira::Resource 56 | property :name, :predicate => DC.title, :type => 'Y' 57 | end 58 | class Y 59 | include Spira::Resource 60 | end 61 | end 62 | Spira.add_repository!(:default, RDF::Repository.new) 63 | end 64 | 65 | it "should find a class based on the string version of the name" do 66 | test = NSTest::X.new 67 | subject = test.subject 68 | test.name = NSTest::Y.new 69 | 70 | test.save! 71 | 72 | test = NSTest::X.for(subject) 73 | test.name.should be_a NSTest::Y 74 | end 75 | end 76 | 77 | context "in another namespace" do 78 | before :all do 79 | module ::NSTest 80 | class Z 81 | include Spira::Resource 82 | property :name, :predicate => DC.title, :type => 'NSTestA::A' 83 | end 84 | end 85 | module ::NSTestA 86 | class A 87 | include Spira::Resource 88 | end 89 | end 90 | end 91 | 92 | it "should find a class based on the string version of the name" do 93 | test = NSTest::Z.new 94 | subject = test.subject 95 | test.name = NSTestA::A.new 96 | 97 | test.save! 98 | 99 | test = NSTest::Z.for(subject) 100 | test.name.should be_a NSTestA::A 101 | end 102 | end 103 | end 104 | 105 | 106 | context "with a one-to-many relationship" do 107 | 108 | before :each do 109 | require 'rdf/ntriples' 110 | @cds_repository = RDF::Repository.load(fixture('relations.nt')) 111 | Spira.add_repository(:default, @cds_repository) 112 | @cd = CD.for 'nevermind' 113 | @artist = Artist.for 'nirvana' 114 | end 115 | 116 | it "should find a cd" do 117 | @cd.should be_a CD 118 | end 119 | 120 | it "should find the artist" do 121 | @artist.should be_a Artist 122 | end 123 | 124 | it "should find an artist for a cd" do 125 | @cd.artist.should be_a Artist 126 | end 127 | 128 | it "should find the correct artist for a cd" do 129 | @cd.artist.uri.should == @artist.uri 130 | end 131 | 132 | it "should find CDs for an artist" do 133 | cds = @artist.cds 134 | cds.should be_a Set 135 | cds.find { |cd| cd.name == 'Nevermind' }.should be_true 136 | cds.find { |cd| cd.name == 'In Utero' }.should be_true 137 | end 138 | 139 | it "should not reload an object for a simple reverse relationship" do 140 | @artist.cds.first.artist.should equal @artist 141 | artist_cd = @cd.artist.cds.find { | list_cd | list_cd.uri == @cd.uri } 142 | @cd.should equal artist_cd 143 | end 144 | 145 | it "should find a model object for a uri" do 146 | @cd.artist.should == @artist 147 | end 148 | 149 | it "should make a valid statement referencing the assigned objects URI" do 150 | @kurt = Artist.for('kurt cobain') 151 | @cd.artist = @kurt 152 | statement = @cd.query(:predicate => CDs.artist).first 153 | statement.subject.should == @cd.uri 154 | statement.predicate.should == CDs.artist 155 | statement.object.should == @kurt.uri 156 | end 157 | 158 | end 159 | 160 | context "with invalid relationships" do 161 | 162 | before :all do 163 | @invalid_repo = RDF::Repository.new 164 | Spira.add_repository(:default, @invalid_repo) 165 | end 166 | 167 | context "when accessing a field named for a non-existant class" do 168 | 169 | before :all do 170 | class ::RelationsTestA 171 | include Spira::Resource 172 | base_uri CDs.cds 173 | property :invalid, :predicate => CDs.artist, :type => :non_existant_type 174 | end 175 | 176 | @uri_b = RDF::URI.new(CDs.cds.to_s + "/invalid_b") 177 | @invalid_repo.insert(RDF::Statement.new(@uri_b, CDs.artist, "whatever")) 178 | end 179 | 180 | it "should raise a NameError when saving an object with the invalid property" do 181 | # RelationsTestA.for('invalid_a', :invalid => Object.new).save! 182 | lambda { RelationsTestA.for('invalid_a', :invalid => Object.new).save! }.should raise_error NameError 183 | end 184 | 185 | it "should raise a NameError when accessing the invalid property on an existing object" do 186 | lambda { RelationsTestA.for('invalid_b').invalid }.should raise_error NameError 187 | end 188 | 189 | end 190 | 191 | context "when accessing a field for a class that is not a Spira::Resource" do 192 | before :all do 193 | class ::RelationsTestB 194 | include Spira::Resource 195 | property :invalid, :predicate => DC.title, :type => 'Object' 196 | end 197 | end 198 | 199 | it "should should raise a TypeError when saving an object with the invalid property" do 200 | lambda { RelationsTestB.new(:invalid => Object.new).save! }.should raise_error TypeError 201 | end 202 | 203 | it "should raise a TypeError when accessing the invalid property on an existing object" do 204 | subject = RDF::Node.new 205 | @invalid_repo.insert [subject, RDF::DC.title, 'something'] 206 | lambda { RelationsTestB.for(subject).invalid }.should raise_error TypeError 207 | end 208 | end 209 | end 210 | end 211 | -------------------------------------------------------------------------------- /spec/hooks.spec: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/spec_helper' 2 | 3 | describe 'Spira resources' do 4 | 5 | before :all do 6 | class ::HookTest < ::Spira::Base 7 | property :name, :predicate => FOAF.name 8 | property :age, :predicate => FOAF.age 9 | end 10 | @subject = RDF::URI.intern('http://example.org/test') 11 | end 12 | 13 | before :each do 14 | @repository = RDF::Repository.new 15 | @repository << RDF::Statement.new(@subject, RDF::FOAF.name, "A name") 16 | Spira.add_repository(:default, @repository) 17 | end 18 | 19 | context "with a before_create method" do 20 | before :all do 21 | class ::BeforeCreateTest < ::HookTest 22 | type FOAF.bc_test 23 | 24 | def before_create 25 | self.name = "Everyone has this name" 26 | end 27 | end 28 | 29 | class ::BeforeCreateWithoutTypeTest < ::HookTest 30 | def before_create 31 | self.name = "Everyone has this name" 32 | end 33 | end 34 | end 35 | 36 | before :each do 37 | @repository << RDF::Statement.new(@subject, RDF.type, RDF::FOAF.bc_test) 38 | end 39 | 40 | it "calls the before_create method before saving a resouce for the first time" do 41 | test = RDF::URI('http://example.org/new').as(::BeforeCreateTest) 42 | test.save! 43 | test.name.should == "Everyone has this name" 44 | @repository.should have_statement RDF::Statement.new(test.subject, RDF::FOAF.name, "Everyone has this name") 45 | end 46 | 47 | it "does not call the before_create method if the resource previously existed" do 48 | test = @subject.as(::BeforeCreateTest) 49 | test.save! 50 | test.name.should == "A name" 51 | @repository.should have_statement RDF::Statement.new(test.subject, RDF::FOAF.name, "A name") 52 | @repository.should_not have_statement RDF::Statement.new(test.subject, RDF::FOAF.name, "Everyone has this name") 53 | end 54 | 55 | it "does not call the before_create method without a type declaration" do 56 | test = RDF::URI('http://example.org/new').as(::BeforeCreateWithoutTypeTest) 57 | test.save! 58 | @repository.should_not have_statement RDF::Statement.new(test.subject, RDF::FOAF.name, "Everyone has this name") 59 | end 60 | end 61 | 62 | context "with an after_create method" do 63 | before :all do 64 | class ::AfterCreateTest < ::HookTest 65 | type FOAF.ac_test 66 | 67 | def after_create 68 | self.name = "Everyone has this unsaved name" 69 | end 70 | end 71 | 72 | class ::AfterCreateWithoutTypeTest < ::HookTest 73 | def after_create 74 | self.name = "Everyone has this unsaved name" 75 | end 76 | end 77 | end 78 | 79 | before :each do 80 | @repository << RDF::Statement.new(@subject, RDF.type, RDF::FOAF.bc_test) 81 | end 82 | 83 | it "calls the after_create method after saving a resource for the first time" do 84 | test = RDF::URI('http://example.org/new').as(::AfterCreateTest) 85 | test.save! 86 | test.name.should == "Everyone has this unsaved name" 87 | @repository.should_not have_statement RDF::Statement.new(test.subject, RDF::FOAF.name, "Everyone has this name") 88 | end 89 | 90 | it "does not call after_create if the resource previously existed" do 91 | test = @subject.as(::AfterCreateTest) 92 | test.save! 93 | test.name.should == "A name" 94 | @repository.should have_statement RDF::Statement.new(test.subject, RDF::FOAF.name, "A name") 95 | @repository.should_not have_statement RDF::Statement.new(test.subject, RDF::FOAF.name, "Everyone has this name") 96 | end 97 | 98 | it "does not call the after_create method without a type declaration" do 99 | test = RDF::URI('http://example.org/new').as(::AfterCreateWithoutTypeTest) 100 | test.save! 101 | test.name.should be_nil 102 | end 103 | end 104 | 105 | context "with an after_update method" do 106 | 107 | before :all do 108 | class ::AfterUpdateTest < ::HookTest 109 | def after_update 110 | self.age = 15 111 | end 112 | end 113 | end 114 | 115 | it "calls the after_update method after updating a field" do 116 | test = @subject.as(AfterUpdateTest) 117 | test.age.should be_nil 118 | test.update(:name => "A new name") 119 | test.age.should == 15 120 | end 121 | 122 | it "does not call the after_update method after simply setting a field" do 123 | test = @subject.as(AfterUpdateTest) 124 | test.age.should be_nil 125 | test.name = "a new name" 126 | test.age.should be_nil 127 | end 128 | end 129 | 130 | context "with a before_save method" do 131 | before :all do 132 | class ::BeforeSaveTest < ::HookTest 133 | def before_save 134 | self.age = 15 135 | end 136 | end 137 | end 138 | 139 | it "calls the before_save method before saving" do 140 | test = @subject.as(::BeforeSaveTest) 141 | test.age.should be_nil 142 | test.save! 143 | test.age.should == 15 144 | @repository.should have_statement RDF::Statement(@subject, RDF::FOAF.age, 15) 145 | end 146 | end 147 | 148 | context "with an after_save method" do 149 | 150 | before :all do 151 | class ::AfterSaveTest < ::HookTest 152 | def after_save 153 | self.age = 15 154 | end 155 | end 156 | end 157 | 158 | it "calls the after_save method after saving" do 159 | test = @subject.as(::AfterSaveTest) 160 | test.age.should be_nil 161 | test.save! 162 | test.age.should == 15 163 | @repository.should_not have_statement RDF::Statement(@subject, RDF::FOAF.age, 15) 164 | end 165 | 166 | end 167 | 168 | context "with a before_destroy method" do 169 | before :all do 170 | class ::BeforeDestroyTest < ::HookTest 171 | def before_destroy 172 | self.class.repository.delete(RDF::Statement.new(nil,RDF::FOAF.other,nil)) 173 | end 174 | end 175 | end 176 | 177 | before :each do 178 | @repository << RDF::Statement.new(RDF::URI('http://example.org/new'), RDF::FOAF.other, "test") 179 | end 180 | 181 | it "calls the before_destroy method before destroying" do 182 | @subject.as(::BeforeDestroyTest).destroy!(:completely) 183 | @repository.should be_empty 184 | end 185 | end 186 | 187 | context "with an after_destroy method" do 188 | before :all do 189 | class ::AfterDestroyTest < ::HookTest 190 | def after_destroy 191 | self.class.repository.delete(RDF::Statement.new(nil,RDF::FOAF.other,nil)) 192 | raise Exception if self.class.repository.has_subject?(self.subject) 193 | end 194 | end 195 | end 196 | 197 | before :each do 198 | @repository << RDF::Statement.new(RDF::URI('http://example.org/new'), RDF::FOAF.other, "test") 199 | end 200 | 201 | it "calls the after_destroy method after destroying" do 202 | # This would raise an exception if after_destroy were called before deleting is confirmed 203 | lambda { @subject.as(::AfterDestroyTest).destroy!(:completely) }.should_not raise_error 204 | # This one makes sure that after_destory got called at all 205 | @repository.should_not have_predicate RDF::FOAF.other 206 | end 207 | 208 | 209 | end 210 | 211 | end 212 | -------------------------------------------------------------------------------- /spec/inheritance.spec: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/spec_helper' 2 | 3 | describe Spira do 4 | 5 | context "inheritance" do 6 | 7 | before :all do 8 | class ::InheritanceItem 9 | include Spira::Resource 10 | 11 | property :title, :predicate => DC.title, :type => String 12 | type SIOC.item 13 | end 14 | 15 | class ::InheritancePost < ::InheritanceItem 16 | type SIOC.post 17 | property :author, :predicate => DC.author 18 | end 19 | 20 | class ::InheritedType < ::InheritanceItem 21 | end 22 | 23 | class ::InheritanceForumPost < ::InheritancePost 24 | end 25 | 26 | class ::InheritanceContainer 27 | include Spira::Resource 28 | type SIOC.container 29 | 30 | has_many :items, :type => 'InheritanceItem', :predicate => SIOC.container_of 31 | end 32 | 33 | class ::InheritanceForum < ::InheritanceContainer 34 | type SIOC.forum 35 | 36 | #property :moderator, :predicate => SIOC.has_moderator 37 | end 38 | 39 | end 40 | 41 | context "when passing properties to children, " do 42 | before :each do 43 | Spira.add_repository(:default, RDF::Repository.new) 44 | @item = RDF::URI('http://example.org/item').as(InheritanceItem) 45 | @post = RDF::URI('http://example.org/post').as(InheritancePost) 46 | @type = RDF::URI('http://example.org/type').as(InheritedType) 47 | @forum = RDF::URI('http://example.org/forum').as(InheritanceForumPost) 48 | end 49 | 50 | it "should respond to a property getter" do 51 | @post.should respond_to :title 52 | end 53 | 54 | it "should respond to a property setter" do 55 | @post.should respond_to :title= 56 | end 57 | 58 | it "should respond to a propety getter on a grandchild class" do 59 | @forum.should respond_to :title 60 | end 61 | 62 | it "should respond to a propety setter on a grandchild class" do 63 | @forum.should respond_to :title= 64 | end 65 | 66 | it "should maintain property metadata" do 67 | InheritancePost.properties.should have_key :title 68 | InheritancePost.properties[:title][:type].should == Spira::Types::String 69 | end 70 | 71 | it "should add properties of child classes" do 72 | @post.should respond_to :author 73 | @post.should respond_to :author= 74 | InheritancePost.properties.should have_key :author 75 | end 76 | 77 | it "should allow setting a property" do 78 | @post.title = "test title" 79 | @post.title.should == "test title" 80 | end 81 | 82 | it "should inherit an RDFS type if one is not given" do 83 | InheritedType.type.should == RDF::SIOC.item 84 | end 85 | 86 | it "should overwrite the RDFS type if one is given" do 87 | InheritancePost.type.should == RDF::SIOC.post 88 | end 89 | 90 | it "should inherit an RDFS type from the most recent ancestor" do 91 | InheritanceForumPost.type.should == RDF::SIOC.post 92 | end 93 | 94 | context "when saving properties" do 95 | before :each do 96 | @post.title = "test title" 97 | @post.save! 98 | @type.title = "type title" 99 | @type.save! 100 | @forum.title = "forum title" 101 | @forum.save! 102 | end 103 | 104 | it "should save an edited property" do 105 | InheritancePost.repository.query(:subject => @post.uri, :predicate => RDF::DC.title).count.should == 1 106 | end 107 | 108 | it "should save an edited property on a grandchild class" do 109 | InheritanceForumPost.repository.query(:subject => @forum.uri, :predicate => RDF::DC.title).count.should == 1 110 | end 111 | 112 | it "should save the new type" do 113 | InheritancePost.repository.query(:subject => @post.uri, :predicate => RDF.type, :object => RDF::SIOC.post).count.should == 1 114 | end 115 | 116 | it "should not save the supertype for a subclass which has specified one" do 117 | InheritancePost.repository.query(:subject => @post.uri, :predicate => RDF.type, :object => RDF::SIOC.item).count.should == 0 118 | end 119 | 120 | it "should save the supertype for a subclass which has not specified one" do 121 | InheritedType.repository.query(:subject => @type.uri, :predicate => RDF.type, :object => RDF::SIOC.item).count.should == 1 122 | end 123 | end 124 | end 125 | 126 | context "when including modules" do 127 | before :all do 128 | module ::SpiraModule1 129 | include Spira::Resource 130 | has_many :names, :predicate => DC.titles 131 | property :name, :predicate => DC.title, :type => String 132 | end 133 | 134 | module ::SpiraModule2 135 | include Spira::Resource 136 | has_many :authors, :predicate => DC.authors 137 | property :author, :predicate => DC.author, :type => String 138 | end 139 | 140 | class ::ModuleIncluder1 141 | include Spira::Resource 142 | include SpiraModule1 143 | has_many :ages, :predicate => FOAF.ages 144 | property :age, :predicate => FOAF.age, :type => Integer 145 | end 146 | 147 | class ::ModuleIncluder2 148 | include Spira::Resource 149 | include SpiraModule1 150 | include SpiraModule2 151 | has_many :ages, :predicate => FOAF.ages 152 | property :age, :predicate => FOAF.age, :type => Integer 153 | end 154 | end 155 | 156 | before :each do 157 | Spira.add_repository(:default, RDF::Repository.new) 158 | @includer1 = RDF::URI('http://example.org/item').as(ModuleIncluder1) 159 | @includer2 = RDF::URI('http://example.org/item').as(ModuleIncluder2) 160 | end 161 | 162 | it "should include a property getter from the module" do 163 | @includer1.should respond_to :name 164 | end 165 | 166 | it "should include a property setter from the module" do 167 | @includer1.should respond_to :name= 168 | end 169 | 170 | it "should maintain property information for included modules" do 171 | ModuleIncluder1.properties[:name][:type].should == Spira::Types::String 172 | end 173 | 174 | it "should maintain propety information for including modules" do 175 | @includer1.should respond_to :age 176 | @includer1.should respond_to :age= 177 | ModuleIncluder1.properties[:age][:type].should == Spira::Types::Integer 178 | end 179 | 180 | context "when including multiple modules" do 181 | before :each do 182 | @includer2 = RDF::URI('http://example.org/item').as(ModuleIncluder2) 183 | end 184 | 185 | it "should maintain property getters from both modules" do 186 | @includer2.should respond_to :name 187 | @includer2.should respond_to :author 188 | end 189 | 190 | it "should maintain property setters from both modules" do 191 | @includer2.should respond_to :name= 192 | @includer2.should respond_to :author= 193 | end 194 | 195 | it "should maintain property information for included modules" do 196 | ModuleIncluder2.properties.should have_key :name 197 | ModuleIncluder2.properties[:name][:type].should == Spira::Types::String 198 | ModuleIncluder2.properties.should have_key :author 199 | ModuleIncluder2.properties[:author][:type].should == Spira::Types::String 200 | end 201 | 202 | it "should maintain property information for the including module" do 203 | @includer2.should respond_to :age 204 | @includer2.should respond_to :age= 205 | ModuleIncluder2.properties[:age][:type].should == Spira::Types::Integer 206 | end 207 | 208 | it "should maintain the list of lists for the included modules" do 209 | @includer2.should respond_to :authors 210 | @includer2.should respond_to :names 211 | @includer2.authors.should == Set.new 212 | @includer2.names.should == Set.new 213 | end 214 | 215 | it "should maintain the list of lists for the including module" do 216 | @includer2.should respond_to :ages 217 | @includer2.ages.should == Set.new 218 | end 219 | end 220 | end 221 | 222 | end 223 | 224 | context "base classes" do 225 | before :all do 226 | class ::BaseChild < Spira::Base ; end 227 | end 228 | 229 | it "should be able to inherit from Spira::Base" do 230 | BaseChild.ancestors.should include Spira::Base 231 | end 232 | 233 | it "should have access to Spira DSL methods" do 234 | BaseChild.should respond_to :property 235 | BaseChild.should respond_to :base_uri 236 | BaseChild.should respond_to :has_many 237 | BaseChild.should respond_to :default_vocabulary 238 | end 239 | end 240 | end 241 | -------------------------------------------------------------------------------- /lib/spira/resource/dsl.rb: -------------------------------------------------------------------------------- 1 | module Spira 2 | module Resource 3 | 4 | ## 5 | # This module consists of Spira::Resource class methods which correspond to 6 | # the Spira resource class declaration DSL. See {Spira::Resource} for more 7 | # information. 8 | # 9 | # @see Spira::Resource 10 | # @see Spira::Resource::ClassMethods 11 | # @see Spira::Resource::InstanceMethods 12 | # @see Spira::Resource::Validations 13 | module DSL 14 | 15 | ## 16 | # The name of the default repository to use for this class. This 17 | # repository will be queried and written to instead of the :default 18 | # repository. 19 | # 20 | # @param [Symbol] name 21 | # @return [Void] 22 | def default_source(name) 23 | @repository_name = name 24 | @repository = Spira.repository(name) 25 | end 26 | 27 | ## 28 | # The base URI for this class. Attempts to create instances for non-URI 29 | # objects will be appended to this base URI. 30 | # 31 | # @param [String, RDF::URI] base uri 32 | # @return [Void] 33 | def base_uri(uri = nil) 34 | @base_uri = uri unless uri.nil? 35 | @base_uri 36 | end 37 | 38 | ## 39 | # The default vocabulary for this class. Setting a default vocabulary 40 | # will allow properties to be defined without a `:predicate` option. 41 | # Predicates will instead be created by appending the property name to 42 | # the given string. 43 | # 44 | # @param [String, RDF::URI] base uri 45 | # @return [Void] 46 | def default_vocabulary(uri) 47 | @default_vocabulary = uri 48 | end 49 | 50 | 51 | ## 52 | # Add a property to this class. A property is an accessor field that 53 | # represents an RDF predicate. 54 | # 55 | # @example A simple string property 56 | # property :name, :predicate => FOAF.name, :type => String 57 | # @example A property which defaults to {Spira::Types::Any} 58 | # property :name, :predicate => FOAF.name 59 | # @example An integer property 60 | # property :age, :predicate => FOAF.age, :type => Integer 61 | # @param [Symbol] name The name of this property 62 | # @param [Hash{Symbol => Any}] opts property options 63 | # @option opts [RDF::URI] :predicate The RDF predicate which will refer to this property 64 | # @option opts [Spira::Type, String] :type (Spira::Types::Any) The 65 | # type for this property. If a Spira::Type is given, that class will be 66 | # used to serialize and unserialize values. If a String is given, it 67 | # should be the String form of a Spira::Resource class name (Strings are 68 | # used to prevent issues with load order). 69 | # @see Spira::Types 70 | # @see Spira::Type 71 | # @return [Void] 72 | def property(name, opts = {} ) 73 | add_accessors(name,opts,:hash_accessors) 74 | end 75 | 76 | ## 77 | # The plural form of `property`. `Has_many` has the same options as 78 | # `property`, but instead of a single value, a Ruby Array of objects will 79 | # be created instead. Be warned that this should be a Set to match RDF 80 | # semantics, but this is not currently implemented. Duplicate values of 81 | # an array will be lost on save. 82 | # 83 | # @see Spira::Resource::DSL#property 84 | def has_many(name, opts = {}) 85 | add_accessors(name,opts,:hash_accessors) 86 | @lists[name] = true 87 | end 88 | 89 | ## 90 | # Validate this model with the given validator function name. 91 | # 92 | # @example 93 | # class Person 94 | # include Spira::Resource 95 | # property :name, :predicate => FOAF.name 96 | # validate :is_awesome 97 | # def is_awesome 98 | # assert(name =~ /Thor/, :name, "not awesome") 99 | # end 100 | # end 101 | # @param [Symbol] validator 102 | # @return [Void] 103 | def validate(validator) 104 | validators << validator unless validators.include?(validator) 105 | end 106 | 107 | 108 | ## 109 | # Associate an RDF type with this class. RDF resources can be multiple 110 | # types at once, but if they have an `RDF.type` statement for the given 111 | # URI, this class can #count them. 112 | # 113 | # @param [RDF::URI] uri The URI object of the `RDF.type` triple 114 | # @return [Void] 115 | # @see http://rdf.rubyforge.net/RDF/URI.html 116 | # @see http://rdf.rubyforge.org/RDF.html#type-class_method 117 | # @see Spira::Resource::ClassMethods#count 118 | def type(uri = nil) 119 | unless uri.nil? 120 | @type = case uri 121 | when RDF::URI 122 | uri 123 | else 124 | raise TypeError, "Cannot assign type #{uri} (of type #{uri.class}) to #{self}, expected RDF::URI" 125 | end 126 | end 127 | @type 128 | end 129 | 130 | # Build a Ruby value from an RDF value. 131 | # 132 | # @private 133 | def build_value(statement, type, cache) 134 | case 135 | when statement == nil 136 | nil 137 | when !cache[statement.object].nil? 138 | cache[statement.object] 139 | when type.respond_to?(:unserialize) 140 | type.unserialize(statement.object) 141 | when type.is_a?(Symbol) || type.is_a?(String) 142 | klass = classize_resource(type) 143 | cache[statement.object] = promise { klass.for(statement.object, :_cache => cache) } 144 | cache[statement.object] 145 | else 146 | raise TypeError, "Unable to unserialize #{statement.object} as #{type}" 147 | end 148 | end 149 | 150 | # Build an RDF value from a Ruby value for a property 151 | # @private 152 | def build_rdf_value(value, type) 153 | case 154 | when type.respond_to?(:serialize) 155 | type.serialize(value) 156 | when value && value.class.ancestors.include?(Spira::Resource) 157 | klass = classize_resource(type) 158 | unless klass.ancestors.include?(value.class) 159 | raise TypeError, "#{value} is an instance of #{value.class}, expected #{klass}" 160 | end 161 | value.subject 162 | when type.is_a?(Symbol) || type.is_a?(String) 163 | klass = classize_resource(type) 164 | else 165 | raise TypeError, "Unable to serialize #{value} as #{type}" 166 | end 167 | end 168 | 169 | private 170 | 171 | # Return the appropriate class object for a string or symbol 172 | # representation. Throws errors correctly if the given class cannot be 173 | # located, or if it is not a Spira::Resource 174 | # 175 | def classize_resource(type) 176 | klass = nil 177 | begin 178 | klass = qualified_const_get(type.to_s) 179 | rescue NameError 180 | raise NameError, "Could not find relation class #{type} (referenced as #{type} by #{self})" 181 | klass.is_a?(Class) && klass.ancestors.include?(Spira::Resource) 182 | end 183 | unless klass.is_a?(Class) && klass.ancestors.include?(Spira::Resource) 184 | raise TypeError, "#{type} is not a Spira Resource (referenced as #{type} by #{self})" 185 | end 186 | klass 187 | end 188 | 189 | # Resolve a constant from a string, relative to this class' namespace, if 190 | # available, and from root, otherwise. 191 | # 192 | # FIXME: this is not really 'qualified', but it's one of those 193 | # impossible-to-name functions. Open to suggestions. 194 | # 195 | # @author njh 196 | # @private 197 | def qualified_const_get(str) 198 | path = str.to_s.split('::') 199 | from_root = path[0].empty? 200 | if from_root 201 | from_root = [] 202 | path = path[1..-1] 203 | else 204 | start_ns = ((Class === self)||(Module === self)) ? self : self.class 205 | from_root = start_ns.to_s.split('::') 206 | end 207 | until from_root.empty? 208 | begin 209 | return (from_root+path).inject(Object) { |ns,name| ns.const_get(name) } 210 | rescue NameError 211 | from_root.delete_at(-1) 212 | end 213 | end 214 | path.inject(Object) { |ns,name| ns.const_get(name) } 215 | end 216 | 217 | ## 218 | # Add getters and setters for a property or list. 219 | # @private 220 | def add_accessors(name, opts, accessors_method) 221 | predicate = case 222 | when opts[:predicate] 223 | opts[:predicate] 224 | when @default_vocabulary.nil? 225 | raise ResourceDeclarationError, "A :predicate option is required for types without a default vocabulary" 226 | else @default_vocabulary 227 | separator = @default_vocabulary.to_s[-1,1] =~ /(\/|#)/ ? '' : '/' 228 | RDF::URI.intern(@default_vocabulary.to_s + separator + name.to_s) 229 | end 230 | if !(predicate.respond_to?(:to_uri)) 231 | raise ResourceDeclarationError, ":predicate options must be RDF::URIs or strings with a default vocabulary declared" 232 | end 233 | type = case 234 | when opts[:type].nil? 235 | Spira::Types::Any 236 | when opts[:type].is_a?(Symbol) || opts[:type].is_a?(String) 237 | opts[:type] 238 | when !(Spira.types[opts[:type]].nil?) 239 | Spira.types[opts[:type]] 240 | else 241 | raise TypeError, "Unrecognized type: #{opts[:type]}" 242 | end 243 | @properties[name] = {} 244 | @properties[name][:predicate] = predicate 245 | @properties[name][:type] = type 246 | name_equals = (name.to_s + "=").to_sym 247 | 248 | self.send(:define_method,name_equals) do |arg| 249 | attribute_set(name, arg) 250 | end 251 | self.send(:define_method,name) do 252 | attribute_get(name) 253 | end 254 | 255 | end 256 | 257 | end 258 | end 259 | end 260 | -------------------------------------------------------------------------------- /lib/spira/resource/class_methods.rb: -------------------------------------------------------------------------------- 1 | module Spira 2 | module Resource 3 | 4 | ## 5 | # This module contains all class methods available to a declared Spira::Resource class. 6 | # {Spira::Resource} contains more information about Spira resources. 7 | # 8 | # @see Spira::Resource 9 | # @see Spira::Resource::InstanceMethods 10 | # @see Spira::Resource::DSL 11 | module ClassMethods 12 | 13 | ## 14 | # A symbol name for the repository this class is currently using. 15 | attr_reader :repository_name 16 | 17 | ## 18 | # The current repository for this class 19 | # 20 | # @return [RDF::Repository, nil] 21 | # @private 22 | def repository 23 | name = @repository_name || :default 24 | Spira.repository(name) 25 | end 26 | 27 | ## 28 | # Get the current repository for this class, and raise a 29 | # Spira::NoRepositoryError if it is nil. 30 | # 31 | # @raise [Spira::NoRepositoryError] 32 | # @return [RDF::Repository] 33 | # @private 34 | def repository_or_fail 35 | repository || (raise Spira::NoRepositoryError, "#{self} is configured to use :#{@repository_name || 'default'} as a repository, but it has not been set.") 36 | end 37 | 38 | ## 39 | # Create a new projection instance of this class for the given URI. If a 40 | # class has a base_uri given, and the argument is not an `RDF::URI`, the 41 | # given identifier will be appended to the base URI. 42 | # 43 | # Spira does not have 'find' or 'create' functions. As RDF identifiers 44 | # are globally unique, they all simply 'are'. 45 | # 46 | # On calling `for`, a new projection is created for the given URI. The 47 | # first time access is attempted on a field, the repository will be 48 | # queried for existing attributes, which will be used for the given URI. 49 | # Underlying repositories are not accessed at the time of calling `for`. 50 | # 51 | # A class with a base URI may still be projected for any URI, whether or 52 | # not it uses the given resource class' base URI. 53 | # 54 | # @raise [TypeError] if an RDF type is given in the attributes and one is 55 | # given in the attributes. 56 | # @raise [ArgumentError] if a non-URI is given and the class does not 57 | # have a base URI. 58 | # @overload for(uri, attributes = {}) 59 | # @param [RDF::URI] uri The URI to create an instance for 60 | # @param [Hash{Symbol => Any}] attributes Initial attributes 61 | # @overload for(identifier, attributes = {}) 62 | # @param [Any] uri The identifier to append to the base URI for this class 63 | # @param [Hash{Symbol => Any}] attributes Initial attributes 64 | # @yield [self] Executes a given block and calls `#save!` 65 | # @yieldparam [self] self The newly created instance 66 | # @return [Spira::Resource] The newly created instance 67 | # @see http://rdf.rubyforge.org/RDF/URI.html 68 | def for(identifier, attributes = {}, &block) 69 | self.project(id_for(identifier), attributes, &block) 70 | end 71 | 72 | ## 73 | # Create a new instance with the given subjet without any modification to 74 | # the given subject at all. This method exists to provide an entry point 75 | # for implementing classes that want to create a more intelligent .for 76 | # and/or .id_for for their given use cases, such as simple string 77 | # appending to base URIs or calculated URIs from other representations. 78 | # 79 | # @example Using simple string concatentation with base_uri in .for instead of joining delimiters 80 | # def for(identifier, attributes = {}, &block) 81 | # self.project(RDF::URI(self.base_uri.to_s + identifier.to_s), attributes, &block) 82 | # end 83 | # @param [RDF::URI, RDF::Node] subject 84 | # @param [Hash{Symbol => Any}] attributes Initial attributes 85 | # @return [Spira::Resource] the newly created instance 86 | def project(subject, attributes = {}, &block) 87 | if !self.type.nil? && attributes[:type] 88 | raise TypeError, "#{self} has an RDF type, #{self.type}, and cannot accept one as an argument." 89 | end 90 | self.new(attributes.merge(:_subject => subject), &block) 91 | end 92 | 93 | ## 94 | # Alias for #for 95 | # 96 | # @see #for 97 | def [](*args) 98 | self.for(*args) 99 | end 100 | 101 | ## 102 | # Creates a URI or RDF::Node based on a potential base_uri and string, 103 | # URI, or Node, or Addressable::URI. If not a URI or Node, the given 104 | # identifier should be a string representing an absolute URI, or 105 | # something responding to to_s which can be appended to a base URI, which 106 | # this class must have. 107 | # 108 | # @param [Any] Identifier 109 | # @return [RDF::URI, RDF::Node] 110 | # @raise [ArgumentError] If this class cannot create an identifier from the given argument 111 | # @see http://rdf.rubyforge.org/RDF/URI.html 112 | # @see Spira::Resource.base_uri 113 | # @see Spira::Resource.for 114 | def id_for(identifier) 115 | case 116 | # Absolute URI's go through unchanged 117 | when identifier.is_a?(RDF::URI) && identifier.absolute? 118 | identifier 119 | # We don't have a base URI to join this fragment with, so go ahead and instantiate it as-is. 120 | when identifier.is_a?(RDF::URI) && self.base_uri.nil? 121 | identifier 122 | # Blank nodes go through unchanged 123 | when identifier.respond_to?(:node?) && identifier.node? 124 | identifier 125 | # Anything that can be an RDF::URI, we re-run this case statement 126 | # on it for the fragment logic above. 127 | when identifier.respond_to?(:to_uri) && !identifier.is_a?(RDF::URI) 128 | id_for(identifier.to_uri) 129 | # see comment with #to_uri above, this might be a fragment 130 | when identifier.is_a?(Addressable::URI) 131 | id_for(RDF::URI.intern(identifier)) 132 | # This is a #to_s or a URI fragment with a base uri. We'll treat them the same. 133 | # FIXME: when #/ makes it into RDF.rb proper, this can all be wrapped 134 | # into the one case statement above. 135 | else 136 | uri = identifier.is_a?(RDF::URI) ? identifier : RDF::URI.intern(identifier.to_s) 137 | case 138 | when uri.absolute? 139 | uri 140 | when self.base_uri.nil? 141 | raise ArgumentError, "Cannot create identifier for #{self} by String without base_uri; an RDF::URI is required" if self.base_uri.nil? 142 | else 143 | separator = self.base_uri.to_s[-1,1] =~ /(\/|#)/ ? '' : '/' 144 | RDF::URI.intern(self.base_uri.to_s + separator + identifier.to_s) 145 | end 146 | end 147 | end 148 | 149 | 150 | ## 151 | # The number of URIs projectable as a given class in the repository. 152 | # This method is only valid for classes which declare a `type` with the 153 | # `type` method in the DSL. 154 | # 155 | # @raise [Spira::NoTypeError] if the resource class does not have an RDF type declared 156 | # @return [Integer] the count 157 | # @see Spira::Resource::DSL 158 | def count 159 | raise Spira::NoTypeError, "Cannot count a #{self} without a reference type URI." if @type.nil? 160 | repository.query(:predicate => RDF.type, :object => @type).subjects.count 161 | end 162 | 163 | ## 164 | # A cache of iterated instances of this projection 165 | # 166 | # @return [RDF::Util::Cache] 167 | # @private 168 | def cache 169 | @cache ||= RDF::Util::Cache.new 170 | end 171 | 172 | ## 173 | # Clear the iteration cache 174 | # 175 | # @return [void] 176 | def reload 177 | @cache = nil 178 | end 179 | 180 | ## 181 | # Enumerate over all resources projectable as this class. This method is 182 | # only valid for classes which declare a `type` with the `type` method in 183 | # the DSL. 184 | # 185 | # @raise [Spira::NoTypeError] if the resource class does not have an RDF type declared 186 | # @overload each 187 | # @yield [instance] A block to perform for each available projection of this class 188 | # @yieldparam [self] instance 189 | # @yieldreturn [Void] 190 | # @return [Void] 191 | # 192 | # @overload each 193 | # @return [Enumerator] 194 | # @see Spira::Resource::DSL 195 | def each(&block) 196 | raise Spira::NoTypeError, "Cannot count a #{self} without a reference type URI." if @type.nil? 197 | case block_given? 198 | when false 199 | enum_for(:each) 200 | else 201 | repository_or_fail.query(:predicate => RDF.type, :object => @type).each_subject do |subject| 202 | self.cache[subject] ||= self.for(subject) 203 | block.call(cache[subject]) 204 | end 205 | end 206 | end 207 | 208 | ## 209 | # Returns true if the given property is a has_many property, false otherwise 210 | # 211 | # @return [true, false] 212 | def is_list?(property) 213 | @lists.has_key?(property) 214 | end 215 | 216 | ## 217 | # Handling inheritance 218 | # 219 | # @private 220 | def inherited(child) 221 | child.instance_eval do 222 | include Spira::Resource 223 | end 224 | # FIXME: This is clearly brittle and ugly. 225 | [:@base_uri, :@default_vocabulary, :@repository_name, :@type].each do |variable| 226 | value = instance_variable_get(variable).nil? ? nil : instance_variable_get(variable).dup 227 | child.instance_variable_set(variable, value) 228 | end 229 | [:@properties, :@lists, :@validators].each do |variable| 230 | if child.instance_variable_get(variable).nil? 231 | if instance_variable_get(variable).nil? 232 | child.instance_variable_set(variable, nil) 233 | else 234 | child.instance_variable_set(variable, instance_variable_get(variable).dup) 235 | end 236 | elsif !(instance_variable_get(variable).nil?) 237 | child.instance_variable_set(variable, instance_variable_get(variable).dup.merge(child.instance_variable_get(variable))) 238 | end 239 | end 240 | end 241 | 242 | ## 243 | # Handling module inclusions 244 | # 245 | # @private 246 | def included(child) 247 | inherited(child) 248 | end 249 | 250 | ## 251 | # The list of validation functions for this projection 252 | # 253 | # @return [Array] 254 | def validators 255 | @validators ||= [] 256 | end 257 | 258 | end 259 | end 260 | end 261 | -------------------------------------------------------------------------------- /spec/update.spec: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/spec_helper' 2 | 3 | # Tests update functionality--update, save, destroy 4 | 5 | describe Spira do 6 | 7 | before :all do 8 | class ::UpdateTest 9 | include Spira::Resource 10 | base_uri "http://example.org/example/people" 11 | property :name, :predicate => RDFS.label 12 | property :age, :predicate => FOAF.age, :type => Integer 13 | end 14 | end 15 | 16 | before :each do 17 | @test_uri = RDF::URI('http://example.org/example/people') 18 | @update_repo = RDF::Repository.new 19 | @update_repo << RDF::Statement.new(@test_uri, RDF::RDFS.label, 'Test') 20 | @update_repo << RDF::Statement.new(@test_uri, RDF::FOAF.age, 15) 21 | Spira.add_repository(:default, @update_repo) 22 | @test = UpdateTest.for(@test_uri) 23 | end 24 | 25 | context "when updating" do 26 | 27 | context "via individual setters" do 28 | it "should allow setting properties" do 29 | @test.name = 'Testing 1 2 3' 30 | @test.name.should == 'Testing 1 2 3' 31 | end 32 | 33 | it "should return the newly set value" do 34 | (@test.name = 'Testing 1 2 3').should == 'Testing 1 2 3' 35 | @test.name.should == 'Testing 1 2 3' 36 | end 37 | end 38 | 39 | context "via #update" do 40 | it "should allow setting a single property" do 41 | @test.update(:name => "Testing") 42 | @test.name.should == "Testing" 43 | end 44 | 45 | it "should allow setting multiple properties" do 46 | @test.update(:name => "Testing", :age => 10) 47 | @test.name.should == "Testing" 48 | @test.age.should == 10 49 | end 50 | 51 | it "should return self on success" do 52 | (@test.update(:name => "Testing", :age => 10)).should == @test 53 | end 54 | end 55 | 56 | context "via #update!" do 57 | it "should allow setting a single property and immediately save it to the repository" do 58 | @test.update!(:name => "Testing") 59 | @test.name.should == "Testing" 60 | @update_repo.should have_statement(RDF::Statement.new(@test_uri, RDF::RDFS.label, 'Testing')) 61 | end 62 | 63 | it "should allow setting multiple properties and immediately save them to the repository" do 64 | @test.update!(:name => "Testing", :age => 10) 65 | @test.name.should == "Testing" 66 | @test.age.should == 10 67 | @update_repo.should have_statement(RDF::Statement.new(@test_uri, RDF::RDFS.label, 'Testing')) 68 | @update_repo.should have_statement(RDF::Statement.new(@test_uri, RDF::FOAF.age, 10)) 69 | end 70 | 71 | it "should return self on success" do 72 | (@test.update!(:name => "Testing", :age => 10)).should == @test 73 | end 74 | 75 | it "should raise an exception on failure" do 76 | @update_repo.should_receive(:delete).once.and_raise(RuntimeError) 77 | lambda { @test.update!(:name => "Testing", :age => 10) }.should raise_error 78 | end 79 | end 80 | end 81 | 82 | context "when saving" do 83 | # @see validations.spec 84 | context "via #save!" do 85 | it "should save a resource's statements to the repository" do 86 | @test.name = "Save" 87 | @test.save! 88 | @update_repo.should have_statement(RDF::Statement.new(@test_uri,RDF::RDFS.label,"Save")) 89 | end 90 | 91 | it "should return self on success" do 92 | @test.name = "Save" 93 | (@test.save!).should == @test 94 | end 95 | 96 | it "should raise an exception on failure" do 97 | # FIXME: not awesome that the test has to know that spira uses :update 98 | @update_repo.should_receive(:insert).once.and_raise(RuntimeError) 99 | @test.name = "Save" 100 | lambda { @test.save! }.should raise_error #FIXME: what kind of error? 101 | end 102 | 103 | it "should delete all existing statements for updated properties to the repository" do 104 | @update_repo << RDF::Statement.new(@test_uri, RDF::RDFS.label, 'Test 1') 105 | @update_repo << RDF::Statement.new(@test_uri, RDF::RDFS.label, 'Test 2') 106 | @update_repo.query(:subject => @test_uri, :predicate => RDF::RDFS.label).count.should == 3 107 | @test.name = "Save" 108 | @test.save! 109 | @update_repo.query(:subject => @test_uri, :predicate => RDF::RDFS.label).count.should == 1 110 | @update_repo.should have_statement(RDF::Statement.new(@test_uri, RDF::RDFS.label, 'Save')) 111 | end 112 | 113 | it "should not update properties unless they are dirty" do 114 | @update_repo.should_receive(:delete).once.with([@test_uri, RDF::RDFS.label, nil]) 115 | @test.name = "Save" 116 | @test.save! 117 | end 118 | 119 | # Tests for a bug wherein the originally loaded attributes were being 120 | # deleted on save!, not the current ones 121 | it "should safely delete old repository information on updates" do 122 | @test.age = 16 123 | @test.save! 124 | @test.age = 17 125 | @test.save! 126 | @update_repo.query(:subject => @test_uri, :predicate => RDF::FOAF.age).size.should == 1 127 | @update_repo.first_value(:subject => @test_uri, :predicate => RDF::FOAF.age).should == "17" 128 | end 129 | 130 | it "should not remove non-model data" do 131 | @update_repo << RDF::Statement.new(@test_uri, RDF.type, RDF::URI('http://example.org/type')) 132 | @test.name = "Testing 1 2 3" 133 | @test.save! 134 | @update_repo.query(:subject => @test_uri, :predicate => RDF.type).size.should == 1 135 | end 136 | 137 | it "should not be dirty afterwards" do 138 | @test.name = "test" 139 | @test.save! 140 | @test.dirty?.should be_false 141 | end 142 | 143 | it "removes items set to nil from the repository" do 144 | @test.name = nil 145 | @test.save! 146 | @update_repo.query(:subject => @test_uri, :predicate => RDF::RDFS.label).size.should == 0 147 | end 148 | 149 | end 150 | end 151 | 152 | context "when destroying" do 153 | context "via #destroy" do 154 | before :each do 155 | @update_repo << RDF::Statement.new(@test_uri, RDF::FOAF.name, 'Not in model') 156 | @update_repo << RDF::Statement.new(RDF::URI('http://example.org/test'), RDF::RDFS.seeAlso, @test_uri) 157 | end 158 | 159 | it "should return true on success" do 160 | @test.destroy!.should == true 161 | end 162 | 163 | it "should raise an exception on failure" do 164 | @update_repo.should_receive(:delete).once.and_raise(RuntimeError) 165 | lambda {@test.destroy!}.should raise_error 166 | end 167 | 168 | context "without options" do 169 | it "should delete all statements in the model" do 170 | @test.destroy! 171 | @update_repo.count.should == 2 172 | @update_repo.should_not have_predicate(RDF::RDFS.label) 173 | @update_repo.should_not have_predicate(RDF::FOAF.age) 174 | end 175 | 176 | it "should not delete statements with predicates not defined in the model" do 177 | @test.destroy! 178 | @update_repo.count.should == 2 179 | @update_repo.should have_predicate(RDF::FOAF.name) 180 | end 181 | end 182 | 183 | context "with :subject" do 184 | it "should delete all statements with self as the subject" do 185 | @test.destroy!(:subject) 186 | @update_repo.should_not have_subject @test_uri 187 | end 188 | 189 | it "should not delete statements with self as the object" do 190 | @test.destroy!(:subject) 191 | @update_repo.should have_object @test_uri 192 | end 193 | end 194 | 195 | context "with :object" do 196 | it "should delete all statements with self as the object" do 197 | @test.destroy!(:object) 198 | @update_repo.should_not have_object @test_uri 199 | end 200 | 201 | it "should not delete statements with self as the subject" do 202 | @test.destroy!(:object) 203 | @update_repo.should have_subject @test_uri 204 | @update_repo.query(:subject => @test_uri).count.should == 3 205 | end 206 | end 207 | 208 | context "with :completely" do 209 | it "should delete all statements referencing the object" do 210 | @test.destroy!(:completely) 211 | @update_repo.count.should == 0 212 | end 213 | end 214 | 215 | end 216 | 217 | end 218 | 219 | context "when copying" do 220 | before :each do 221 | @new_uri = RDF::URI('http://example.org/people/test2') 222 | @update_repo << RDF::Statement.new(@test_uri, RDF::FOAF.name, 'Not in model') 223 | end 224 | 225 | context "with #copy" do 226 | it "supports #copy" do 227 | @test.respond_to?(:copy).should be_true 228 | end 229 | 230 | it "copies to a given subject" do 231 | new = @test.copy(@new_uri) 232 | new.subject.should == @new_uri 233 | end 234 | 235 | it "copies model data" do 236 | new = @test.copy(@new_uri) 237 | new.name.should == @test.name 238 | new.age.should == @test.age 239 | end 240 | 241 | end 242 | 243 | context "with #copy!" do 244 | it "supports #copy!" do 245 | @test.respond_to?(:copy!).should be_true 246 | end 247 | 248 | it "copies to a given subject" do 249 | new = @test.copy!(@new_uri) 250 | new.subject.should == @new_uri 251 | end 252 | 253 | it "copies model data" do 254 | new = @test.copy!(@new_uri) 255 | new.name.should == @test.name 256 | new.age.should == @test.age 257 | end 258 | 259 | it "saves the copy immediately" do 260 | new = @test.copy!(@new_uri) 261 | @update_repo.should have_statement RDF::Statement.new(@new_uri, RDF::RDFS.label, @test.name) 262 | @update_repo.should have_statement RDF::Statement.new(@new_uri, RDF::FOAF.age, @test.age) 263 | end 264 | end 265 | 266 | context "with #copy_resource!" do 267 | it "supports #copy_resource!" do 268 | @test.respond_to?(:copy_resource!).should be_true 269 | end 270 | 271 | it "copies all resource data to the new subject in the repository" do 272 | @test.copy_resource!(@new_uri) 273 | @update_repo.query(:subject => @test_uri).each do |statement| 274 | @update_repo.should have_statement RDF::Statement(@new_uri, statement.predicate, statement.object) 275 | end 276 | end 277 | 278 | it "returns an instance projecting the new copied resource" do 279 | new = @test.copy_resource!(@new_uri) 280 | new.should be_a ::UpdateTest 281 | new.name.should == @test.name 282 | new.age.should == @test.age 283 | end 284 | end 285 | end 286 | 287 | context "when renaming" do 288 | before :each do 289 | @new_uri = RDF::URI('http://example.org/people/test2') 290 | @other_uri = RDF::URI('http://example.org/people/test3') 291 | @update_repo << RDF::Statement.new(@test_uri, RDF::FOAF.name, 'Not in model') 292 | @update_repo << RDF::Statement.new(@other_uri, RDF::RDFS.seeAlso, @test_uri) 293 | @name = @test.name 294 | @age = @test.age 295 | end 296 | 297 | context "with #rename!" do 298 | it "supports #rename!" do 299 | @test.respond_to?(:rename!).should be_true 300 | end 301 | 302 | it "copies model data to a given subject" do 303 | new = @test.rename!(@new_uri) 304 | new.name.should == @name 305 | new.age.should == @age 306 | end 307 | 308 | it "updates references to the old subject as objects" do 309 | new = @test.rename!(@new_uri) 310 | @update_repo.should have_statement RDF::Statement.new(@other_uri, RDF::RDFS.seeAlso, @new_uri) 311 | @update_repo.should_not have_statement RDF::Statement.new(@other_uri, RDF::RDFS.seeAlso, @test_uri) 312 | end 313 | 314 | it "saves the copy immediately" do 315 | @test.rename!(@new_uri) 316 | @update_repo.should have_statement RDF::Statement.new(@new_uri, RDF::RDFS.label, @name) 317 | @update_repo.should have_statement RDF::Statement.new(@new_uri, RDF::FOAF.age, @age) 318 | end 319 | 320 | it "deletes the old model data" do 321 | @test.rename!(@new_uri) 322 | @update_repo.should_not have_statement RDF::Statement.new(@test_uri, RDF::RDFS.label, @name) 323 | @update_repo.should_not have_statement RDF::Statement.new(@test_uri, RDF::FOAF.age, @age) 324 | end 325 | 326 | it "copies non-model data to the given subject" do 327 | new = @test.rename!(@new_uri) 328 | @update_repo.should have_statement RDF::Statement.new(@new_uri, RDF::FOAF.name, 'Not in model') 329 | end 330 | 331 | it "deletes all data about the old subject" do 332 | new = @test.rename!(@new_uri) 333 | @update_repo.query(:subject => @test_uri).size.should == 0 334 | @update_repo.query(:object => @test_uri).size.should == 0 335 | end 336 | end 337 | end 338 | 339 | end 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spira 2 | 3 | It's time to breathe life into your linked data. 4 | 5 | --- 6 | 7 | ## Synopsis 8 | Spira is a framework for using the information in [RDF.rb][] repositories as model 9 | objects. It gives you the ability to work in a resource-oriented way without 10 | losing access to statement-oriented nature of linked data, if you so choose. 11 | It can be used either to access existing RDF data in a resource-oriented way, 12 | or to create a new store of RDF data based on simple defaults. 13 | 14 | An introductory blog post is at 15 | 16 | A changelog is available in the {file:CHANGES.md} file. 17 | 18 | ### Example 19 | 20 | class Person 21 | 22 | include Spira::Resource 23 | 24 | base_uri "http://example.org/example/people" 25 | 26 | property :name, :predicate => FOAF.name, :type => String 27 | property :age, :predicate => FOAF.age, :type => Integer 28 | 29 | end 30 | 31 | bob = RDF::URI("http://example.org/people/bob").as(Person) 32 | bob.age = 15 33 | bob.name = "Bob Smith" 34 | bob.save! 35 | 36 | bob.each_statement {|s| puts s} 37 | #=> RDF::Statement:0x80abb80c( "Bob Smith" .) 38 | #=> RDF::Statement:0x80abb8fc( "15"^^ .) 39 | 40 | ### Features 41 | 42 | * Extensible validations system 43 | * Extensible types system 44 | * Easy to use multiple data sources 45 | * Easy to adapt models to existing data 46 | * Open-world semantics 47 | * Objects are still RDF.rb-compatible enumerable objects 48 | * No need to put everything about an object into Spira 49 | * Easy to use a resource as multiple models 50 | 51 | ## Getting Started 52 | 53 | The easiest way to work with Spira is to install it via Rubygems: 54 | 55 | $ sudo gem install spira 56 | 57 | Downloads will be available on the github project page, as well as on Rubyforge. 58 | 59 | ## Defining Model Classes 60 | 61 | To use Spira, define model classes for your RDF data. Spira classes include 62 | RDF, and thus have access to all `RDF::Vocabulary` classes and `RDF::URI` 63 | without the `RDF::` prefix. For example: 64 | 65 | require 'spira' 66 | 67 | class CD 68 | include Spira::Resource 69 | base_uri 'http://example.org/cds' 70 | property :name, :predicate => DC.title, :type => XSD.string 71 | property :artist, :predicate => URI.new('http://example.org/vocab/artist'), :type => :artist 72 | end 73 | 74 | class Artist 75 | include Spira::Resource 76 | base_uri 'http://example.org/artists' 77 | property :name, :predicate => DC.title, :type => XSD.string 78 | has_many :cds, :predicate => URI.new('http://example.org/vocab/published_cd'), :type => XSD.string 79 | end 80 | 81 | Then use your model classes, in a way more or less similar to any number of ORMs: 82 | 83 | cd = CD.for("queens-greatest-hits") 84 | cd.name = "Queen's greatest hits" 85 | artist = Artist.for("queen") 86 | artist.name = "Queen" 87 | 88 | cd.artist = artist 89 | cd.save! 90 | artist.cds = [cd] 91 | artist.save! 92 | 93 | queen = Artist.for('queen') 94 | hits = CD.for 'queens-greatest-hits' 95 | hits.artist == artist == queen 96 | 97 | ### URIs and Blank Nodes 98 | 99 | Spira instances have a subject, which is either a URI or a blank node. 100 | 101 | A class with a base URI can instantiate with a string (or anything, via to_s), 102 | and it will have a URI representation: 103 | 104 | Artist.for('queen') 105 | 106 | However, a class is not required to have a base URI, and even if it does, it 107 | can always access classes with a full URI: 108 | 109 | nk = Artist.for(RDF::URI.new('http://example.org/my-hidden-cds/new-kids')) 110 | 111 | If you have a URI that you would like to look at as a Spira resource, you can instantiate it from the URI: 112 | 113 | RDF::URI.new('http://example.org/my-hidden-cds/new-kids').as(Artist) 114 | # => 115 | 116 | Any call to 'for' with a valid identifier will always return an object with nil 117 | fields. It's a way of looking at a given resource, not a closed-world mapping 118 | to one. 119 | 120 | You can also use blank nodes more or less as you would a URI: 121 | 122 | remix_artist = Artist.for(RDF::Node.new) 123 | # => > 124 | RDF::Node.new.as(Artist) 125 | # => > 126 | 127 | Finally, you can create an instance of a Spira projection with #new, and you'll 128 | get an instance with a shiny new blank node subject: 129 | 130 | formerly_known_as_prince = Artist.new 131 | # => > 132 | 133 | ### Class Options 134 | 135 | A number of options are available for Spira classes. 136 | 137 | #### base_uri 138 | 139 | A class with a `base_uri` set (either an `RDF::URI` or a `String`) will 140 | use that URI as a base URI for non-absolute `for` calls. 141 | 142 | Example 143 | CD.for 'queens-greatest-hits' # is the same as... 144 | CD.for RDF::URI.new('http://example.org/cds/queens-greatest-hits') 145 | 146 | #### type 147 | 148 | A class with a `type` set is assigned an `RDF.type` on creation and saving. 149 | 150 | class Album 151 | include Spira::Resource 152 | type URI.new('http://example.org/types/album') 153 | property :name, :predicate => DC.title 154 | end 155 | 156 | rolling_stones = Album.for RDF::URI.new('http://example.org/cds/rolling-stones-hits') 157 | # See RDF.rb at http://rdf.rubyforge.org/RDF/Enumerable.html for more information about #has_predicate? 158 | rolling_stones.has_predicate?(RDF.type) #=> true 159 | Album.type #=> RDF::URI('http://example.org/types/album') 160 | 161 | In addition, one can count the members of a class with a `type` defined: 162 | 163 | Album.count #=> 1 164 | 165 | #### property 166 | 167 | A class declares property members with the `property` function. See `Property Options` for more information. 168 | 169 | #### has_many 170 | 171 | A class declares list members with the `has_many` function. See `Property Options` for more information. 172 | 173 | #### default_vocabulary 174 | 175 | A class with a `default_vocabulary` set will transparently create predicates for defined properties: 176 | 177 | class Song 178 | include Spira::Resource 179 | default_vocabulary URI.new('http://example.org/vocab') 180 | base_uri 'http://example.org/songs' 181 | property :title 182 | property :author, :type => :artist 183 | end 184 | 185 | dancing_queen = Song.for 'dancing-queen' 186 | dancing_queen.title = "Dancing Queen" 187 | dancing_queen.artist = abba 188 | # See RDF::Enumerable for #has_predicate? 189 | dancing_queen.has_predicate?(RDF::URI.new('http://example.org/vocab/title')) #=> true 190 | dancing_queen.has_predicate?(RDF::URI.new('http://example.org/vocab/artist')) #=> true 191 | 192 | #### default_source 193 | 194 | Provides this class with a default repository to use instead of the `:default` 195 | repository if one is not set. 196 | 197 | class Song 198 | default_source :songs 199 | end 200 | 201 | See 'Defining Repositories' for more information. 202 | 203 | #### validate 204 | 205 | Provides the name of a function which does some sort of validation. See 206 | 'Validations' for more information. 207 | 208 | ### Property Options 209 | 210 | Spira classes can have properties that are either singular or a list. For a 211 | list, define the property with `has_many`, for a property with a single item, 212 | use `property`. The semantics are otherwise the same. A `has_many` property 213 | will always return a list, including an empty list for no value. All options 214 | for `property` work for `has_many`. 215 | 216 | property :artist, :type => :artist #=> cd.artist returns a single value 217 | has_many :cds, :type => :cd #=> artist.cds returns an array 218 | 219 | Property always takes a symbol name as a name, and a variable list of options. The supported options are: 220 | 221 | * `:type`: The type for this property. This can be a Ruby base class, an 222 | RDF::XSD entry, or another Spira model class, referenced as a symbol. See 223 | **Types** below. Default: `Any` 224 | * `:predicate`: The predicate to use for this type. This can be any RDF URI. 225 | This option is required unless the `default_vocabulary` has been used. 226 | 227 | ### Types 228 | 229 | A property's type can be either a class which includes Spira::Type or a 230 | reference to another Spira model class, given as a symbol. 231 | 232 | #### Relations 233 | 234 | If the `:type` of a spira class is the name of another Spira class as a symbol, 235 | such as `:artist` for `Artist`, Spira will attempt to load the referenced 236 | object when the appropriate property is accessed. 237 | 238 | In the RDF store, this will be represented by the URI of the referenced object. 239 | 240 | #### Type Classes 241 | 242 | A type class includes Spira::Type, and can implement serialization and 243 | deserialization functions, and register aliases to themselves if their datatype 244 | is usually expressed as a URI. Here is the built-in Spira Integer class: 245 | 246 | module Spira::Types 247 | class Integer 248 | 249 | include Spira::Type 250 | 251 | def self.unserialize(value) 252 | value.object 253 | end 254 | 255 | def self.serialize(value) 256 | RDF::Literal.new(value) 257 | end 258 | 259 | register_alias XSD.integer 260 | end 261 | end 262 | 263 | Classes can now use this particular type like so: 264 | 265 | class Test 266 | include Spira::Resource 267 | property :test1, :type => Integer 268 | property :test2, :type => XSD.integer 269 | end 270 | 271 | Spira classes include the Spira::Types namespace, where several default types 272 | are implemented: 273 | 274 | * `Integer` 275 | * `Float` 276 | * `Boolean` 277 | * `String` 278 | * `Any` 279 | 280 | The default type for a Spira property is `Spira::Types::Any`, which uses 281 | `RDF::Literal`'s automatic boxing/unboxing of XSD types as best it can. See 282 | `[RDF::Literal](http://rdf.rubyforge.org/RDF/Literal.html)` for more information. 283 | 284 | You can implement your own types as well. Your class' serialize method should 285 | turn an RDF::Value into a ruby object, and vice versa. 286 | 287 | module MyModule 288 | class MyType 289 | include Spira::Type 290 | def self.serialize(value) 291 | ... 292 | end 293 | 294 | def self.unserialize(value) 295 | ... 296 | end 297 | end 298 | end 299 | 300 | class MyClass 301 | include Spira::Resource 302 | property :property1, :type => MyModule::MyType 303 | end 304 | 305 | ## Defining Repositories 306 | 307 | You can define multiple repositories with Spira, and use more than one at a time: 308 | 309 | require 'rdf/ntriples' 310 | require 'rdf/sesame' 311 | Spira.add_repository! :cds, RDF::Sesame::Repository.new 'some_server' 312 | Spira.add_repository! :albums, RDF::Repository.load('some_file.nt') 313 | 314 | CD.repository = :cds 315 | Album.repository = :albums 316 | 317 | Objects can reference each other cross-repository. 318 | 319 | If no repository has been specified, the `:default` repository will be used. 320 | 321 | repo = RDF::Repository.new 322 | Spira.add_repository! :default, repo 323 | Artist.repository == repo #=> true 324 | 325 | Classes can specify a default repository to use other than `:default` with the 326 | `default_source` function: 327 | 328 | class Song 329 | default_source :songs 330 | end 331 | 332 | Song.repository #=> nil, won't use :default 333 | 334 | ## Validations 335 | 336 | You may declare any number of validation functions with the `validate` function. 337 | Before saving, each referenced validation will be run, and the instance's 338 | {Spira::Errors} object will be populated with any errors. You can use the 339 | built in `assert` and assert helpers such as `assert_set` and 340 | `asssert_numeric`. 341 | 342 | 343 | class CD 344 | validate :is_real_music 345 | def is_real_music 346 | assert(artist.name != "Nickelback", :artist, "cannot be Nickelback") 347 | end 348 | 349 | validate :track_count_numeric 350 | def track_count_numeric 351 | assert_numeric(track_count) 352 | end 353 | end 354 | 355 | dancing_queen.artist = nickelback 356 | dancing_queen.save! #=> ValidationError 357 | dancing_queen.errors.each #=> ["artist cannot be Nickelback"] 358 | 359 | dancing_queen.artist = abba 360 | dancing_queen.save! #=> true 361 | 362 | ## Hooks 363 | 364 | Spira supports `before_create`, `after_create`, `after_update`, `before_save`, 365 | `after_save`, and `before_destroy` hooks: 366 | 367 | class CD 368 | def before_save 369 | self.publisher = 'No publisher set' if self.publisher.nil? 370 | end 371 | end 372 | 373 | The `after_update` hook only fires on the `update` method, not simple property 374 | accessors (to allow you to easily set properties in these without going into a 375 | recursive loop): 376 | 377 | class CD 378 | def after_update 379 | self.artist = 'Queen' # every artist should be Queen! 380 | end 381 | end 382 | 383 | # ...snip ... 384 | dancing_queen.artist 385 | #=> "ABBA" 386 | dancing_queen.name = "Dancing Queen" 387 | dancing_queen.artist 388 | #=> "ABBA" 389 | dancing_queen.update(:name => "Dancing Queen") 390 | dancing_queen.artist 391 | #=> "Queen" 392 | 393 | ## Inheritance 394 | 395 | You can extend Spira resources without a problem: 396 | 397 | class BoxedSet < CD 398 | include Spira::Resource 399 | property cd_count, :predicate => CD.count, :type => Integer 400 | end 401 | 402 | You can also make Spira modules and include them into other classes: 403 | 404 | module Media 405 | include Spira::Resource 406 | property :format, :predicate => Media.format 407 | end 408 | 409 | class CD 410 | include Spira::Resource 411 | include Media 412 | end 413 | 414 | 415 | ## Using Model Objects as RDF.rb Objects 416 | 417 | All model objects are fully-functional as `RDF::Enumerable`, `RDF::Queryable`, 418 | and `RDF::Mutable`. This lets you manipulate objects on the RDF statement 419 | level. You can also access attributes that are not defined as properties. 420 | 421 | ## Support 422 | 423 | There are a number of ways to ask for help. In declining order of preference: 424 | 425 | * Fork the project and write a failing test, or a pending test for a feature request 426 | * Ask on the [public-rdf-ruby w3c mailing list][] 427 | * You can post issues to the Github issue queue 428 | * (there might one day be a google group or other such support channel, but not yet) 429 | 430 | ## Authors, Development, and License 431 | 432 | #### Authors 433 | * Ben Lavender 434 | 435 | #### 'License' 436 | Spira is free and unemcumbered software released into the public 437 | domain. For more information, see the included UNLICENSE file. 438 | 439 | #### Contributing 440 | Fork it on Github and go. Please make sure you're kosher with the UNLICENSE 441 | file before contributing. 442 | 443 | [public-rdf-ruby w3c mailing list]: http://lists.w3.org/Archives/Public/public-rdf-ruby/ 444 | [RDF.rb]: http://rdf.rubyforge.org 445 | -------------------------------------------------------------------------------- /lib/spira/resource/instance_methods.rb: -------------------------------------------------------------------------------- 1 | require 'rdf/isomorphic' 2 | require 'set' 3 | 4 | module Spira 5 | module Resource 6 | 7 | ## 8 | # This module contains instance methods for Spira resources. See 9 | # {Spira::Resource} for more information. 10 | # 11 | # @see Spira::Resource 12 | # @see Spira::Resource::ClassMethods 13 | # @see Spira::Resource::DSL 14 | # @see Spira::Resource::Validations 15 | module InstanceMethods 16 | 17 | # Marker for whether or not a field has been set or not; distinguishes 18 | # nil and unset. 19 | # @private 20 | NOT_SET = ::Object.new.freeze 21 | 22 | ## 23 | # This instance's URI. 24 | # 25 | # @return [RDF::URI] 26 | attr_reader :subject 27 | 28 | ## 29 | # Initialize a new Spira::Resource instance of this resource class using 30 | # a new blank node subject. Accepts a hash of arguments for initial 31 | # attributes. To use a URI or existing blank node as a subject, use 32 | # {Spira::Resource::ClassMethods#for} instead. 33 | # 34 | # @param [Hash{Symbol => Any}] opts Default attributes for this instance 35 | # @yield [self] Executes a given block and calls `#save!` 36 | # @yieldparam [self] self The newly created instance 37 | # @see Spira::Resource::ClassMethods#for 38 | # @see RDF::URI#as 39 | # @see RDF::Node#as 40 | def initialize(opts = {}) 41 | @subject = opts[:_subject] || RDF::Node.new 42 | reload(opts) 43 | if block_given? 44 | yield(self) 45 | save! 46 | end 47 | self 48 | end 49 | 50 | ## 51 | # Reload all attributes for this instance, overwriting or setting 52 | # defaults with the given opts. This resource will block if the 53 | # underlying repository blocks the next time it accesses attributes. 54 | # 55 | # @param [Hash{Symbol => Any}] opts 56 | # @option opts [Symbol] :any A property name. Sets the given property to the given value. 57 | def reload(opts = {}) 58 | @cache = opts[:_cache] || RDF::Util::Cache.new 59 | @cache[subject] = self 60 | @dirty = {} 61 | @attributes = {} 62 | @attributes[:current] = {} 63 | @attributes[:copied] = {} 64 | self.class.properties.each do |name, predicate| 65 | case opts[name].nil? 66 | when false 67 | attribute_set(name, opts[name]) 68 | when true 69 | @attributes[:copied][name] = NOT_SET 70 | end 71 | end 72 | @attributes[:original] = promise { reload_attributes } 73 | end 74 | 75 | ## 76 | # Load this instance's attributes. Overwrite loaded values with attributes in the given options. 77 | # 78 | # @return [Hash{Symbol => Any}] attributes 79 | # @private 80 | def reload_attributes() 81 | statements = self.class.repository_or_fail.query(:subject => @subject) 82 | attributes = {} 83 | 84 | # Set attributes for each statement corresponding to a predicate 85 | self.class.properties.each do |name, property| 86 | if self.class.is_list?(name) 87 | values = Set.new 88 | collection = statements.query(:subject => @subject, :predicate => property[:predicate]) unless statements.empty? 89 | unless collection.nil? 90 | collection.each do |statement| 91 | values << self.class.build_value(statement,property[:type], @cache) 92 | end 93 | end 94 | attributes[name] = values 95 | else 96 | statement = statements.query(:subject => @subject, :predicate => property[:predicate]).first unless statements.empty? 97 | attributes[name] = self.class.build_value(statement, property[:type], @cache) 98 | end 99 | end 100 | attributes 101 | end 102 | 103 | ## 104 | # Returns a hash of name => value for this instance's attributes 105 | # 106 | # @return [Hash{Symbol => Any}] attributes 107 | def attributes 108 | attributes = {} 109 | self.class.properties.keys.each do |property| 110 | attributes[property] = attribute_get(property) 111 | end 112 | attributes 113 | end 114 | 115 | ## 116 | # Remove the given attributes from the repository 117 | # 118 | # @param [Hash] attributes The hash of attributes to delete 119 | # @param [Hash{Symbol => Any}] opts Options for deletion 120 | # @option opts [true] :destroy_type Destroys the `RDF.type` statement associated with this class as well 121 | # @private 122 | def _destroy_attributes(attributes, opts = {}) 123 | repository = repository_for_attributes(attributes) 124 | repository.insert([@subject, RDF.type, self.class.type]) if (self.class.type && opts[:destroy_type]) 125 | self.class.repository_or_fail.delete(*repository) 126 | end 127 | 128 | ## 129 | # Delete this instance from the repository. 130 | # 131 | # @param [Symbol] what 132 | # @example Delete all fields defined in the model 133 | # @object.destroy! 134 | # @example Delete all instances of this object as the subject of a triple, including non-model data @object.destroy! 135 | # @object.destroy!(:subject) 136 | # @example Delete all instances of this object as the object of a triple 137 | # @object.destroy!(:object) 138 | # @example Delete all triples with this object as the subject or object 139 | # @object.destroy!(:completely) 140 | # @return [true, false] Whether or not the destroy was successful 141 | def destroy!(what = nil) 142 | before_destroy if self.respond_to?(:before_destroy) 143 | result = case what 144 | when nil 145 | _destroy_attributes(attributes, :destroy_type => true) != nil 146 | when :subject 147 | self.class.repository_or_fail.delete([subject, nil, nil]) != nil 148 | when :object 149 | self.class.repository_or_fail.delete([nil, nil, subject]) != nil 150 | when :completely 151 | destroy!(:subject) && destroy!(:object) 152 | end 153 | after_destroy if self.respond_to?(:after_destroy) if result 154 | result 155 | end 156 | 157 | ## 158 | # Save changes in this instance to the repository. 159 | # 160 | # @return [self] self 161 | def save! 162 | existed = (self.respond_to?(:before_create) || self.respond_to?(:after_create)) && !self.type.nil? && exists? 163 | before_create if self.respond_to?(:before_create) && !self.type.nil? && !existed 164 | before_save if self.respond_to?(:before_save) 165 | unless self.class.validators.empty? 166 | errors.clear 167 | self.class.validators.each do | validator | self.send(validator) end 168 | if errors.empty? 169 | _update! 170 | else 171 | raise(ValidationError, "Could not save #{self.inspect} due to validation errors: " + errors.each.join(';')) 172 | end 173 | else 174 | _update! 175 | end 176 | after_create if self.respond_to?(:after_create) && !self.type.nil? && !existed 177 | after_save if self.respond_to?(:after_save) 178 | self 179 | end 180 | 181 | ## 182 | # Update multiple attributes of this repository. 183 | # 184 | # @example Update multiple attributes 185 | # person.update(:name => 'test', :age => 10) 186 | # #=> person 187 | # person.name 188 | # #=> 'test' 189 | # person.age 190 | # #=> 10 191 | # person.dirty? 192 | # #=> true 193 | # @param [Hash{Symbol => Any}] properties 194 | # @return [self] 195 | def update(properties) 196 | properties.each do |property, value| 197 | attribute_set(property, value) 198 | end 199 | after_update if self.respond_to?(:after_update) 200 | self 201 | end 202 | 203 | ## 204 | # Equivalent to #update followed by #save! 205 | # 206 | # @example Update multiple attributes and save the changes 207 | # person.update!(:name => 'test', :age => 10) 208 | # #=> person 209 | # person.name 210 | # #=> 'test' 211 | # person.age 212 | # #=> 10 213 | # person.dirty? 214 | # #=> false 215 | # @param [Hash{Symbol => Any}] properties 216 | # @return [self] 217 | def update!(properties) 218 | update(properties) 219 | save! 220 | end 221 | 222 | ## 223 | # Save changes to the repository 224 | # 225 | # @private 226 | def _update! 227 | self.class.properties.each do |property, predicate| 228 | if dirty?(property) 229 | self.class.repository_or_fail.delete([subject, predicate[:predicate], nil]) 230 | if self.class.is_list?(property) 231 | repo = RDF::Repository.new 232 | attribute_get(property).each do |value| 233 | repo << RDF::Statement.new(subject, predicate[:predicate], self.class.build_rdf_value(value, self.class.properties[property][:type])) 234 | end 235 | self.class.repository_or_fail.insert(*repo) 236 | else 237 | self.class.repository_or_fail.insert(RDF::Statement.new(subject, predicate[:predicate], self.class.build_rdf_value(attribute_get(property), self.class.properties[property][:type]))) unless attribute_get(property).nil? 238 | end 239 | end 240 | @attributes[:original][property] = attribute_get(property) 241 | @dirty[property] = nil 242 | @attributes[:copied][property] = NOT_SET 243 | end 244 | self.class.repository_or_fail.insert(RDF::Statement.new(@subject, RDF.type, type)) unless type.nil? 245 | end 246 | 247 | ## 248 | # The `RDF.type` associated with this class. 249 | # 250 | # @return [nil,RDF::URI] The RDF type associated with this instance's class. 251 | def type 252 | self.class.type 253 | end 254 | 255 | ## 256 | # `type` is a special property which is associated with the class and not 257 | # the instance. Always raises a TypeError to try and assign it. 258 | # 259 | # @raise [TypeError] always 260 | def type=(type) 261 | raise TypeError, "Cannot reassign RDF.type for #{self}; consider appending to a has_many :types" 262 | end 263 | 264 | ## 265 | # Returns the RDF representation of this resource. 266 | # 267 | # @return [RDF::Enumerable] 268 | def to_rdf 269 | self 270 | end 271 | 272 | ## 273 | # A developer-friendly view of this projection 274 | # 275 | # @private 276 | def inspect 277 | "<#{self.class}:#{self.object_id} @subject: #{@subject}>" 278 | end 279 | 280 | ## 281 | # Enumerate each RDF statement that makes up this projection. This makes 282 | # each instance an `RDF::Enumerable`, with all of the nifty benefits 283 | # thereof. See for 284 | # information on arguments. 285 | # 286 | # @see http://rdf.rubyforge.org/RDF/Enumerable.html 287 | def each(*args, &block) 288 | return enum_for(:each) unless block_given? 289 | repository = repository_for_attributes(attributes) 290 | repository.insert(RDF::Statement.new(@subject, RDF.type, type)) unless type.nil? 291 | repository.each(*args, &block) 292 | end 293 | 294 | ## 295 | # The number of RDF::Statements this projection has. 296 | # 297 | # @see http://rdf.rubyforge.org/RDF/Enumerable.html#count 298 | def count 299 | each.size 300 | end 301 | 302 | ## 303 | # Sets the given attribute to the given value. 304 | # 305 | # @private 306 | def attribute_set(name, value) 307 | @dirty[name] = true 308 | @attributes[:current][name] = value 309 | end 310 | 311 | ## 312 | # Returns true if the given attribute has been changed from the backing store 313 | # 314 | def dirty?(name = nil) 315 | case name 316 | when nil 317 | self.class.properties.keys.any? { |key| dirty?(key) } 318 | else 319 | case 320 | when @dirty[name] == true 321 | true 322 | else 323 | case @attributes[:copied][name] 324 | when NOT_SET 325 | false 326 | else 327 | @attributes[:copied][name] != @attributes[:original][name] 328 | end 329 | end 330 | end 331 | end 332 | 333 | ## 334 | # Get the current value for the given attribute 335 | # 336 | # @private 337 | def attribute_get(name) 338 | case @dirty[name] 339 | when true 340 | @attributes[:current][name] 341 | else 342 | case @attributes[:copied][name].equal?(NOT_SET) 343 | when true 344 | dup = if @attributes[:original][name].is_a?(Spira::Resource) 345 | @attributes[:original][name] 346 | else 347 | begin 348 | @attributes[:original][name].dup 349 | rescue TypeError 350 | @attributes[:original][name] 351 | end 352 | end 353 | @attributes[:copied][name] = dup 354 | when false 355 | @attributes[:copied][name] 356 | end 357 | end 358 | end 359 | 360 | ## 361 | # Create an RDF::Repository for the given attributes hash. This could 362 | # just as well be a class method but is only used here in #save! and 363 | # #destroy!, so it is defined here for simplicity. 364 | # 365 | # @param [Hash] attributes The attributes to create a repository for 366 | # @private 367 | def repository_for_attributes(attributes) 368 | repo = RDF::Repository.new 369 | attributes.each do | name, attribute | 370 | if self.class.is_list?(name) 371 | new = [] 372 | attribute.each do |value| 373 | value = self.class.build_rdf_value(value, self.class.properties[name][:type]) 374 | new << RDF::Statement.new(@subject, self.class.properties[name][:predicate], value) 375 | end 376 | repo.insert(*new) 377 | else 378 | value = self.class.build_rdf_value(attribute, self.class.properties[name][:type]) 379 | repo.insert(RDF::Statement.new(@subject, self.class.properties[name][:predicate], value)) 380 | end 381 | end 382 | repo 383 | end 384 | 385 | ## 386 | # Compare this instance with another instance. The comparison is done on 387 | # an RDF level, and will work across subclasses as long as the attributes 388 | # are the same. 389 | # 390 | # @see http://rdf.rubyforge.org/isomorphic/ 391 | def ==(other) 392 | case other 393 | # TODO: define behavior for equality on subclasses. 394 | # TODO: should we compare attributes here? 395 | when self.class 396 | @subject == other.uri 397 | when RDF::Enumerable 398 | self.isomorphic_with?(other) 399 | else 400 | false 401 | end 402 | end 403 | 404 | ## 405 | # Returns true for :to_uri if this instance's subject is a URI, and false if it is not. 406 | # Returns true for :to_node if this instance's subject is a Node, and false if it is not. 407 | # Calls super otherwise. 408 | # 409 | # @private 410 | def respond_to?(*args) 411 | case args[0] 412 | when :to_uri 413 | @subject.respond_to?(:to_uri) 414 | when :to_node 415 | @subject.node? 416 | else 417 | super(*args) 418 | end 419 | end 420 | 421 | ## 422 | # Returns the RDF::URI associated with this instance if this instance's 423 | # subject is an RDF::URI, and nil otherwise. 424 | # 425 | # @return [RDF::URI,nil] 426 | def uri 427 | @subject.respond_to?(:to_uri) ? @subject : nil 428 | end 429 | 430 | ## 431 | # Returns the URI representation of this resource, if available. If this 432 | # resource's subject is a BNode, raises a NoMethodError. 433 | # 434 | # @return [RDF::URI] 435 | # @raise [NoMethodError] 436 | def to_uri 437 | uri || (raise NoMethodError, "No such method: :to_uri (this instance's subject is not a URI)") 438 | end 439 | 440 | ## 441 | # Returns true if the subject associated with this instance is a blank node. 442 | # 443 | # @return [true, false] 444 | def node? 445 | @subject.node? 446 | end 447 | 448 | ## 449 | # Returns the Node subject of this resource, if available. If this 450 | # resource's subject is a URI, raises a NoMethodError. 451 | # 452 | # @return [RDF::Node] 453 | # @raise [NoMethodError] 454 | def to_node 455 | @subject.node? ? @subject : (raise NoMethodError, "No such method: :to_uri (this instance's subject is not a URI)") 456 | end 457 | 458 | ## 459 | # The validation errors collection associated with this instance. 460 | # 461 | # @return [Spira::Errors] 462 | # @see Spira::Errors 463 | def errors 464 | @errors ||= Spira::Errors.new 465 | end 466 | 467 | ## 468 | # Returns true if any data exists for this subject in the backing RDF store 469 | # 470 | # @return [Boolean] 471 | def exists? 472 | !data.empty? 473 | end 474 | alias_method :exist?, :exists? 475 | 476 | ## 477 | # Returns an Enumerator of all RDF data for this subject, not just model data. 478 | # 479 | # @see #each 480 | # @see http://rdf.rubyforge.org/RDF/Enumerable.html 481 | # @return [Enumerator] 482 | def data 483 | self.class.repository.query(:subject => subject) 484 | end 485 | 486 | ## 487 | # Returns a new instance of this class with the new subject instead of self.subject 488 | # 489 | # @param [RDF::Resource] new_subject 490 | # @return [Spira::Resource] copy 491 | def copy(new_subject) 492 | copy = self.class.for(new_subject) 493 | self.class.properties.each_key { |property| copy.attribute_set(property, self.attribute_get(property)) } 494 | copy 495 | end 496 | 497 | ## 498 | # Returns a new instance of this class with the new subject instead of 499 | # self.subject after saving the new copy to the repository. 500 | # 501 | # @param [RDF::Resource] new_subject 502 | # @return [Spira::Resource, String] copy 503 | def copy!(new_subject) 504 | copy(new_subject).save! 505 | end 506 | 507 | ## 508 | # Copies all data, including non-model data, about this resource to 509 | # another URI. The copy is immediately saved to the repository. 510 | # 511 | # @param [RDF::Resource] new_subject 512 | # @return [Spira::Resource, String] copy 513 | def copy_resource!(new_subject) 514 | new_subject = self.class.id_for(new_subject) 515 | update_repository = RDF::Repository.new 516 | data.each do |statement| 517 | update_repository << RDF::Statement.new(new_subject, statement.predicate, statement.object) 518 | end 519 | self.class.repository.insert(update_repository) 520 | new_subject.as(self.class) 521 | end 522 | 523 | ## 524 | # Rename this resource in the repository to the new given subject. 525 | # Changes are immediately saved to the repository. 526 | # 527 | # @param [RDF::Resource] new_subject 528 | # @return [Spira::Resource, String] new_resource 529 | def rename!(new_subject) 530 | new = copy_resource!(new_subject) 531 | object_statements = self.class.repository.query(:object => subject) 532 | update_repository = RDF::Repository.new 533 | object_statements.each do |statement| 534 | update_repository << RDF::Statement.new(statement.subject, statement.predicate, new.subject) 535 | end 536 | self.class.repository.insert(update_repository) 537 | destroy!(:completely) 538 | new 539 | end 540 | 541 | ## We have defined #each and can do this fun RDF stuff by default 542 | include ::RDF::Enumerable, ::RDF::Queryable 543 | 544 | ## Include the base validation functions 545 | include Spira::Resource::Validations 546 | 547 | end 548 | end 549 | end 550 | --------------------------------------------------------------------------------