active_attr 0.4.1 → 0.5.0.alpha1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of active_attr might be problematic. Click here for more details.

Files changed (53) hide show
  1. data/.travis.yml +7 -0
  2. data/CHANGELOG.md +14 -0
  3. data/README.md +57 -25
  4. data/Rakefile +2 -0
  5. data/active_attr.gemspec +3 -2
  6. data/gemfiles/rails_3_1.gemfile +6 -0
  7. data/lib/active_attr.rb +3 -2
  8. data/lib/active_attr/attribute_defaults.rb +120 -0
  9. data/lib/active_attr/attribute_definition.rb +25 -2
  10. data/lib/active_attr/attributes.rb +44 -33
  11. data/lib/active_attr/logger.rb +8 -8
  12. data/lib/active_attr/mass_assignment.rb +2 -1
  13. data/lib/active_attr/matchers/have_attribute_matcher.rb +28 -10
  14. data/lib/active_attr/model.rb +2 -0
  15. data/lib/active_attr/query_attributes.rb +2 -5
  16. data/lib/active_attr/typecasted_attributes.rb +77 -0
  17. data/lib/active_attr/typecasting.rb +60 -0
  18. data/lib/active_attr/typecasting/boolean.rb +6 -0
  19. data/lib/active_attr/typecasting/boolean_typecaster.rb +21 -0
  20. data/lib/active_attr/typecasting/date_time_typecaster.rb +14 -0
  21. data/lib/active_attr/typecasting/date_typecaster.rb +14 -0
  22. data/lib/active_attr/typecasting/float_typecaster.rb +11 -0
  23. data/lib/active_attr/typecasting/integer_typecaster.rb +12 -0
  24. data/lib/active_attr/typecasting/object_typecaster.rb +9 -0
  25. data/lib/active_attr/typecasting/string_typecaster.rb +11 -0
  26. data/lib/active_attr/version.rb +1 -1
  27. data/spec/functional/active_attr/attribute_defaults_spec.rb +136 -0
  28. data/spec/functional/active_attr/attributes_spec.rb +19 -5
  29. data/spec/functional/active_attr/model_spec.rb +34 -4
  30. data/spec/functional/active_attr/typecasted_attributes_spec.rb +116 -0
  31. data/spec/support/mass_assignment_shared_examples.rb +1 -1
  32. data/spec/unit/active_attr/attribute_defaults_spec.rb +43 -0
  33. data/spec/unit/active_attr/attribute_definition_spec.rb +12 -8
  34. data/spec/unit/active_attr/attributes_spec.rb +22 -10
  35. data/spec/unit/active_attr/mass_assignment_spec.rb +1 -0
  36. data/spec/unit/active_attr/matchers/have_attribute_matcher_spec.rb +94 -15
  37. data/spec/unit/active_attr/query_attributes_spec.rb +1 -1
  38. data/spec/unit/active_attr/typecasted_attributes_spec.rb +76 -0
  39. data/spec/unit/active_attr/typecasting/boolean_spec.rb +10 -0
  40. data/spec/unit/active_attr/typecasting/boolean_typecaster_spec.rb +147 -0
  41. data/spec/unit/active_attr/typecasting/date_time_typecaster_spec.rb +67 -0
  42. data/spec/unit/active_attr/typecasting/date_typecaster_spec.rb +55 -0
  43. data/spec/unit/active_attr/typecasting/float_typecaster_spec.rb +27 -0
  44. data/spec/unit/active_attr/typecasting/integer_typecaster_spec.rb +35 -0
  45. data/spec/unit/active_attr/typecasting/object_typecaster_spec.rb +15 -0
  46. data/spec/unit/active_attr/typecasting/string_typecaster_spec.rb +24 -0
  47. data/spec/unit/active_attr/typecasting_spec.rb +87 -0
  48. data/spec/unit/active_attr/version_spec.rb +11 -2
  49. metadata +75 -29
  50. data/lib/active_attr/strict_mass_assignment.rb +0 -44
  51. data/lib/active_attr/unknown_attributes_error.rb +0 -18
  52. data/spec/unit/active_attr/strict_mass_assignment_spec.rb +0 -38
  53. data/spec/unit/active_attr/unknown_attributes_error_spec.rb +0 -9
@@ -19,14 +19,7 @@ module ActiveAttr
19
19
  #
