dry-schema 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +3 -0
  3. data/LICENSE +20 -0
  4. data/README.md +21 -0
  5. data/config/errors.yml +91 -0
  6. data/lib/dry-schema.rb +1 -0
  7. data/lib/dry/schema.rb +51 -0
  8. data/lib/dry/schema/compiler.rb +31 -0
  9. data/lib/dry/schema/config.rb +52 -0
  10. data/lib/dry/schema/constants.rb +13 -0
  11. data/lib/dry/schema/dsl.rb +382 -0
  12. data/lib/dry/schema/extensions.rb +3 -0
  13. data/lib/dry/schema/extensions/monads.rb +18 -0
  14. data/lib/dry/schema/json.rb +16 -0
  15. data/lib/dry/schema/key.rb +166 -0
  16. data/lib/dry/schema/key_coercer.rb +37 -0
  17. data/lib/dry/schema/key_map.rb +133 -0
  18. data/lib/dry/schema/macros.rb +6 -0
  19. data/lib/dry/schema/macros/core.rb +51 -0
  20. data/lib/dry/schema/macros/dsl.rb +74 -0
  21. data/lib/dry/schema/macros/each.rb +18 -0
  22. data/lib/dry/schema/macros/filled.rb +24 -0
  23. data/lib/dry/schema/macros/hash.rb +46 -0
  24. data/lib/dry/schema/macros/key.rb +137 -0
  25. data/lib/dry/schema/macros/maybe.rb +37 -0
  26. data/lib/dry/schema/macros/optional.rb +17 -0
  27. data/lib/dry/schema/macros/required.rb +17 -0
  28. data/lib/dry/schema/macros/value.rb +41 -0
  29. data/lib/dry/schema/message.rb +103 -0
  30. data/lib/dry/schema/message_compiler.rb +193 -0
  31. data/lib/dry/schema/message_compiler/visitor_opts.rb +30 -0
  32. data/lib/dry/schema/message_set.rb +123 -0
  33. data/lib/dry/schema/messages.rb +42 -0
  34. data/lib/dry/schema/messages/abstract.rb +143 -0
  35. data/lib/dry/schema/messages/i18n.rb +60 -0
  36. data/lib/dry/schema/messages/namespaced.rb +53 -0
  37. data/lib/dry/schema/messages/yaml.rb +82 -0
  38. data/lib/dry/schema/params.rb +16 -0
  39. data/lib/dry/schema/predicate.rb +80 -0
  40. data/lib/dry/schema/predicate_inferrer.rb +49 -0
  41. data/lib/dry/schema/predicate_registry.rb +38 -0
  42. data/lib/dry/schema/processor.rb +151 -0
  43. data/lib/dry/schema/result.rb +164 -0
  44. data/lib/dry/schema/rule_applier.rb +45 -0
  45. data/lib/dry/schema/trace.rb +103 -0
  46. data/lib/dry/schema/type_registry.rb +42 -0
  47. data/lib/dry/schema/types.rb +12 -0
  48. data/lib/dry/schema/value_coercer.rb +27 -0
  49. data/lib/dry/schema/version.rb +5 -0
  50. metadata +255 -0
