rschema 3.0.1.pre3 → 3.0.1.pre4

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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +278 -330
  3. data/lib/rschema.rb +104 -17
  4. data/lib/rschema/coercers.rb +3 -0
  5. data/lib/rschema/coercers/any.rb +40 -0
  6. data/lib/rschema/coercers/boolean.rb +30 -0
  7. data/lib/rschema/coercers/chain.rb +41 -0
  8. data/lib/rschema/coercers/date.rb +25 -0
  9. data/lib/rschema/coercers/fixed_hash/default_booleans_to_false.rb +62 -0
  10. data/lib/rschema/coercers/fixed_hash/remove_extraneous_attributes.rb +42 -0
  11. data/lib/rschema/coercers/fixed_hash/symbolize_keys.rb +62 -0
  12. data/lib/rschema/coercers/float.rb +18 -0
  13. data/lib/rschema/coercers/integer.rb +18 -0
  14. data/lib/rschema/coercers/symbol.rb +21 -0
  15. data/lib/rschema/coercers/time.rb +25 -0
  16. data/lib/rschema/coercion_wrapper.rb +46 -0
  17. data/lib/rschema/coercion_wrapper/rack_params.rb +21 -0
  18. data/lib/rschema/dsl.rb +271 -42
  19. data/lib/rschema/error.rb +12 -30
  20. data/lib/rschema/options.rb +2 -2
  21. data/lib/rschema/result.rb +18 -4
  22. data/lib/rschema/schemas.rb +3 -0
  23. data/lib/rschema/schemas/anything.rb +14 -12
  24. data/lib/rschema/schemas/boolean.rb +20 -21
  25. data/lib/rschema/schemas/coercer.rb +37 -0
  26. data/lib/rschema/schemas/convenience.rb +53 -0
  27. data/lib/rschema/schemas/enum.rb +25 -25
  28. data/lib/rschema/schemas/fixed_hash.rb +110 -91
  29. data/lib/rschema/schemas/fixed_length_array.rb +48 -48
  30. data/lib/rschema/schemas/maybe.rb +18 -17
  31. data/lib/rschema/schemas/pipeline.rb +20 -19
  32. data/lib/rschema/schemas/predicate.rb +24 -21
  33. data/lib/rschema/schemas/set.rb +40 -45
  34. data/lib/rschema/schemas/sum.rb +24 -28
  35. data/lib/rschema/schemas/type.rb +22 -21
  36. data/lib/rschema/schemas/variable_hash.rb +53 -52
  37. data/lib/rschema/schemas/variable_length_array.rb +39 -38
  38. data/lib/rschema/version.rb +1 -1
  39. metadata +49 -5
  40. data/lib/rschema/http_coercer.rb +0 -218
@@ -2,45 +2,27 @@ module RSchema
2
2
  class Error
3
3
  attr_reader :schema, :value, :symbolic_name, :vars
4
4
 
5
- def initialize(schema:, value:, symbolic_name:, vars: nil)
5
+ def initialize(schema:, value:, symbolic_name:, vars: {})
6
+ raise ArgumentError.new("vars must be a hash") unless vars.is_a?(Hash)
7
+
6
8
  @schema = schema
7
9
  @value = value
8
10
  @symbolic_name = symbolic_name
9
11
  @vars = vars
10
- freeze
11
- end
12
12
 
13
- def to_s(detailed=false)
14
- if detailed
15
- <<~EOS
16
- Error: #{symbolic_name}
17
- Schema: #{schema.class.name}
18
- Value: #{value.inspect}
19
- Vars: #{vars.inspect}
20
- EOS
21
- else
22
- "Error #{schema.class}/#{symbolic_name} for value: #{value.inspect}"
23
- end
13
+ freeze
24
14
  end
25
15
 
26
- def to_json
27
- {
28
- schema: schema.class.name,
29
- error: symbolic_name.to_s,
30
- value: jsonify(value),
31
- vars: jsonify(vars),
32
- }
16
+ def to_s
17
+ "#{schema.class}/#{symbolic_name}"
33
18
  end
