toolchain 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 988af450afb20a81dae7333ff7ded62a2fc6ca52
4
+ data.tar.gz: f0cecb5a9bfe8ec82cbadab21019f4267f8ae5e5
5
+ SHA512:
6
+ metadata.gz: 3f707cd58b11de78ce0166874d9712d6197e572dcd6919787355a56cf2821b52127b0748732d2207f238721d45a25bf28c1fc8dc7ffc5d56255682accc1fe6d0
7
+ data.tar.gz: 62c64f506a01f32f050f17b4418e1828a13e8c8f9f0dae62b324b3af615e5cc64f469f69af33fd2c7decb651e6fd48c4fd8eab4f579beaf69e5ab4738e6479f6
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ .DS_Store
2
+ .yardoc
3
+ /doc
4
+ /coverage
5
+ /pkg
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color --format documentation
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "rake"
4
+
5
+ group :test do
6
+ gem "yard"
7
+ gem "rspec"
8
+ gem "mocha"
9
+ gem "simplecov"
10
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,34 @@
1
+ GEM
2
+ remote: https://rubygems.org/
3
+ specs:
4
+ diff-lcs (1.2.5)
5
+ docile (1.1.3)
6
+ metaclass (0.0.4)
7
+ mocha (1.0.0)
8
+ metaclass (~> 0.0.1)
9
+ multi_json (1.9.0)
10
+ rake (10.1.1)
11
+ rspec (2.14.1)
12
+ rspec-core (~> 2.14.0)
13
+ rspec-expectations (~> 2.14.0)
14
+ rspec-mocks (~> 2.14.0)
15
+ rspec-core (2.14.8)
16
+ rspec-expectations (2.14.5)
17
+ diff-lcs (>= 1.1.3, < 2.0)
18
+ rspec-mocks (2.14.6)
19
+ simplecov (0.8.2)
20
+ docile (~> 1.1.0)
21
+ multi_json
22
+ simplecov-html (~> 0.8.0)
23
+ simplecov-html (0.8.0)
24
+ yard (0.8.7.3)
25
+
26
+ PLATFORMS
27
+ ruby
28
+
29
+ DEPENDENCIES
30
+ mocha
31
+ rake
32
+ rspec
33
+ simplecov
34
+ yard
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) Machinery ( http://machinery.io )
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,254 @@
1
+ # Toolchain
2
+
3
+ Collection of Ruby light-weight modules that enhance plain Ruby classes with zero runtime dependencies.
4
+
5
+ ##### Available modules
6
+
7
+ * Attributes
8
+ * For defining rich attributes without a data store mapper like ActiveRecord
9
+ * Useful for applying principles such as the Single Responsibility Principe (SRP)
10
+ * Validations
11
+ * Inspired by `ActiveModel::Validations`
12
+ * Supports nested validations for Hash data types
13
+ * Useful for applying principles such as the Single Responsibility Principe (SRP)
14
+ * Lightweight (~250 LOC) instead of `ActiveModel::Validations` (~1600 LOC)
15
+ * Note: This isn't a clone. Don't expect all of the features in `ActiveModel::Validations` to be available here. Part of the public api behaves in a similar fashion, there are however also a bunch of subtle differences.
16
+
17
+ ## Installation
18
+
19
+ In `Gemfile`:
20
+
21
+ ```rb
22
+ gem "toolchain"
23
+ ```
24
+
25
+ Or, if you wish to only include certain modules:
26
+
27
+ In `Gemfile`:
28
+
29
+ ```rb
30
+ gem "toolchain", require: false
31
+ ```
32
+
33
+ Then from anywhere in your program/app:
34
+
35
+ ```rb
36
+ require "toolchain/attributes"
37
+ require "toolchain/validations"
38
+ ```
39
+
40
+ ## Modules
41
+
42
+ Below follows a list of modules.
43
+
44
+ ### Toolchain::Attributes
45
+
46
+ The `Toolchain::Attributes` module provides your class with the `attribute` class method which you can use to define rich attributes. Think of it as `attr_accessor` but with type-definitions, optional defaults and a few convenience methods.
47
+
48
+ ```rb
49
+ require "toolchain/attributes"
50
+
51
+ class Person
52
+ include Toolchain::Attributes
53
+
54
+ attribute :name, String
55
+ attribute :cash, Integer
56
+ attribute :birthdate, DateTime
57
+ attribute :pocket, Array
58
+ attribute :misc, Hash
59
+ attribute :winning, Boolean, true
60
+ end
61
+ ```
62
+
63
+ ##### Defined attributes (keys)
64
+
65
+ You can get a list of defined attributes usind the `Person.keys` method.
66
+
67
+ ##### All attributes/values as a Hash
68
+
69
+ You can access a Hash of attributes by calling the `person.attributes` instance method. By default this Hash returns all attributes with Symbol keys (including nested Hashes stored in Hash-type attributes).
70
+
71
+ Hash-type attributes automatically convert String-type keys to Symbol-type keys when setting a new Hash.
72
+
73
+ ##### (Global) Configuration
74
+
75
+ You can configure whether you want to have all your attribute keys as String- or Symbol type like so:
76
+
77
+ ```rb
78
+ Toolchain::Attributes::Configuration.configure do |config|
79
+ config.hash_transformation = :stringify_keys # defaults to :symbolize_keys
80
+ end
81
+ ```
82
+
83
+ ##### Mass-assignment
84
+
85
+ You can mass-assign attributes using the `person.attributes` instance method. Note that any attributes you mass-assign that you haven't defined on the model are simply ignored and will not be set. Defined attributes automatically act as a whitelist for what can and cannot be mass-assigned.
86
+
87
+ ## Toolchain::Validations
88
+
89
+ The `Toolchain::Validations` module provides your class with the `validates` and `validate` class methods which you can use to validate attributes. Note that this **DOES NOT** require `Toolchain::Attributes`, any instance method that returns a value can be validated.
90
+
91
+ ```rb
92
+ require "toolchain/validations"
93
+
94
+ class Person
95
+ include Toolchain::Validations
96
+
97
+ validates :name, presence: true
98
+ validates :email, email: { message: "isn't valid" }
99
+ validate :domain_validation
100
+
101
+ attr_accessor :name
102
+ attr_accessor :domain
103
+
104
+ def email; @email end
105
+ def email=(value); @email = value end
106
+
107
+ private
108
+
109
+ def domain_validation
110
+ if true # host validation logic here
111
+ errors.add(:domain, "couldn't connect")
112
+ end
113
+ end
114
+ end
115
+ ```
116
+
117
+ ##### Nested Validations
118
+
119
+ While `Toolchain::Validations` mirrors `ActiveModel::Validations` in most cases for the sake of consistency, there is an important diference when defining a validation using the `validates` method. In `Toolchain::Validation`, this creates a nested (Hash) validation:
120
+
121
+ ```rb
122
+ require "toolchain/validations"
123
+
124
+ class Person
125
+ include Toolchain::Attributes # optional
126
+ include Toolchain::Validations
127
+
128
+ attribute :info, Hash, {}
129
+ validates :info, :phone, presence: true
130
+ end
131
+
132
+ Person.new.tap do |person|
133
+ person.validate
134
+ person.errors[:info][:phone] # => ["can't be blank"]
135
+ end
136
+ ```
137
+
138
+ This feature isn't limited to single-level validation. You can have an infinitely deep nested Hash and validate each known attribute on it and it'll properly map the attributes/errors 1:1.
139
+
140
+ This is useful when you keep a Hash of serialized data in a database, but you still want a hassle-free way of validating it's attributes that doesn't require you to create custom validators and re-invent the wheel just for nested attributes.
141
+
142
+ ##### Class-level Validators
143
+
144
+ Similar to `ActiveModel::Validations` you can create your own class-level validations.
145
+
146
+ ```rb
147
+ require "toolchain/validations"
148
+
149
+ class Person
150
+ include Toolchain::Attributes # optional
151
+ include Toolchain::Validations
152
+
153
+ attribute :name, String
154
+ attribute :info, Hash, {}
155
+
156
+ validate :name_validation
157
+ validate :info_phone_validation
158
+
159
+ private
160
+
161
+ def name_validation
162
+ if true # name validation logic here
163
+ errors.add(:name, "is invalid")
164
+ end
165
+ end
166
+
167
+ def info_validation
168
+ if true # info[:phone] validation logic here
169
+ errors.add(:info, :phone, "not a number")
170
+ end
171
+ end
172
+ end
173
+
174
+ Person.new.tap do |person|
175
+ person.validate
176
+ person.errors[:info][:phone] # => ["can't be blank"]
177
+ end
178
+ ```
179
+
180
+ ##### Custom (re-usable) Validators
181
+
182
+ To define a validator, simply create and load a file like this:
183
+
184
+ ```rb
185
+ module Toolchain::Validations::Validators
186
+ class MyValidator < Base
187
+
188
+ def validate
189
+ errors.add(key_path, message || "is invalid") if invalid?
190
+ end
191
+
192
+ private
193
+
194
+ def invalid?
195
+ # logic that determines that this value is invalid
196
+ #
197
+ # list of instance methods to work with:
198
+ #
199
+ # - object
200
+ # - errors
201
+ # - key_path
202
+ # - data
203
+ # - message
204
+ end
205
+ end
206
+ end
207
+ ```
208
+
209
+ ###### object
210
+
211
+ A reference to the object that contains the attribute which is being validated.
212
+
213
+ ###### errors
214
+
215
+ A reference to the errors object on the `object`. Write potential validation errors to it.
216
+
217
+ ###### key_path
218
+
219
+ An array of one or more keys. Single key means it's a root-level attribute validation. If this includes two or more keys, it means it's a nested validation on a Hash-type attribute.
220
+
221
+ You generally don't need to worry about this. Just pass it in to `errors.add`'s first argument and it'll take care of properly assigning the error message at the right level.
222
+
223
+ ###### data
224
+
225
+ This contains the options data that was passed in to the `validates` method.
226
+
227
+ For example:
228
+
229
+ ```rb
230
+ validates :cash, format: {
231
+ with: /^\d+$/,
232
+ message: "not a valid cash format"
233
+ }
234
+
235
+ data[:with] # /^\d+$/
236
+ data[:message] # "not a valid cash format"
237
+ ```
238
+
239
+ This can optionally be used in your validations.
240
+
241
+ ###### message
242
+
243
+ If `data[:message]` is set (see `data` example), the `message` method will be contain it. Else, `message` will return `nil`, in which case you'll to use a default error message.
244
+
245
+
246
+
247
+ ## Contributing
248
+
249
+ 1. Fork it ( http://github.com/<my-github-username>/toolchain/fork )
250
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
251
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
252
+ 4. Push to the branch (`git push origin my-new-feature`)
253
+ 5. Create new Pull Request
254
+
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/lib/toolchain.rb ADDED
@@ -0,0 +1,2 @@
1
+ require_relative "toolchain/attributes"
2
+ require_relative "toolchain/validations"
@@ -0,0 +1,154 @@
1
+ require_relative "version"
2
+ require_relative "attributes/ext/boolean"
3
+
4
+ module Toolchain::Attributes
5
+ require_relative "attributes/configuration"
6
+ require_relative "attributes/errors"
7
+ require_relative "attributes/helpers"
8
+
9
+ module ClassMethods
10
+
11
+ # @return [Array<Symbol>] all defined attributes.
12
+ #
13
+ def keys
14
+ @keys ||= []
15
+ end
16
+
17
+ # Defines an attribute on the Class.
18
+ #
19
+ # @example
20
+ # class Company
21
+ # attribute :name, String, "Unnamed"
22
+ # attribute :email, String
23
+ # end
24
+ #
25
+ # @param key [Symbol, String]
26
+ # @param type [Class]
27
+ # @param default [Object]
28
+ #
29
+ def attribute(key, type, default = nil)
30
+ type = [TrueClass, FalseClass] if type == Boolean
31
+
32
+ if Helpers.invalid_value?(default, type)
33
+ raise Errors::TypeMismatch,
34
+ "expected #{self.name}##{key} to have default value " +
35
+ "of #{type} type, but received #{default.class} (#{default})."
36
+ end
37
+
38
+ keys.push(key.to_sym)
39
+
40
+ define_method(key) do
41
+ value = instance_variable_get("@#{key}")
42
+
43
+ if value.nil?
44
+ new_value =
45
+ begin
46
+ if default.kind_of?(Proc)
47
+ default.call
48
+ else
49
+ default.dup
50
+ end
51
+ rescue TypeError
52
+ default
53
+ end
54
+
55
+ send("#{key}=", new_value)
56
+ else
57
+ value
58
+ end
59
+ end
60
+
61
+ define_method("#{key}=") do |value|
62
+ if value.kind_of?(String) && type.respond_to?(:parse)
63
+ value = type.parse(value)
64
+ end
65
+
66
+ if type == [TrueClass, FalseClass] && [0, "0"].include?(value)
67
+ value = false
68
+ end
69
+
70
+ if type == [TrueClass, FalseClass] && [1, "1"].include?(value)
71
+ value = true
72
+ end
73
+
74
+ if Helpers.invalid_value?(value, type)
75
+ raise Errors::TypeMismatch,
76
+ "#{self.class}##{key} expected #{type} type, " +
77
+ "but received #{value.class} (#{value})."
78
+ end
79
+
80
+ if value.kind_of?(Hash)
81
+ transformation = Configuration.hash_transformation
82
+ value = Helpers.send(transformation, value)
83
+ end
84
+
85
+ instance_variable_set("@#{key}", value)
86
+ end
87
+ end
88
+
89
+ # Takes a Proc that contains attribute definitions and applies
90
+ # that to this class.
91
+ #
92
+ # @param attributes [Proc]
93
+ #
94
+ def include_attributes(attributes)
95
+ class_eval(&attributes)
96
+ end
97
+ end
98
+
99
+ module InstanceMethods
100
+
101
+ # @param attributes [Hash]
102
+ #
103
+ def initialize(attributes = {})
104
+ self.attributes = attributes
105
+ end
106
+
107
+ # @return [Hash] keys and values for each defined attribute.
108
+ #
109
+ def attributes
110
+ include_nil = Configuration.include_nil_in_attributes
111
+
112
+ attributes = Hash.new.tap do |attrs|
113
+ Helpers.each_key(self.class) do |key|
114
+ value = send(key)
115
+
116
+ if !value.nil? || (value.nil? && include_nil)
117
+ attrs[key] = value
118
+ end
119
+ end
120
+ end
121
+
122
+ transformation = Configuration.hash_transformation
123
+ Helpers.send(transformation, attributes)
124
+ end
125
+
126
+ # Mass-assignment for the defined attributes by passing
127
+ # in a Hash. Non-existing attributes are ignored.
128
+ #
129
+ # @param value [Hash]
130
+ #
131
+ def attributes=(value)
132
+ if !value.kind_of?(Hash)
133
+ raise Errors::InvalidMassAssignment,
134
+ "Can't mass-assign #{value.class} (#{value}) type " +
135
+ "to #{self.class}#attributes."
136
+ end
137
+
138
+ value = Helpers.symbolize_keys(value)
139
+ keys = Array.new.tap do |keys|
140
+ Helpers.each_key(self.class) { |key| keys << key }
141
+ keys.uniq!
142
+ end
143
+
144
+ value.each do |key, value|
145
+ send("#{key}=", value) if keys.include?(key)
146
+ end
147
+ end
148
+ end
149
+
150
+ def self.included(base)
151
+ base.send(:include, InstanceMethods)
152
+ base.extend(ClassMethods)
153
+ end
154
+ end