lib/spira/resource.rb
require "active_support/core_ext/class"
require "spira/association_reflection"
module Spira
module Resource
##
# Configuration options for the Spira::Resource:
#
# @params[Hash] options
# :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
{ 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
##
# 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.
#
# @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
@types = ts << uri
else
raise TypeError, "Type must be a RDF::URI"
end
else
types.first
end
end
##
# 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: RDF::Vocab::FOAF.name, type: String
# @example A property which defaults to {Spira::Types::Any}
# property :name, predicate: RDF::Vocab::FOAF.name
# @example An integer property
# property :age, predicate: RDF::Vocab::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 = {})
if opts.delete(:localized)
raise 'Only Spira::Types::Any properties can accept the :localized option' unless type_for(opts[:type]) == Spira::Types::Any
define_localized_property_methods(name, opts)
has_many "#{name}_native", opts.merge(type: Spira::Types::Native)
else
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
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
##
# Create the localized specific getter/setter for a given property
#
# @private
def define_localized_property_methods(name, opts)
define_method "#{name}=" do |arg|
new_value = merge_localized_property(name, arg)
write_attribute "#{name}_native", new_value
end
define_method name do
value = read_attribute("#{name}_native")
unserialize_localized_properties(value, I18n.locale)
end
define_method "#{name}_with_locales" do
value = read_attribute("#{name}_native")
hash_localized_properties(value)
end
define_method "#{name}_with_locales=" do |arg|
value = serialize_hash_localized_properties(arg)
write_attribute "#{name}_native", value
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