definition 0.1.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/definition.rb ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "definition/version"
4
+ require "definition/dsl"
5
+
6
+ module Definition
7
+ extend Dsl
8
+ end
@@ -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