toolchain 0.1.0

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.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.rspec +1 -0
  4. data/Gemfile +10 -0
  5. data/Gemfile.lock +34 -0
  6. data/LICENSE +22 -0
  7. data/README.md +254 -0
  8. data/Rakefile +1 -0
  9. data/lib/toolchain.rb +2 -0
  10. data/lib/toolchain/attributes.rb +154 -0
  11. data/lib/toolchain/attributes/configuration.rb +41 -0
  12. data/lib/toolchain/attributes/errors.rb +5 -0
  13. data/lib/toolchain/attributes/errors/invalid_hash_transformation.rb +4 -0
  14. data/lib/toolchain/attributes/errors/invalid_mass_assignment.rb +4 -0
  15. data/lib/toolchain/attributes/errors/type_mismatch.rb +4 -0
  16. data/lib/toolchain/attributes/ext/boolean.rb +4 -0
  17. data/lib/toolchain/attributes/helpers.rb +72 -0
  18. data/lib/toolchain/validations.rb +103 -0
  19. data/lib/toolchain/validations/delegation.rb +31 -0
  20. data/lib/toolchain/validations/delegator.rb +51 -0
  21. data/lib/toolchain/validations/helpers.rb +58 -0
  22. data/lib/toolchain/validations/validation_errors.rb +82 -0
  23. data/lib/toolchain/validations/validators.rb +12 -0
  24. data/lib/toolchain/validations/validators/acceptance.rb +25 -0
  25. data/lib/toolchain/validations/validators/base.rb +54 -0
  26. data/lib/toolchain/validations/validators/confirmation.rb +39 -0
  27. data/lib/toolchain/validations/validators/email.rb +26 -0
  28. data/lib/toolchain/validations/validators/exclusion.rb +29 -0
  29. data/lib/toolchain/validations/validators/format.rb +25 -0
  30. data/lib/toolchain/validations/validators/inclusion.rb +29 -0
  31. data/lib/toolchain/validations/validators/length.rb +60 -0
  32. data/lib/toolchain/validations/validators/presence.rb +25 -0
  33. data/lib/toolchain/version.rb +3 -0
  34. data/spec/spec_helper.rb +11 -0
  35. data/spec/toolchain/attributes/attribute_spec.rb +47 -0
  36. data/spec/toolchain/attributes/attributes_spec.rb +193 -0
  37. data/spec/toolchain/attributes/base_helper.rb +37 -0
  38. data/spec/toolchain/attributes/boolean_spec.rb +38 -0
  39. data/spec/toolchain/attributes/configuration_spec.rb +33 -0
  40. data/spec/toolchain/attributes/date_time_spec.rb +21 -0
  41. data/spec/toolchain/attributes/hash_spec.rb +61 -0
  42. data/spec/toolchain/attributes/include_attributes_spec.rb +19 -0
  43. data/spec/toolchain/attributes/initializer_spec.rb +32 -0
  44. data/spec/toolchain/validations/base_helper.rb +2 -0
  45. data/spec/toolchain/validations/custom_validations_spec.rb +26 -0
  46. data/spec/toolchain/validations/delegation_spec.rb +70 -0
  47. data/spec/toolchain/validations/include_validations_spec.rb +20 -0
  48. data/spec/toolchain/validations/inheritence_spec.rb +26 -0
  49. data/spec/toolchain/validations/multiple_validations_spec.rb +19 -0
  50. data/spec/toolchain/validations/nested_validations_spec.rb +68 -0
  51. data/spec/toolchain/validations/validation_errors_spec.rb +9 -0
  52. data/spec/toolchain/validations/validators/acceptance_spec.rb +48 -0
  53. data/spec/toolchain/validations/validators/confirmation_spec.rb +86 -0
  54. data/spec/toolchain/validations/validators/email_spec.rb +41 -0
  55. data/spec/toolchain/validations/validators/exclusion_spec.rb +53 -0
  56. data/spec/toolchain/validations/validators/format_spec.rb +44 -0
  57. data/spec/toolchain/validations/validators/inclusion_spec.rb +50 -0
  58. data/spec/toolchain/validations/validators/length_spec.rb +134 -0
  59. data/spec/toolchain/validations/validators/presence_spec.rb +34 -0
  60. data/toolchain.gemspec +21 -0
  61. metadata +161 -0
