From a9ff28b01fa95d1a3ceb0b9cc7d058e8d636d820 Mon Sep 17 00:00:00 2001 From: Brian Stien Date: Fri, 17 Feb 2017 10:07:57 -0700 Subject: [PATCH 01/11] Bring in remaining parts of active_attr --- lib/active_remote/attribute_definition.rb | 100 ++++++++ lib/active_remote/attributes.rb | 226 ++++++++++++++++++ lib/active_remote/base.rb | 29 ++- lib/active_remote/block_initialization.rb | 37 +++ lib/active_remote/chainable_initialization.rb | 53 ++++ lib/active_remote/mass_assignment.rb | 92 +++++++ lib/active_remote/serialization.rb | 1 + 7 files changed, 529 insertions(+), 9 deletions(-) create mode 100644 lib/active_remote/attribute_definition.rb create mode 100644 lib/active_remote/block_initialization.rb create mode 100644 lib/active_remote/chainable_initialization.rb create mode 100644 lib/active_remote/mass_assignment.rb diff --git a/lib/active_remote/attribute_definition.rb b/lib/active_remote/attribute_definition.rb new file mode 100644 index 0000000..c797c01 --- /dev/null +++ b/lib/active_remote/attribute_definition.rb @@ -0,0 +1,100 @@ +module ActiveRemote + # Represents an attribute for reflection + # + # @example Usage + # AttributeDefinition.new(:amount) + # + # @since 0.2.0 + class AttributeDefinition + include Comparable + + # The attribute name + # @since 0.2.0 + attr_reader :name + + # Compare attribute definitions + # + # @example + # attribute_definition <=> other + # + # @param [ActiveAttr::AttributeDefinition, Object] other The other + # attribute definition to compare with. + # + # @return [-1, 0, 1, nil] + # + # @since 0.2.1 + def <=>(other) + return nil unless other.instance_of? self.class + return nil if name == other.name && options != other.options + self.name.to_s <=> other.name.to_s + end + + # Read an attribute option + # + # @example + # attribute_definition[:type] + # + # @param [Symbol] key The option key + # + # @since 0.5.0 + def [](key) + @options[key] + end + + # Creates a new AttributeDefinition + # + # @example Create an attribute defintion + # AttributeDefinition.new(:amount) + # + # @param [Symbol, String, #to_sym] name attribute name + # @param [Hash{Symbol => Object}] options attribute options + # + # @return [ActiveAttr::AttributeDefinition] + # + # @since 0.2.0 + def initialize(name, options={}) + raise TypeError, "can't convert #{name.class} into Symbol" unless name.respond_to? :to_sym + @name = name.to_sym + @options = options + end + + # Returns the code that would generate the attribute definition + # + # @example Inspect the attribute definition + # attribute.inspect + # + # @return [String] Human-readable presentation of the attribute + # definition + # + # @since 0.6.0 + def inspect + options_description = options.map { |key, value| "#{key.inspect} => #{value.inspect}" }.sort.join(", ") + inspected_options = ", #{options_description}" unless options_description.empty? + "attribute :#{name}#{inspected_options}" + end + + # The attribute name + # + # @return [String] the attribute name + # + # @since 0.2.0 + def to_s + name.to_s + end + + # The attribute name + # + # @return [Symbol] the attribute name + # + # @since 0.2.1 + def to_sym + name + end + + protected + + # The attribute options + # @since 0.5.0 + attr_reader :options + end +end diff --git a/lib/active_remote/attributes.rb b/lib/active_remote/attributes.rb index 57477d2..438e9e0 100644 --- a/lib/active_remote/attributes.rb +++ b/lib/active_remote/attributes.rb @@ -1,5 +1,34 @@ module ActiveRemote module Attributes + extend ActiveSupport::Concern + include ActiveModel::AttributeMethods + + # Methods deprecated on the Object class which can be safely overridden + # @since 0.3.0 + DEPRECATED_OBJECT_METHODS = %w(id type) + + included do + attribute_method_suffix "" if attribute_method_matchers.none? { |matcher| matcher.prefix == "" && matcher.suffix == "" } + attribute_method_suffix "=" + end + + + # Performs equality checking on the result of attributes and its type. + # + # @example Compare for equality. + # model == other + # + # @param [ActiveAttr::Attributes, Object] other The other model to compare + # + # @return [true, false] True if attributes are equal and other is instance + # of the same Class, false if not. + # + # @since 0.2.0 + def ==(other) + return false unless other.instance_of? self.class + attributes == other.attributes + end + def attributes @attributes ||= begin attribute_names = self.class.attribute_names @@ -8,6 +37,21 @@ def attributes @attributes.dup end + # Returns the class name plus its attributes + # + # @example Inspect the model. + # person.inspect + # + # @return [String] Human-readable presentation of the attribute + # definitions + # + # @since 0.2.0 + def inspect + attribute_descriptions = attributes.sort.map { |key, value| "#{key}: #{value.inspect}" }.join(", ") + separator = " " unless attribute_descriptions.empty? + "#<#{self.class.name}#{separator}#{attribute_descriptions}>" + end + # Read attribute from the attributes hash # def read_attribute(name) @@ -33,5 +77,187 @@ def write_attribute(name, value) end end alias_method :[]=, :write_attribute + + # Read an attribute from the attributes hash + # + # @since 0.2.1 + def attribute(name) + @attributes ||= {} + @attributes[name] + end + + # Write an attribute to the attributes hash + # + # @since 0.2.1 + def attribute=(name, value) + @attributes ||= {} + @attributes[name] = value + end + + # Maps all attributes using the given block + # + # @example Stringify attributes + # person.attributes_map { |name| send(name).to_s } + # + # @yield [name] block called to return hash value + # @yieldparam [String] name The name of the attribute to map. + # + # @return [Hash{String => Object}] The Hash of mapped attributes + # + # @since 0.7.0 + def attributes_map + Hash[ self.class.attribute_names.map { |name| [name, yield(name)] } ] + end + + module ClassMethods + # Defines an attribute + # + # For each attribute that is defined, a getter and setter will be + # added as an instance method to the model. An + # {AttributeDefinition} instance will be added to result of the + # attributes class method. + # + # @example Define an attribute. + # attribute :name + # + # @param (see AttributeDefinition#initialize) + # + # @raise [DangerousAttributeError] if the attribute name conflicts with + # existing methods + # + # @return [AttributeDefinition] Attribute's definition + # + # @since 0.2.0 + def attribute(name, options={}) + if dangerous_attribute_method_name = dangerous_attribute?(name) + raise DangerousAttributeError, %{an attribute method named "#{dangerous_attribute_method_name}" would conflict with an existing method} + else + attribute! name, options + end + end + + # Defines an attribute without checking for conflicts + # + # Allows you to define an attribute whose methods will conflict + # with an existing method. For example, Ruby's Timeout library + # adds a timeout method to Object. Attempting to define a timeout + # attribute using .attribute will raise a + # {DangerousAttributeError}, but .attribute! will not. + # + # @example Define a dangerous attribute. + # attribute! :timeout + # + # @param (see AttributeDefinition#initialize) + # + # @return [AttributeDefinition] Attribute's definition + # + # @since 0.6.0 + def attribute!(name, options={}) + AttributeDefinition.new(name, options).tap do |attribute_definition| + attribute_name = attribute_definition.name.to_s + # Force active model to generate attribute methods + remove_instance_variable("@attribute_methods_generated") if instance_variable_defined?("@attribute_methods_generated") + define_attribute_methods([attribute_definition.name]) unless attribute_names.include? attribute_name + attributes[attribute_name] = attribute_definition + end + end + + # Returns an Array of attribute names as Strings + # + # @example Get attribute names + # Person.attribute_names + # + # @return [Array] The attribute names + # + # @since 0.5.0 + def attribute_names + attributes.keys + end + + # Returns a Hash of AttributeDefinition instances + # + # @example Get attribute definitions + # Person.attributes + # + # @return [ActiveSupport::HashWithIndifferentAccess{String => ActiveAttr::AttributeDefinition}] + # The Hash of AttributeDefinition instances + # + # @since 0.2.0 + def attributes + @attributes ||= ActiveSupport::HashWithIndifferentAccess.new + end + + # Determine if a given attribute name is dangerous + # + # Some attribute names can cause conflicts with existing methods + # on an object. For example, an attribute named "timeout" would + # conflict with the timeout method that Ruby's Timeout library + # mixes into Object. + # + # @example Testing a harmless attribute + # Person.dangerous_attribute? :name #=> false + # + # @example Testing a dangerous attribute + # Person.dangerous_attribute? :nil #=> "nil?" + # + # @param name Attribute name + # + # @return [false, String] False or the conflicting method name + # + # @since 0.6.0 + def dangerous_attribute?(name) + attribute_methods(name).detect do |method_name| + !DEPRECATED_OBJECT_METHODS.include?(method_name.to_s) && allocate.respond_to?(method_name, true) + end unless attribute_names.include? name.to_s + end + + # Returns the class name plus its attribute names + # + # @example Inspect the model's definition. + # Person.inspect + # + # @return [String] Human-readable presentation of the attributes + # + # @since 0.2.0 + def inspect + inspected_attributes = attribute_names.sort + attributes_list = "(#{inspected_attributes.join(", ")})" unless inspected_attributes.empty? + "#{name}#{attributes_list}" + end + + protected + + # Assign a set of attribute definitions, used when subclassing models + # + # @param [Array] The Array of + # AttributeDefinition instances + # + # @since 0.2.2 + def attributes=(attributes) + @attributes = attributes + end + + # Overrides ActiveModel::AttributeMethods to backport 3.2 fix + def instance_method_already_implemented?(method_name) + generated_attribute_methods.method_defined?(method_name) + end + + private + + # Expand an attribute name into its generated methods names + # + # @since 0.6.0 + def attribute_methods(name) + attribute_method_matchers.map { |matcher| matcher.method_name name } + end + + # Ruby inherited hook to assign superclass attributes to subclasses + # + # @since 0.2.2 + def inherited(subclass) + super + subclass.attributes = attributes.dup + end + end end end diff --git a/lib/active_remote/base.rb b/lib/active_remote/base.rb index 82380a5..64f352f 100644 --- a/lib/active_remote/base.rb +++ b/lib/active_remote/base.rb @@ -3,12 +3,16 @@ require 'active_remote/association' require 'active_remote/attribute_defaults' +require 'active_remote/attribute_definition' require 'active_remote/attributes' require 'active_remote/bulk' +require 'active_remote/block_initialization' require 'active_remote/config' +require 'active_remote/chainable_initialization' require 'active_remote/dirty' require 'active_remote/dsl' require 'active_remote/integration' +require 'active_remote/mass_assignment' require 'active_remote/persistence' require 'active_remote/primary_key' require 'active_remote/publication' @@ -23,22 +27,29 @@ module ActiveRemote class Base extend ActiveModel::Callbacks - include ActiveAttr::BasicModel - include ActiveAttr::Attributes - include ActiveAttr::BlockInitialization - include ActiveAttr::ChainableInitialization - include ActiveAttr::Logger - include ActiveAttr::MassAssignment - include ActiveAttr::AttributeDefaults - include ActiveAttr::QueryAttributes - include ActiveAttr::Serialization + # include ActiveAttr::BasicModel + extend ::ActiveModel::Naming + include ActiveModel::Conversion + include ActiveModel::Validations + + # include ActiveAttr::Attributes + # include ActiveAttr::BlockInitialization + # include ActiveAttr::ChainableInitialization + # include ActiveAttr::Logger + # include ActiveAttr::MassAssignment + # include ActiveAttr::AttributeDefaults + # include ActiveAttr::QueryAttributes + # include ActiveAttr::Serialization include Association include AttributeDefaults include Attributes include Bulk + include BlockInitialization + include ChainableInitialization include DSL include Integration + include MassAssignment include Persistence include PrimaryKey include Publication diff --git a/lib/active_remote/block_initialization.rb b/lib/active_remote/block_initialization.rb new file mode 100644 index 0000000..ffe5ad3 --- /dev/null +++ b/lib/active_remote/block_initialization.rb @@ -0,0 +1,37 @@ +require "active_support/concern" +require "active_remote/chainable_initialization" + +module ActiveRemote + # BlockInitialization allows you to build an instance in a block + # + # Imported from ActiveAttr + # + # Including the BlockInitialization module into your class will yield the + # model instance to a block passed to when creating a new instance. + # + # @example Usage + # class Person + # include ActiveAttr::BlockInitialization + # end + # + module BlockInitialization + extend ::ActiveSupport::Concern + include ::ActiveRemote::ChainableInitialization + + # Initialize a model and build via a block + # + # @example + # person = Person.new do |p| + # p.first_name = "Chris" + # p.last_name = "Griego" + # end + # + # person.first_name #=> "Chris" + # person.last_name #=> "Griego" + # + def initialize(*) + super + yield self if block_given? + end + end +end diff --git a/lib/active_remote/chainable_initialization.rb b/lib/active_remote/chainable_initialization.rb new file mode 100644 index 0000000..9114448 --- /dev/null +++ b/lib/active_remote/chainable_initialization.rb @@ -0,0 +1,53 @@ +module ActiveRemote + # Allows classes and modules to safely invoke super in its initialize method + # + # Added from ActiveAttr + # + # Many ActiveAttr modules enhance the behavior of the \#initialize method, + # and in doing so, these methods need to accept arguments. However, Ruby's + # Object and BasicObject classes, in most versions of Ruby, do not allow any + # arguments to be passed in. This module halts the propagation of + # initialization arguments before invoking the Object class' + # initialization. + # + # In order to still allow a subclass mixing in this module (directly or + # through an ActiveSupport::Concern) to still pass its initialization + # arguments to its superclass, the module has to install itself into the + # ancestors of the base class, the class that inherits directly from Object + # or BasicObject. + # + # @since 0.2.2 + module ChainableInitialization + class << self + # A collection of Ruby base objects + # [Object] on Ruby 1.8 + # [Object, BasicObject] on Ruby 1.9 + # + # @private + BASE_OBJECTS = [].tap do |base_objects| + superclass = Class.new + base_objects << superclass while superclass = superclass.superclass + end + + # Only append the features of this module to the class that inherits + # directly from one of the BASE_OBJECTS + # + # @private + def append_features(base) + if base.respond_to? :superclass + base = base.superclass while !BASE_OBJECTS.include? base.superclass + end + + super + end + end + + # Continue to propagate any superclass calls, but stop passing arguments + # + # This prevents problems in versions of Ruby where Object#initialize does + # not take arguments + def initialize(*) + super() + end + end +end diff --git a/lib/active_remote/mass_assignment.rb b/lib/active_remote/mass_assignment.rb new file mode 100644 index 0000000..89501ed --- /dev/null +++ b/lib/active_remote/mass_assignment.rb @@ -0,0 +1,92 @@ +module ActiveRemote + # MassAssignment allows you to bulk set and update attributes + # + # Including MassAssignment into your model gives it a set of mass assignment + # methods, similar to those found in ActiveRecord. + # + # @example Usage + # class Person + # include ActiveAttr::MassAssignment + # end + # + # @since 0.1.0 + module MassAssignment + extend ::ActiveSupport::Concern + include ::ActiveRemote::ChainableInitialization + + # Mass update a model's attributes + # + # @example Assigning a hash + # person.assign_attributes(:first_name => "Chris", :last_name => "Griego") + # person.first_name #=> "Chris" + # person.last_name #=> "Griego" + # + # @param [Hash{#to_s => Object}, #each] attributes Attributes used to + # populate the model + # @param [Hash, #[]] options Options that affect mass assignment + # + # @option options [Symbol] :as (:default) Mass assignment role + # @option options [true, false] :without_protection (false) Bypass mass + # assignment security if true + # + # @since 0.1.0 + def assign_attributes(new_attributes, options={}) + sanitized_new_attributes = sanitize_for_mass_assignment_if_sanitizer new_attributes, options + + sanitized_new_attributes.each do |name, value| + writer = "#{name}=" + send writer, value if respond_to? writer + end if sanitized_new_attributes + end + + # Mass update a model's attributes + # + # @example Assigning a hash + # person.attributes = { :first_name => "Chris", :last_name => "Griego" } + # person.first_name #=> "Chris" + # person.last_name #=> "Griego" + # + # @param (see #assign_attributes) + # + # @since 0.1.0 + def attributes=(new_attributes) + assign_attributes new_attributes + end + + # Initialize a model with a set of attributes + # + # @example Initializing with a hash + # person = Person.new(:first_name => "Chris", :last_name => "Griego") + # person.first_name #=> "Chris" + # person.last_name #=> "Griego" + # + # @param (see #assign_attributes) + # + # @since 0.1.0 + def initialize(attributes=nil, options={}) + assign_attributes attributes, options + super + end + + private + + # @since 0.8.0 + def sanitize_for_mass_assignment_if_sanitizer(new_attributes, options={}) + if new_attributes && !options[:without_protection] && respond_to?(:sanitize_for_mass_assignment, true) + sanitize_for_mass_assignment_with_or_without_role new_attributes, options + else + new_attributes + end + end + + # Rails 3.0 and 4.0 do not take a role argument for the sanitizer + # @since 0.7.0 + def sanitize_for_mass_assignment_with_or_without_role(new_attributes, options) + if method(:sanitize_for_mass_assignment).arity.abs > 1 + sanitize_for_mass_assignment new_attributes, options[:as] || :default + else + sanitize_for_mass_assignment new_attributes + end + end + end +end diff --git a/lib/active_remote/serialization.rb b/lib/active_remote/serialization.rb index 702075e..236287d 100644 --- a/lib/active_remote/serialization.rb +++ b/lib/active_remote/serialization.rb @@ -5,6 +5,7 @@ module Serialization extend ActiveSupport::Concern included do + include ::ActiveModel::Serializers::JSON include Serializers::JSON end From 5754feadf5e79a47f9f3f4ba1c56b6d98cbf4286 Mon Sep 17 00:00:00 2001 From: Brian Stien Date: Fri, 17 Feb 2017 10:09:29 -0700 Subject: [PATCH 02/11] Remove active_attr --- active_remote.gemspec | 2 +- lib/active_remote.rb | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/active_remote.gemspec b/active_remote.gemspec index dc42709..e4c52fa 100644 --- a/active_remote.gemspec +++ b/active_remote.gemspec @@ -19,7 +19,7 @@ Gem::Specification.new do |s| ## # Dependencies # - s.add_dependency "active_attr", ">= 0.8" + s.add_dependency "activemodel", ">= 3.2" s.add_dependency "activesupport", ">= 3.2" s.add_dependency "protobuf", ">= 3.0" diff --git a/lib/active_remote.rb b/lib/active_remote.rb index c8ab364..b47767d 100644 --- a/lib/active_remote.rb +++ b/lib/active_remote.rb @@ -1,4 +1,3 @@ -require 'active_attr' require 'active_model' require 'active_support' require 'protobuf' From ca8749cdf60bbe8a2caf06f26f9df0c96e149e7c Mon Sep 17 00:00:00 2001 From: Brian Stien Date: Fri, 17 Feb 2017 10:10:33 -0700 Subject: [PATCH 03/11] Remove active_attr requires --- lib/active_remote/base.rb | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/lib/active_remote/base.rb b/lib/active_remote/base.rb index 64f352f..5e9be2c 100644 --- a/lib/active_remote/base.rb +++ b/lib/active_remote/base.rb @@ -1,5 +1,4 @@ require 'active_model/callbacks' -require 'active_attr/model' require 'active_remote/association' require 'active_remote/attribute_defaults' @@ -27,20 +26,10 @@ module ActiveRemote class Base extend ActiveModel::Callbacks - # include ActiveAttr::BasicModel extend ::ActiveModel::Naming include ActiveModel::Conversion include ActiveModel::Validations - # include ActiveAttr::Attributes - # include ActiveAttr::BlockInitialization - # include ActiveAttr::ChainableInitialization - # include ActiveAttr::Logger - # include ActiveAttr::MassAssignment - # include ActiveAttr::AttributeDefaults - # include ActiveAttr::QueryAttributes - # include ActiveAttr::Serialization - include Association include AttributeDefaults include Attributes From 30115bf038932d9992732e399ab737a64b031d4d Mon Sep 17 00:00:00 2001 From: Brian Stien Date: Fri, 17 Feb 2017 10:23:18 -0700 Subject: [PATCH 04/11] Make sure string ext is loaded --- lib/active_remote/typecasting.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/active_remote/typecasting.rb b/lib/active_remote/typecasting.rb index 3f595f2..e8536a5 100644 --- a/lib/active_remote/typecasting.rb +++ b/lib/active_remote/typecasting.rb @@ -1,3 +1,5 @@ +require "active_support/core_ext/string/conversions" + require "active_remote/typecasting/big_decimal_typecaster" require "active_remote/typecasting/boolean" require "active_remote/typecasting/boolean_typecaster" From 5fd3b8140f4f2b5839bed3183f62544ee8462149 Mon Sep 17 00:00:00 2001 From: Brian Stien Date: Fri, 17 Feb 2017 10:23:35 -0700 Subject: [PATCH 05/11] Replace UnknownAttributeError with our own. --- lib/active_remote/attributes.rb | 4 ++-- lib/active_remote/errors.rb | 3 +++ spec/lib/active_remote/association_spec.rb | 4 ++-- spec/lib/active_remote/typecasting_spec.rb | 1 + 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/active_remote/attributes.rb b/lib/active_remote/attributes.rb index 438e9e0..ab801d9 100644 --- a/lib/active_remote/attributes.rb +++ b/lib/active_remote/attributes.rb @@ -60,7 +60,7 @@ def read_attribute(name) if respond_to? name attribute(name) else - raise ActiveAttr::UnknownAttributeError, "unknown attribute: #{name}" + raise ::ActiveRemote::UnknownAttributeError, "unknown attribute: #{name}" end end alias_method :[], :read_attribute @@ -73,7 +73,7 @@ def write_attribute(name, value) if respond_to? "#{name}=" __send__("attribute=", name, value) else - raise ActiveAttr::UnknownAttributeError, "unknown attribute: #{name}" + raise ::ActiveRemote::UnknownAttributeError, "unknown attribute: #{name}" end end alias_method :[]=, :write_attribute diff --git a/lib/active_remote/errors.rb b/lib/active_remote/errors.rb index 96b5283..fd9a567 100644 --- a/lib/active_remote/errors.rb +++ b/lib/active_remote/errors.rb @@ -41,4 +41,7 @@ def initialize(class_or_message = "") # when remote record cannot be saved because it is invalid. class RemoteRecordNotSaved < ActiveRemoteError end + + class UnknownAttributeError < ActiveRemoteError + end end diff --git a/spec/lib/active_remote/association_spec.rb b/spec/lib/active_remote/association_spec.rb index 565938c..7ec763e 100644 --- a/spec/lib/active_remote/association_spec.rb +++ b/spec/lib/active_remote/association_spec.rb @@ -154,7 +154,7 @@ before { allow(subject).to receive(:respond_to?).with("user_guid").and_return(false) } it 'raises an error' do - expect {subject.user_posts}.to raise_error(::ActiveAttr::UnknownAttributeError) + expect {subject.user_posts}.to raise_error(::ActiveRemote::UnknownAttributeError) end end @@ -237,7 +237,7 @@ before { allow(subject).to receive(:respond_to?).with("user_guid").and_return(false) } it 'raises an error' do - expect {subject.chief_editor}.to raise_error(::ActiveAttr::UnknownAttributeError) + expect {subject.chief_editor}.to raise_error(::ActiveRemote::UnknownAttributeError) end end diff --git a/spec/lib/active_remote/typecasting_spec.rb b/spec/lib/active_remote/typecasting_spec.rb index d672a3d..89e7686 100644 --- a/spec/lib/active_remote/typecasting_spec.rb +++ b/spec/lib/active_remote/typecasting_spec.rb @@ -12,6 +12,7 @@ describe "datetime" do it "casts to datetime" do record = test_class.new(:birthday => "2016-01-01") + binding.pry expect(record.birthday).to eq(DateTime.parse("2016-01-01")) end end From b5e79018b960bb729f109c333c885d9bdb6d0846 Mon Sep 17 00:00:00 2001 From: Brian Stien Date: Fri, 17 Feb 2017 10:23:58 -0700 Subject: [PATCH 06/11] Remove pry --- spec/lib/active_remote/typecasting_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/lib/active_remote/typecasting_spec.rb b/spec/lib/active_remote/typecasting_spec.rb index 89e7686..d672a3d 100644 --- a/spec/lib/active_remote/typecasting_spec.rb +++ b/spec/lib/active_remote/typecasting_spec.rb @@ -12,7 +12,6 @@ describe "datetime" do it "casts to datetime" do record = test_class.new(:birthday => "2016-01-01") - binding.pry expect(record.birthday).to eq(DateTime.parse("2016-01-01")) end end From 90c4378d533e0ba98ff43a39f32faf5a687c909d Mon Sep 17 00:00:00 2001 From: Brian Stien Date: Fri, 17 Feb 2017 10:35:59 -0700 Subject: [PATCH 07/11] Add console script --- bin/console | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100755 bin/console diff --git a/bin/console b/bin/console new file mode 100755 index 0000000..c147218 --- /dev/null +++ b/bin/console @@ -0,0 +1,10 @@ +#!/usr/bin/env ruby + +require "bundler/setup" +require "active_remote" + +# You can add fixtures and/or initialization code here to make experimenting +# with your gem easier. You can also use a different console, if you like. + +require "pry" +Pry.start From 52bbf9d19b840bc1209749421c541d3d0ed0f58c Mon Sep 17 00:00:00 2001 From: Brian Stien Date: Fri, 17 Feb 2017 10:36:24 -0700 Subject: [PATCH 08/11] Remove attributes map method --- lib/active_remote/attribute_defaults.rb | 5 ++++- lib/active_remote/attributes.rb | 15 --------------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/lib/active_remote/attribute_defaults.rb b/lib/active_remote/attribute_defaults.rb index b5470ba..d05c9a1 100644 --- a/lib/active_remote/attribute_defaults.rb +++ b/lib/active_remote/attribute_defaults.rb @@ -73,7 +73,10 @@ def apply_defaults(defaults=attribute_defaults) # Person.new.attribute_defaults #=> {"first_name"=>"John"} # def attribute_defaults - attributes_map { |name| _attribute_default name } + self.class.attribute_names.inject({}) do |defaults, name| + defaults[name] = _attribute_default(name) + defaults + end end # Applies attribute default values diff --git a/lib/active_remote/attributes.rb b/lib/active_remote/attributes.rb index ab801d9..6114aea 100644 --- a/lib/active_remote/attributes.rb +++ b/lib/active_remote/attributes.rb @@ -94,21 +94,6 @@ def attribute=(name, value) @attributes[name] = value end - # Maps all attributes using the given block - # - # @example Stringify attributes - # person.attributes_map { |name| send(name).to_s } - # - # @yield [name] block called to return hash value - # @yieldparam [String] name The name of the attribute to map. - # - # @return [Hash{String => Object}] The Hash of mapped attributes - # - # @since 0.7.0 - def attributes_map - Hash[ self.class.attribute_names.map { |name| [name, yield(name)] } ] - end - module ClassMethods # Defines an attribute # From 5733d0fd8213a143a13170df19aae471c5d3c568 Mon Sep 17 00:00:00 2001 From: Brian Stien Date: Fri, 17 Feb 2017 11:53:28 -0700 Subject: [PATCH 09/11] Add specs for attributes module --- lib/active_remote/attributes.rb | 22 +-- lib/active_remote/errors.rb | 3 + spec/lib/active_remote/attributes_spec.rb | 177 ++++++++++++++++++++++ spec/support/models.rb | 1 + spec/support/models/no_attributes.rb | 2 + 5 files changed, 187 insertions(+), 18 deletions(-) create mode 100644 spec/lib/active_remote/attributes_spec.rb create mode 100644 spec/support/models/no_attributes.rb diff --git a/lib/active_remote/attributes.rb b/lib/active_remote/attributes.rb index 6114aea..9addd60 100644 --- a/lib/active_remote/attributes.rb +++ b/lib/active_remote/attributes.rb @@ -12,7 +12,6 @@ module Attributes attribute_method_suffix "=" end - # Performs equality checking on the result of attributes and its type. # # @example Compare for equality. @@ -115,7 +114,7 @@ module ClassMethods # @since 0.2.0 def attribute(name, options={}) if dangerous_attribute_method_name = dangerous_attribute?(name) - raise DangerousAttributeError, %{an attribute method named "#{dangerous_attribute_method_name}" would conflict with an existing method} + raise ::ActiveRemote::DangerousAttributeError, %{an attribute method named "#{dangerous_attribute_method_name}" would conflict with an existing method} else attribute! name, options end @@ -138,7 +137,7 @@ def attribute(name, options={}) # # @since 0.6.0 def attribute!(name, options={}) - AttributeDefinition.new(name, options).tap do |attribute_definition| + ::ActiveRemote::AttributeDefinition.new(name, options).tap do |attribute_definition| attribute_name = attribute_definition.name.to_s # Force active model to generate attribute methods remove_instance_variable("@attribute_methods_generated") if instance_variable_defined?("@attribute_methods_generated") @@ -152,9 +151,6 @@ def attribute!(name, options={}) # @example Get attribute names # Person.attribute_names # - # @return [Array] The attribute names - # - # @since 0.5.0 def attribute_names attributes.keys end @@ -164,12 +160,8 @@ def attribute_names # @example Get attribute definitions # Person.attributes # - # @return [ActiveSupport::HashWithIndifferentAccess{String => ActiveAttr::AttributeDefinition}] - # The Hash of AttributeDefinition instances - # - # @since 0.2.0 def attributes - @attributes ||= ActiveSupport::HashWithIndifferentAccess.new + @attributes ||= ::ActiveSupport::HashWithIndifferentAccess.new end # Determine if a given attribute name is dangerous @@ -183,13 +175,8 @@ def attributes # Person.dangerous_attribute? :name #=> false # # @example Testing a dangerous attribute - # Person.dangerous_attribute? :nil #=> "nil?" + # Person.dangerous_attribute? :timeout #=> "timeout" # - # @param name Attribute name - # - # @return [false, String] False or the conflicting method name - # - # @since 0.6.0 def dangerous_attribute?(name) attribute_methods(name).detect do |method_name| !DEPRECATED_OBJECT_METHODS.include?(method_name.to_s) && allocate.respond_to?(method_name, true) @@ -203,7 +190,6 @@ def dangerous_attribute?(name) # # @return [String] Human-readable presentation of the attributes # - # @since 0.2.0 def inspect inspected_attributes = attribute_names.sort attributes_list = "(#{inspected_attributes.join(", ")})" unless inspected_attributes.empty? diff --git a/lib/active_remote/errors.rb b/lib/active_remote/errors.rb index fd9a567..ddf4207 100644 --- a/lib/active_remote/errors.rb +++ b/lib/active_remote/errors.rb @@ -5,6 +5,9 @@ module ActiveRemote class ActiveRemoteError < StandardError end + class DangerousAttributeError < ActiveRemoteError + end + # Raised by ActiveRemove::Base.save when the remote record is readonly. class ReadOnlyRemoteRecord < ActiveRemoteError end diff --git a/spec/lib/active_remote/attributes_spec.rb b/spec/lib/active_remote/attributes_spec.rb new file mode 100644 index 0000000..7c6676f --- /dev/null +++ b/spec/lib/active_remote/attributes_spec.rb @@ -0,0 +1,177 @@ +require "spec_helper" + +describe ::ActiveRemote::Attributes do + let(:model_class) do + ::Class.new do + include ::ActiveRemote::Attributes + + attribute :name + attribute :address + + def self.name + "TestClass" + end + end + end + subject { ::Author.new } + + describe ".attribute" do + context "a dangerous attribute" do + it "raises an error" do + expect { model_class.attribute(:timeout) }.to raise_error(::ActiveRemote::DangerousAttributeError) + end + end + + context "a harmless attribute" do + it "creates an attribute with no options" do + expect(model_class.attributes.values).to include(::ActiveRemote::AttributeDefinition.new(:name)) + end + + it "returns the attribute definition" do + expect(model_class.attribute(:name)).to eq(::ActiveRemote::AttributeDefinition.new(:name)) + end + + it "defines an attribute reader that calls #attribute" do + expect(subject).to receive(:attribute).with("name") + subject.name + end + + it "defines an attribute writer that calls #attribute=" do + expect(subject).to receive(:attribute=).with("name", "test") + subject.name = "test" + end + end + end + + describe ".attribute!" do + it "can create an attribute with no options" do + model_class.attribute!(:first_name) + expect(model_class.attributes.values).to include(::ActiveRemote::AttributeDefinition.new(:first_name)) + end + + it "returns the attribute definition" do + expect(model_class.attribute!(:address)).to eq(::ActiveRemote::AttributeDefinition.new(:address)) + end + end + + describe ".attributes" do + it "can access AttributeDefinition with a Symbol" do + expect(::Author.attributes[:name]).to eq(::ActiveRemote::AttributeDefinition.new(:name)) + end + + it "can access AttributeDefinition with a String" do + expect(::Author.attributes["name"]).to eq(::ActiveRemote::AttributeDefinition.new(:name)) + end + end + + describe ".inspect" do + it "renders the class name" do + expect(model_class.inspect).to match(/^TestClass\(.*\)$/) + end + + it "renders the attribute names in alphabetical order" do + expect(model_class.inspect).to match("(address, name)") + end + end + + describe "#==" do + it "returns true when all attributes are equal" do + expect(::Author.new(:guid => "test")).to eq(::Author.new(:guid => "test")) + end + + it "returns false when compared to another type" do + expect(::Category.new(:guid => "test")).to_not eq(::Author.new(:name => "test")) + end + end + + describe "#attributes" do + context "when no attributes are defined" do + it "returns an empty Hash" do + expect(::NoAttributes.new.attributes).to eq({}) + end + end + + context "when an attribute is defined" do + it "returns the key value pairs" do + subject.name = "test" + expect(subject.attributes).to include("name" => "test") + end + + it "returns a new Hash " do + subject.attributes.merge!("foobar" => "foobar") + expect(subject.attributes).to_not include("foobar" => "foobar") + end + + it "returns all attributes" do + expect(subject.attributes.keys).to eq(["guid", "name", "user_guid", "chief_editor_guid", "editor_guid", "category_guid"]) + end + end + end + + describe "#inspect" do + before { subject.name = "test" } + + it "includes the class name and all attribute values in alphabetical order by attribute name" do + expect(subject.inspect).to eq(%{#}) + end + + it "doesn't format the inspection string for attributes if the model does not have any" do + expect(::NoAttributes.new.inspect).to eq(%{#}) + end + end + + [:[], :read_attribute].each do |method| + describe "##{method}" do + context "when an attribute is not set" do + it "returns nil" do + expect(subject.send(method, :name)).to be_nil + end + end + + context "when an attribute is set" do + before { subject.write_attribute(:name, "test") } + + it "returns the attribute using a Symbol" do + expect(subject.send(method, :name)).to eq("test") + end + + it "returns the attribute using a String" do + expect(subject.send(method, "name")).to eq("test") + end + end + + it "raises when getting an undefined attribute" do + expect { subject.send(method, :foobar) }.to raise_error(::ActiveRemote::UnknownAttributeError) + end + end + end + + [:[]=, :write_attribute].each do |method| + describe "##{method}" do + it "raises ArgumentError with one argument" do + expect { subject.send(method, :name) }.to raise_error(::ArgumentError) + end + + it "raises ArgumentError with no arguments" do + expect { subject.send(method) }.to raise_error(::ArgumentError) + end + + it "sets an attribute using a Symbol and value" do + expect { subject.send(method, :name, "test") }.to change { subject.attributes["name"] }.from(nil).to("test") + end + + it "sets an attribute using a String and value" do + expect { subject.send(method, "name", "test") }.to change { subject.attributes["name"] }.from(nil).to("test") + end + + it "is able to set an attribute to nil" do + subject.name = "test" + expect { subject.send(method, :name, nil) }.to change { subject.attributes["name"] }.from("test").to(nil) + end + + it "raises when setting an undefined attribute" do + expect { subject.send(method, :foobar, "test") }.to raise_error(::ActiveRemote::UnknownAttributeError) + end + end + end +end diff --git a/spec/support/models.rb b/spec/support/models.rb index d738d4c..066f7a3 100644 --- a/spec/support/models.rb +++ b/spec/support/models.rb @@ -2,6 +2,7 @@ require 'support/models/author' require 'support/models/default_author' require 'support/models/category' +require 'support/models/no_attributes' require 'support/models/post' require 'support/models/tag' require 'support/models/typecasted_author' diff --git a/spec/support/models/no_attributes.rb b/spec/support/models/no_attributes.rb new file mode 100644 index 0000000..7578797 --- /dev/null +++ b/spec/support/models/no_attributes.rb @@ -0,0 +1,2 @@ +class NoAttributes < ::ActiveRemote::Base +end From 31115939cb1d565927b9445a7a4d455ecdd186e5 Mon Sep 17 00:00:00 2001 From: Brian Stien Date: Mon, 20 Feb 2017 10:19:27 -0700 Subject: [PATCH 10/11] Drop support for rails 3 --- active_remote.gemspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/active_remote.gemspec b/active_remote.gemspec index e4c52fa..82db960 100644 --- a/active_remote.gemspec +++ b/active_remote.gemspec @@ -19,8 +19,8 @@ Gem::Specification.new do |s| ## # Dependencies # - s.add_dependency "activemodel", ">= 3.2" - s.add_dependency "activesupport", ">= 3.2" + s.add_dependency "activemodel", ">= 4.0" + s.add_dependency "activesupport", ">= 4.0" s.add_dependency "protobuf", ">= 3.0" ## From f275e8b832877bef66fcdbf161cba627f3b968e6 Mon Sep 17 00:00:00 2001 From: Brian Stien Date: Mon, 20 Feb 2017 11:24:05 -0700 Subject: [PATCH 11/11] Make constants explicit --- lib/active_remote/base.rb | 48 +++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/lib/active_remote/base.rb b/lib/active_remote/base.rb index 5e9be2c..df8867a 100644 --- a/lib/active_remote/base.rb +++ b/lib/active_remote/base.rb @@ -24,37 +24,37 @@ module ActiveRemote class Base - extend ActiveModel::Callbacks - + extend ::ActiveModel::Callbacks extend ::ActiveModel::Naming - include ActiveModel::Conversion - include ActiveModel::Validations - include Association - include AttributeDefaults - include Attributes - include Bulk - include BlockInitialization - include ChainableInitialization - include DSL - include Integration - include MassAssignment - include Persistence - include PrimaryKey - include Publication - include RPC - include ScopeKeys - include Search - include Serialization - include Typecasting + include ::ActiveModel::Conversion + include ::ActiveModel::Validations + + include ::ActiveRemote::Association + include ::ActiveRemote::AttributeDefaults + include ::ActiveRemote::Attributes + include ::ActiveRemote::Bulk + include ::ActiveRemote::BlockInitialization + include ::ActiveRemote::ChainableInitialization + include ::ActiveRemote::DSL + include ::ActiveRemote::Integration + include ::ActiveRemote::MassAssignment + include ::ActiveRemote::Persistence + include ::ActiveRemote::PrimaryKey + include ::ActiveRemote::Publication + include ::ActiveRemote::RPC + include ::ActiveRemote::ScopeKeys + include ::ActiveRemote::Search + include ::ActiveRemote::Serialization + include ::ActiveRemote::Typecasting # Overrides some methods, providing support for dirty tracking, # so it needs to be included last. - include Dirty + include ::ActiveRemote::Dirty # Overrides persistence methods, so it must included after - include Validations - include ActiveModel::Validations::Callbacks + include ::ActiveRemote::Validations + include ::ActiveModel::Validations::Callbacks attr_reader :last_request, :last_response