toolchain 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|