@@ -0,0 +1,12 @@
1
+ module Toolchain::Validations::Validators
2
+ require_relative "validators/base"
3
+
4
+ require_relative "validators/acceptance"
5
+ require_relative "validators/confirmation"
6
+ require_relative "validators/email"
7
+ require_relative "validators/exclusion"
8
+ require_relative "validators/format"
9
+ require_relative "validators/inclusion"
10
+ require_relative "validators/length"
11
+ require_relative "validators/presence"
12
+ end
@@ -0,0 +1,25 @@
1
+ module Toolchain::Validations::Validators
2
+
3
+ # Validates the acceptance of an attribute.
4
+ # Accepted values are as follows: `true`, `1` and `"1"`.
5
+ #
6
+ # @example
7
+ # class User::Creator
8
+ # validates :terms, acceptance: {
9
+ # message: "the terms of service must be accepted"
10
+ # }
11
+ # end
12
+ #
13
+ class Acceptance < Base
14
+
15
+ def validate
16
+ errors.add(key_path, message || "must be accepted") if not_accepted?
17
+ end
18
+
19
+ private
20
+
21
+ def not_accepted?
22
+ ![1, "1", true].include?(value)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,54 @@
1
+ class Toolchain::Validations::Validators::Base
2
+
3
+ # @return [Object] the object that contains the attribute
4
+ # that'll be validated.
5
+ #
6
+ attr_reader :object
7
+
8
+ # @return [Toolchain::Validations::ValidationErrors] the errors object
9
+ # of the object that's being validated.
10
+ #
11
+ attr_reader :errors
12
+
13
+ # @return [Symbol] The key_path of the attribute that's being validated.
14
+ #
15
+ attr_reader :key_path
16
+
17
+ # @return [Hash] The value that was passed in to the validator
18
+ # at definition time. i.e. a Hash containing the regular expression
19
+ # for Toolchain::Validations::Validators::Formatter along with a message.
20
+ #
21
+ attr_reader :data
22
+
23
+ # @return [String] A custom message to override the
24
+ # default message with in case the attribute is invalid.
25
+ #
26
+ attr_reader :message
27
+
28
+ # Instantiates a new Toolchain::Validations::Validators object
29
+ # which'll be used to validate the target object.
30
+ #
31
+ # @param object [Object]
32
+ # @param key_path [Object]
33
+ # @param data [Object]
34
+ #
35
+ def initialize(object, key_path, data)
36
+ @object = object
37
+ @errors = object.errors
38
+ @key_path = key_path
39
+ @data = data
40
+ @message = data[:message] if data.is_a?(Hash)
41
+ end
42
+
43
+ # @return [Object] The value that's stored in the attribute of
44
+ # the object that's being validated.
45
+ #
46
+ def value
47
+ @value ||= (
48
+ keys = key_path.dup
49
+ keys.inject(object.send(keys.shift)) do |memo, key|
50
+ memo[key] if memo.kind_of?(Hash)
51
+ end
52
+ )
53
+ end
54
+ end
@@ -0,0 +1,39 @@
1
+ module Toolchain::Validations::Validators
2
+
3
+ # Validates the confirmation of an attribute.
4
+ # In the example both `password` and `password_confirmation`
5
+ # attributes must match. One or more `nil` values will result in the
6
+ # validation error being triggered as well.
7
+ #
8
+ # @example
9
+ # class User::Creator
10
+ # validates :password, confirmation: {
11
+ # message: "doesn't match confirmation"
12
+ # }
13
+ # end
14
+ #
15
+ class Confirmation < Base
16
+
17
+ def validate
18
+ if no_match?
19
+ errors.add(key_path, message || "doesn't match confirmation")
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def no_match?
26
+ value.nil? || confirmation.nil? || value != confirmation
27
+ end
28
+
29
+ def confirmation
30
+ @confirmation ||= (
31
+ keys = key_path.dup
32
+ keys << :"#{keys.pop}_confirmation"
33
+ keys.inject(object.send(keys.shift)) do |memo, key|
34
+ memo[key] if memo.kind_of?(Hash)
35
+ end
36
+ )
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,26 @@
1
+ module Toolchain::Validations::Validators
2
+
3
+ # Validates the format of an email address.
4
+ #
5
+ # @example
6
+ # class User::Creator
7
+ # validates :email, email: {
8
+ # message: "isn't valid"
9
+ # }
10
+ # end
11
+ #
12
+ class Email < Base
13
+
14
+ PATTERN = /\A[^@\s]+@([^@\s]+\.)+[^@\s]+\z/
15
+
16
+ def validate
17
+ errors.add(key_path, message || "is invalid") if no_email?
18
+ end
19
+
20
+ private
21
+
22
+ def no_email?
23
+ value.nil? || !value.match(PATTERN)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,29 @@
1
+ module Toolchain::Validations::Validators
2
+
3
+ # Validates the exclusion of values in an attribute.
4
+ # In the example only games without a pegi rating
5
+ # of 12, 16, 18 are valid.
6
+ #
7
+ # @example
8
+ # class Game::Creator
9
+ # validates :pegi, exclusion: {
10
+ # in: [12, 16, 18],
11
+ # message: "invalid pegi rating"
12
+ # }
13
+ # end
14
+ #
15
+ class Exclusion < Base
16
+
17
+ def validate
18
+ if included?
19
+ errors.add(key_path, message || "is invalid")
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def included?
26
+ data[:in].include?(value)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,25 @@
1
+ module Toolchain::Validations::Validators
2
+
3
+ # Validates the format of an attribute using a regular expression.
4
+ #
5
+ # @example
6
+ # class Company::Creator
7
+ # validates :credit_card, format: {
8
+ # with: /^\d+{4}-$\d+{4}-\d+{4}-\d+{4}/,
9
+ # message: "invalid credit card format"
10
+ # }
11
+ # end
12
+ #
13
+ class Format < Base
14
+
15
+ def validate
16
+ errors.add(key_path, message || "is invalid") if no_match?
17
+ end
18
+
19
+ private
20
+
21
+ def no_match?
22
+ value.nil? || !value.match(data[:with])
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,29 @@
1
+ module Toolchain::Validations::Validators
2
+
3
+ # Validates the inclusion of values in an attribute.
4
+ # In the example, only `pending` and `dispatched` are
5
+ # allowed to be set.
6
+ #
7
+ # @example
8
+ # class Shipment::Creator
9
+ # validates :status, inclusion: {
10
+ # in: ["pending", "dispatched"],
11
+ # message: "available options are: pending, dispatched"
12
+ # }
13
+ # end
14
+ #
15
+ class Inclusion < Base
16
+
17
+ def validate
18
+ if not_included?
19
+ errors.add(key_path, message || "is invalid")
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def not_included?
26
+ !data.include?(value)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,60 @@
1
+ module Toolchain::Validations::Validators
2
+
3
+ # Validates the lengt of an attribute.
4
+ #
5
+ # @example
6
+ # class User::Creator
7
+ # validates :password, length: {
8
+ # between: 5..100,
9
+ # message: "must be between 5 and 100 characters long"
10
+ # }
11
+ # end
12
+ #
13
+ # class User::Creator
14
+ # validates :password, length: {
15
+ # greater_than: 5,
16
+ # less_than 100,
17
+ # message: "must be between 5 and 100 characters long"
18
+ # }
19
+ # end
20
+ #
21
+ class Length < Base
22
+
23
+ def validate
24
+ errors.add(key_path, message || "is invalid") unless valid?
25
+ end
26
+
27
+ private
28
+
29
+ def valid?
30
+ val =
31
+ if value.kind_of?(String) && value =~ /^\d+$/
32
+ value.to_i
33
+ elsif value.kind_of?(String) && value =~ /^\d+\.\d+$/
34
+ value.to_f
35
+ elsif value.kind_of?(String)
36
+ value.length
37
+ else
38
+ value
39
+ end
40
+
41
+ if data[:between]
42
+ return data[:between].include?(val)
43
+ end
44
+
45
+ if data[:greater_than] && data[:less_than]
46
+ return val > data[:greater_than] && val < data[:less_than]
47
+ end
48
+
49
+ if data[:greater_than]
50
+ return val > data[:greater_than]
51
+ end
52
+
53
+ if data[:less_than]
54
+ return val < data[:less_than]
55
+ end
56
+
57
+ false
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,25 @@
1
+ module Toolchain::Validations::Validators
2
+
3
+ # Validates the presence of an attribute. If the attribute
4
+ # is either `nil` or `""` (empty String) it is considered invalid.
5
+ #
6
+ # @example
7
+ # class User::Creator
8
+ # validates :name, presence: {
9
+ # message: "is required"
10
+ # }
11
+ # end
12
+ #
13
+ class Presence < Base
14
+
15
+ def validate
16
+ errors.add(key_path, message || "can't be blank") if blank?
17
+ end
18
+
19
+ private
20
+
21
+ def blank?
22
+ [nil, ""].include?(value)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,3 @@
1
+ module Toolchain
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,11 @@
1
+ require "bundler"
2
+ Bundler.require(:test)
3
+
4
+ require "simplecov"
5
+ SimpleCov.start
6
+
7
+ RSpec.configure do |config|
8
+ config.mock_with :mocha
9
+ end
10
+
11
+ TOOLCHAIN_ROOT = File.expand_path("../../lib/toolchain", __FILE__)
@@ -0,0 +1,47 @@
1
+ require_relative "base_helper"
2
+
3
+ describe Toolchain::Attributes, "Attribute" do
4
+
5
+ let(:model) { ModelData.new }
6
+
7
+ it "should be nil by default" do
8
+ model.string.should == nil
9
+ end
10
+
11
+ it "should be empty by default" do
12
+ model.string_default.should == ""
13
+ end
14
+
15
+ it "should set a new String" do
16
+ expect { model.string = "Example" }.
17
+ to change { model.string }.from(nil).to("Example")
18
+ end
19
+
20
+ it "should set to nil, resulting in default" do
21
+ model.string_default = "Example"
22
+ expect { model.string_default = nil }.
23
+ to change { model.string_default }.from("Example").to("")
24
+ end
25
+
26
+ it "should instance-variable cache the result" do
27
+ model.time_default.should == model.time_default
28
+ end
29
+
30
+ it "requires the type to match the configured type" do
31
+ expect { model.string = 0 }.to raise_error(
32
+ Toolchain::Attributes::Errors::TypeMismatch,
33
+ "ModelData#string expected String type, but received Fixnum (0).")
34
+ end
35
+
36
+ it "should not allow defaults of other data-type" do
37
+ expect {
38
+ class InvalidModelData
39
+ include Toolchain::Attributes
40
+ attribute :string, String, 0
41
+ end
42
+ }.to raise_error(
43
+ Toolchain::Attributes::Errors::TypeMismatch,
44
+ "expected InvalidModelData#string to have default value "+
45
+ "of String type, but received Fixnum (0).")
46
+ end
47
+ end
@@ -0,0 +1,193 @@
1
+ require_relative "base_helper"
2
+
3
+ describe Toolchain::Attributes, "Attributes (Method)" do
4
+
5
+ let(:model) { ModelData.new }
6
+
7
+ def expect_date_time
8
+ [
9
+ Date.today.tap { |d| Date.stubs(:today).returns(d) },
10
+ Time.now.tap { |t| Time.stubs(:now).returns(t) },
11
+ DateTime.now.tap { |dt| DateTime.stubs(:now).returns(dt) }
12
+ ]
13
+ end
14
+
15
+ it "should return attributes with Symbol keys" do
16
+ date, time, date_time = expect_date_time
17
+
18
+ model.attributes.should == {
19
+ array_default: [],
20
+ boolean_default: false,
21
+ float_default: 0.0,
22
+ hash_default: {},
23
+ integer_default: 0,
24
+ string_default: "",
25
+ date_default: date,
26
+ time_default: time,
27
+ date_time_default: date_time
28
+ }
29
+ end
30
+
31
+ it "should return attributes with String keys" do
32
+ Toolchain::Attributes::Configuration.
33
+ hash_transformation = :stringify_keys
34
+
35
+ date, time, date_time = expect_date_time
36
+
37
+ model.attributes.should == {
38
+ "array_default" => [],
39
+ "boolean_default" => false,
40
+ "float_default" => 0.0,
41
+ "hash_default" => {},
42
+ "integer_default" => 0,
43
+ "string_default" => "",
44
+ "date_default" => date,
45
+ "time_default" => time,
46
+ "date_time_default" => date_time
47
+ }
48
+ end
49
+
50
+ it "should symbolize hash, hash_default" do
51
+ model.hash = { "key" => { "value" => true } }
52
+ model.hash_default = { "key" => { "value" => true } }
53
+ model.attributes.tap do |attrs|
54
+ attrs[:hash].should == { key: { value: true } }
55
+ attrs[:hash_default].should == { key: { value: true } }
56
+ end
57
+ end
58
+
59
+ it "should stringify hash, hash_default" do
60
+ Toolchain::Attributes::Configuration.
61
+ hash_transformation = :stringify_keys
62
+
63
+ model.hash = { key: { value: true } }
64
+ model.hash_default = { key: { value: true } }
65
+ model.attributes.tap do |attrs|
66
+ attrs["hash"].should == { "key" => { "value" => true } }
67
+ attrs["hash_default"].should == { "key" => { "value" => true } }
68
+ end
69
+ end
70
+
71
+ it "should include nil attributes" do
72
+ Toolchain::Attributes::Configuration.
73
+ include_nil_in_attributes = true
74
+
75
+ date, time, date_time = expect_date_time
76
+
77
+ model.attributes.should == {
78
+ array: nil,
79
+ array_default: [],
80
+ boolean: nil,
81
+ boolean_default: false,
82
+ float: nil,
83
+ float_default: 0.0,
84
+ hash: nil,
85
+ hash_default: {},
86
+ integer: nil,
87
+ integer_default: 0,
88
+ string: nil,
89
+ string_default: "",
90
+ date: nil,
91
+ date_default: date,
92
+ time: nil,
93
+ time_default: time,
94
+ date_time: nil,
95
+ date_time_default: date_time
96
+ }
97
+ end
98
+
99
+ it "should mass-assign attributes" do
100
+ date, time, date_time = expect_date_time
101
+
102
+ model.attributes = {
103
+ string: "Example",
104
+ integer: 123,
105
+ float: 1.23,
106
+ hash: { foo: "bar" },
107
+ array: [1, 2, 3],
108
+ boolean: true,
109
+ date: date,
110
+ date_default: date,
111
+ time: time,
112
+ time_default: time,
113
+ date_time: date_time,
114
+ date_time_default: date_time,
115
+ }
116
+
117
+ model.attributes.should == {
118
+ array: [1, 2, 3],
119
+ array_default: [],
120
+ boolean: true,
121
+ boolean_default: false,
122
+ float: 1.23,
123
+ float_default: 0.0,
124
+ hash: { foo: "bar" },
125
+ hash_default: {},
126
+ integer: 123,
127
+ integer_default: 0,
128
+ string: "Example",
129
+ string_default: "",
130
+ date: date,
131
+ date_default: date,
132
+ time: time,
133
+ time_default: time,
134
+ date_time: date_time,
135
+ date_time_default: date_time
136
+ }
137
+ end
138
+
139
+ it "should not assign undefined attributes" do
140
+ date, time, date_time = expect_date_time
141
+ model.attributes = { unknown_attribute: true }
142
+ model.attributes.should == {
143
+ array_default: [],
144
+ boolean_default: false,
145
+ float_default: 0.0,
146
+ hash_default: {},
147
+ integer_default: 0,
148
+ string_default: "",
149
+ date_default: date,
150
+ time_default: time,
151
+ date_time_default: date_time,
152
+ }
153
+ end
154
+
155
+ it "should raise an exception non-Hash type is passed" do
156
+ expect { model.attributes = 0 }.
157
+ to raise_error(
158
+ Toolchain::Attributes::Errors::InvalidMassAssignment,
159
+ "Can't mass-assign Fixnum (0) type to ModelData#attributes.")
160
+ end
161
+
162
+ class InheritedModelData < ModelData
163
+ end
164
+
165
+ it "should inherit all attribute definitions" do
166
+ date, time, date_time = expect_date_time
167
+ InheritedModelData.new.attributes.should == {
168
+ array_default: [],
169
+ boolean_default: false,
170
+ float_default: 0.0,
171
+ hash_default: {},
172
+ integer_default: 0,
173
+ string_default: "",
174
+ date_default: date,
175
+ time_default: time,
176
+ date_time_default: date_time
177
+ }
178
+ end
179
+
180
+ it "should mass-assign superclass keys" do
181
+ date, time, date_time = expect_date_time
182
+ model = InheritedModelData.new
183
+ model.attributes = { string: "Michael" }
184
+ model.string.should == "Michael"
185
+ end
186
+
187
+ it "should mass-assing superclass string keys" do
188
+ date, time, date_time = expect_date_time
189
+ model = InheritedModelData.new
190
+ model.attributes = { "string" => "Michael" }
191
+ model.string.should == "Michael"
192
+ end
193
+ end