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
@@ -1,36 +1,123 @@
1
+ require 'docile'
2
+
1
3
  require 'rschema/options'
2
4
  require 'rschema/error'
3
5
  require 'rschema/result'
4
- require 'rschema/schemas/type'
5
- require 'rschema/schemas/maybe'
6
- require 'rschema/schemas/enum'
7
- require 'rschema/schemas/boolean'
8
- require 'rschema/schemas/sum'
9
- require 'rschema/schemas/pipeline'
10
- require 'rschema/schemas/anything'
11
- require 'rschema/schemas/predicate'
12
- require 'rschema/schemas/set'
13
- require 'rschema/schemas/variable_hash'
14
- require 'rschema/schemas/fixed_hash'
15
- require 'rschema/schemas/variable_length_array'
16
- require 'rschema/schemas/fixed_length_array'
6
+ require 'rschema/schemas'
17
7
  require 'rschema/dsl'
18
- require 'rschema/http_coercer'
8
+ require 'rschema/coercers'
9
+ require 'rschema/coercion_wrapper'
19
10
 
11
+ #
12
+ # Schema-based validation and coercion
13
+ #
20
14
  module RSchema
21
- def self.define(&block)
22
- default_dsl.instance_eval(&block)
15
+
16
+ #
17
+ # Runs a block using a DSL.
18
+ #
19
+ # @param dsl [Object] An optional DSL object to run the block with.
20
+ # Uses {RSchema#default_dsl} if nil.
21
+ # @yield Invokes the given block with access to the methods on `dsl`.
22
+ # @return The return value of the given block (usually some kind of schema object)
23
+ #
24
+ # @example Creating a typical fixed hash schema
25
+ # person_schema = RSchema.define do
26
+ # fixed_hash(
27
+ # name: _String,
28
+ # age: _Integer,
29
+ # )
30
+ # end
31
+ #
32
+ def self.define(dsl = nil, &block)
33
+ schema = dsl_eval(dsl, &block)
34
+ Schemas::Convenience.wrap(schema)
23
35
  end
24
36
 
37
+ def self.dsl_eval(dsl = nil, &block)
38
+ Docile::Execution.exec_in_proxy_context(
39
+ dsl || default_dsl,
40
+ Docile::FallbackContextProxy,
41
+ &block
42
+ )
43
+ end
44
+
45
+ #
46
+ # A shortcut for:
47
+ #
48
+ # RSchema.define do
49
+ # fixed_hash(...)
50
+ # end
51
+ #
52
+ # @yield Invokes the given block with access to the methods of the default DSL.
53
+ # @yieldreturn The attributes of the hash schema (the argument to {DSL#fixed_hash}).
54
+ # @return [Schemas::FixedHash]
55
+ #
56
+ # @example A typical schema
57
+ #
58
+ # person_schema = RSchema.define_hash {{
59
+ # name: _String,
60
+ # age: _Integer,
61
+ # }}
62
+ #
25
63
  def self.define_hash(&block)
26
- default_dsl.Hash(define(&block))
64
+ Schemas::Convenience.wrap(
65
+ default_dsl.fixed_hash(dsl_eval(&block))
66
+ )
27
67
  end
28
68
 
69
+ #
70
+ # A shortcut for:
71
+ #
72
+ # RSchema.define do
73
+ # predicate { ... }
74
+ # end
75
+ #
76
+ # @param name [String] An arbitraty name for the predicate schema.
77
+ # @yield [value] Yields a single value.
78
+ # @yieldreturn [Boolean] Truthy if the value is valid, otherwise falsey.
79
+ # @return [Schemas::Predicate]
80
+ #
81
+ # @example A predicate schema that only allows `odd?` objects.
82
+ #
83
+ # odd_schema = RSchema.define_predicate('odd') do |x|
84
+ # x.odd?
85
+ # end
86
+ #
87
+ # @see DSL#predicate
88
+ #
89
+ def self.define_predicate(name = nil, &block)
90
+ Schemas::Convenience.wrap(
91
+ default_dsl.predicate(name, &block)
92
+ )
93
+ end
94
+
95
+ #
96
+ # @return The default DSL object.
97
+ # @see DefaultDSL
98
+ #
29
99
  def self.default_dsl
30
100
  @default_dsl ||= DefaultDSL.new
31
101
  end
32
102
 
103
+ #
104
+ # The class of the default RSchema DSL.
105
+ #
106
+ # By default, this only includes the methods from the {RSchema::DSL} mixin.
107
+ #
108
+ # Your own code and other gems may include modules into this class, in order
109
+ # to add new methods to the default DSL.
110
+ #
33
111
  class DefaultDSL
