active_attr 0.4.1 → 0.5.0.alpha1

Sign up to get free protection for your applications and to get access to all the features.

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