20
20
  # @since 0.3.0
21
21
  def self.logger
22
- @logger
23
- end
24
-
25
- # Determin if a global default logger is configured
26
- #
27
- # @since 0.3.0
28
- def self.logger?
29
- !!logger
22
+ @logger ||= nil
30
23
  end
31
24
 
32
25
  # Configure the global default logger
@@ -38,6 +31,13 @@ module ActiveAttr
38
31
  @logger = new_logger
39
32
  end
40
33
 
34
+ # Determine if a global default logger is configured
35
+ #
36
+ # @since 0.3.0
37
+ def self.logger?
38
+ !!logger
39
+ end
40
+
41
41
  included do
42
42
  class_attribute :logger
43
43
  self.logger = Logger.logger
@@ -24,7 +24,8 @@ module ActiveAttr
24
24
  # person.first_name #=> "Chris"
25
25
  # person.last_name #=> "Griego"
26
26
  #
27
- # @param [Hash, #each] attributes Attributes used to populate the model
27
+ # @param [Hash{#to_s => Object}, #each] attributes Attributes used to
28
+ # populate the model
28
29
  #
29
30
  # @since 0.1.0
30
31
  def assign_attributes(new_attributes, options={})
@@ -1,10 +1,11 @@
1
1
  module ActiveAttr
2
2
  module Matchers
3
- # Specify that a model should have an attribute matching the criteria
3
+ # Specify that a model should have an attribute matching the criteria. See
4
+ # {HaveAttributeMatcher}
4
5
  #
5
6
  # @example Person should have a name attribute
6
7
  # describe Person do
7
- # it { should have_attribute(:name) }
8
+ # it { should have_attribute(:first_name) }
8
9
  # end
9
10
  #
10
11
  # @param [Symbol, String, #to_sym] attribute_name
@@ -17,18 +18,35 @@ module ActiveAttr
17
18
  end
18
19
 
19
20
  # Verifies that an ActiveAttr-based model has an attribute matching the
20
- # given criteria
21
+ # given criteria. See {Matchers#have_attribute}
21
22
  #
22
23
  # @since 0.2.0
23
24
  class HaveAttributeMatcher
24
- # @return [Symbol]
25
- # @private
26
- attr_reader :attribute_name
25
+ attr_reader :attribute_name, :default_value
26
+ private :attribute_name, :default_value
27
+
28
+ # Specify that the attribute should have the given default value
29
+ #
30
+ # @example Person's first name should default to John
31
+ # describe Person do
32
+ # it do
33
+ # should have_attribute(:first_name).with_default_value_of("John")
34
+ # end
35
+ # end
36
+ #
37
+ # @param [Object]
38
+ #
39
+ # @since 0.5.0
40
+ def with_default_value_of(default_value)
41
+ @default_value_set = true
42
+ @default_value = default_value
43
+ self
44
+ end
27
45
 
28
46
  # @return [String] Description
29
47
  # @private
30
48
  def description
31
- "have attribute named #{attribute_name}"
49
+ "have attribute named #{attribute_name}#{ " with a default value of #{default_value.inspect}" if @default_value_set}"
32
50
  end
33
51
 
34
52
  # @return [String] Failure message
@@ -42,15 +60,15 @@ module ActiveAttr
42
60
  def initialize(attribute_name)
43
61
  raise TypeError, "can't convert #{attribute_name.class} into Symbol" unless attribute_name.respond_to? :to_sym
44
62
  @attribute_name = attribute_name.to_sym
63
+ @default_value_set = false
45
64
  end
46
65
 
47
66
  # @private
48
67
  def matches?(model_or_model_class)
49
68
  @model_class = class_from(model_or_model_class)
69
+ @attribute_definition = @model_class.attributes[attribute_name]
50
70
 
51
- @model_class.attributes.any? do |attribute|
52
- attribute.name == attribute_name
53
- end
71
+ !!(@attribute_definition && (!@default_value_set || @attribute_definition[:default] == default_value))
54
72
  end
55
73
 
56
74
  # @return [String] Negative failure message
@@ -1,3 +1,4 @@
1
+ require "active_attr/attribute_defaults"
1
2
  require "active_attr/basic_model"
2
3
  require "active_attr/block_initialization"
3
4
  require "active_attr/logger"
@@ -24,6 +25,7 @@ module ActiveAttr
24
25
  include BlockInitialization
