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