Skip to content

Commit

Permalink
define_schema for Active Resource
Browse files Browse the repository at this point in the history
Signed-off-by: Joshua Peek <josh@joshpeek.com>
  • Loading branch information
taryneast authored and josh committed Dec 21, 2009
1 parent 33a6bd3 commit fc9b3e4
Show file tree
Hide file tree
Showing 3 changed files with 609 additions and 2 deletions.
144 changes: 142 additions & 2 deletions activeresource/lib/active_resource/base.rb
Expand Up @@ -13,6 +13,7 @@
require 'uri'

require 'active_resource/exceptions'
require 'active_resource/schema_definition'

module ActiveResource
# ActiveResource::Base is the main class for mapping RESTful resources as models in a Rails application.
Expand Down Expand Up @@ -241,6 +242,127 @@ class Base
cattr_accessor :logger

class << self
# This will shortly disappear to be replaced by the migration-style
# usage of this for defining a schema. At that point, all the doc
# currenlty on <tt>schema=</tt> will move back here...
def schema # :nodoc:
@schema ||= nil
end
# Creates a schema for this resource - setting the attributes that are
# known prior to fetching an instance from the remote system.
#
# The schema helps define the set of <tt>known_attributes</tt> of the
# current resource.
#
# There is no need to specify a schema for your Active Resource. If
# you do not, the <tt>known_attributes</tt> will be guessed from the
# instance attributes returned when an instance is fetched from the
# remote system.
#
# example:
# class Person < ActiveResource::Base
# define_schema do |s|
# # define each attribute separately
# s.attribute 'name', :string
#
# # or use the convenience methods and pass >=1 attribute names
# s.string 'eye_colour', 'hair_colour'
# s.integer 'age'
# s.float 'height', 'weight'
#
# # unsupported types should be left as strings
# # overload the accessor methods if you need to convert them
# s.attribute 'created_at', 'string'
# end
# end
#
# p = Person.new
# p.respond_to? :name # => true
# p.respond_to? :age # => true
# p.name # => nil
# p.age # => nil
#
# j = Person.find_by_name('John') # <person><name>John</name><age>34</age><num_children>3</num_children></person>
# j.respond_to? :name # => true
# j.respond_to? :age # => true
# j.name # => 'John'
# j.age # => '34' # note this is a string!
# j.num_children # => '3' # note this is a string!
#
# p.num_children # => NoMethodError
#
# Attribute-types must be one of:
# string, integer, float
#
# Note: at present the attribute-type doesn't do anything, but stay
# tuned...
# Shortly it will also *cast* the value of the returned attribute.
# ie:
# j.age # => 34 # cast to an integer
# j.weight # => '65' # still a string!
#
def define_schema
schema_definition = SchemaDefinition.new
yield schema_definition if block_given?

# skip out if we didn't define anything
return unless schema_definition.attrs.present?

@schema ||= {}.with_indifferent_access
@known_attributes ||= []

schema_definition.attrs.each do |k,v|
@schema[k] = v
@known_attributes << k
end

schema
end


# Alternative, direct way to specify a <tt>schema</tt> for this
# Resource. <tt>define_schema</tt> is more flexible, but this is quick
# for a very simple schema.
#
# Pass the schema as a hash with the keys being the attribute-names
# and the value being one of the accepted attribute types (as defined
# in <tt>define_schema</tt>)
#
# example:
#
# class Person < ActiveResource::Base
# schema = {'name' => :string, 'age' => :integer }
# end
#
# The keys/values can be strings or symbols. They will be converted to
# strings.
#
def schema=(the_schema)
unless the_schema.present?
# purposefully nulling out the schema
@schema = nil
@known_attributes = []
return
end

raise ArgumentError, "Expected a hash" unless the_schema.kind_of? Hash

define_schema do |s|
the_schema.each {|k,v| s.attribute(k,v) }
end
end