25
26
  include Logger
26
27
  include MassAssignmentSecurity
28
+ include AttributeDefaults
27
29
  include QueryAttributes
28
30
 
29
31
  if defined? ActiveModel::Serializable
@@ -1,4 +1,5 @@
1
1
  require "active_attr/attributes"
2
+ require "active_attr/typecasting/boolean_typecaster"
2
3
  require "active_attr/unknown_attribute_error"
3
4
  require "active_support/concern"
4
5
  require "active_support/core_ext/object/blank"
@@ -52,11 +53,7 @@ module ActiveAttr
52
53
  private
53
54
 
54
55
  def attribute?(name)
55
- case value = read_attribute(name)
56
- when "false", "FALSE", "f", "F" then false
57
- when Numeric, /^\-?[0-9]/ then !value.to_f.zero?
58
- else value.present?
59
- end
56
+ Typecasting::BooleanTypecaster.new.call(read_attribute(name))
60
57
  end
61
58
  end
62
59
  end
@@ -0,0 +1,77 @@
1
+ require "active_attr/attributes"
2
+ require "active_attr/typecasting"
3
+ require "active_support/concern"
4
+
5
+ module ActiveAttr
6
+ # TypecastedAttributes enhances attribute handling with typecasting
7
+ #
8
+ # @example Usage
9
+ # class Person
10
+ # include ActiveAttr::TypecastedAttributes
11
+ # attribute :name
12
+ # attribute :age, :type => Integer
13
+ # end
14
+ #
15
+ # person = Person.new
16
+ # person.name = "Ben Poweski"
17
+ # person.age = "29"
18
+ #
19
+ # person.age #=> 29
20
+ #
21
+ # @since 0.5.0
22
+ module TypecastedAttributes
23
+ extend ActiveSupport::Concern
24
+ include Attributes
25
+ include Typecasting
26
+
27
+ included do
28
+ attribute_method_suffix "_before_type_cast"
29
+ end
30
+
31
+ # TODO Documentation
32
+ def attribute_before_type_cast(name)
33
+ @attributes[name.to_s]
34
+ end
35
+
36
+ private
37
+
38
+ # Reads the attribute and typecasts the result
39
+ #
40
+ # @since 0.5.0
41
+ def attribute(name)
42
+ typecast_attribute(_attribute_type(name), super)
43
+ end
44
+
45
+ # Calculates an attribute type
46
+ #
47
+ # @private
48
+ # @since 0.5.0
49
+ def _attribute_type(attribute_name)
50
+ self.class._attribute_type(attribute_name)
51
+ end
52
+
53
+ module ClassMethods
54
+ # Returns the class name plus its attribute names and types
55
+ #
56
+ # @example Inspect the model's definition.
57
+ # Person.inspect
58
+ #
59
+ # @return [String] Human-readable presentation of the attributes
60
+ #
61
+ # @since 0.5.0
62
+ def inspect
63
+ inspected_attributes = attribute_names.sort.map { |name| "#{name}: #{_attribute_type(name)}" }
64
+ attributes_list = "(#{inspected_attributes.join(", ")})" unless inspected_attributes.empty?
65
+ "#{self.name}#{attributes_list}"
66
+ end
67
+
68
+ # Calculates an attribute type
69
+ #
70
+ # @private
71
+ # @since 0.5.0
72
+ def _attribute_type(attribute_name)
73
+ attributes[attribute_name][:type] || Object
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,60 @@
1
+ require "active_attr/typecasting/boolean"
2
+ require "active_attr/typecasting/boolean_typecaster"
3
+ require "active_attr/typecasting/date_time_typecaster"
4
+ require "active_attr/typecasting/date_typecaster"
5
+ require "active_attr/typecasting/float_typecaster"
6
+ require "active_attr/typecasting/integer_typecaster"
7
+ require "active_attr/typecasting/object_typecaster"
8
+ require "active_attr/typecasting/string_typecaster"
9
+ require "active_support/concern"
10
+
11
+ module ActiveAttr
12
+ # Typecasting provides methods to typecast a value to a different type
13
+ #
14
+ # @since 0.5.0
15
+ module Typecasting
16
+ extend ActiveSupport::Concern
17
+
18
+ # @private
19
+ TYPECASTERS = {
20
+ Boolean => BooleanTypecaster,
21
+ Date => DateTypecaster,
22
+ DateTime => DateTimeTypecaster,
23
+ Float => FloatTypecaster,
24
+ Integer => IntegerTypecaster,
25
+ Object => ObjectTypecaster,
26
+ String => StringTypecaster,
27
+ }
28
+
29
+ # Typecasts a value using a Class
30
+ #
31
+ # @param [Class] type The type to cast to
32
+ # @param [Object] value The value to be typecasted
33
+ #
34
+ # @return [Object, nil] The typecasted value or nil if it cannot be
35
+ # typecasted
36
+ #
37
+ # @since 0.5.0
38
+ def typecast_attribute(type, value)
39
+ raise ArgumentError, "a Class must be given" unless type
40
+ return value if value.nil?
41
+ typecast_value(type, value)
42
+ end
43
+
44
+ # Typecasts a value according to a predefined set of mapping rules defined
45
+ # in TYPECASTING_METHODS
46
+ #
47
+ # @param [Class] type The type to cast to
48
+ # @param [Object] value The value to be typecasted
49
+ #
50
+ # @return [Object, nil] The result of a method call defined in
51
+ # TYPECASTING_METHODS, nil if no method is found
52
+ #
53
+ # @since 0.5.0
54
+ def typecast_value(type, value)
55
+ if typecaster = TYPECASTERS[type]
56
+ typecaster.new.call(value)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,6 @@
1
+ module ActiveAttr
2
+ module Typecasting
3
+ class Boolean
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,21 @@
1
+ require "active_support/core_ext/object/blank"
2
+
3
+ module ActiveAttr
4
+ module Typecasting
5
+ # TODO documentation
6
+ class BooleanTypecaster
7
+ # TODO documentation
8
+ # http://yaml.org/type/bool.html
9
+ FALSE_VALUES = ["n", "N", "no", "No", "NO", "false", "False", "FALSE", "off", "Off", "OFF", "f", "F"]
10
+
11
+ # TODO documentation
12
+ def call(value)
13
+ case value
14
+ when *FALSE_VALUES then false
15
+ when Numeric, /^\-?[0-9]/ then !value.to_f.zero?
16
+ else value.present?
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,14 @@
1
+ require "active_support/core_ext/string/conversions"
2
+ require "active_support/time"
3
+
4
+ module ActiveAttr
5
+ module Typecasting
6
+ # TODO documentation
7
+ class DateTimeTypecaster
8
+ # TODO documentation
9
+ def call(value)
10
+ value.to_datetime if value.respond_to? :to_datetime
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ require "active_support/core_ext/string/conversions"
2
+ require "active_support/time"
3
+
4
+ module ActiveAttr
5
+ module Typecasting
6
+ # TODO documentation
7
+ class DateTypecaster
8
+ # TODO documentation
9
+ def call(value)
10
+ value.to_date if value.respond_to? :to_date
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,11 @@
1
+ module ActiveAttr
2
+ module Typecasting
3
+ # TODO documentation
4
+ class FloatTypecaster
5
+ # TODO documentation
6
+ def call(value)
7
+ value.to_f if value.respond_to? :to_f
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ module ActiveAttr
2
+ module Typecasting
3
+ # TODO documentation
4
+ class IntegerTypecaster
5
+ # TODO documentation
6
+ def call(value)
7
+ value.to_i if value.respond_to? :to_i
8
+ rescue FloatDomainError
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,9 @@
1
+ module ActiveAttr
2
+ module Typecasting
3
+ class ObjectTypecaster
4
+ def call(value)
5
+ value
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ module ActiveAttr
2
+ module Typecasting
3
+ # TODO documentation
4
+ class StringTypecaster
5
+ # TODO documentation
6
+ def call(value)
7
+ value.to_s if value.respond_to? :to_s
8
+ end
9
+ end
10
+ end
11
+ end
@@ -1,5 +1,5 @@
1
1
  module ActiveAttr
