diff --git a/lib/active_resource/base.rb b/lib/active_resource/base.rb
index 29376f081d..ca0ce01070 100644
--- a/lib/active_resource/base.rb
+++ b/lib/active_resource/base.rb
@@ -335,6 +335,12 @@ def self.logger=(logger)
class_attribute :connection_class
self.connection_class = Connection
+ class_attribute :cast_values, instance_accessor: false, instance_predicate: false # :nodoc:
+ self.cast_values = false
+
+ class_attribute :schema_definition, instance_accessor: false, instance_predicate: false # :nodoc:
+ self.schema_definition = Schema
+
class << self
include ThreadsafeAttributes
threadsafe_attribute :_headers, :_connection, :_user, :_password, :_bearer_token, :_site, :_proxy
@@ -385,16 +391,49 @@ class << self
#
# Attribute-types must be one of: string, text, integer, float, decimal, datetime, timestamp, time, date, binary, boolean
#
- # 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!
+ # Note: By default, the attribute-type is ignored and will not cast its
+ # value.
+ #
+ # To cast values to their specified types, declare the Schema with the
+ # +:cast_values+ set to true.
+ #
+ # class Person < ActiveResource::Base
+ # schema cast_values: true do
+ # integer 'age'
+ # end
+ # end
+ #
+ # p = Person.new
+ # p.age = "18"
+ # p.age # => 18
+ #
+ # To configure inheriting resources to cast values, set the +cast_values+
+ # class attribute:
+ #
+ # class ApplicationResource < ActiveResource::Base
+ # self.cast_values = true
+ # end
+ #
+ # class Person < ApplicationResource
+ # schema do
+ # integer 'age'
+ # end
+ # end
+ #
+ # p = Person.new
+ # p.age = "18"
+ # p.age # => 18
+ #
+ # To set all resources application-wide to cast values, set
+ # +config.active_resource.cast_values+:
+ #
+ # # config/application.rb
+ # config.active_resource.cast_values = true
#
- def schema(&block)
+ def schema(cast_values: self.cast_values, &block)
if block_given?
- schema_definition = Schema.new
+ self.schema_definition = Class.new(Schema)
+ schema_definition.cast_values = cast_values
schema_definition.instance_eval(&block)
# skip out if we didn't define anything
@@ -1213,6 +1252,7 @@ def known_attributes
def initialize(attributes = {}, persisted = false)
@attributes = {}.with_indifferent_access
@prefix_options = {}
+ @schema = self.class.schema_definition.new
@persisted = persisted
load(attributes, false, persisted)
end
@@ -1246,6 +1286,7 @@ def clone
resource = self.class.new({})
resource.prefix_options = self.prefix_options
resource.send :instance_variable_set, "@attributes", cloned
+ resource.send :instance_variable_set, "@schema", @schema.clone
resource
end
@@ -1285,12 +1326,24 @@ def persisted?
# Gets the \id attribute of the resource.
def id
- attributes[self.class.primary_key]
+ primary_key = self.class.primary_key
+
+ if @schema.respond_to?(primary_key)
+ @schema.send(primary_key)
+ else
+ attributes[primary_key]
+ end
end
# Sets the \id attribute of the resource.
def id=(id)
- attributes[self.class.primary_key] = id
+ primary_key = self.class.primary_key
+
+ if @schema.respond_to?(:"#{primary_key}=")
+ @schema.send(:"#{primary_key}=", id)
+ else
+ attributes[primary_key] = id
+ end
end
# Test for equality. Resource are equal if and only if +other+ is the same object or
@@ -1481,7 +1534,7 @@ def load(attributes, remove_root = false, persisted = false)
attributes = Formats.remove_root(attributes) if remove_root
attributes.each do |key, value|
- @attributes[key.to_s] =
+ value =
case value
when Array
resource = nil
@@ -1499,6 +1552,12 @@ def load(attributes, remove_root = false, persisted = false)
else
value.duplicable? ? value.dup : value
end
+
+ if @schema.respond_to?("#{key}=")
+ @schema.send("#{key}=", value)
+ else
+ @attributes[key.to_s] = value
+ end
end
self
end
@@ -1541,7 +1600,9 @@ def update_attributes(attributes)
# my_person.respond_to?(:name?).
def respond_to_missing?(method, include_priv = false)
method_name = method.to_s
- if attributes.nil?
+ if @schema.respond_to?(method)
+ true
+ elsif attributes.nil?
super
elsif known_attributes.include?(method_name)
true
@@ -1701,7 +1762,9 @@ def split_options(options = {})
def method_missing(method_symbol, *arguments) # :nodoc:
method_name = method_symbol.to_s
- if method_name =~ /(=|\?)$/
+ if @schema.respond_to?(method_name)
+ @schema.send(method_name, *arguments)
+ elsif method_name =~ /(=|\?)$/
case $1
when "="
attributes[$`] = arguments.first
diff --git a/lib/active_resource/schema.rb b/lib/active_resource/schema.rb
index 40206c6441..5107fa15de 100644
--- a/lib/active_resource/schema.rb
+++ b/lib/active_resource/schema.rb
@@ -2,14 +2,24 @@
module ActiveResource # :nodoc:
class Schema # :nodoc:
+ include ActiveModel::Model
+ include ActiveModel::Attributes
+
# attributes can be known to be one of these types. They are easy to
# cast to/from.
KNOWN_ATTRIBUTE_TYPES = %w( string text integer float decimal datetime timestamp time date binary boolean )
# An array of attribute definitions, representing the attributes that
# have been defined.
- attr_accessor :attrs
+ class_attribute :attrs, instance_accessor: false, instance_predicate: false # :nodoc:
+ self.attrs = {}
+
+ class_attribute :cast_values, instance_accessor: false, instance_predicate: false # :nodoc:
+ self.cast_values = false
+ ##
+ # :method: initialize
+ #
# The internals of an Active Resource Schema 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
@@ -22,39 +32,56 @@ class Schema # :nodoc:
# The schema stores the name and type of each attribute. That is then
# read out by the schema method to populate the schema of the actual
# resource.
- def initialize
- @attrs = {}
- end
-
- def attribute(name, type, options = {})
- raise ArgumentError, "Unknown Attribute type: #{type.inspect} for key: #{name.inspect}" unless type.nil? || Schema::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
+ class << self
+ def inherited(subclass)
+ super
+ subclass.attrs = attrs.dup
+ end
- # The following are the attribute types supported by Active Resource
- # migrations.
- KNOWN_ATTRIBUTE_TYPES.each do |attr_type|
- # def string(*args)
- # options = args.extract_options!
- # attr_names = args
+ # The internals of an Active Resource Schema 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:
+ # schema do
+ # string :foo
+ # integer :bar
+ # end
#
- # attr_names.each { |name| attribute(name, 'string', options) }
- # end
- class_eval <<-EOV, __FILE__, __LINE__ + 1
- # frozen_string_literal: true
- def #{attr_type}(*args)
- options = args.extract_options!
- attr_names = args
-
- attr_names.each { |name| attribute(name, '#{attr_type}', options) }
- end
- EOV
+ # The schema stores the name and type of each attribute. That is then
+ # read out by the schema method to populate the schema of the actual
+ # resource.
+ def attribute(name, type, **options)
+ raise ArgumentError, "Unknown Attribute type: #{type.inspect} for key: #{name.inspect}" unless type.nil? || Schema::KNOWN_ATTRIBUTE_TYPES.include?(type.to_s)
+
+ the_type = type.to_s
+ attrs[name.to_s] = the_type
+
+ type = cast_values ? type.to_sym : nil
+
+ super
+ self
+ end
+
+ # The following are the attribute types supported by Active Resource
+ # migrations.
+ KNOWN_ATTRIBUTE_TYPES.each do |attr_type|
+ # def string(*args)
+ # options = args.extract_options!
+ # attr_names = args
+ #
+ # attr_names.each { |name| attribute(name, 'string', options) }
+ # end
+ class_eval <<-EOV, __FILE__, __LINE__ + 1
+ # frozen_string_literal: true
+ def #{attr_type}(*args)
+ options = args.extract_options!
+ attr_names = args
+
+ attr_names.each { |name| attribute(name, :#{attr_type}, **options) }
+ end
+ EOV
+ end
end
end
end
diff --git a/test/cases/base/schema_test.rb b/test/cases/base/schema_test.rb
index 5b9de29b5e..fdfd89afa0 100644
--- a/test/cases/base/schema_test.rb
+++ b/test/cases/base/schema_test.rb
@@ -15,6 +15,7 @@ def setup
def teardown
Person.schema = nil # hack to stop test bleedthrough...
+ Person.cast_values = false
end
@@ -425,4 +426,42 @@ def teardown
Person.schema = new_schema
assert_equal Person.new(age: 20, name: "Matz").known_attributes, ["age", "name"]
end
+
+ test "known primary_key attributes should be cast" do
+ Person.schema cast_values: true do
+ attribute Person.primary_key, :integer
+ end
+
+ person = Person.new(Person.primary_key => "1")
+
+ assert_equal 1, person.send(Person.primary_key)
+ end
+
+ test "known attributes should be cast" do
+ Person.schema cast_values: true do
+ attribute :born_on, :date
+ end
+
+ person = Person.new(born_on: "2000-01-01")
+
+ assert_equal Date.new(2000, 1, 1), person.born_on
+ end
+
+ test "known attributes should be support default values" do
+ Person.schema cast_values: true do
+ attribute :name, :string, default: "Default Name"
+ end
+
+ person = Person.new
+
+ assert_equal "Default Name", person.name
+ end
+
+ test "unknown attributes should not be cast" do
+ Person.cast_values = true
+
+ person = Person.new(age: "10")
+
+ assert_equal "10", person.age
+ end
end
diff --git a/test/cases/base_test.rb b/test/cases/base_test.rb
index 3689b4234d..fb1a2bb53b 100644
--- a/test/cases/base_test.rb
+++ b/test/cases/base_test.rb
@@ -1117,6 +1117,18 @@ def test_clone
end
end
+ def test_clone_with_schema_that_casts_values
+ Person.cast_values = true
+ Person.schema = { "age" => "integer" }
+ person = Person.new({ Person.primary_key => 1, "age" => "10" }, true)
+
+ person_c = person.clone
+
+ assert_predicate person_c, :new?
+ assert_nil person_c.send(Person.primary_key)
+ assert_equal 10, person_c.age
+ end
+
def test_nested_clone
addy = StreetAddress.find(1, params: { person_id: 1 })
addy_c = addy.clone