34
19
 
35
- private
20
+ def inspect
21
+ attrs = vars.merge(value: value)
22
+ .map{ |k, v| "#{k}=#{v.inspect}" }
23
+ .join(' ')
36
24
 
37
- def jsonify(value)
38
- case value
39
- when String, Symbol, Numeric, TrueClass, FalseClass, NilClass then value
40
- when Array then value.map{ |element| jsonify(element) }
41
- when Hash then value.map{ |k, v| [jsonify(k), jsonify(v)] }.to_h
42
- else String(value)
43
- end
44
- end
25
+ "<#{self.class} #{to_s} #{attrs}>"
26
+ end
45
27
  end
46
28
  end
@@ -4,8 +4,8 @@ module RSchema
4
4
  @default ||= new
5
5
  end
6
6
 
7
- def initialize(vars={})
8
- @fail_fast = vars.fetch(:fail_fast, false)
7
+ def initialize(fail_fast: false)
8
+ @fail_fast = fail_fast
9
9
  end
10
10
 
11
11
  def fail_fast?
@@ -1,17 +1,26 @@
1
1
  module RSchema
2
2
  class Result
3
- def self.success(value)
4
- new(true, value, nil)
3
+ def self.success(value = nil)
4
+ if value.nil?
5
+ NIL_SUCCESS
6
+ else
7
+ new(true, value, nil)
8
+ end
5
9
  end
6
10
 
7
- def self.failure(error)
8
- new(false, nil, error)
11
+ def self.failure(error = nil)
12
+ if error.nil?
13
+ NIL_FAILURE
14
+ else
15
+ new(false, nil, error)
16
+ end
9
17
  end
10
18
 
11
19
  def initialize(valid, value, error)
12
20
  @valid = valid
13
21
  @value = value
14
22
  @error = error
23
+ freeze
15
24
  end
16
25
 
17
26
  def valid?
@@ -35,5 +44,10 @@ module RSchema
35
44
  end
36
45
 
37
46
  class InvalidError < StandardError; end
47
+
48
+ # @!visibility private
49
+ NIL_SUCCESS = new(true, nil, nil)
50
+ # @!visibility private
51
+ NIL_FAILURE = new(false, nil, nil)
38
52
  end
39
53
  end
@@ -0,0 +1,3 @@
1
+ Dir.glob(File.join(__dir__, 'schemas/**/*.rb')).each do |path|
2
+ require path
3
+ end
@@ -1,17 +1,19 @@
1
1
  module RSchema
2
- module Schemas
3
- class Anything
4
- def self.instance
5
- @instance ||= new
6
- end
2
+ module Schemas
3
+ class Anything
7
4
 
8
- def call(value, options=Options.default)
9
- Result.success(value)
10
- end
5
+ def self.instance
6
+ @instance ||= new
7
+ end
8
+
9
+ def call(value, options)
10
+ Result.success(value)
11
+ end
11
12
 
12
- def with_wrapped_subschemas(wrapper)
13
- self
14
- end
15
- end
13
+ def with_wrapped_subschemas(wrapper)
14
+ self
16
15
  end
16
+
17
+ end
18
+ end
17
19
  end
@@ -1,27 +1,26 @@
1
1
  module RSchema
2
- module Schemas
3
-
4
- class Boolean
5
- def self.instance
6
- @instance ||= new
7
- end
8
-
9
- def call(value, options=Options.default)
10
- if value.equal?(true) || value.equal?(false)
11
- Result.success(value)
12
- else
13
- Result.failure(Error.new(
14
- schema: self,
15
- value: value,
16
- symbolic_name: :not_a_boolean,
17
- ))
18
- end
19
- end
2
+ module Schemas
3
+ class Boolean
4
+ def self.instance
5
+ @instance ||= new
6
+ end
20
7
 