@@ -0,0 +1,3 @@
1
+ Dry::Schema.register_extension(:monads) do
2
+ require 'dry/schema/extensions/monads'
3
+ end
@@ -0,0 +1,18 @@
1
+ require 'dry/monads/result'
2
+
3
+ module Dry
4
+ module Schema
5
+ class Result
6
+ include Dry::Monads::Result::Mixin
7
+
8
+ def to_monad(options = EMPTY_HASH)
9
+ if success?
10
+ Success(output)
11
+ else
12
+ Failure(messages(options))
13
+ end
14
+ end
15
+ alias_method :to_result, :to_monad
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,16 @@
1
+ require 'dry/schema/processor'
2
+
3
+ module Dry
4
+ module Schema
5
+ # JSON schema type
6
+ #
7
+ # @see Processor
8
+ # @see Schema.JSON
9
+ #
10
+ # @api public
11
+ class JSON < Processor
12
+ config.key_map_type = :stringified
13
+ config.type_registry = config.type_registry.namespaced(:json)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,166 @@
1
+ module Dry
2
+ module Schema
3
+ # Key objects used by key maps
4
+ #
5
+ # @api public
6
+ class Key
7
+ extend Dry::Core::Cache
8
+
9
+ DEFAULT_COERCER = :itself.to_proc.freeze
10
+
11
+ include Dry.Equalizer(:name, :coercer)
12
+
13
+ # @!attribute[r] id
14
+ # @return [Symbol] The key identifier
15
+ # @api public
16
+ attr_reader :id
17
+
18
+ # @!attribute[r] name
19
+ # @return [Symbol, String, Object] The actual key name expected in an input hash
20
+ # @api public
21
+ attr_reader :name
22
+
23
+ # @!attribute[r] id
24
+ # @return [Proc, #call] A key name coercer function
25
+ # @api public
26
+ attr_reader :coercer
27
+
28
+ # @api private
29
+ def self.[](name, **opts)
30
+ new(name, **opts)
31
+ end
32
+
33
+ # @api private
34
+ def self.new(*args)
35
+ fetch_or_store(*args) { super }
36
+ end
37
+
38
+ # @api private
39
+ def initialize(id, name: id, coercer: DEFAULT_COERCER)
40
+ @id = id
41
+ @name = name
42
+ @coercer = coercer
43
+ end
44
+
45
+ # @api private
46
+ def read(source)
47
+ if source.key?(name)
48
+ yield(source[name])
49
+ elsif source.key?(coerced_name)
50
+ yield(source[coerced_name])
51
+ end
52
+ end
53
+
54
+ # @api private
55
+ def write(source, target)
56
+ read(source) { |value| target[coerced_name] = value }
57
+ end
58
+
59
+ # @api private
60
+ def coercible(&coercer)
61
+ new(coercer: coercer)
62
+ end
63
+
64
+ # @api private
65
+ def stringified
66
+ new(name: name.to_s)
67
+ end
68
+
69
+ # @api private
70
+ def new(new_opts = EMPTY_HASH)
71
+ self.class.new(id, { name: name, coercer: coercer }.merge(new_opts))
72
+ end
73
+
74
+ # @api private
75
+ def dump
76
+ name
77
+ end
78
+
79
+ private
80
+
81
+ # @api private
82
+ def coerced_name
83
+ @__coerced_name__ ||= coercer[name]
84
+ end
85
+ end
86
+
87
+ # A specialized key type which handles nested hashes
88
+ #
89
+ # @api private
90
+ class Key::Hash < Key
91
+ include Dry.Equalizer(:name, :members, :coercer)
92
+
93
+ # @api private
94
+ attr_reader :members
95
+
96
+ # @api private
97
+ def initialize(id, members:, **opts)
98
+ super(id, **opts)
99
+ @members = members
100
+ end
101
+
102
+ # @api private
103
+ def read(source)
104
+ super if source.is_a?(::Hash)
105
+ end
106
+
107
+ def write(source, target)
108
+ read(source) { |value|
109
+ target[coerced_name] = value.is_a?(::Hash) ? members.write(value) : value
110
+ }
111
+ end
112
+
113
+ # @api private
114
+ def coercible(&coercer)
115
+ new(coercer: coercer, members: members.coercible(&coercer))
116
+ end
117
+
118
+ # @api private
119
+ def stringified
120
+ new(name: name.to_s, members: members.stringified)
121
+ end
122
+
123
+ # @api private
124
+ def dump
125
+ { name => members.map(&:dump) }
126
+ end
127
+ end
128
+
129
+ # A specialized key type which handles nested arrays
130
+ #
131
+ # @api private
132
+ class Key::Array < Key
133
+ include Dry.Equalizer(:name, :member, :coercer)
134
+
135
+ attr_reader :member
136
+
137
+ # @api private
138
+ def initialize(id, member:, **opts)
139
+ super(id, **opts)
140
+ @member = member
141
+ end
142
+
143
+ # @api private
144
+ def write(source, target)
145
+ read(source) { |value|
146
+ target[coerced_name] = value.is_a?(::Array) ? value.map { |el| member.write(el) } : value
147
+ }
148
+ end
149
+
150
+ # @api private
151
+ def coercible(&coercer)
152
+ new(coercer: coercer, member: member.coercible(&coercer))
153
+ end
154
+
155
+ # @api private
156
+ def stringified
157
+ new(name: name.to_s, member: member.stringified)
158
+ end
159
+
160
+ # @api private
161
+ def dump
162
+ [name, member.dump]
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,37 @@
1
+ require 'dry/core/cache'
2
+
3
+ module Dry
4
+ module Schema
5
+ # Coerces keys in a hash using provided coercer function
6
+ #
7
+ # @api private
8
+ class KeyCoercer
9
+ extend Dry::Core::Cache
10
+
11
+ TO_SYM = :to_sym.to_proc.freeze
12
+
13
+ attr_reader :key_map, :coercer
14
+
15
+ # @api private
16
+ def self.new(*args, &coercer)
17
+ fetch_or_store(*args, coercer) { super(*args, &coercer) }
18
+ end
19
+
20
+ # @api private
21
+ def self.symbolized(*args)
22
+ new(*args, &TO_SYM)
23
+ end
24
+
25
+ # @api private
26
+ def initialize(key_map, &coercer)
27
+ @key_map = key_map.coercible(&coercer)
28
+ end
29
+
30
+ # @api private
31
+ def call(source)
32
+ key_map.write(Hash(source))
33
+ end
34
+ alias_method :[], :call
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,133 @@
1
+ require 'dry/equalizer'
2
+ require 'dry/core/cache'
3
+ require 'dry/schema/constants'
4
+ require 'dry/schema/key'
5
+
6
+ module Dry
7
+ module Schema
8
+ # Represents a list of keys defined by the DSL
9
+ #
10
+ # KeyMap objects expose an API for introspecting schema keys and the ability
11
+ # to rebuild an input hash using configured coercer function.
12
+ #
13
+ # Instances of this class are used as the very first step by the schema processors.
14
+ #
15
+ # @api public
16
+ class KeyMap
17
+ extend Dry::Core::Cache
18
+
19
+ include Dry.Equalizer(:keys)
20
+ include Enumerable
21
+
22
+ # @!attribute[r] keys
23
+ # @return [Array<Key>] A list of defined key objects
24
+ attr_reader :keys
25
+
26
+ # Coerce a list of key specs into a key map
27
+ #
28
+ # @example
29
+ # KeyMap[:id, :name]
30
+ # KeyMap[:title, :artist, tags: [:name]]
31
+ # KeyMap[:title, :artist, [:tags]]
32
+ #
33
+ # @return [KeyMap]
34
+ #
35
+ # @api public
36
+ def self.[](*keys)
37
+ new(keys)
38
+ end
39
+
40
+ # Build new, or returned a cached instance of a key map
41
+ #
42
+ # @param [Array<Symbol, Array, Hash<Symbol=>Array>>
43
+ #
44
+ # @return [KeyMap]
45
+ def self.new(*args)
46
+ fetch_or_store(*args) { super }
47
+ end
48
+
49
+ # Set key objects
50
+ #
51
+ # @api private
52
+ def initialize(keys)
53
+ @keys = keys.map { |key|
54
+ case key
55
+ when Hash
56
+ root, rest = key.flatten
57
+ Key::Hash[root, members: KeyMap[*rest]]
58
+ when Array
59
+ root, rest = key
60
+ Key::Array[root, member: KeyMap[*rest]]
61
+ when Key
62
+ key
63
+ else
64
+ Key[key]
65
+ end
66
+ }
67
+ end
68
+
69
+ # Write a new hash based on the source hash
70
+ #
71
+ # @param [Hash] source The source hash
72
+ # @param [Hash] target The target hash
73
+ #
74
+ # @return [Hash]
75
+ #
76
+ # @api public
77
+ def write(source, target = EMPTY_HASH.dup)
78
+ each { |key| key.write(source, target) }
79
+ target
80
+ end
81
+
82
+ # Return a new key map that is configured to coerce keys using provided coercer function
83
+ #
84
+ # @return [KeyMap]
85
+ #
86
+ # @api public
87
+ def coercible(&coercer)
88
+ self.class.new(map { |key| key.coercible(&coercer) })
89
+ end
90
+
91
+ # Return a new key map with stringified keys
92
+ #
93
+ # A stringified key map is suitable for reading hashes with string keys
94
+ #
95
+ # @return [KeyMap]
96
+ #
97
+ # @api public
98
+ def stringified
99
+ self.class.new(map(&:stringified))
100
+ end
101
+
102
+ # Iterate over keys
103
+ #
104
+ # @api public
105
+ def each(&block)
106
+ keys.each(&block)
107
+ end
108
+
109
+ # Return a new key map merged with the provided one
110
+ #
111
+ # @param [KeyMap, Array] other Either a key map or an array with key specs
112
+ #
113
+ # @return [KeyMap]
114
+ def +(other)
115
+ self.class.new(keys + other.to_a)
116
+ end
117
+
118
+ # Return a string representation of a key map
119
+ #
120
+ # @return [String]
121
+ def inspect
122
+ "#<#{self.class}[#{keys.map(&:dump).map(&:inspect).join(", ")}]>"
123
+ end
124
+
125
+ # Dump keys to their spec format
126
+ #
127
+ # @return [Array]
128
+ def dump
129
+ keys.map(&:dump)
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,6 @@
1
+ require 'dry/schema/macros/each'
2
+ require 'dry/schema/macros/filled'
3
+ require 'dry/schema/macros/hash'
4
+ require 'dry/schema/macros/maybe'
5
+ require 'dry/schema/macros/optional'
6
+ require 'dry/schema/macros/required'
@@ -0,0 +1,51 @@
1
+ require 'dry/initializer'
2
+
3
+ require 'dry/schema/constants'
4
+ require 'dry/schema/compiler'
5
+ require 'dry/schema/trace'
6
+
7
+ module Dry
8
+ module Schema
9
+ module Macros
10
+ # Abstract macro class
11
+ #
12
+ # @api private
13
+ class Core
14
+ extend Dry::Initializer
15
+
16
+ # @api private
17
+ option :name, default: proc { nil }, optional: true
18
+
19
+ # @api private
20
+ option :compiler, default: proc { Compiler.new }
21
+
22
+ # @api private
23
+ option :trace, default: proc { Trace.new }
24
+
25
+ # @api private
26
+ option :schema_dsl, optional: true
27
+
28
+ # @api private
29
+ def new(options = EMPTY_HASH)
30
+ self.class.new({ name: name, compiler: compiler, schema_dsl: schema_dsl }.merge(options))
31
+ end
32
+
33
+ # @api private
34
+ def to_rule
35
+ compiler.visit(to_ast)
36
+ end
37
+
38
+ # @api private
39
+ def to_ast(*)
40
+ trace.to_ast
41
+ end
42
+ alias_method :ast, :to_ast
43
+
44
+ # @api private
45
+ def operation
46
+ raise NotImplementedError
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,74 @@
1
+ require 'dry/logic/operators'
2
+
3
+ require 'dry/schema/macros/core'
4
+
5
+ module Dry
6
+ module Schema
7
+ module Macros
8
+ # Macro specialization used within the DSL
9
+ #
10
+ # @api public
11
+ class DSL < Core
12
+ include Dry::Logic::Operators
13
+
14
+ undef :eql?
15
+ undef :nil?
16
+
17
+ # @api private
18
+ option :chain, default: -> { true }
19
+
20
+ # Specify predicates that should be applied to a value
21
+ #
22
+ # @api public
23
+ def value(*predicates, **opts, &block)
24
+ append_macro(Macros::Value) do |macro|
25
+ macro.call(*predicates, **opts, &block)
26
+ end
27
+ end
28
+
29
+ # Prepends `:filled?` predicate
30
+ #
31
+ # @api public
32
+ def filled(*args, &block)
33
+ append_macro(Macros::Filled) do |macro|
34
+ macro.call(*args, &block)
35
+ end
36
+ end
37
+
38
+ # Specify a nested schema
39
+ #
40
+ # @api public
41
+ def schema(*args, &block)
42
+ append_macro(Macros::Hash) do |macro|
43
+ macro.call(*args, &block)
44
+ end
45
+ end
46
+
47
+ # Specify predicates that should be applied to each element of an array
48
+ #
49
+ # @api public
50
+ def each(*args, &block)
51
+ append_macro(Macros::Each) do |macro|
52
+ macro.value(*args, &block)
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ # @api private
59
+ def append_macro(macro_type, &block)
60
+ macro = macro_type.new(schema_dsl: schema_dsl, name: name)
61
+
62
+ yield(macro)
63
+
64
+ if chain
65
+ trace << macro
66
+ self
67
+ else
68
+ macro
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end