Skip to content
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

Remove active attr #48

Merged
merged 11 commits into from
Feb 20, 2017
Merged
Show file tree
Hide file tree
Changes from 10 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
4 changes: 2 additions & 2 deletions active_remote.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ Gem::Specification.new do |s|
##
# Dependencies
#
s.add_dependency "active_attr", ">= 0.8"
s.add_dependency "activesupport", ">= 3.2"
s.add_dependency "activemodel", ">= 4.0"
s.add_dependency "activesupport", ">= 4.0"
s.add_dependency "protobuf", ">= 3.0"

##
Expand Down
10 changes: 10 additions & 0 deletions bin/console
Original file line number Diff line number Diff line change
@@ -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
1 change: 0 additions & 1 deletion lib/active_remote.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
require 'active_attr'
require 'active_model'
require 'active_support'
require 'protobuf'
Expand Down
5 changes: 4 additions & 1 deletion lib/active_remote/attribute_defaults.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
100 changes: 100 additions & 0 deletions lib/active_remote/attribute_definition.rb
Original file line number Diff line number Diff line change
@@ -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
201 changes: 199 additions & 2 deletions lib/active_remote/attributes.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,33 @@
module ActiveRemote
module Attributes
extend ActiveSupport::Concern
include ActiveModel::AttributeMethods
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work? I can see specs are passing, but I'd expect this to have to be included in the base class.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a direct holdover from active_attr, and the ability it has to be used piecemeal.

https://github.com/cgriego/active_attr/blob/master/lib/active_attr/attributes.rb#L24

Certainly will be refactoring this in the next pull request, since we do not need the ability to use pieces.


# 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
Expand All @@ -8,6 +36,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)
Expand All @@ -16,7 +59,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
Expand All @@ -29,9 +72,163 @@ 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

# 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

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 ::ActiveRemote::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={})
::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")
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
#
def attribute_names
attributes.keys
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what I was thinking we could check instead of respond_to when we get to optimizations.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the 3.0 roadmap to make sure we addres any performance issues with the write_attribute method.

end

# Returns a Hash of AttributeDefinition instances
#
# @example Get attribute definitions
# Person.attributes
#
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? :timeout #=> "timeout"
#
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
#
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<ActiveAttr::AttributeDefinition>] 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
Loading