21
- def with_wrapped_subschemas(wrapper)
22
- self
23
- end
8
+ def call(value, options)
9
+ if value.equal?(true) || value.equal?(false)
10
+ Result.success(value)
11
+ else
12
+ Result.failure(Error.new(
13
+ schema: self,
14
+ value: value,
15
+ symbolic_name: :not_a_boolean,
16
+ ))
24
17
  end
18
+ end
25
19
 
20
+ def with_wrapped_subschemas(wrapper)
21
+ self
26
22
  end
23
+
24
+ end
25
+ end
27
26
  end
@@ -0,0 +1,37 @@
1
+ module RSchema
2
+ module Schemas
3
+ class Coercer
4
+ attr_reader :coercer, :subschema
5
+
6
+ def initialize(coercer, subschema)
7
+ byebug if coercer.is_a?(Array)
8
+ @coercer = coercer
9
+ @subschema = subschema
10
+ end
11
+
12
+ def call(value, options)
13
+ result = coercer.call(value)
14
+ if result.valid?
15
+ @subschema.call(result.value, options)
16
+ else
17
+ failure(value, result.error)
18
+ end
19
+ end
20
+
21
+ def with_wrapped_subschemas(wrapper)
22
+ self.class.new(coercer, wrapper.wrap(subschema))
23
+ end
24
+
25
+ private
26
+
27
+ def failure(value, name)
28
+ return Result.failure(Error.new(
29
+ schema: self,
30
+ value: value,
31
+ symbolic_name: name || :coercion_failure,
32
+ ))
33
+ end
34
+
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,53 @@
1
+ require 'delegate'
2
+
3
+ module RSchema
4
+ module Schemas
5
+ class Convenience < SimpleDelegator
6
+ def initialize(raw_schema)
7
+ super
8
+ end
9
+
10
+ def raw_schema
11
+ __getobj__
12
+ end
13
+
14
+ def validate(value, options=Options.default)
15
+ call(value, options)
16
+ end
17
+
18
+ def validate!(value, options=Options.default)
19
+ result = call(value, options)
20
+ if result.valid?
21
+ result.value
22
+ else
23
+ raise RSchema::Invalid.new(result.error)
24
+ end
25
+ end
26
+
27
+ def valid?(value)
28
+ result = call(value, Options.new(fail_fast: true))
29
+ result.valid?
30
+ end
31
+
32
+ def self.wrap(schema)
33
+ if schema.is_a?(self)
34
+ schema
35
+ else
36
+ new(schema)
37
+ end
38
+ end
39
+
40
+ def self.unwrap(schema)
41
+ while schema.is_a?(self)
42
+ schema = schema.raw_schema
43
+ end
44
+ schema
45
+ end
46
+
47
+ def with_wrapped_subschemas(wrapper)
48
+ self.class.new(wrapper.wrap(raw_schema))
49
+ end
50
+
51
+ end
52
+ end
53
+ end
@@ -1,31 +1,31 @@
1
1
  module RSchema
2
- module Schemas
3
- class Enum
4
- attr_reader :members, :subschema
2
+ module Schemas
3
+ class Enum
4
+ attr_reader :members, :subschema
5
5
 
6
- def initialize(members, subschema)
7
- @members = members
8
- @subschema = subschema
9
- end
10
-
11
- def call(value, options=Options.default)
12
- subresult = subschema.call(value, options)
13
- if subresult.invalid?
14
- subresult
15
- elsif members.include?(subresult.value)
16
- subresult
17
- else
18
- Result.failure(Error.new(
19
- schema: self,
20
- value: subresult.value,
21
- symbolic_name: :not_a_member,
22
- ))
23
- end
24
- end
6
+ def initialize(members, subschema)
7
+ @members = members
8
+ @subschema = subschema
9
+ end
25
10
 