# Returns the list of known attributes for this resource, gathered
# from the provided <tt>schema</tt>
# Attributes that are known will cause your resource to return 'true'
# when <tt>respond_to?</tt> is called on them. A known attribute will
# return nil if not set (rather than <t>MethodNotFound</tt>); thus
# known attributes can be used with <tt>validates_presence_of</tt>
# without a getter-method.
def known_attributes
@known_attributes ||= []
end

# Gets the URI of the REST resources to map for this class. The site variable is required for
# Active Resource's mapping to work.
def site
Expand Down Expand Up @@ -776,6 +898,21 @@ def uri_parser
attr_accessor :attributes #:nodoc:
attr_accessor :prefix_options #:nodoc:

# If no schema has been defined for the class (see
# <tt>ActiveResource::schema=</tt>), the default automatic schema is
# generated from the current instance's attributes
def schema
self.class.schema || self.attributes
end

# This is a list of known attributes for this resource. Either
# gathered fromthe provided <tt>schema</tt>, or from the attributes
# set on this instance after it has been fetched from the remote system.
def known_attributes
self.class.known_attributes + self.attributes.keys.map(&:to_s)
end


# Constructor method for \new resources; the optional +attributes+ parameter takes a \hash
# of attributes for the \new resource.
#
Expand Down Expand Up @@ -1157,7 +1294,7 @@ def respond_to?(method, include_priv = false)
method_name = method.to_s
if attributes.nil?
super
elsif attributes.has_key?(method_name)
elsif known_attributes.include?(method_name)
true
elsif method_name =~ /(?:=|\?)$/ && attributes.include?($`)
true
Expand Down Expand Up @@ -1262,7 +1399,10 @@ def method_missing(method_symbol, *arguments) #:nodoc:
attributes[$`]
end
else
attributes.include?(method_name) ? attributes[method_name] : super
return attributes[method_name] if attributes.include?(method_name)
# not set right now but we know about it
return nil if known_attributes.include?(method_name)
super
end
end
end
Expand Down
58 changes: 58 additions & 0 deletions activeresource/lib/active_resource/schema_definition.rb
@@ -0,0 +1,58 @@
require 'active_resource/exceptions'

module ActiveResource # :nodoc:
class SchemaDefinition # :nodoc:

# attributes can be known to be one of these types. They are easy to
# cast to/from.
KNOWN_ATTRIBUTE_TYPES = %w( string integer float )

# An array of attribute definitions, representing the attributes that
# have been defined.
attr_accessor :attrs

# The internals of an Active Resource SchemaDefinition are very simple -
# unlike an Active Record TableDefinition (on which it is based).
# It provides a set of convenience methods for people to define their
# schema using the syntax:
# define_schema do |s|
# s.string :foo
# s.integer :bar
# end
#
# The schema stores the name and type of each attribute. That is then
# read out by the define_schema method to populate the actual
# Resource's schema
def initialize
@attrs = {}
end

def attribute(name, type, options = {})
raise ArgumentError, "Unknown Attribute type: #{type.inspect} for key: #{name.inspect}" unless type.nil? || SchemaDefinition::KNOWN_ATTRIBUTE_TYPES.include?(type.to_s)

the_type = type.to_s
# TODO: add defaults
#the_attr = [type.to_s]
#the_attr << options[:default] if options.has_key? :default
@attrs[name.to_s] = the_type
self
end

# The following are the attribute types supported by Active Resource
# migrations.
# TODO: We should eventually support all of these:
# %w( string text integer float decimal datetime timestamp time date binary boolean ).each do |attr_type|
KNOWN_ATTRIBUTE_TYPES.each do |attr_type|
class_eval <<-EOV
def #{attr_type.to_s}(*args) # def string(*args)
options = args.extract_options! # options = args.extract_options!
attr_names = args # attr_names = args
#
attr_names.each { |name| attribute(name, '#{attr_type}', options) } # attr_names.each { |name| attribute(name, 'string', options) }
end # end
EOV

end

end
end

0 comments on commit fc9b3e4

Please sign in to comment.