2
2
  # Complete version string
3
3
  # @since 0.1.0
4
- VERSION = "0.4.1"
4
+ VERSION = "0.5.0.alpha1"
5
5
  end
@@ -0,0 +1,136 @@
1
+ require "spec_helper"
2
+ require "active_attr/attribute_defaults"
3
+ require "active_attr/mass_assignment"
4
+ require "active_attr/typecasted_attributes"
5
+ require "active_model"
6
+
7
+ module ActiveAttr
8
+ describe AttributeDefaults do
9
+ subject { model_class.new }
10
+
11
+ let :model_class do
12
+ Class.new do
13
+ include ActiveAttr::AttributeDefaults
14
+ end
15
+ end
16
+
17
+ context "an attribute with a default string" do
18
+ before { model_class.attribute :first_name, :default => "John" }
19
+
20
+ it "the attribute getter returns the string by default" do
21
+ subject.first_name.should == "John"
22
+ end
23
+
24
+ it "#attributes includes the default attributes" do
25
+ subject.attributes["first_name"].should == "John"
26
+ end
27
+
28
+ it "#read_attribute returns the string by default" do
29
+ subject.read_attribute("first_name").should == "John"
30
+ end
31
+
32
+ it "assigning nil sets nil as the attribute value" do
33
+ subject.first_name = nil
34
+ subject.first_name.should be_nil
35
+ end
36
+
37
+ it "mutating the default value does not mutate the attribute definition" do
38
+ model_class.new.first_name.upcase!
39
+ model_class.new.first_name.should == "John"
40
+ end
41
+ end
42
+
43
+ context "an attribute with a default of false" do
44
+ before { model_class.attribute :admin, :default => false }
45
+
46
+ it "the attribute getter returns false by default" do
47
+ subject.admin.should == false
48
+ end
49
+ end
50
+
51
+ context "an attribute with a default of true" do
52
+ before { model_class.attribute :remember_me, :default => true }
53
+
54
+ it "the attribute getter returns true by default" do
55
+ subject.remember_me.should == true
56
+ end
57
+ end
58
+
59
+ context "an attribute with a dynamic Time.now default" do
60
+ before { model_class.attribute :created_at, :default => lambda { Time.now } }
61
+
62
+ it "the attribute getter returns a Time instance" do
63
+ subject.created_at.should be_a_kind_of Time
64
+ end
65
+
66
+ it "the attribute default is only evaulated once per instance" do
67
+ subject.created_at.should == subject.created_at
68
+ end
69
+
70
+ it "the attribute default is different per instance" do
71
+ model_class.new.created_at.should_not == model_class.new.created_at
72
+ end
73
+ end
74
+
75
+ context "an attribute with a default based on the instance" do
76
+ before { model_class.attribute :id, :default => lambda { object_id } }
77
+
78
+ it "the attribute getter returns the default based on the instance" do
79
+ subject.id.should == subject.object_id
80
+ end
81
+ end
82
+
83
+ context "combined with MassAssignment" do
84
+ let :model_class do
85
+ Class.new do
86
+ include ActiveAttr::MassAssignment
87
+ include ActiveAttr::AttributeDefaults
88
+
89
+ attribute :start_date
90
+ attribute :end_date, :default => lambda { start_date }
91
+ attribute :age_limit, :default => 21
92
+ end
93
+ end
94
+
95
+ it "applies the default attributes" do
96
+ model_class.new.age_limit.should == 21
97
+ end
98
+
99
+ it "mass assignment at initialization overrides the defaults" do
100
+ model_class.new(:age_limit => 18).age_limit.should == 18
101
+ end
102
+
103
+ it "mass assignment at initialization can override the default with a nil value" do
104
+ model_class.new(:age_limit => nil).age_limit.should be_nil
105
+ end
106
+
107
+ it "dynamic defaults have access to other attribute methods" do
108
+ model_class.new.end_date.should be_nil
109
+ end
110
+
111
+ it "dynamic defaults have access to attributes mass assigned at initialization" do
112
+ model_class.new(:start_date => Date.today).end_date.should == Date.today
113
+ end
114
+ end
115
+
116
+ context "combined with TypecastedAttributes" do
117
+ let :model_class do
118
+ Class.new do
119
+ include ActiveAttr::TypecastedAttributes
120
+ include ActiveAttr::AttributeDefaults
121
+
122
+ attribute :age, :type => Integer, :default => "21"
123
+ attribute :start_date, :type => String, :default => lambda { Date.today }
124
+ end
125
+ end
126
+
127
+ it "typecasts a default value literal" do
128
+ model_class.new.age.should == 21
129
+ end
130
+
131
+ it "typecasts a dynamic default" do
132
+ model_class.new.start_date.should == Date.today.to_s
133
+ end
134
+ end
135
+ end
136
+ end