Skip to content

Integrate with Active Model Attributes #410

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 76 additions & 14 deletions lib/active_resource/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -385,16 +391,49 @@ class << self
#
# Attribute-types must be one of: <tt>string, text, integer, float, decimal, datetime, timestamp, time, date, binary, boolean</tt>
#
# 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
Expand Down Expand Up @@ -434,6 +473,7 @@ def schema(&block)
def schema=(the_schema)
unless the_schema.present?
# purposefully nulling out the schema
self.schema_definition = Schema
@schema = nil
@known_attributes = []
return
Expand Down Expand Up @@ -1213,6 +1253,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
Expand Down Expand Up @@ -1246,6 +1287,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

Expand Down Expand Up @@ -1285,12 +1327,12 @@ def persisted?

# Gets the <tt>\id</tt> attribute of the resource.
def id
attributes[self.class.primary_key]
_read_attribute(self.class.primary_key)
end

# Sets the <tt>\id</tt> attribute of the resource.
def id=(id)
attributes[self.class.primary_key] = id
_write_attribute(self.class.primary_key, id)
end

# Test for equality. Resource are equal if and only if +other+ is the same object or
Expand Down Expand Up @@ -1481,7 +1523,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] =
_write_attribute(key,
case value
when Array
resource = nil
Expand All @@ -1498,7 +1540,7 @@ def load(attributes, remove_root = false, persisted = false)
resource.new(value, persisted)
else
value.duplicable? ? value.dup : value
end
end)
end
self
end
Expand Down Expand Up @@ -1541,7 +1583,9 @@ def update_attributes(attributes)
# <tt>my_person.respond_to?(:name?)</tt>.
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
Expand Down Expand Up @@ -1698,10 +1742,28 @@ def split_options(options = {})
self.class.__send__(:split_options, options)
end

def _read_attribute(name)
if @schema.respond_to?(name)
@schema.send(name)
else
attributes[name.to_s]
end
end

def _write_attribute(name, value)
if @schema.respond_to?("#{name}=")
@schema.send("#{name}=", value)
else
attributes[name.to_s] = value
end
end

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
Expand Down
89 changes: 58 additions & 31 deletions lib/active_resource/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
51 changes: 51 additions & 0 deletions test/cases/base/schema_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ def setup

def teardown
Person.schema = nil # hack to stop test bleedthrough...
Person.cast_values = false # hack to stop test bleedthrough...
end


Expand Down Expand Up @@ -425,4 +426,54 @@ def teardown
Person.schema = new_schema
assert_equal Person.new(age: 20, name: "Matz").known_attributes, ["age", "name"]
end

test "clone with schema that casts values" do
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

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
Loading