Permalink
Browse files

define_schema for Active Resource

Signed-off-by: Joshua Peek <josh@joshpeek.com>
  • Loading branch information...
1 parent 33a6bd3 commit fc9b3e4a45a81b7526f8154049c825e3755903ad @taryneast taryneast committed with josh Dec 21, 2009
@@ -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.
@@ -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
@@ -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.
#
@@ -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
@@ -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
@@ -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
Oops, something went wrong.

0 comments on commit fc9b3e4

Please sign in to comment.