lib/spira/resource.rb in spira-0.0.12 vs lib/spira/resource.rb in spira-0.5.0
- old
+ new
@@ -1,72 +1,177 @@
-module Spira
+require "active_support/core_ext/class"
+require "spira/association_reflection"
- ##
- # Spira::Resource is the main interface to Spira. Classes and modules
- # include Spira::Resource to create projections of RDF data as a class. For
- # an overview, see the {file:README}.
- #
- # Projections are a mapping of RDF predicates to fields.
- #
- # class Person
- # include Spira::Resource
- #
- # property :name, :predicate => FOAF.name
- # property :age, :predicate => FOAF.age, :type => Integer
- # end
- #
- # RDF::URI('http://example.org/people/bob').as(Person) #=> <#Person @uri=http://example.org/people/bob>
- #
- # Spira resources include the RDF namespace, and can thus reference all of
- # the default RDF.rb vocabularies without the RDF:: prefix:
- #
- # property :name, :predicate => FOAF.name
- #
- # The Spira::Resource documentation is broken into several parts, vaguely
- # related by functionality:
- # * {Spira::Resource::DSL} contains methods used during the declaration of a class or module
- # * {Spira::Resource::ClassMethods} contains class methods for use by declared classes
- # * {Spira::Resource::InstanceMethods} contains methods for use by instances of Spira resource classes
- # * {Spira::Resource::Validations} contains some default validation functions
- #
- # @see Spira::Resource::DSL
- # @see Spira::Resource::ClassMethods
- # @see Spira::Resource::InstanceMethods
- # @see Spira::Resource::Validations
+module Spira
module Resource
-
- autoload :DSL, 'spira/resource/dsl'
- autoload :ClassMethods, 'spira/resource/class_methods'
- autoload :InstanceMethods, 'spira/resource/instance_methods'
- autoload :Validations, 'spira/resource/validations'
+ ##
+ # Configuration options for the Spira::Resource:
+ #
+ # @params[Hash] options
+ # :repository_name :: name of the repository to use
+ # :base_uri :: base URI to be used for the resource
+ # :default_vocabulary :: default vocabulary to use for the properties
+ # defined for this resource
+ # All these configuration options are readable via
+ # their respectively named Spira resource methods.
+ #
+ def configure(options = {})
+ singleton_class.class_eval do
+ { :repository_name => options[:repository_name],
+ :base_uri => options[:base_uri],
+ :default_vocabulary => options[:default_vocabulary]
+ }.each do |name, value|
+ # redefine reader methods only when required,
+ # otherwise, use the ancestor methods
+ if value
+ define_method name do
+ value
+ end
+ end
+ end
+ end
+ end
##
- # When a child class includes Spira::Resource, this does the magic to make
- # it a Spira resource.
+ # Declare a type for the Spira::Resource.
+ # You can declare multiple types for a resource
+ # with multiple "type" assignments.
+ # If no types are declared for a resource,
+ # they are inherited from the parent resource.
#
- # @private
- def self.included(child)
- # Don't do inclusion work twice. Checking for the properties accessor is
- # a proxy for a proper check to see if this is a resource already. Ruby
- # has already extended the child class' ancestors to include
- # Spira::Resource by the time we get here.
- # FIXME: Find a 'more correct' check.
- unless child.respond_to?(:properties)
- child.extend DSL
- child.extend ClassMethods
- child.instance_eval do
- class << self
- attr_accessor :properties, :lists
+ # @params[RDF::URI] uri
+ #
+ def type(uri = nil)
+ if uri
+ if uri.is_a?(RDF::URI)
+ ts = @types ? types : Set.new
+ singleton_class.class_eval do
+ define_method :types do
+ ts
+ end
end
- @properties = {}
- @lists = {}
+ @types = ts << uri
+ else
+ raise TypeError, "Type must be a RDF::URI"
end
+ else
+ types.first
end
end
-
- # This lets including classes reference vocabularies without the RDF:: prefix
- include Spira::Types
- include ::RDF
- include InstanceMethods
+
+ ##
+ # Add a property to this class. A property is an accessor field that
+ # represents an RDF predicate.
+ #
+ # @example A simple string property
+ # property :name, :predicate => FOAF.name, :type => String
+ # @example A property which defaults to {Spira::Types::Any}
+ # property :name, :predicate => FOAF.name
+ # @example An integer property
+ # property :age, :predicate => FOAF.age, :type => Integer
+ # @param [Symbol] name The name of this property
+ # @param [Hash{Symbol => Any}] opts property options
+ # @option opts [RDF::URI] :predicate The RDF predicate which will refer to this property
+ # @option opts [Spira::Type, String] :type (Spira::Types::Any) The
+ # type for this property. If a Spira::Type is given, that class will be
+ # used to serialize and unserialize values. If a String is given, it
+ # should be the String form of a Spira::Base class name (Strings are
+ # used to prevent issues with load order).
+ # @see Spira::Types
+ # @see Spira::Type
+ # @return [Void]
+ def property(name, opts = {})
+ unset_has_many(name)
+ predicate = predicate_for(opts[:predicate], name)
+ type = type_for(opts[:type])
+ properties[name] = HashWithIndifferentAccess.new(:predicate => predicate, :type => type)
+
+ define_attribute_method name
+ define_method "#{name}=" do |arg|
+ write_attribute name, arg
+ end
+ define_method name do
+ read_attribute name
+ end
+ end
+
+ ##
+ # The plural form of `property`. `Has_many` has the same options as
+ # `property`, but instead of a single value, a Ruby Array of objects will
+ # be created instead.
+ #
+ # has_many corresponds to an RDF subject with several triples of the same
+ # predicate. This corresponds to a Ruby Array, which will be returned when
+ # the property is accessed. Arrays will be accepted for new values, but
+ # ordering and duplicate values will be lost on save.
+ #
+ # @see Spira::Base::DSL#property
+ def has_many(name, opts = {})
+ property(name, opts)
+
+ reflections[name] = AssociationReflection.new(:has_many, name, opts)
+
+ define_method "#{name.to_s.singularize}_ids" do
+ records = send(name) || []
+ records.map(&:id).compact
+ end
+ define_method "#{name.to_s.singularize}_ids=" do |ids|
+ records = ids.map {|id| self.class.reflect_on_association(name).klass.unserialize(id) }.compact
+ send "#{name}=", records
+ end
+ end
+
+
+ private
+
+ # Unset a has_many relation if it exists. Allow to redefine the cardinality of a relation in a subClass
+ #
+ # @private
+ def unset_has_many(name)
+ if reflections[name]
+ reflections.delete(name)
+ undef_method "#{name.to_s.singularize}_ids"
+ undef_method "#{name.to_s.singularize}_ids="
+ end
+ end
+
+ ##
+ # Determine the predicate for a property based on the given predicate, name, and default vocabulary
+ #
+ # @param [#to_s, #to_uri] predicate
+ # @param [Symbol] name
+ # @return [RDF::URI]
+ # @private
+ def predicate_for(predicate, name)
+ case
+ when predicate.respond_to?(:to_uri) && predicate.to_uri.absolute?
+ predicate
+ when default_vocabulary.nil?
+ raise ResourceDeclarationError, "A :predicate option is required for types without a default vocabulary"
+ else
+ # FIXME: use rdf.rb smart separator after 0.3.0 release
+ separator = default_vocabulary.to_s[-1,1] =~ /(\/|#)/ ? '' : '/'
+ RDF::URI.intern(default_vocabulary.to_s + separator + name.to_s)
+ end
+ end
+
+ ##
+ # Determine the type for a property based on the given type option
+ #
+ # @param [nil, Spira::Type, Constant] type
+ # @return Spira::Type
+ # @private
+ def type_for(type)
+ case
+ when type.nil?
+ Spira::Types::Any
+ when type.is_a?(Symbol) || type.is_a?(String)
+ type
+ when Spira.types[type]
+ Spira.types[type]
+ else
+ raise TypeError, "Unrecognized type: #{type}"
+ end
+ end
end
end