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.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/.rspec +1 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +34 -0
- data/LICENSE +22 -0
- data/README.md +254 -0
- data/Rakefile +1 -0
- data/lib/toolchain.rb +2 -0
- data/lib/toolchain/attributes.rb +154 -0
- data/lib/toolchain/attributes/configuration.rb +41 -0
- data/lib/toolchain/attributes/errors.rb +5 -0
- data/lib/toolchain/attributes/errors/invalid_hash_transformation.rb +4 -0
- data/lib/toolchain/attributes/errors/invalid_mass_assignment.rb +4 -0
- data/lib/toolchain/attributes/errors/type_mismatch.rb +4 -0
- data/lib/toolchain/attributes/ext/boolean.rb +4 -0
- data/lib/toolchain/attributes/helpers.rb +72 -0
- data/lib/toolchain/validations.rb +103 -0
- data/lib/toolchain/validations/delegation.rb +31 -0
- data/lib/toolchain/validations/delegator.rb +51 -0
- data/lib/toolchain/validations/helpers.rb +58 -0
- data/lib/toolchain/validations/validation_errors.rb +82 -0
- data/lib/toolchain/validations/validators.rb +12 -0
- data/lib/toolchain/validations/validators/acceptance.rb +25 -0
- data/lib/toolchain/validations/validators/base.rb +54 -0
- data/lib/toolchain/validations/validators/confirmation.rb +39 -0
- data/lib/toolchain/validations/validators/email.rb +26 -0
- data/lib/toolchain/validations/validators/exclusion.rb +29 -0
- data/lib/toolchain/validations/validators/format.rb +25 -0
- data/lib/toolchain/validations/validators/inclusion.rb +29 -0
- data/lib/toolchain/validations/validators/length.rb +60 -0
- data/lib/toolchain/validations/validators/presence.rb +25 -0
- data/lib/toolchain/version.rb +3 -0
- data/spec/spec_helper.rb +11 -0
- data/spec/toolchain/attributes/attribute_spec.rb +47 -0
- data/spec/toolchain/attributes/attributes_spec.rb +193 -0
- data/spec/toolchain/attributes/base_helper.rb +37 -0
- data/spec/toolchain/attributes/boolean_spec.rb +38 -0
- data/spec/toolchain/attributes/configuration_spec.rb +33 -0
- data/spec/toolchain/attributes/date_time_spec.rb +21 -0
- data/spec/toolchain/attributes/hash_spec.rb +61 -0
- data/spec/toolchain/attributes/include_attributes_spec.rb +19 -0
- data/spec/toolchain/attributes/initializer_spec.rb +32 -0
- data/spec/toolchain/validations/base_helper.rb +2 -0
- data/spec/toolchain/validations/custom_validations_spec.rb +26 -0
- data/spec/toolchain/validations/delegation_spec.rb +70 -0
- data/spec/toolchain/validations/include_validations_spec.rb +20 -0
- data/spec/toolchain/validations/inheritence_spec.rb +26 -0
- data/spec/toolchain/validations/multiple_validations_spec.rb +19 -0
- data/spec/toolchain/validations/nested_validations_spec.rb +68 -0
- data/spec/toolchain/validations/validation_errors_spec.rb +9 -0
- data/spec/toolchain/validations/validators/acceptance_spec.rb +48 -0
- data/spec/toolchain/validations/validators/confirmation_spec.rb +86 -0
- data/spec/toolchain/validations/validators/email_spec.rb +41 -0
- data/spec/toolchain/validations/validators/exclusion_spec.rb +53 -0
- data/spec/toolchain/validations/validators/format_spec.rb +44 -0
- data/spec/toolchain/validations/validators/inclusion_spec.rb +50 -0
- data/spec/toolchain/validations/validators/length_spec.rb +134 -0
- data/spec/toolchain/validations/validators/presence_spec.rb +34 -0
- data/toolchain.gemspec +21 -0
- metadata +161 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
module Toolchain::Attributes::Configuration
|
2
|
+
extend self
|
3
|
+
|
4
|
+
# @param type [Symbol] :symbol or :string.
|
5
|
+
#
|
6
|
+
def hash_transformation=(type)
|
7
|
+
if [:symbolize_keys, :stringify_keys].include?(type)
|
8
|
+
@hash_transformation = type
|
9
|
+
else
|
10
|
+
raise Toolchain::Attributes::Errors::InvalidHashTransformation,
|
11
|
+
"valid types: :symbolize_keys, :stringify_keys."
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# @return [Symbol] :symbolize_keys or :stringify_keys.
|
16
|
+
#
|
17
|
+
def hash_transformation
|
18
|
+
@hash_transformation ||= :symbolize_keys
|
19
|
+
end
|
20
|
+
|
21
|
+
# Determines whether or not to include nil values
|
22
|
+
# in the object.attributes Hash.
|
23
|
+
#
|
24
|
+
# @param value [Boolean]
|
25
|
+
#
|
26
|
+
def include_nil_in_attributes=(value)
|
27
|
+
@include_nil_in_attributes = value
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [Boolean]
|
31
|
+
#
|
32
|
+
def include_nil_in_attributes
|
33
|
+
@include_nil_in_attributes ||= false
|
34
|
+
end
|
35
|
+
|
36
|
+
# @yield Toolchain::Attributes::Configuration
|
37
|
+
#
|
38
|
+
def configure
|
39
|
+
yield self
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Toolchain::Attributes::Helpers
|
2
|
+
extend self
|
3
|
+
|
4
|
+
# @param klass [Class]
|
5
|
+
#
|
6
|
+
# @yield [Symbol] Each defined attribute (key) name.
|
7
|
+
#
|
8
|
+
def each_key(klass)
|
9
|
+
while ![Class, Module, Object, BasicObject, nil].include?(klass)
|
10
|
+
klass.keys.each { |key| yield key }
|
11
|
+
klass = klass.superclass
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# @param value [Object]
|
16
|
+
# @param types [Array<Class>]
|
17
|
+
#
|
18
|
+
# @return [Boolean] true if the provided value doesn't
|
19
|
+
# match any of the provided type classes.
|
20
|
+
#
|
21
|
+
def invalid_value?(value, *types)
|
22
|
+
value = value.call if value.kind_of?(Proc)
|
23
|
+
|
24
|
+
return false if value.nil?
|
25
|
+
|
26
|
+
types.flatten.each do |type|
|
27
|
+
return false if value.kind_of?(type)
|
28
|
+
end
|
29
|
+
|
30
|
+
true
|
31
|
+
end
|
32
|
+
|
33
|
+
# Converts all keys to Symbol-type, including
|
34
|
+
# nested hashes.
|
35
|
+
#
|
36
|
+
# @param value [Hash]
|
37
|
+
# @return [Hash]
|
38
|
+
#
|
39
|
+
def symbolize_keys(value)
|
40
|
+
deep_transform_keys(value) { |key| key.to_sym rescue key }
|
41
|
+
end
|
42
|
+
|
43
|
+
# Converts all keys to String-type, including
|
44
|
+
# nested hashes.
|
45
|
+
#
|
46
|
+
# @param value [Hash]
|
47
|
+
# @return [Hash]
|
48
|
+
#
|
49
|
+
def stringify_keys(value)
|
50
|
+
deep_transform_keys(value) { |key| key.to_s rescue key }
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
# Recursive key transformation method.
|
56
|
+
#
|
57
|
+
# @param value [Hash]
|
58
|
+
# @param block [Proc]
|
59
|
+
#
|
60
|
+
def deep_transform_keys(value, &block)
|
61
|
+
Hash.new.tap do |result|
|
62
|
+
value.each do |key, value|
|
63
|
+
result[yield(key)] =
|
64
|
+
if value.kind_of?(Hash)
|
65
|
+
deep_transform_keys(value, &block)
|
66
|
+
else
|
67
|
+
value
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require_relative "version"
|
2
|
+
|
3
|
+
module Toolchain::Validations
|
4
|
+
require_relative "validations/helpers"
|
5
|
+
require_relative "validations/validation_errors"
|
6
|
+
require_relative "validations/validators"
|
7
|
+
require_relative "validations/delegator"
|
8
|
+
require_relative "validations/delegation"
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
|
12
|
+
# @return [Array] all the standard validation operations to run.
|
13
|
+
#
|
14
|
+
def validations
|
15
|
+
@validations ||= []
|
16
|
+
end
|
17
|
+
|
18
|
+
# @return [Array] all the custom validation operations to run.
|
19
|
+
#
|
20
|
+
def custom_validations
|
21
|
+
@custom_validations ||= []
|
22
|
+
end
|
23
|
+
|
24
|
+
# Adds the provided validator to the validations array for
|
25
|
+
# the provided attribute key_path.
|
26
|
+
#
|
27
|
+
# @param args [Array<Symbol, Hash>]
|
28
|
+
#
|
29
|
+
# @example
|
30
|
+
# class Device
|
31
|
+
# validates :name, presence: true
|
32
|
+
# validates :config, :url, presence: true
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
def validates(*args)
|
36
|
+
options, key_path = args.pop, args
|
37
|
+
|
38
|
+
options.each do |key, data|
|
39
|
+
if validator = Helpers.find_validator(key)
|
40
|
+
validations.push(key_path: key_path, validator: validator, data: data)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Adds the provided method name to the validations array.
|
46
|
+
# This method will be called during validation time and allows
|
47
|
+
# you to do program-specific validations.
|
48
|
+
#
|
49
|
+
# @param method [Symbol, String]
|
50
|
+
#
|
51
|
+
def validate(method)
|
52
|
+
custom_validations.push(method.to_sym)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Takes a Proc that contains validation logic and applies
|
56
|
+
# that to this class.
|
57
|
+
#
|
58
|
+
# @param validations [Proc]
|
59
|
+
#
|
60
|
+
def include_validations(validations)
|
61
|
+
class_eval(&validations)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
module InstanceMethods
|
66
|
+
|
67
|
+
# @return [Toolchain::Validations::ValidationErrors]
|
68
|
+
#
|
69
|
+
def errors
|
70
|
+
@errors ||= ValidationErrors.new
|
71
|
+
end
|
72
|
+
|
73
|
+
# Runs all the validation operations and will apply error
|
74
|
+
# messages to the ValidationErrors object when validator errors occur.
|
75
|
+
#
|
76
|
+
def validate
|
77
|
+
Helpers.each_validation(self.class) do |v|
|
78
|
+
v[:validator].new(self, v[:key_path], v[:data]).validate
|
79
|
+
end
|
80
|
+
|
81
|
+
Helpers.each_validation(self.class, :custom_validations) { |v| send(v) }
|
82
|
+
end
|
83
|
+
|
84
|
+
# Resets the ValidationErrors object, re-validates the current object
|
85
|
+
# and then determines whether or not this object is valid.
|
86
|
+
#
|
87
|
+
# @note If invalid (returns `false`), errors will have been
|
88
|
+
# added to the `object.errors` class.
|
89
|
+
#
|
90
|
+
# @return [Boolean]
|
91
|
+
#
|
92
|
+
def valid?
|
93
|
+
errors.reset
|
94
|
+
validate
|
95
|
+
errors.empty?
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.included(base)
|
100
|
+
base.send(:include, InstanceMethods)
|
101
|
+
base.extend(ClassMethods)
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Toolchain::Validations::Delegation
|
2
|
+
module InstanceMethods
|
3
|
+
|
4
|
+
# @return [Object]
|
5
|
+
#
|
6
|
+
attr_reader :delegator
|
7
|
+
|
8
|
+
# @param delegator [Object]
|
9
|
+
#
|
10
|
+
def initialize(delegator)
|
11
|
+
@delegator = delegator
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# @param base [Class]
|
16
|
+
#
|
17
|
+
def self.included(base)
|
18
|
+
base.send(:include, InstanceMethods)
|
19
|
+
base.class_eval do
|
20
|
+
|
21
|
+
alias_method :_valid?, :valid?
|
22
|
+
|
23
|
+
# @return [Boolean]
|
24
|
+
#
|
25
|
+
def valid?
|
26
|
+
self.attributes = delegator.attributes
|
27
|
+
_valid?.tap { delegator.errors = errors }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Toolchain::Validations::Delegator
|
2
|
+
module InstanceMethods
|
3
|
+
|
4
|
+
# @param value [Hash]
|
5
|
+
#
|
6
|
+
attr_writer :errors
|
7
|
+
|
8
|
+
# @return [Hash]
|
9
|
+
#
|
10
|
+
def errors
|
11
|
+
@errors || {}
|
12
|
+
end
|
13
|
+
|
14
|
+
# @return [Boolean]
|
15
|
+
#
|
16
|
+
def valid?
|
17
|
+
validator.valid?
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module ClassMethods
|
22
|
+
|
23
|
+
# @param block [Proc]
|
24
|
+
#
|
25
|
+
# @example
|
26
|
+
# class MyClass
|
27
|
+
# include Toolchain::Validations::Delegator
|
28
|
+
#
|
29
|
+
# validate_with do |instance|
|
30
|
+
# if instance.new?
|
31
|
+
# NewResourceValidator
|
32
|
+
# else
|
33
|
+
# ExistingResourceValidator
|
34
|
+
# end
|
35
|
+
# end
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
def validate_with(&block)
|
39
|
+
define_method(:validator) do
|
40
|
+
@validator ||= block.call(self).new(self)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# @param base [Class]
|
46
|
+
#
|
47
|
+
def self.included(base)
|
48
|
+
base.send(:include, InstanceMethods)
|
49
|
+
base.extend(ClassMethods)
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Toolchain::Validations::Helpers
|
2
|
+
extend self
|
3
|
+
|
4
|
+
# Finds the validator by key name.
|
5
|
+
#
|
6
|
+
# @param key [Symbol, String]
|
7
|
+
#
|
8
|
+
# @return [Object, nil] nil if the validator doesn't exist.
|
9
|
+
#
|
10
|
+
def find_validator(key)
|
11
|
+
Toolchain::Validations::Validators.const_get(classify(key))
|
12
|
+
rescue NameError
|
13
|
+
end
|
14
|
+
|
15
|
+
# Yields all defined validations from the current class,
|
16
|
+
# as well as all the (relavant) super-classes.
|
17
|
+
#
|
18
|
+
# @param klass [Class]
|
19
|
+
# @param method [Symbol, String]
|
20
|
+
#
|
21
|
+
# @yield [Hash]
|
22
|
+
#
|
23
|
+
def each_validation(klass, method = :validations)
|
24
|
+
while ![Class, Module, Object, BasicObject, nil].include?(klass)
|
25
|
+
klass.send(method).each { |v| yield v } if klass.respond_to?(method)
|
26
|
+
klass = klass.superclass
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Converts the provided value to it's class name.
|
31
|
+
#
|
32
|
+
# @param value [Symbol, String]
|
33
|
+
#
|
34
|
+
# @return [String]
|
35
|
+
#
|
36
|
+
def classify(value)
|
37
|
+
value.to_s.split("_").map(&:capitalize).join
|
38
|
+
end
|
39
|
+
|
40
|
+
# Custom inject method that includes a 3rd yield argument
|
41
|
+
# that determines whether the current iteration is the last.
|
42
|
+
#
|
43
|
+
# @param array [Array]
|
44
|
+
# @param init [Object] uses first value of array if nil.
|
45
|
+
#
|
46
|
+
# @yield memo [Object], value [Object], last [Boolean]
|
47
|
+
#
|
48
|
+
# @return [Object] The final memo result.
|
49
|
+
#
|
50
|
+
def inject(array, init = nil)
|
51
|
+
memo = init || array.shift
|
52
|
+
length = array.length
|
53
|
+
array.each_with_index do |value, index|
|
54
|
+
memo = yield memo, value, index == length - 1
|
55
|
+
end
|
56
|
+
memo
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
class Toolchain::Validations::ValidationErrors
|
2
|
+
|
3
|
+
Helpers = Toolchain::Validations::Helpers
|
4
|
+
|
5
|
+
# @return [Hash]
|
6
|
+
#
|
7
|
+
attr_reader :errors
|
8
|
+
|
9
|
+
# Creates a new instance of Toolchain::Validations::ValidationErrors
|
10
|
+
# and sets the initial errors state to an empty Hash.
|
11
|
+
#
|
12
|
+
def initialize
|
13
|
+
@errors = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
# Adds a new message to the Toolchain::Validations::ValidationErrors object.
|
17
|
+
#
|
18
|
+
# @param args [Array<Symbol, String>]
|
19
|
+
#
|
20
|
+
# @example
|
21
|
+
# errors.add(:name, "is required")
|
22
|
+
# errors.add(:config, :url, "isn't valid")
|
23
|
+
#
|
24
|
+
def add(*args)
|
25
|
+
message, key_path = args.pop, args
|
26
|
+
key_path = [key_path].flatten
|
27
|
+
|
28
|
+
Helpers.inject(key_path, errors) do |memo, key, last|
|
29
|
+
if last
|
30
|
+
memo[key] ||= []
|
31
|
+
memo[key].push(message)
|
32
|
+
else
|
33
|
+
memo[key] ||= {}
|
34
|
+
end
|
35
|
+
|
36
|
+
memo[key]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Resets all errors to an empty Hash.
|
41
|
+
#
|
42
|
+
def reset
|
43
|
+
@errors = {}
|
44
|
+
end
|
45
|
+
|
46
|
+
# Proxy method to allow individual values of the errors Hash
|
47
|
+
# to be accessed directly through the Toolchain::Validations::ValidationErrors object.
|
48
|
+
#
|
49
|
+
# @param key [Symbol] The key of the value you want to retrieve.
|
50
|
+
#
|
51
|
+
# @return [Object] the value of the key you want to retrieve.
|
52
|
+
#
|
53
|
+
def [](key)
|
54
|
+
errors[key]
|
55
|
+
end
|
56
|
+
|
57
|
+
# Proxy method to delegate the comparison of two values to
|
58
|
+
# the errors Hash.
|
59
|
+
#
|
60
|
+
# @param value [Object]
|
61
|
+
#
|
62
|
+
# @return [Boolean] true if both values are equal.
|
63
|
+
#
|
64
|
+
def ==(value)
|
65
|
+
errors == value
|
66
|
+
end
|
67
|
+
|
68
|
+
# Proxy method to check if there currently are any errors.
|
69
|
+
#
|
70
|
+
# @return [Boolean] true if there aren't any errors.
|
71
|
+
#
|
72
|
+
def empty?
|
73
|
+
errors.empty?
|
74
|
+
end
|
75
|
+
|
76
|
+
# @return [Hash] the Hash representation of the
|
77
|
+
# Toolchain::Validations::ValidationErrors.
|
78
|
+
#
|
79
|
+
def to_hash
|
80
|
+
errors
|
81
|
+
end
|
82
|
+
end
|