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,60 @@
1
+ require 'i18n'
2
+ require 'dry/schema/messages/abstract'
3
+
4
+ module Dry
5
+ module Schema
6
+ # I18n message backend
7
+ #
8
+ # @api public
9
+ class Messages::I18n < Messages::Abstract
10
+ attr_reader :t
11
+
12
+ ::I18n.load_path.concat(config.paths)
13
+
14
+ # @api private
15
+ def initialize
16
+ super
17
+ @t = I18n.method(:t)
18
+ end
19
+
20
+ # Get a message for the given key and its options
21
+ #
22
+ # @param [Symbol] key
23
+ # @param [Hash] options
24
+ #
25
+ # @return [String]
26
+ #
27
+ # @api public
28
+ def get(key, options = {})
29
+ t.(key, options) if key
30
+ end
31
+
32
+ # Check if given key is defined
33
+ #
34
+ # @return [Boolean]
35
+ #
36
+ # @api public
37
+ def key?(key, options)
38
+ ::I18n.exists?(key, options.fetch(:locale, default_locale)) ||
39
+ ::I18n.exists?(key, I18n.default_locale)
40
+ end
41
+
42
+ # Merge messages from an additional path
43
+ #
44
+ # @param [String] path
45
+ #
46
+ # @return [Messages::I18n]
47
+ #
48
+ # @api public
49
+ def merge(path)
50
+ ::I18n.load_path << path
51
+ self
52
+ end
53
+
54
+ # @api private
55
+ def default_locale
56
+ I18n.locale || I18n.default_locale || super
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,53 @@
1
+ module Dry
2
+ module Schema
3
+ module Messages
4
+ # Namespaced messages backend
5
+ #
6
+ # @api public
7
+ class Namespaced <Dry::Schema::Messages::Abstract
8
+ # @api private
9
+ attr_reader :namespace
10
+
11
+ # @api private
12
+ attr_reader :messages
13
+
14
+ # @api private
15
+ attr_reader :root
16
+
17
+ # @api private
18
+ def initialize(namespace, messages)
19
+ super()
20
+ @namespace = namespace
21
+ @messages = messages
22
+ @root = messages.root
23
+ end
24
+
25
+ # Get a message for the given key and its options
26
+ #
27
+ # @param [Symbol] key
28
+ # @param [Hash] options
29
+ #
30
+ # @return [String]
31
+ #
32
+ # @api public
33
+ def get(key, options = {})
34
+ messages.get(key, options)
35
+ end
36
+
37
+ # Check if given key is defined
38
+ #
39
+ # @return [Boolean]
40
+ #
41
+ # @api public
42
+ def key?(key, *args)
43
+ messages.key?(key, *args)
44
+ end
45
+
46
+ # @api private
47
+ def lookup_paths(tokens)
48
+ super(tokens.merge(root: "#{root}.rules.#{namespace}")) + super
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,82 @@
1
+ require 'yaml'
2
+ require 'pathname'
3
+
4
+ require 'dry/equalizer'
5
+ require 'dry/schema/messages/abstract'
6
+
7
+ module Dry
8
+ module Schema
9
+ # Plain YAML message backend
10
+ #
11
+ # @api public
12
+ class Messages::YAML < Messages::Abstract
13
+ include Dry::Equalizer(:data)
14
+
15
+ attr_reader :data
16
+
17
+ # @api private
18
+ configure do |config|
19
+ config.root = '%{locale}.errors'.freeze
20
+ end
21
+
22
+ # @api private
23
+ def self.load(paths = config.paths)
24
+ new(paths.map { |path| load_file(path) }.reduce(:merge))
25
+ end
26
+
27
+ # @api private
28
+ def self.load_file(path)
29
+ flat_hash(YAML.load_file(path))
30
+ end
31
+
32
+ # @api private
33
+ def self.flat_hash(h, f = [], g = {})
34
+ return g.update(f.join('.'.freeze) => h) unless h.is_a? Hash
35
+ h.each { |k, r| flat_hash(r, f + [k], g) }
36
+ g
37
+ end
38
+
39
+ # @api private
40
+ def initialize(data)
41
+ super()
42
+ @data = data
43
+ end
44
+
45
+ # Get a message for the given key and its options
46
+ #
47
+ # @param [Symbol] key
48
+ # @param [Hash] options
49
+ #
50
+ # @return [String]
51
+ #
52
+ # @api public
53
+ def get(key, options = {})
54
+ data[key % { locale: options.fetch(:locale, default_locale) }]
55
+ end
56
+
57
+ # Check if given key is defined
58
+ #
59
+ # @return [Boolean]
60
+ #
61
+ # @api public
62
+ def key?(key, options = {})
63
+ data.key?(key % { locale: options.fetch(:locale, default_locale) })
64
+ end
65
+
66
+ # Merge messages from an additional path
67
+ #
68
+ # @param [String] path
69
+ #
70
+ # @return [Messages::I18n]
71
+ #
72
+ # @api public
73
+ def merge(overrides)
74
+ if overrides.is_a?(Hash)
75
+ self.class.new(data.merge(self.class.flat_hash(overrides)))
76
+ else
77
+ self.class.new(data.merge(Messages::YAML.load_file(overrides)))
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,16 @@
1
+ require 'dry/schema/processor'
2
+
3
+ module Dry
4
+ module Schema
5
+ # Params schema type
6
+ #
7
+ # @see Processor
8
+ # @see Schema.Params
9
+ #
10
+ # @api public
11
+ class Params < Processor
12
+ config.key_map_type = :stringified
13
+ config.type_registry = config.type_registry.namespaced(:params)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,80 @@
1
+ require 'dry/equalizer'
2
+ require 'dry/logic/operators'
3
+
4
+ module Dry
5
+ module Schema
6
+ # Predicate objects used within the DSL
7
+ #
8
+ # @api public
9
+ class Predicate
10
+ # A negated predicate
11
+ #
12
+ # @api private
13
+ class Negation
14
+ # @api private
15
+ attr_reader :predicate
16
+
17
+ # @api private
18
+ def initialize(predicate)
19
+ @predicate = predicate
20
+ end
21
+
22
+ # @api private
23
+ def to_ast(*args)
24
+ [:not, predicate.to_ast(*args)]
25
+ end
26
+ alias_method :ast, :to_ast
27
+ end
28
+
29
+ include Dry::Logic::Operators
30
+ include Dry::Equalizer(:name, :args, :block)
31
+
32
+ # @api private
33
+ attr_reader :compiler
34
+
35
+ # @api private
36
+ attr_reader :name
37
+
38
+ # @api private
39
+ attr_reader :args
40
+
41
+ # @api private
42
+ attr_reader :block
43
+
44
+ # @api private
45
+ def initialize(compiler, name, args, block)
46
+ @compiler = compiler
47
+ @name = name
48
+ @args = args
49
+ @block = block
50
+ end
51
+
52
+ # Negate a predicate
53
+ #
54
+ # @return [Negation]
55
+ #
56
+ # @api public
57
+ def !
58
+ Negation.new(self)
59
+ end
60
+
61
+ # @api private
62
+ def ensure_valid
63
+ if compiler.predicates[name].arity - 1 != args.size
64
+ raise ArgumentError, "#{name} predicate arity is invalid"
65
+ end
66
+ end
67
+
68
+ # @api private
69
+ def to_rule
70
+ compiler.visit(to_ast)
71
+ end
72
+
73
+ # @api private
74
+ def to_ast(*)
75
+ [:predicate, [name, compiler.predicates.arg_list(name, *args)]]
76
+ end
77
+ alias_method :ast, :to_ast
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,49 @@
1
+ require 'dry/core/cache'
2
+
3
+ module Dry
4
+ module Schema
5
+ # PredicateInferrer is used internally by `Macros::Value`
6
+ # for inferring type-check predicates from type specs.
7
+ #
8
+ # @api private
9
+ class PredicateInferrer
10
+ extend Dry::Core::Cache
11
+
12
+ TYPE_TO_PREDICATE = Hash.new do |hash, type|
13
+ primitive = type.meta[:maybe] ? type.right.primitive : type.primitive
14
+
15
+ if hash.key?(primitive)
16
+ hash[primitive]
17
+ else
18
+ :"#{primitive.name.split('::').last.downcase}?"
19
+ end
20
+ end
21
+
22
+ TYPE_TO_PREDICATE.update(
23
+ FalseClass => :false?,
24
+ Integer => :int?,
25
+ NilClass => :nil?,
26
+ String => :str?,
27
+ TrueClass => :true?
28
+ ).freeze
29
+
30
+ # Infer predicate identifier from the provided type
31
+ #
32
+ # @return [Symbol]
33
+ #
34
+ # @api private
35
+ def self.[](type)
36
+ fetch_or_store(type.hash) {
37
+ predicates =
38
+ if type.is_a?(Dry::Types::Sum) && !type.meta[:maybe]
39
+ [self[type.left], self[type.right]]
40
+ else
41
+ TYPE_TO_PREDICATE[type]
42
+ end
43
+
44
+ Array(predicates).flatten
45
+ }
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,38 @@
1
+ require 'dry/logic/predicates'
2
+
3
+ module Dry
4
+ module Schema
5
+ # A registry with predicate objects from `Dry::Logic::Predicates`
6
+ #
7
+ # @api private
8
+ class PredicateRegistry
9
+ # @api private
10
+ attr_reader :predicates
11
+
12
+ # @api private
13
+ def initialize(predicates = Dry::Logic::Predicates)
14
+ @predicates = predicates
15
+ end
16
+
17
+ # @api private
18
+ def [](name)
19
+ predicates[name]
20
+ end
21
+
22
+ # @api private
23
+ def key?(name)
24
+ predicates.respond_to?(name)
25
+ end
26
+
27
+ # @api private
28
+ def arg_list(name, *values)
29
+ predicate = self[name]
30
+
31
+ predicate
32
+ .parameters
33
+ .map(&:last)
34
+ .zip(values + Array.new(predicate.arity - values.size, Undefined))
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,151 @@
1
+ require 'dry/configurable'
2
+ require 'dry/initializer'
3
+
4
+ require 'dry/schema/type_registry'
5
+ require 'dry/schema/rule_applier'
6
+ require 'dry/schema/key_coercer'
7
+ require 'dry/schema/value_coercer'
8
+
9
+ module Dry
10
+ module Schema
11
+ # Processes input data using objects configured within the DSL
12
+ #
13
+ # Processing is split into 4 main steps:
14
+ #
15
+ # 1. Prepare input hash using a key map
16
+ # 2. Apply pre-coercion filtering rules (optional step, used only when `filter` was used)
17
+ # 3. Apply value coercions based on type specifications
18
+ # 4. Apply rules
19
+ #
20
+ # @see Params
21
+ # @see JSON
22
+ #
23
+ # @api public
24
+ class Processor
25
+ extend Dry::Initializer
26
+ extend Dry::Configurable
27
+
28
+ setting :key_map_type
29
+ setting :type_registry, TypeRegistry.new
30
+
31
+ param :steps, default: -> { EMPTY_ARRAY.dup }
32
+
33
+ # Define a schema for your processor class
34
+ #
35
+ # @see Params
36
+ # @see JSON
37
+ #
38
+ # @return [Class]
39
+ #
40
+ # @api public
41
+ def self.define(&block)
42
+ @__definition__ ||= DSL.new(
43
+ processor_type: self, parent: superclass.definition, **config, &block
44
+ )
45
+ self
46
+ end
47
+
48
+ # Return DSL configured via #define
49
+ #
50
+ # @return [DSL]
51
+ #
52
+ # @api private
53
+ def self.definition
54
+ @__definition__
55
+ end
56
+
57
+ # Build a new processor object
58
+ #
59
+ # @return [Processor]
60
+ #
61
+ # @api public
62
+ def self.new(&block)
63
+ if block
64
+ super.tap(&block)
65
+ elsif definition
66
+ definition.call
67
+ else
68
+ raise ArgumentError, 'Cannot create a schema without a definition'
69
+ end
70
+ end
71
+
72
+ # Append a step
73
+ #
74
+ # @return [Processor]
75
+ #
76
+ # @api private
77
+ def <<(step)
78
+ steps << step
79
+ self
80
+ end
81
+
82
+ # Apply processing steps to the provided input
83
+ #
84
+ # @param [Hash] input
85
+ #
86
+ # @return [Result]
87
+ #
88
+ # @api public
89
+ def call(input)
90
+ Result.new(input, message_compiler: message_compiler) do |result|
91
+ steps.each do |step|
92
+ output = step.(result)
93
+ result.replace(output) if output.is_a?(::Hash)
94
+ end
95
+ end
96
+ end
97
+
98
+ # Return the key map
99
+ #
100
+ # @return [KeyMap]
101
+ #
102
+ # @api public
103
+ def key_map
104
+ @__key_map__ ||= steps.detect { |s| s.is_a?(KeyCoercer) }.key_map
105
+ end
106
+
107
+ # Return the type schema
108
+ #
109
+ # @return [Dry::Types::Safe]
110
+ #
111
+ # @api private
112
+ def type_schema
113
+ @__type_schema__ ||= steps.detect { |s| s.is_a?(ValueCoercer) }.type_schema
114
+ end
115
+
116
+ # Return AST representation of the rules
117
+ #
118
+ # @api private
119
+ def to_ast
120
+ rule_applier.to_ast
121
+ end
122
+
123
+ # Return the message compiler
124
+ #
125
+ # @return [MessageCompiler]
126
+ #
127
+ # @api private
128
+ def message_compiler
129
+ rule_applier.message_compiler
130
+ end
131
+
132
+ # Return the rules from rule applier
133
+ #
134
+ # @return [MessageCompiler]
135
+ #
136
+ # @api private
137
+ def rules
138
+ rule_applier.rules
139
+ end
140
+
141
+ # Return the rule applier
142
+ #
143
+ # @api private
144
+ def rule_applier
145
+ # TODO: make this more explicit through class types
146
+ @__rule_applier__ ||= steps.last
147
+ end
148
+ alias_method :to_rule, :rule_applier
149
+ end
150
+ end
151
+ end