rschema 3.0.1.pre3 → 3.0.1.pre4

Sign up to get free protection for your applications and to get access to all the features.
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