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.
- 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
|