26
- def with_wrapped_subschemas(wrapper)
27
- self.class.new(members, wrapper.wrap(subschema))
28
- end
11
+ def call(value, options)
12
+ subresult = subschema.call(value, options)
13
+ if subresult.invalid?
14
+ subresult
15
+ elsif members.include?(subresult.value)
16
+ subresult
17
+ else
18
+ Result.failure(Error.new(
19
+ schema: self,
20
+ value: subresult.value,
21
+ symbolic_name: :not_a_member,
22
+ ))
29
23
  end
30
24
  end
25
+
26
+ def with_wrapped_subschemas(wrapper)
27
+ self.class.new(members, wrapper.wrap(subschema))
28
+ end
29
+ end
30
+ end
31
31
  end
@@ -1,118 +1,137 @@
1
1
  module RSchema
2
- module Schemas
2
+ module Schemas
3
+ class FixedHash
4
+ attr_reader :attributes
3
5
 
4
- class FixedHash
5
- attr_reader :attributes
6
+ def initialize(attributes)
7
+ @attributes = attributes
8
+ end
6
9
 
7
- def initialize(attributes)
8
- @attributes = attributes
9
- end
10
+ def call(value, options)
11
+ return not_a_hash_result(value) unless value.is_a?(Hash)
12
+ return missing_attrs_result(value) if missing_keys(value).any?
13
+ return extraneous_attrs_result(value) if extraneous_keys(value).any?
10
14
 
11
- def call(value, options=Options.default)
12
- return not_a_hash_result(value) unless value.is_a?(Hash)
13
- return missing_attrs_result(value) if missing_keys(value).any?
14
- return extraneous_attrs_result(value) if extraneous_keys(value).any?
15
-
16
- subresults = attr_subresults(value, options)
17
- if subresults.values.any?(&:invalid?)
18
- Result.failure(failure_error(subresults))
19
- else
20
- Result.success(success_value(subresults))
21
- end
22
- end
15
+ subresults = attr_subresults(value, options)
16
+ if subresults.values.any?(&:invalid?)
17
+ Result.failure(failure_error(subresults))
18
+ else
19
+ Result.success(success_value(subresults))
20
+ end
21
+ end
23
22
 
24
- def with_wrapped_subschemas(wrapper)
25
- wrapped_attributes = attributes.map do |attr|
26
- attr.with_wrapped_value_schema(wrapper)
27
- end
23
+ def with_wrapped_subschemas(wrapper)
24
+ wrapped_attributes = attributes.map do |attr|
25
+ attr.with_wrapped_value_schema(wrapper)
26
+ end
28
27
 
29
- self.class.new(wrapped_attributes)
30
- end
28
+ self.class.new(wrapped_attributes)
29
+ end
31
30
 
32
- def [](attr_key)
33
- attributes.find{ |attr| attr.key == attr_key }
34
- end
31
+ def [](attr_key)
32
+ attributes.find{ |attr| attr.key == attr_key }
33
+ end
35
34
 
36
- Attribute = Struct.new(:key, :value_schema, :optional) do
37
- def with_wrapped_value_schema(wrapper)
38
- self.class.new(key, wrapper.wrap(value_schema), optional)
39
- end
40
- end
35
+ def merge(new_attributes)
36
+ merged_attrs = (attributes + new_attributes)
37
+ .map { |attr| [attr.key, attr] }
38
+ .to_h
39
+ .values
41
40
 
42
- private
41
+ self.class.new(merged_attrs)
42
+ end
43
43
 
44
- def missing_keys(value)
45
- attributes
46
- .reject(&:optional)
47
- .map(&:key)
48
- .reject{ |k| value.has_key?(k) }
49
- end
44
+ def without(attribute_keys)
45
+ filtered_attrs = attributes
46
+ .reject { |attr| attribute_keys.include?(attr.key) }
50
47
 
51
- def missing_attrs_result(value)
52
- Result.failure(Error.new(
53
- schema: self,
54
- value: value,
55
- symbolic_name: :missing_attributes,
56
- vars: missing_keys(value),
57
- ))
58
- end
48
+ self.class.new(filtered_attrs)
49
+ end
59
50
 
