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.
- data/.travis.yml +7 -0
- data/CHANGELOG.md +14 -0
- data/README.md +57 -25
- data/Rakefile +2 -0
- data/active_attr.gemspec +3 -2
- data/gemfiles/rails_3_1.gemfile +6 -0
- data/lib/active_attr.rb +3 -2
- data/lib/active_attr/attribute_defaults.rb +120 -0
- data/lib/active_attr/attribute_definition.rb +25 -2
- data/lib/active_attr/attributes.rb +44 -33
- data/lib/active_attr/logger.rb +8 -8
- data/lib/active_attr/mass_assignment.rb +2 -1
- data/lib/active_attr/matchers/have_attribute_matcher.rb +28 -10
- data/lib/active_attr/model.rb +2 -0
- data/lib/active_attr/query_attributes.rb +2 -5
- data/lib/active_attr/typecasted_attributes.rb +77 -0
- data/lib/active_attr/typecasting.rb +60 -0
- data/lib/active_attr/typecasting/boolean.rb +6 -0
- data/lib/active_attr/typecasting/boolean_typecaster.rb +21 -0
- data/lib/active_attr/typecasting/date_time_typecaster.rb +14 -0
- data/lib/active_attr/typecasting/date_typecaster.rb +14 -0
- data/lib/active_attr/typecasting/float_typecaster.rb +11 -0
- data/lib/active_attr/typecasting/integer_typecaster.rb +12 -0
- data/lib/active_attr/typecasting/object_typecaster.rb +9 -0
- data/lib/active_attr/typecasting/string_typecaster.rb +11 -0
- data/lib/active_attr/version.rb +1 -1
- data/spec/functional/active_attr/attribute_defaults_spec.rb +136 -0
- data/spec/functional/active_attr/attributes_spec.rb +19 -5
- data/spec/functional/active_attr/model_spec.rb +34 -4
- data/spec/functional/active_attr/typecasted_attributes_spec.rb +116 -0
- data/spec/support/mass_assignment_shared_examples.rb +1 -1
- data/spec/unit/active_attr/attribute_defaults_spec.rb +43 -0
- data/spec/unit/active_attr/attribute_definition_spec.rb +12 -8
- data/spec/unit/active_attr/attributes_spec.rb +22 -10
- data/spec/unit/active_attr/mass_assignment_spec.rb +1 -0
- data/spec/unit/active_attr/matchers/have_attribute_matcher_spec.rb +94 -15
- data/spec/unit/active_attr/query_attributes_spec.rb +1 -1
- data/spec/unit/active_attr/typecasted_attributes_spec.rb +76 -0
- data/spec/unit/active_attr/typecasting/boolean_spec.rb +10 -0
- data/spec/unit/active_attr/typecasting/boolean_typecaster_spec.rb +147 -0
- data/spec/unit/active_attr/typecasting/date_time_typecaster_spec.rb +67 -0
- data/spec/unit/active_attr/typecasting/date_typecaster_spec.rb +55 -0
- data/spec/unit/active_attr/typecasting/float_typecaster_spec.rb +27 -0
- data/spec/unit/active_attr/typecasting/integer_typecaster_spec.rb +35 -0
- data/spec/unit/active_attr/typecasting/object_typecaster_spec.rb +15 -0
- data/spec/unit/active_attr/typecasting/string_typecaster_spec.rb +24 -0
- data/spec/unit/active_attr/typecasting_spec.rb +87 -0
- data/spec/unit/active_attr/version_spec.rb +11 -2
- metadata +75 -29
- data/lib/active_attr/strict_mass_assignment.rb +0 -44
- data/lib/active_attr/unknown_attributes_error.rb +0 -18
- data/spec/unit/active_attr/strict_mass_assignment_spec.rb +0 -38
- data/spec/unit/active_attr/unknown_attributes_error_spec.rb +0 -9
data/lib/active_attr/logger.rb
CHANGED
@@ -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
|
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(:
|
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
|
-
|
25
|
-
|
26
|
-
|
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
|
-
@
|
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
|
data/lib/active_attr/model.rb
CHANGED
@@ -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
|
-
|
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,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
|
data/lib/active_attr/version.rb
CHANGED
@@ -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
|