dry-schema 0.1.0

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