34
112
  include RSchema::DSL
35
113
  end
114
+
115
+ class Invalid < StandardError
116
+ attr_reader :validation_error
117
+
118
+ def initialize(validation_error)
119
+ super()
120
+ @validation_error = validation_error
121
+ end
122
+ end
36
123
  end
@@ -0,0 +1,3 @@
1
+ Dir.glob(File.join(__dir__, 'coercers/**/*.rb')).each do |path|
2
+ require path
3
+ end
@@ -0,0 +1,40 @@
1
+ module RSchema
2
+ module Coercers
3
+
4
+ class Any
5
+ attr_reader :subcoercers
6
+
7
+ def self.[](*subbuilders)
8
+ Builder.new(subbuilders)
9
+ end
10
+
11
+ def initialize(subcoercers)
12
+ @subcoercers = subcoercers
13
+ end
14
+
15
+ def call(value)
16
+ subcoercers.each do |coercer|
17
+ result = coercer.call(value)
18
+ return result if result.valid?
19
+ end
20
+ Result.failure
21
+ end
22
+
23
+ class Builder
24
+ attr_reader :subbuilders
25
+
26
+ def initialize(subbuilders)
27
+ @subbuilders = subbuilders
28
+ end
29
+
30
+ def build(schema)
31
+ subcoercers = subbuilders.map do |builder|
32
+ builder.build(schema)
33
+ end
34
+ Any.new(subcoercers)
35
+ end
36
+ end
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,30 @@
1
+ module RSchema
2
+ module Coercers
3
+
4
+ module Boolean
5
+ extend self
6
+
7
+ TRUTHY_STRINGS = ['on', '1', 'true', 'yes']
8
+ FALSEY_STRINGS = ['off', '0', 'false', 'no']
9
+
10
+ def build(schema)
11
+ self
12
+ end
13
+
14
+ def call(value)
15
+ case value
16
+ when true, false then Result.success(value)
17
+ when nil then Result.success(false)
18
+ when String
19
+ case
20
+ when TRUTHY_STRINGS.include?(value.downcase) then Result.success(true)
21
+ when FALSEY_STRINGS.include?(value.downcase) then Result.success(false)
22
+ else Result.failure
23
+ end
24
+ else Result.failure
25
+ end
26
+ end
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,41 @@
1
+ module RSchema
2
+ module Coercers
3
+
4
+ class Chain
5
+ attr_reader :subcoercers
6
+
7
+ def self.[](*subbuilders)
8
+ Builder.new(subbuilders)
9
+ end
10
+
11
+ def initialize(subcoercers)
12
+ @subcoercers = subcoercers
13
+ end
14
+
15
+ def call(value)
16
+ result = Result.success(value)
17
+ subcoercers.each do |coercer|
18
+ result = coercer.call(result.value)
19
+ break if result.invalid?
20
+ end
21
+ result
22
+ end
23
+
24
+ class Builder
25
+ attr_reader :subbuilders
26
+
27
+ def initialize(subbuilders)
28
+ @subbuilders = subbuilders
29
+ end
30
+
31
+ def build(schema)
32
+ subcoercers = subbuilders.map do |builder|
33
+ builder.build(schema)
34
+ end
35
+ Chain.new(subcoercers)
36
+ end
37
+ end
38
+ end
39
+
40
+ end
41
+ end
@@ -0,0 +1,25 @@
1
+ module RSchema
2
+ module Coercers
3
+
4
+ module Date
5
+ extend self
6
+
7
+ def build(schema)
8
+ self
9
+ end
10
+
11
+ def call(value)
12
+ case value
13
+ when ::Date
14
+ Result.success(value)
15
+ when ::String
16
+ date = ::Date.parse(value) rescue nil
17
+ date ? Result.success(date) : Result.failure
18
+ else
19
+ Result.failure
20
+ end
21
+ end
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,62 @@
1
+ require 'set'
2
+
3
+ module RSchema
4
+ module Coercers
5
+ module FixedHash
6
+
7
+ # The HTTP standard says that when a form is submitted, all unchecked
8
+ # check boxes will _not_ be sent to the server. That is, they will not
9
+ # be present at all in the params hash.
10
+ #
11
+ # This class coerces these missing values into `false`.
12
+ class DefaultBooleansToFalse
13
+ attr_reader :hash_attributes
14
+
15
+ def self.build(schema)
16
+ new(schema)
17
+ end
18
+
19
+ def initialize(fixed_hash_schema)
20
+ #TODO: make fixed hash attributes frozen, and eliminate dup
21
+ @hash_attributes = fixed_hash_schema.attributes.map(&:dup)
22
+ end
23
+
24
+ def call(value)
25
+ Result.success(default_bools_to_false(value))
26
+ end
27
+
28
+ private
29
+ def default_bools_to_false(hash)
30
+ missing_keys = keys_for_bool_defaulting - hash.keys
31
+
32
+ if missing_keys.any?
33
+ defaults = missing_keys.map{ |k| [k, false] }.to_h
34
+ hash.merge(defaults)
35
+ else
36
+ hash # no coercion necessary
37
+ end
38
+ end
39
+
40
+ def keys_for_bool_defaulting
41
+ @keys_for_bool_defaulting ||= Set.new(
42
+ hash_attributes
43
+ .reject(&:optional)
44
+ .select { |attr| is_bool_schema?(attr.value_schema) }
45
+ .map(&:key)
46
+ )
47
+ end
48
+
49
+ def is_bool_schema?(schema)
50
+ # dig through all the coercers
51
+ non_coercer = schema
52
+ while non_coercer.is_a?(Schemas::Coercer)
53
+ non_coercer = non_coercer.subschema
54
+ end
55
+
56
+ non_coercer.is_a?(Schemas::Boolean)
57
+ end
58
+ end
59
+
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,42 @@
1
+ module RSchema
2
+ module Coercers
3
+ module FixedHash
4
+
5
+ class RemoveExtraneousAttributes
6
+ attr_reader :hash_attributes
7
+
8
+ def self.build(schema)
9
+ new(schema)
10
+ end
11
+
12
+ def initialize(fixed_hash_schema)
13
+ #TODO: make fixed hash attributes frozen, and eliminate dup
14
+ @hash_attributes = fixed_hash_schema.attributes.map(&:dup)
15
+ end
16
+
17
+ def call(value)
18
+ Result.success(remove_extraneous_elements(value))
19
+ end
20
+
21
+ private
22
+
23
+ def remove_extraneous_elements(hash)
24
+ keys_to_remove = hash.keys - valid_keys
25
+
26
+ if keys_to_remove.any?
27
+ hash.dup.tap do |stripped_hash|
28
+ keys_to_remove.each { |k| stripped_hash.delete(k) }
29
+ end
30
+ else
31
+ hash
32
+ end
33
+ end
34
+
35
+ def valid_keys
36
+ @valid_keys ||= hash_attributes.map(&:key)
37
+ end
38
+ end
39
+
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,62 @@
1
+ require 'set'
2
+
3
+ module RSchema
4
+ module Coercers
5
+ module FixedHash
6
+
7
+ class SymbolizeKeys
8
+ attr_reader :hash_attributes
9
+
10
+ def self.build(schema)
11
+ new(schema)
12
+ end
13
+
14
+ def initialize(fixed_hash_schema)
15
+ #TODO: make fixed hash attributes frozen, and eliminate dup
16
+ @hash_attributes = fixed_hash_schema.attributes.map(&:dup)
17
+ end
18
+
19
+ def call(value)
20
+ Result.success(symbolize_keys(value))
21
+ end
22
+
23
+ private
24
+
25
+ def symbolize_keys(hash)
26
+ keys = keys_to_symbolize(hash)
27
+ if keys.any?
28
+ hash.dup.tap do |new_hash|
29
+ keys.each { |k| new_hash[k.to_sym] = new_hash.delete(k) }
30
+ end
31
+ else
32
+ hash
33
+ end
34
+ end
35
+
36
+ def keys_to_symbolize(hash)
37
+ non_string_keys = Set.new(hash.keys) - string_keys
38
+ non_string_keys.intersection(symbol_keys_as_strings)
39
+ end
40
+
41
+ def symbol_keys_as_strings
42
+ @symbol_keys_as_strings ||= Set.new(
43
+ all_keys
44
+ .select{ |k| k.is_a?(::Symbol) }
45
+ .map(&:to_s)
46
+ )
47
+ end
48
+
49
+ def string_keys
50
+ @string_keys ||= Set.new(
51
+ all_keys.select { |k| k.is_a?(::String) }
52
+ )
53
+ end
54
+
55
+ def all_keys
56
+ @all_keys ||= hash_attributes.map(&:key)
57
+ end
58
+ end
59
+
60
+ end
61
+ end
62
+ end