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