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