Skip to content
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
6 changes: 3 additions & 3 deletions lib/active_resource/associations.rb
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ def defines_belongs_to_finder_method(reflection)
if instance_variable_defined?(ivar_name)
instance_variable_get(ivar_name)
elsif attributes.include?(method_name)
attributes[method_name]
read_attribute(method_name)
elsif association_id = send(reflection.foreign_key)
instance_variable_set(ivar_name, reflection.klass.find(association_id))
end
Expand All @@ -146,7 +146,7 @@ def defines_has_many_finder_method(reflection)
if instance_variable_defined?(ivar_name)
instance_variable_get(ivar_name)
elsif attributes.include?(method_name)
attributes[method_name]
read_attribute(method_name)
elsif !new_record?
instance_variable_set(ivar_name, reflection.klass.find(:all, params: { "#{self.class.element_name}_id": self.id }))
else
Expand All @@ -164,7 +164,7 @@ def defines_has_one_finder_method(reflection)
if instance_variable_defined?(ivar_name)
instance_variable_get(ivar_name)
elsif attributes.include?(method_name)
attributes[method_name]
read_attribute(method_name)
elsif reflection.klass.respond_to?(:singleton_name)
instance_variable_set(ivar_name, reflection.klass.find(params: { "#{self.class.element_name}_id": self.id }))
else
Expand Down
108 changes: 90 additions & 18 deletions lib/active_resource/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,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 @@ -430,16 +436,48 @@ 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
#
def schema(&block)
# 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(cast_values: self.cast_values, &block)
if block_given?
schema_definition = Schema.new
self.schema_definition = Class.new(schema_definition)
schema_definition.cast_values = cast_values
schema_definition.instance_eval(&block)

# skip out if we didn't define anything
Expand Down Expand Up @@ -479,6 +517,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 @@ -1308,6 +1347,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 @@ -1341,6 +1381,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 @@ -1380,12 +1421,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 @@ -1596,7 +1637,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.to_s,
case value
when Array
resource = nil
Expand All @@ -1614,6 +1655,7 @@ def load(attributes, remove_root = false, persisted = false)
else
value.duplicable? ? value.dup : value
end
)
end
self
end
Expand Down Expand Up @@ -1673,7 +1715,7 @@ def respond_to_missing?(method, include_priv = false)
method_name = method.to_s
if attributes.nil?
super
elsif known_attributes.include?(method_name)
elsif known_attributes.include?(method_name) || @schema.respond_to?(method)
true
elsif method_name =~ /(?:=|\?)$/ && known_attributes.include?($`)
true
Expand All @@ -1684,6 +1726,10 @@ def respond_to_missing?(method, include_priv = false)
end
end

def serializable_hash(options = nil)
@schema.serializable_hash(options).merge!(super)
end

def to_json(options = {})
super(include_root_in_json ? { root: self.class.element_name }.merge(options) : options)
end
Expand All @@ -1693,13 +1739,37 @@ def to_xml(options = {})
end

def read_attribute_for_serialization(n)
if !attributes[n].nil?
attributes[n]
value = read_attribute(n)

if !value.nil?
value
elsif respond_to?(n)
send(n)
end
end

def read_attribute(attr_name)
name = attr_name.to_s

name = self.class.primary_key if name == "id" && self.class.primary_key
if @schema.respond_to?(name)
@schema.send(name)
else
@attributes[name]
end
end

def write_attribute(attr_name, value)
name = attr_name.to_s

name = self.class.primary_key if name == "id" && self.class.primary_key
if @schema.respond_to?("#{name}=")
@schema.send("#{name}=", value)
else
attributes[name] = value
end
end

protected
def connection(refresh = false)
self.class.connection(refresh)
Expand Down Expand Up @@ -1839,15 +1909,17 @@ 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
write_attribute($`, arguments.first)
when "?"
attributes[$`]
read_attribute($`)
end
else
return attributes[method_name] if attributes.include?(method_name)
return read_attribute(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
Expand Down
90 changes: 59 additions & 31 deletions lib/active_resource/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,26 @@

module ActiveResource # :nodoc:
class Schema # :nodoc:
include ActiveModel::Model
include ActiveModel::Attributes
include ActiveModel::Serialization

# 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 = {}.freeze

class_attribute :cast_values, instance_accessor: false, instance_predicate: false # :nodoc:
self.cast_values = false

attribute_method_suffix "?", parameters: false

alias_method :attribute?, :send
private :attribute?

# The internals of an Active Resource Schema are very simple -
# unlike an Active Record TableDefinition (on which it is based).
Expand All @@ -22,39 +35,54 @@ 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)
class << self
def inherited(subclass)
super
subclass.attrs = attrs.dup
end

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.
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 = nil, 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

super(name, cast_values ? type.try(:to_sym) : nil, **options)
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
Loading