60
- def extraneous_keys(value)
61
- allowed_keys = attributes.map(&:key)
62
- value.keys.reject{ |k| allowed_keys.include?(k) }
63
- end
51
+ Attribute = Struct.new(:key, :value_schema, :optional) do
52
+ def with_wrapped_value_schema(wrapper)
53
+ self.class.new(key, wrapper.wrap(value_schema), optional)
54
+ end
55
+ end
64
56
 
65
- def extraneous_attrs_result(value)
66
- Result.failure(Error.new(
67
- schema: self,
68
- value: value,
69
- symbolic_name: :extraneous_attributes,
70
- vars: extraneous_keys(value),
71
- ))
72
- end
57
+ private
73
58
 
74
- def attr_subresults(value, options)
75
- subresults_by_key = {}
59
+ def missing_keys(value)
60
+ attributes
61
+ .reject(&:optional)
62
+ .map(&:key)
63
+ .reject{ |k| value.has_key?(k) }
64
+ end
76
65
 
77
- @attributes.map do |attr|
78
- if value.has_key?(attr.key)
79
- subresult = attr.value_schema.call(value[attr.key], options)
80
- subresults_by_key[attr.key] = subresult
81
- break if subresult.invalid? && options.fail_fast?
82
- end
83
- end
66
+ def missing_attrs_result(value)
67
+ Result.failure(Error.new(
68
+ schema: self,
69
+ value: value,
70
+ symbolic_name: :missing_attributes,
71
+ vars: {
72
+ missing_keys: missing_keys(value),
73
+ }
74
+ ))
75
+ end
84
76
 
85
- subresults_by_key
86
- end
77
+ def extraneous_keys(value)
78
+ allowed_keys = attributes.map(&:key)
79
+ value.keys.reject{ |k| allowed_keys.include?(k) }
80
+ end
87
81
 
88
- def failure_error(results)
89
- error = {}
82
+ def extraneous_attrs_result(value)
83
+ Result.failure(Error.new(
84
+ schema: self,
85
+ value: value,
86
+ symbolic_name: :extraneous_attributes,
87
+ vars: {
88
+ extraneous_keys: extraneous_keys(value),
89
+ },
90
+ ))
91
+ end
90
92
 
91
- results.each do |key, attr_result|
92
- if attr_result.invalid?
93
- error[key] = attr_result.error
94
- end
95
- end
93
+ def attr_subresults(value, options)
94
+ subresults_by_key = {}
96
95
 
97
- error
96
+ @attributes.map do |attr|
97
+ if value.has_key?(attr.key)
98
+ subresult = attr.value_schema.call(value[attr.key], options)
99
+ subresults_by_key[attr.key] = subresult
100
+ break if subresult.invalid? && options.fail_fast?
98
101
  end
102
+ end
99
103
 
100
- def success_value(subresults)
101
- subresults
102
- .map{ |key, attr_result| [key, attr_result.value] }
103
- .to_h
104
- end
104
+ subresults_by_key
105
+ end
105
106
 
106
- def not_a_hash_result(value)
107
- Result.failure(
108
- Error.new(
109
- schema: self,
110
- value: value,
111
- symbolic_name: :not_a_hash,
112
- )
113
- )
107
+ def failure_error(results)
108
+ error = {}
109
+
110
+ results.each do |key, attr_result|
111
+ if attr_result.invalid?
112
+ error[key] = attr_result.error
114
113
  end
115
114
  end
116
115
 
116
+ error
117
+ end
118
+
119
+ def success_value(subresults)
120
+ subresults
121
+ .map{ |key, attr_result| [key, attr_result.value] }
122
+ .to_h
123
+ end
124
+
125
+ def not_a_hash_result(value)
126
+ Result.failure(
127
+ Error.new(
128
+ schema: self,
129
+ value: value,
130
+ symbolic_name: :not_a_hash,
131
+ )
132
+ )
117
133
  end
118
134
  end
135
+
136
+ end
137
+ end