definition 0.1.0.rc1
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/.approvals +0 -0
- data/.gitignore +12 -0
- data/.rspec +2 -0
- data/.rubocop.yml +77 -0
- data/.travis.yml +14 -0
- data/Gemfile +6 -0
- data/Guardfile +72 -0
- data/README.md +205 -0
- data/Rakefile +22 -0
- data/definition.gemspec +39 -0
- data/lib/definition.rb +8 -0
- data/lib/definition/conform_error.rb +22 -0
- data/lib/definition/conform_result.rb +20 -0
- data/lib/definition/dsl.rb +74 -0
- data/lib/definition/types.rb +9 -0
- data/lib/definition/types/and.rb +60 -0
- data/lib/definition/types/base.rb +27 -0
- data/lib/definition/types/each.rb +67 -0
- data/lib/definition/types/include.rb +50 -0
- data/lib/definition/types/keys.rb +120 -0
- data/lib/definition/types/lambda.rb +34 -0
- data/lib/definition/types/or.rb +61 -0
- data/lib/definition/types/type.rb +39 -0
- data/lib/definition/version.rb +5 -0
- metadata +281 -0
data/lib/definition.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Definition
|
4
|
+
class ConformError
|
5
|
+
def initialize(definition, message, sub_errors: [])
|
6
|
+
self.definition = definition
|
7
|
+
self.message = message
|
8
|
+
self.sub_errors = sub_errors
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_accessor :definition, :sub_errors
|
12
|
+
attr_writer :message
|
13
|
+
|
14
|
+
def message
|
15
|
+
if sub_errors.empty?
|
16
|
+
@message
|
17
|
+
else
|
18
|
+
"#{@message}: { " + sub_errors.map(&:message).join(", ") + " }"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Definition
|
4
|
+
class ConformResult
|
5
|
+
def initialize(value, errors: [])
|
6
|
+
self.value = value
|
7
|
+
self.errors = errors
|
8
|
+
end
|
9
|
+
|
10
|
+
attr_accessor :value, :errors
|
11
|
+
|
12
|
+
def passed?
|
13
|
+
errors.empty?
|
14
|
+
end
|
15
|
+
|
16
|
+
def error_message
|
17
|
+
errors.map(&:message).join(", ")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "definition/types"
|
4
|
+
|
5
|
+
module Definition
|
6
|
+
module Dsl
|
7
|
+
# Example:
|
8
|
+
# Keys do
|
9
|
+
# required :name, Types::Type(String)
|
10
|
+
# optional :age, Types::Type(Integer)
|
11
|
+
# end
|
12
|
+
def Keys(&block) # rubocop:disable Naming/MethodName
|
13
|
+
Types::Keys.new(:hash).tap do |instance|
|
14
|
+
instance.instance_exec(&block)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Example:
|
19
|
+
# And(Types::Type(Float), Types::GreaterThen(10.0))
|
20
|
+
def And(*definitions) # rubocop:disable Naming/MethodName
|
21
|
+
Types::And.new(:and, *definitions)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Example:
|
25
|
+
# Or(Types::Type(Float), Types::Type(Integer))
|
26
|
+
def Or(*definitions) # rubocop:disable Naming/MethodName
|
27
|
+
Types::Or.new(:or, *definitions)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Example:
|
31
|
+
# Type(Integer)
|
32
|
+
def Type(klass) # rubocop:disable Naming/MethodName
|
33
|
+
Types::Type.new(:type, klass)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Example:
|
37
|
+
# CoercibleType(Integer)
|
38
|
+
def CoercibleType(klass) # rubocop:disable Naming/MethodName
|
39
|
+
unless Kernel.respond_to?(klass.name)
|
40
|
+
raise ArgumentError.new("#{klass} can't be used as CoercibleType because its not "\
|
41
|
+
"a primitive that has a coercion function defined")
|
42
|
+
end
|
43
|
+
Types::Type.new(:type, klass) do |value|
|
44
|
+
begin
|
45
|
+
method(klass.name).call(value)
|
46
|
+
rescue ArgumentError
|
47
|
+
value
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Example:
|
53
|
+
# Lambda(:even) do |value|
|
54
|
+
# value.even?
|
55
|
+
# end
|
56
|
+
def Lambda(name, &block) # rubocop:disable Naming/MethodName
|
57
|
+
Types::Lambda.new(name, &block)
|
58
|
+
end
|
59
|
+
|
60
|
+
# Example:
|
61
|
+
# Enum("allowed_value1", "allowed_value2")
|
62
|
+
def Enum(*allowed_values) # rubocop:disable Naming/MethodName
|
63
|
+
Lambda("enum #{allowed_values.inspect}") do |value|
|
64
|
+
conform_with(value) if allowed_values.include?(value)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Example:
|
69
|
+
# Each(Definition::Type(Integer))
|
70
|
+
def Each(definition) # rubocop:disable Naming/MethodName
|
71
|
+
Types::Each.new(:each, definition: definition)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "definition/types/and"
|
4
|
+
require "definition/types/or"
|
5
|
+
require "definition/types/include"
|
6
|
+
require "definition/types/keys"
|
7
|
+
require "definition/types/lambda"
|
8
|
+
require "definition/types/type"
|
9
|
+
require "definition/types/each"
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "definition/types/base"
|
4
|
+
|
5
|
+
module Definition
|
6
|
+
module Types
|
7
|
+
class And < Base
|
8
|
+
module Dsl
|
9
|
+
def validate(definition)
|
10
|
+
definitions << definition
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
include Dsl
|
15
|
+
attr_accessor :definitions
|
16
|
+
|
17
|
+
def initialize(name, *args)
|
18
|
+
self.definitions = *args
|
19
|
+
super(name)
|
20
|
+
end
|
21
|
+
|
22
|
+
def conform(value)
|
23
|
+
Conformer.new(self).conform(value)
|
24
|
+
end
|
25
|
+
|
26
|
+
class Conformer
|
27
|
+
def initialize(definition)
|
28
|
+
self.definition = definition
|
29
|
+
end
|
30
|
+
|
31
|
+
def conform(value)
|
32
|
+
results = conform_all(value)
|
33
|
+
|
34
|
+
if results.all? { |r| r.errors.empty? }
|
35
|
+
ConformResult.new(results.last.value)
|
36
|
+
else
|
37
|
+
ConformResult.new(value, errors: [
|
38
|
+
ConformError.new(definition, "Not all children are valid for #{definition.name}",
|
39
|
+
sub_errors: results.map(&:errors).flatten)
|
40
|
+
])
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
attr_accessor :definition
|
47
|
+
|
48
|
+
def conform_all(value)
|
49
|
+
results = []
|
50
|
+
definition.definitions.each do |definition|
|
51
|
+
result = definition.conform(value)
|
52
|
+
value = result.value
|
53
|
+
results << result
|
54
|
+
end
|
55
|
+
results
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "definition/conform_result"
|
4
|
+
require "definition/conform_error"
|
5
|
+
|
6
|
+
module Definition
|
7
|
+
module Types
|
8
|
+
class Base
|
9
|
+
attr_accessor :name
|
10
|
+
|
11
|
+
def initialize(name)
|
12
|
+
self.name = name
|
13
|
+
end
|
14
|
+
|
15
|
+
def explain(value)
|
16
|
+
result = conform(value)
|
17
|
+
return "value passes definition" if result.passed?
|
18
|
+
|
19
|
+
result.error_message
|
20
|
+
end
|
21
|
+
|
22
|
+
def conform(_value)
|
23
|
+
raise NotImplementedError
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "definition/types/base"
|
4
|
+
|
5
|
+
module Definition
|
6
|
+
module Types
|
7
|
+
class Each < Base
|
8
|
+
attr_accessor :item_definition
|
9
|
+
|
10
|
+
def initialize(name, definition:)
|
11
|
+
self.item_definition = definition
|
12
|
+
super(name)
|
13
|
+
end
|
14
|
+
|
15
|
+
def conform(value)
|
16
|
+
Conformer.new(self).conform(value)
|
17
|
+
end
|
18
|
+
|
19
|
+
class Conformer
|
20
|
+
def initialize(definition)
|
21
|
+
self.definition = definition
|
22
|
+
end
|
23
|
+
|
24
|
+
def conform(value)
|
25
|
+
return non_array_error(value) unless value.is_a?(Array)
|
26
|
+
|
27
|
+
results = conform_all(value)
|
28
|
+
|
29
|
+
if results.all? { |r| r.errors.empty? }
|
30
|
+
ConformResult.new(results.map(&:value))
|
31
|
+
else
|
32
|
+
ConformResult.new(value, errors: [ConformError.new(definition,
|
33
|
+
"Not all items conform with #{definition.name}",
|
34
|
+
sub_errors: errors(results))])
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
attr_accessor :definition
|
41
|
+
|
42
|
+
def errors(results)
|
43
|
+
results.reject(&:passed?).map do |r|
|
44
|
+
ConformError.new(
|
45
|
+
definition,
|
46
|
+
"Item #{r.value.inspect} did not conform to #{definition.name}",
|
47
|
+
sub_errors: r.errors
|
48
|
+
)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def conform_all(values)
|
53
|
+
values.map do |value|
|
54
|
+
definition.item_definition.conform(value)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def non_array_error(value)
|
59
|
+
ConformResult.new(value, errors: [
|
60
|
+
ConformError.new(definition,
|
61
|
+
"Non-Array value does not conform with #{definition.name}")
|
62
|
+
])
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
require "definition/types/base"
|
6
|
+
|
7
|
+
module Definition
|
8
|
+
module Types
|
9
|
+
class Include < Base
|
10
|
+
attr_accessor :required_items
|
11
|
+
|
12
|
+
def initialize(name, *args)
|
13
|
+
self.required_items = *args
|
14
|
+
super(name)
|
15
|
+
end
|
16
|
+
|
17
|
+
def conform(value)
|
18
|
+
Conformer.new(self).conform(value)
|
19
|
+
end
|
20
|
+
|
21
|
+
class Conformer
|
22
|
+
def initialize(definition)
|
23
|
+
self.definition = definition
|
24
|
+
end
|
25
|
+
|
26
|
+
def conform(value)
|
27
|
+
errors = gather_errors(value)
|
28
|
+
|
29
|
+
if errors.empty?
|
30
|
+
ConformResult.new(value)
|
31
|
+
else
|
32
|
+
ConformResult.new(value, errors: errors)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def gather_errors(value)
|
39
|
+
definition.required_items.map do |item|
|
40
|
+
next if value.include?(item)
|
41
|
+
|
42
|
+
ConformError.new(definition, "#{definition.name} does not include #{item.inspect}")
|
43
|
+
end.compact
|
44
|
+
end
|
45
|
+
|
46
|
+
attr_accessor :definition
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
require "definition/types/base"
|
6
|
+
require "definition/types/include"
|
7
|
+
|
8
|
+
module Definition
|
9
|
+
module Types
|
10
|
+
class Keys < Base
|
11
|
+
module Dsl
|
12
|
+
def required(key, definition)
|
13
|
+
required_definitions[key] = definition
|
14
|
+
end
|
15
|
+
|
16
|
+
def optional(key, definition)
|
17
|
+
optional_definitions[key] = definition
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
include Dsl
|
22
|
+
attr_accessor :required_definitions, :optional_definitions
|
23
|
+
|
24
|
+
def initialize(name, req: {}, opt: {})
|
25
|
+
super(name)
|
26
|
+
self.required_definitions = req
|
27
|
+
self.optional_definitions = opt
|
28
|
+
end
|
29
|
+
|
30
|
+
def conform(value)
|
31
|
+
Conformer.new(self, value).conform
|
32
|
+
end
|
33
|
+
|
34
|
+
class Conformer
|
35
|
+
def initialize(definition, value)
|
36
|
+
self.definition = definition
|
37
|
+
self.value = value
|
38
|
+
self.errors = []
|
39
|
+
end
|
40
|
+
|
41
|
+
def conform
|
42
|
+
add_extra_key_errors
|
43
|
+
add_missing_key_errors
|
44
|
+
values = conform_all_keys
|
45
|
+
|
46
|
+
ConformResult.new(values, errors: errors)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
attr_accessor :errors
|
52
|
+
|
53
|
+
def add_extra_key_errors
|
54
|
+
extra_keys = value.keys - all_keys
|
55
|
+
return if extra_keys.empty?
|
56
|
+
|
57
|
+
errors.push(ConformError.new(
|
58
|
+
definition,
|
59
|
+
"#{definition.name} has extra keys: #{extra_keys.map(&:inspect).join(', ')}"
|
60
|
+
))
|
61
|
+
end
|
62
|
+
|
63
|
+
def conform_all_keys
|
64
|
+
required_keys_values = conform_definitions(required_definitions)
|
65
|
+
optional_keys_values = conform_definitions(optional_definitions)
|
66
|
+
|
67
|
+
required_keys_values.merge!(optional_keys_values)
|
68
|
+
end
|
69
|
+
|
70
|
+
def all_keys
|
71
|
+
required_keys + optional_keys
|
72
|
+
end
|
73
|
+
|
74
|
+
def required_definitions
|
75
|
+
definition.required_definitions
|
76
|
+
end
|
77
|
+
|
78
|
+
def required_keys
|
79
|
+
required_definitions.keys
|
80
|
+
end
|
81
|
+
|
82
|
+
def optional_definitions
|
83
|
+
definition.optional_definitions
|
84
|
+
end
|
85
|
+
|
86
|
+
def optional_keys
|
87
|
+
optional_definitions.keys
|
88
|
+
end
|
89
|
+
|
90
|
+
def conform_definitions(keys)
|
91
|
+
keys.each_with_object({}) do |(key, key_definition), result_value|
|
92
|
+
next unless value.key?(key)
|
93
|
+
|
94
|
+
result = key_definition.conform(value[key])
|
95
|
+
result_value[key] = result.value
|
96
|
+
next if result.passed?
|
97
|
+
|
98
|
+
errors.push(ConformError.new(key_definition,
|
99
|
+
"#{definition.name} fails validation for key #{key}",
|
100
|
+
sub_errors: result.errors))
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def add_missing_key_errors
|
105
|
+
required_definition = Types::Include.new(
|
106
|
+
definition.name,
|
107
|
+
*required_keys
|
108
|
+
)
|
109
|
+
|
110
|
+
result = required_definition.conform(value)
|
111
|
+
return if result.passed?
|
112
|
+
|
113
|
+
errors.concat(result.errors)
|
114
|
+
end
|
115
|
+
|
116
|
+
attr_accessor :definition, :value
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|