dry-validation 0.13.3 → 1.7.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 (207) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +506 -95
  3. data/LICENSE +1 -1
  4. data/README.md +16 -12
  5. data/config/errors.yml +3 -88
  6. data/dry-validation.gemspec +37 -24
  7. data/lib/dry/validation/config.rb +24 -0
  8. data/lib/dry/validation/constants.rb +43 -0
  9. data/lib/dry/validation/contract/class_interface.rb +230 -0
  10. data/lib/dry/validation/contract.rb +171 -0
  11. data/lib/dry/validation/evaluator.rb +224 -0
  12. data/lib/dry/validation/extensions/hints.rb +67 -0
  13. data/lib/dry/validation/extensions/monads.rb +24 -8
  14. data/lib/dry/validation/extensions/predicates_as_macros.rb +75 -0
  15. data/lib/dry/validation/failures.rb +70 -0
  16. data/lib/dry/validation/function.rb +44 -0
  17. data/lib/dry/validation/macro.rb +38 -0
  18. data/lib/dry/validation/macros.rb +104 -0
  19. data/lib/dry/validation/message.rb +80 -80
  20. data/lib/dry/validation/message_set.rb +80 -105
  21. data/lib/dry/validation/messages/resolver.rb +131 -0
  22. data/lib/dry/validation/result.rb +183 -41
  23. data/lib/dry/validation/rule.rb +135 -0
  24. data/lib/dry/validation/schema_ext.rb +19 -0
  25. data/lib/dry/validation/values.rb +104 -0
  26. data/lib/dry/validation/version.rb +3 -1
  27. data/lib/dry/validation.rb +45 -28
  28. data/lib/dry-validation.rb +3 -1
  29. metadata +46 -344
  30. data/.codeclimate.yml +0 -17
  31. data/.gitignore +0 -9
  32. data/.rspec +0 -3
  33. data/.travis.yml +0 -29
  34. data/CONTRIBUTING.md +0 -31
  35. data/Gemfile +0 -25
  36. data/Rakefile +0 -22
  37. data/benchmarks/benchmark_form_invalid.rb +0 -64
  38. data/benchmarks/benchmark_form_valid.rb +0 -64
  39. data/benchmarks/benchmark_schema_invalid_huge.rb +0 -52
  40. data/benchmarks/profile_schema_call_invalid.rb +0 -20
  41. data/benchmarks/profile_schema_call_valid.rb +0 -20
  42. data/benchmarks/profile_schema_definition.rb +0 -14
  43. data/benchmarks/profile_schema_huge_invalid.rb +0 -30
  44. data/benchmarks/profile_schema_messages_invalid.rb +0 -20
  45. data/benchmarks/suite.rb +0 -5
  46. data/examples/basic.rb +0 -15
  47. data/examples/each.rb +0 -14
  48. data/examples/json.rb +0 -12
  49. data/examples/multiple.rb +0 -27
  50. data/examples/nested.rb +0 -22
  51. data/examples/params.rb +0 -11
  52. data/lib/dry/validation/compat/form.rb +0 -67
  53. data/lib/dry/validation/deprecations.rb +0 -24
  54. data/lib/dry/validation/executor.rb +0 -91
  55. data/lib/dry/validation/extensions/struct.rb +0 -32
  56. data/lib/dry/validation/extensions.rb +0 -7
  57. data/lib/dry/validation/input_processor_compiler/json.rb +0 -45
  58. data/lib/dry/validation/input_processor_compiler/params.rb +0 -49
  59. data/lib/dry/validation/input_processor_compiler/sanitizer.rb +0 -47
  60. data/lib/dry/validation/input_processor_compiler.rb +0 -137
  61. data/lib/dry/validation/message_compiler/visitor_opts.rb +0 -37
  62. data/lib/dry/validation/message_compiler.rb +0 -188
  63. data/lib/dry/validation/messages/abstract.rb +0 -119
  64. data/lib/dry/validation/messages/i18n.rb +0 -47
  65. data/lib/dry/validation/messages/namespaced.rb +0 -39
  66. data/lib/dry/validation/messages/yaml.rb +0 -61
  67. data/lib/dry/validation/messages.rb +0 -14
  68. data/lib/dry/validation/predicate_registry.rb +0 -115
  69. data/lib/dry/validation/predicates.rb +0 -19
  70. data/lib/dry/validation/schema/check.rb +0 -37
  71. data/lib/dry/validation/schema/class_interface.rb +0 -190
  72. data/lib/dry/validation/schema/deprecated.rb +0 -30
  73. data/lib/dry/validation/schema/dsl.rb +0 -118
  74. data/lib/dry/validation/schema/form.rb +0 -9
  75. data/lib/dry/validation/schema/json.rb +0 -21
  76. data/lib/dry/validation/schema/key.rb +0 -71
  77. data/lib/dry/validation/schema/params.rb +0 -22
  78. data/lib/dry/validation/schema/rule.rb +0 -202
  79. data/lib/dry/validation/schema/value.rb +0 -211
  80. data/lib/dry/validation/schema.rb +0 -126
  81. data/lib/dry/validation/schema_compiler.rb +0 -81
  82. data/lib/dry/validation/template.rb +0 -66
  83. data/lib/dry/validation/type_specs.rb +0 -70
  84. data/log/.gitkeep +0 -0
  85. data/spec/extensions/monads/result_spec.rb +0 -40
  86. data/spec/extensions/struct/schema_spec.rb +0 -32
  87. data/spec/fixtures/locales/en.yml +0 -8
  88. data/spec/fixtures/locales/pl.yml +0 -22
  89. data/spec/integration/custom_error_messages_spec.rb +0 -54
  90. data/spec/integration/custom_predicates_spec.rb +0 -228
  91. data/spec/integration/hints_spec.rb +0 -170
  92. data/spec/integration/injecting_rules_spec.rb +0 -30
  93. data/spec/integration/json/defining_base_schema_spec.rb +0 -41
  94. data/spec/integration/localized_error_messages_spec.rb +0 -72
  95. data/spec/integration/message_compiler_spec.rb +0 -405
  96. data/spec/integration/messages/i18n_spec.rb +0 -104
  97. data/spec/integration/optional_keys_spec.rb +0 -28
  98. data/spec/integration/params/predicates/array_spec.rb +0 -287
  99. data/spec/integration/params/predicates/empty_spec.rb +0 -263
  100. data/spec/integration/params/predicates/eql_spec.rb +0 -327
  101. data/spec/integration/params/predicates/even_spec.rb +0 -455
  102. data/spec/integration/params/predicates/excluded_from_spec.rb +0 -455
  103. data/spec/integration/params/predicates/excludes_spec.rb +0 -391
  104. data/spec/integration/params/predicates/false_spec.rb +0 -455
  105. data/spec/integration/params/predicates/filled_spec.rb +0 -467
  106. data/spec/integration/params/predicates/format_spec.rb +0 -454
  107. data/spec/integration/params/predicates/gt_spec.rb +0 -519
  108. data/spec/integration/params/predicates/gteq_spec.rb +0 -519
  109. data/spec/integration/params/predicates/included_in_spec.rb +0 -455
  110. data/spec/integration/params/predicates/includes_spec.rb +0 -391
  111. data/spec/integration/params/predicates/key_spec.rb +0 -67
  112. data/spec/integration/params/predicates/lt_spec.rb +0 -519
  113. data/spec/integration/params/predicates/lteq_spec.rb +0 -519
  114. data/spec/integration/params/predicates/max_size_spec.rb +0 -391
  115. data/spec/integration/params/predicates/min_size_spec.rb +0 -391
  116. data/spec/integration/params/predicates/none_spec.rb +0 -265
  117. data/spec/integration/params/predicates/not_eql_spec.rb +0 -327
  118. data/spec/integration/params/predicates/odd_spec.rb +0 -455
  119. data/spec/integration/params/predicates/size/fixed_spec.rb +0 -393
  120. data/spec/integration/params/predicates/size/range_spec.rb +0 -396
  121. data/spec/integration/params/predicates/true_spec.rb +0 -455
  122. data/spec/integration/params/predicates/type_spec.rb +0 -391
  123. data/spec/integration/result_spec.rb +0 -81
  124. data/spec/integration/schema/array_schema_spec.rb +0 -59
  125. data/spec/integration/schema/check_rules_spec.rb +0 -119
  126. data/spec/integration/schema/check_with_nested_el_spec.rb +0 -37
  127. data/spec/integration/schema/check_with_nth_el_spec.rb +0 -25
  128. data/spec/integration/schema/default_settings_spec.rb +0 -11
  129. data/spec/integration/schema/defining_base_schema_spec.rb +0 -41
  130. data/spec/integration/schema/dynamic_predicate_args_spec.rb +0 -43
  131. data/spec/integration/schema/each_with_set_spec.rb +0 -70
  132. data/spec/integration/schema/extending_dsl_spec.rb +0 -27
  133. data/spec/integration/schema/form_spec.rb +0 -236
  134. data/spec/integration/schema/hash_schema_spec.rb +0 -47
  135. data/spec/integration/schema/inheriting_schema_spec.rb +0 -31
  136. data/spec/integration/schema/input_processor_spec.rb +0 -46
  137. data/spec/integration/schema/json/explicit_types_spec.rb +0 -157
  138. data/spec/integration/schema/json_spec.rb +0 -163
  139. data/spec/integration/schema/macros/confirmation_spec.rb +0 -35
  140. data/spec/integration/schema/macros/each_spec.rb +0 -268
  141. data/spec/integration/schema/macros/filled_spec.rb +0 -87
  142. data/spec/integration/schema/macros/input_spec.rb +0 -139
  143. data/spec/integration/schema/macros/maybe_spec.rb +0 -99
  144. data/spec/integration/schema/macros/rule_spec.rb +0 -75
  145. data/spec/integration/schema/macros/value_spec.rb +0 -119
  146. data/spec/integration/schema/macros/when_spec.rb +0 -62
  147. data/spec/integration/schema/nested_schemas_spec.rb +0 -236
  148. data/spec/integration/schema/nested_values_spec.rb +0 -46
  149. data/spec/integration/schema/not_spec.rb +0 -34
  150. data/spec/integration/schema/numbers_spec.rb +0 -19
  151. data/spec/integration/schema/option_with_default_spec.rb +0 -64
  152. data/spec/integration/schema/or_spec.rb +0 -87
  153. data/spec/integration/schema/params/defining_base_schema_spec.rb +0 -41
  154. data/spec/integration/schema/params/explicit_types_spec.rb +0 -195
  155. data/spec/integration/schema/params_spec.rb +0 -234
  156. data/spec/integration/schema/predicate_verification_spec.rb +0 -9
  157. data/spec/integration/schema/predicates/array_spec.rb +0 -295
  158. data/spec/integration/schema/predicates/custom_spec.rb +0 -103
  159. data/spec/integration/schema/predicates/empty_spec.rb +0 -263
  160. data/spec/integration/schema/predicates/eql_spec.rb +0 -327
  161. data/spec/integration/schema/predicates/even_spec.rb +0 -455
  162. data/spec/integration/schema/predicates/excluded_from/array_spec.rb +0 -459
  163. data/spec/integration/schema/predicates/excluded_from/range_spec.rb +0 -459
  164. data/spec/integration/schema/predicates/excludes_spec.rb +0 -391
  165. data/spec/integration/schema/predicates/filled_spec.rb +0 -467
  166. data/spec/integration/schema/predicates/format_spec.rb +0 -455
  167. data/spec/integration/schema/predicates/gt_spec.rb +0 -519
  168. data/spec/integration/schema/predicates/gteq_spec.rb +0 -519
  169. data/spec/integration/schema/predicates/hash_spec.rb +0 -69
  170. data/spec/integration/schema/predicates/included_in/array_spec.rb +0 -459
  171. data/spec/integration/schema/predicates/included_in/range_spec.rb +0 -459
  172. data/spec/integration/schema/predicates/includes_spec.rb +0 -391
  173. data/spec/integration/schema/predicates/key_spec.rb +0 -88
  174. data/spec/integration/schema/predicates/lt_spec.rb +0 -520
  175. data/spec/integration/schema/predicates/lteq_spec.rb +0 -519
  176. data/spec/integration/schema/predicates/max_size_spec.rb +0 -391
  177. data/spec/integration/schema/predicates/min_size_spec.rb +0 -391
  178. data/spec/integration/schema/predicates/none_spec.rb +0 -265
  179. data/spec/integration/schema/predicates/not_eql_spec.rb +0 -391
  180. data/spec/integration/schema/predicates/odd_spec.rb +0 -455
  181. data/spec/integration/schema/predicates/size/fixed_spec.rb +0 -398
  182. data/spec/integration/schema/predicates/size/range_spec.rb +0 -395
  183. data/spec/integration/schema/predicates/type_spec.rb +0 -413
  184. data/spec/integration/schema/reusing_schema_spec.rb +0 -33
  185. data/spec/integration/schema/using_types_spec.rb +0 -135
  186. data/spec/integration/schema/validate_spec.rb +0 -120
  187. data/spec/integration/schema/xor_spec.rb +0 -35
  188. data/spec/integration/schema_builders_spec.rb +0 -17
  189. data/spec/integration/schema_spec.rb +0 -173
  190. data/spec/shared/message_compiler.rb +0 -11
  191. data/spec/shared/predicate_helper.rb +0 -15
  192. data/spec/shared/rule_compiler.rb +0 -8
  193. data/spec/spec_helper.rb +0 -62
  194. data/spec/support/define_struct.rb +0 -25
  195. data/spec/support/matchers.rb +0 -38
  196. data/spec/support/mutant.rb +0 -9
  197. data/spec/support/predicates_integration.rb +0 -7
  198. data/spec/unit/input_processor_compiler/json_spec.rb +0 -283
  199. data/spec/unit/input_processor_compiler/params_spec.rb +0 -328
  200. data/spec/unit/message_compiler/visit_failure_spec.rb +0 -38
  201. data/spec/unit/message_compiler/visit_spec.rb +0 -16
  202. data/spec/unit/message_compiler_spec.rb +0 -7
  203. data/spec/unit/predicate_registry_spec.rb +0 -34
  204. data/spec/unit/schema/key_spec.rb +0 -38
  205. data/spec/unit/schema/rule_spec.rb +0 -42
  206. data/spec/unit/schema/value_spec.rb +0 -131
  207. data/spec/unit/schema_spec.rb +0 -35
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2015 Dryrb Team
3
+ Copyright (c) 2015-2021 dry-rb team
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy of
6
6
  this software and associated documentation files (the "Software"), to deal in
data/README.md CHANGED
@@ -1,25 +1,29 @@
1
+ <!--- this file is synced from dry-rb/template-gem project -->
1
2
  [gem]: https://rubygems.org/gems/dry-validation
2
- [travis]: https://travis-ci.org/dry-rb/dry-validation
3
- [codeclimate]: https://codeclimate.com/github/dry-rb/dry-validation
4
- [coveralls]: https://coveralls.io/r/dry-rb/dry-validation
3
+ [actions]: https://github.com/dry-rb/dry-validation/actions
4
+ [codacy]: https://www.codacy.com/gh/dry-rb/dry-validation
5
+ [chat]: https://dry-rb.zulipchat.com
5
6
  [inchpages]: http://inch-ci.org/github/dry-rb/dry-validation
6
7
 
7
- # dry-validation [![Join the chat at https://gitter.im/dry-rb/chat](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/dry-rb/chat)
8
+ # dry-validation [![Join the chat at https://dry-rb.zulipchat.com](https://img.shields.io/badge/dry--rb-join%20chat-%23346b7a.svg)][chat]
8
9
 
9
10
  [![Gem Version](https://badge.fury.io/rb/dry-validation.svg)][gem]
10
- [![Build Status](https://travis-ci.org/dry-rb/dry-validation.svg?branch=master)][travis]
11
- [![Code Climate](https://codeclimate.com/github/dry-rb/dry-validation/badges/gpa.svg)][codeclimate]
12
- [![Test Coverage](https://codeclimate.com/github/dry-rb/dry-validation/badges/coverage.svg)][codeclimate]
11
+ [![CI Status](https://github.com/dry-rb/dry-validation/workflows/CI/badge.svg)][actions]
12
+ [![Codacy Badge](https://api.codacy.com/project/badge/Grade/f30e3ff5ec304c55a73868cdbf055c67)][codacy]
13
+ [![Codacy Badge](https://api.codacy.com/project/badge/Coverage/f30e3ff5ec304c55a73868cdbf055c67)][codacy]
13
14
  [![Inline docs](http://inch-ci.org/github/dry-rb/dry-validation.svg?branch=master)][inchpages]
14
15
 
15
- ## Status
16
+ ## Links
16
17
 
17
- We're working on a new foundation for dry-validation, called dry-schema. You can see progress in [this PR](https://github.com/dry-rb/dry-schema/pull/3). This will result in a partial rewrite for 1.0.0 version. Currently known bugs/issues will be addressed in 1.0.0, **not in 0.x** due to lack of time. More info about 1.0.0 plans can be found [in this thread](https://discourse.dry-rb.org/t/plans-for-dry-validation-dry-schema-a-new-gem/215/3).
18
+ * [User documentation](https://dry-rb.org/gems/dry-validation)
19
+ * [API documentation](http://rubydoc.info/gems/dry-validation)
18
20
 
19
- ## Links
21
+ ## Supported Ruby versions
22
+
23
+ This library officially supports the following Ruby versions:
20
24
 
21
- * [Documentation](http://dry-rb.org/gems/dry-validation)
22
- * [Guidelines for contributing](CONTRIBUTING.md)
25
+ * MRI `>= 2.6.0`
26
+ * ~~jruby~~ `>= 9.3` (we are waiting for [2.6 support](https://github.com/jruby/jruby/issues/6161))
23
27
 
24
28
  ## License
25
29
 
data/config/errors.yml CHANGED
@@ -1,89 +1,4 @@
1
1
  en:
2
- errors:
3
- or: "or"
4
- array?: "must be an array"
5
-
6
- empty?: "must be empty"
7
-
8
- excludes?: "must not include %{value}"
9
-
10
- excluded_from?:
11
- arg:
12
- default: "must not be one of: %{list}"
13
- range: "must not be one of: %{list_left} - %{list_right}"
14
- exclusion?: "must not be one of: %{list}"
15
-
16
- eql?: "must be equal to %{left}"
17
-
18
- not_eql?: "must not be equal to %{left}"
19
-
20
- filled?: "must be filled"
21
-
22
- format?: "is in invalid format"
23
-
24
- number?: "must be a number"
25
-
26
- odd?: "must be odd"
27
-
28
- even?: "must be even"
29
-
30
- gt?: "must be greater than %{num}"
31
-
32
- gteq?: "must be greater than or equal to %{num}"
33
-
34
- hash?: "must be a hash"
35
-
36
- included_in?:
37
- arg:
38
- default: "must be one of: %{list}"
39
- range: "must be one of: %{list_left} - %{list_right}"
40
- inclusion?: "must be one of: %{list}"
41
-
42
- includes?: "must include %{value}"
43
-
44
- bool?: "must be boolean"
45
-
46
- true?: "must be true"
47
-
48
- false?: "must be false"
49
-
50
- int?: "must be an integer"
51
-
52
- float?: "must be a float"
53
-
54
- decimal?: "must be a decimal"
55
-
56
- date?: "must be a date"
57
-
58
- date_time?: "must be a date time"
59
-
60
- time?: "must be a time"
61
-
62
- key?: "is missing"
63
-
64
- attr?: "is missing"
65
-
66
- lt?: "must be less than %{num}"
67
-
68
- lteq?: "must be less than or equal to %{num}"
69
-
70
- max_size?: "size cannot be greater than %{num}"
71
-
72
- min_size?: "size cannot be less than %{num}"
73
-
74
- none?: "cannot be defined"
75
-
76
- str?: "must be a string"
77
-
78
- type?: "must be %{type}"
79
-
80
- size?:
81
- arg:
82
- default: "size must be %{size}"
83
- range: "size must be within %{size_left} - %{size_right}"
84
-
85
- value:
86
- string:
87
- arg:
88
- default: "length must be %{size}"
89
- range: "length must be within %{size_left} - %{size_right}"
2
+ dry_validation:
3
+ errors:
4
+ acceptance: "must accept %{key}"
@@ -1,28 +1,41 @@
1
- require File.expand_path('../lib/dry/validation/version', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ # this file is synced from dry-rb/template-gem project
4
+
5
+ lib = File.expand_path("lib", __dir__)
6
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
7
+ require "dry/validation/version"
2
8
 
3
9
  Gem::Specification.new do |spec|
4
- spec.name = 'dry-validation'
5
- spec.version = Dry::Validation::VERSION
6
- spec.authors = ['Andy Holland', 'Piotr Solnica']
7
- spec.email = ['andyholland1991@aol.com', 'piotr.solnica@gmail.com']
8
- spec.summary = 'A simple validation library'
9
- spec.homepage = 'https://github.com/dry-rb/dry-validation'
10
- spec.license = 'MIT'
11
-
12
- spec.files = `git ls-files -z`.split("\x0") - ['bin/console']
10
+ spec.name = "dry-validation"
11
+ spec.authors = ["Piotr Solnica"]
12
+ spec.email = ["piotr.solnica@gmail.com"]
13
+ spec.license = "MIT"
14
+ spec.version = Dry::Validation::VERSION.dup
15
+
16
+ spec.summary = "Validation library"
17
+ spec.description = spec.summary
18
+ spec.homepage = "https://dry-rb.org/gems/dry-validation"
19
+ spec.files = Dir["CHANGELOG.md", "LICENSE", "README.md", "dry-validation.gemspec", "lib/**/*", "config/*.yml"]
20
+ spec.bindir = "bin"
13
21
  spec.executables = []
14
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
15
- spec.require_paths = ['lib']
16
- spec.required_ruby_version = ">= 2.4.0"
17
-
18
- spec.add_runtime_dependency 'concurrent-ruby', '~> 1.0'
19
- spec.add_runtime_dependency 'dry-configurable', '~> 0.1', '>= 0.1.3'
20
- spec.add_runtime_dependency 'dry-equalizer', '~> 0.2'
21
- spec.add_runtime_dependency 'dry-logic', '~> 0.5', '>= 0.5.0'
22
- spec.add_runtime_dependency 'dry-types', '~> 0.14.0'
23
- spec.add_runtime_dependency 'dry-core', '~> 0.2', '>= 0.2.1'
24
-
25
- spec.add_development_dependency 'bundler'
26
- spec.add_development_dependency 'rake'
27
- spec.add_development_dependency 'rspec'
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
25
+ spec.metadata["changelog_uri"] = "https://github.com/dry-rb/dry-validation/blob/master/CHANGELOG.md"
26
+ spec.metadata["source_code_uri"] = "https://github.com/dry-rb/dry-validation"
27
+ spec.metadata["bug_tracker_uri"] = "https://github.com/dry-rb/dry-validation/issues"
28
+
29
+ spec.required_ruby_version = ">= 2.6.0"
30
+
31
+ # to update dependencies edit project.yml
32
+ spec.add_runtime_dependency "concurrent-ruby", "~> 1.0"
33
+ spec.add_runtime_dependency "dry-container", "~> 0.7", ">= 0.7.1"
34
+ spec.add_runtime_dependency "dry-core", "~> 0.5", ">= 0.5"
35
+ spec.add_runtime_dependency "dry-initializer", "~> 3.0"
36
+ spec.add_runtime_dependency "dry-schema", "~> 1.8", ">= 1.8.0"
37
+
38
+ spec.add_development_dependency "bundler"
39
+ spec.add_development_dependency "rake"
40
+ spec.add_development_dependency "rspec"
28
41
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/schema/config"
4
+ require "dry/validation/macros"
5
+
6
+ module Dry
7
+ module Validation
8
+ # Configuration for contracts
9
+ #
10
+ # @see Contract#config
11
+ #
12
+ # @api public
13
+ class Config < Schema::Config
14
+ setting :macros, default: Macros::Container.new, constructor: :dup.to_proc
15
+
16
+ # @api private
17
+ def dup
18
+ config = super
19
+ config.macros = macros.dup
20
+ config
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require "dry/core/constants"
5
+
6
+ module Dry
7
+ module Validation
8
+ include Dry::Core::Constants
9
+
10
+ DOT = "."
11
+
12
+ # Root path is used for base errors in hash representation of error messages
13
+ ROOT_PATH = [nil].freeze
14
+
15
+ # Path to the default errors locale file
16
+ DEFAULT_ERRORS_NAMESPACE = "dry_validation"
17
+
18
+ # Path to the default errors locale file
19
+ DEFAULT_ERRORS_PATH = Pathname(__FILE__).join("../../../../config/errors.yml").realpath.freeze
20
+
21
+ # Mapping for block kwarg options used by block_options
22
+ #
23
+ # @see Rule#block_options
24
+ BLOCK_OPTIONS_MAPPINGS = Hash.new { |_, key| key }.update(context: :_context).freeze
25
+
26
+ # Error raised when `rule` specifies one or more keys that the schema doesn't specify
27
+ InvalidKeysError = Class.new(StandardError)
28
+
29
+ # Error raised when a localized message was not found
30
+ MissingMessageError = Class.new(StandardError)
31
+
32
+ # Error raised when trying to define a schema in a contract class that already has a schema
33
+ DuplicateSchemaError = Class.new(StandardError)
34
+
35
+ # Error raised during initialization of a contract that has no schema defined
36
+ SchemaMissingError = Class.new(StandardError) do
37
+ # @api private
38
+ def initialize(klass)
39
+ super("#{klass} cannot be instantiated without a schema defined")
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,230 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/schema"
4
+ require "dry/schema/messages"
5
+ require "dry/schema/path"
6
+ require "dry/schema/key_map"
7
+
8
+ require "dry/validation/constants"
9
+ require "dry/validation/macros"
10
+ require "dry/validation/schema_ext"
11
+
12
+ module Dry
13
+ module Validation
14
+ class Contract
15
+ # Contract's class interface
16
+ #
17
+ # @see Contract
18
+ #
19
+ # @api public
20
+ module ClassInterface
21
+ include Macros::Registrar
22
+
23
+ # @api private
24
+ def inherited(klass)
25
+ super
26
+ klass.instance_variable_set("@config", config.dup)
27
+ end
28
+
29
+ # Configuration
30
+ #
31
+ # @example
32
+ # class MyContract < Dry::Validation::Contract
33
+ # config.messages.backend = :i18n
34
+ # end
35
+ #
36
+ # @return [Config]
37
+ #
38
+ # @api public
39
+ def config
40
+ @config ||= Validation::Config.new
41
+ end
42
+
43
+ # Return macros registered for this class
44
+ #
45
+ # @return [Macros::Container]
46
+ #
47
+ # @api public
48
+ def macros
49
+ config.macros
50
+ end
51
+
52
+ # Define a params schema for your contract
53
+ #
54
+ # This type of schema is suitable for HTTP parameters
55
+ #
56
+ # @return [Dry::Schema::Params,NilClass]
57
+ # @see https://dry-rb.org/gems/dry-schema/params/
58
+ #
59
+ # @api public
60
+ def params(*external_schemas, &block)
61
+ define(:Params, external_schemas, &block)
62
+ end
63
+
64
+ # Define a JSON schema for your contract
65
+ #
66
+ # This type of schema is suitable for JSON data
67
+ #
68
+ # @return [Dry::Schema::JSON,NilClass]
69
+ # @see https://dry-rb.org/gems/dry-schema/json/
70
+ #
71
+ # @api public
72
+ def json(*external_schemas, &block)
73
+ define(:JSON, external_schemas, &block)
74
+ end
75
+
76
+ # Define a plain schema for your contract
77
+ #
78
+ # This type of schema does not offer coercion out of the box
79
+ #
80
+ # @return [Dry::Schema::Processor,NilClass]
81
+ # @see https://dry-rb.org/gems/dry-schema/
82
+ #
83
+ # @api public
84
+ def schema(*external_schemas, &block)
85
+ define(:schema, external_schemas, &block)
86
+ end
87
+
88
+ # Define a rule for your contract
89
+ #
90
+ # @example using a symbol
91
+ # rule(:age) do
92
+ # failure('must be at least 18') if values[:age] < 18
93
+ # end
94
+ #
95
+ # @example using a path to a value and a custom predicate
96
+ # rule('address.street') do
97
+ # failure('please provide a valid street address') if valid_street?(values[:street])
98
+ # end
99
+ #
100
+ # @return [Rule]
101
+ #
102
+ # @api public
103
+ def rule(*keys, &block)
104
+ ensure_valid_keys(*keys) if __schema__
105
+
106
+ Rule.new(keys: keys, block: block).tap do |rule|
107
+ rules << rule
108
+ end
109
+ end
110
+
111
+ # A shortcut that can be used to define contracts that won't be reused or inherited
112
+ #
113
+ # @example
114
+ # my_contract = Dry::Validation::Contract.build do
115
+ # params do
116
+ # required(:name).filled(:string)
117
+ # end
118
+ # end
119
+ #
120
+ # my_contract.call(name: "Jane")
121
+ #
122
+ # @return [Contract]
123
+ #
124
+ # @api public
125
+ def build(options = EMPTY_HASH, &block)
126
+ Class.new(self, &block).new(**options)
127
+ end
128
+
129
+ # @api private
130
+ def __schema__
131
+ @__schema__ if defined?(@__schema__)
132
+ end
133
+
134
+ # Return rules defined in this class
135
+ #
136
+ # @return [Array<Rule>]
137
+ #
138
+ # @api private
139
+ def rules
140
+ @rules ||= EMPTY_ARRAY
141
+ .dup
142
+ .concat(superclass.respond_to?(:rules) ? superclass.rules : EMPTY_ARRAY)
143
+ end
144
+
145
+ # Return messages configured for this class
146
+ #
147
+ # @return [Dry::Schema::Messages]
148
+ #
149
+ # @api private
150
+ def messages
151
+ @messages ||= Schema::Messages.setup(config.messages)
152
+ end
153
+
154
+ private
155
+
156
+ # @api private
157
+ def ensure_valid_keys(*keys)
158
+ valid_paths = key_map.to_dot_notation
159
+ key_paths = key_paths(keys)
160
+
161
+ invalid_keys = key_paths.map { |(key, path)|
162
+ unless valid_paths.any? { |vp| vp.include?(path) || vp.include?("#{path}[]") }
163
+ key
164
+ end
165
+ }.compact.uniq
166
+
167
+ return if invalid_keys.empty?
168
+
169
+ raise InvalidKeysError, <<~STR.strip
170
+ #{name}.rule specifies keys that are not defined by the schema: #{invalid_keys.inspect}
171
+ STR
172
+ end
173
+
174
+ # @api private
175
+ def key_paths(keys)
176
+ keys.map { |key|
177
+ case key
178
+ when Hash
179
+ path = Schema::Path[key]
180
+ if path.multi_value?
181
+ *head, tail = Array(path)
182
+ [key].product(
183
+ tail.map { |el| [*head, *el] }.map { |parts| parts.join(DOT) }
184
+ )
185
+ else
186
+ [[key, path.to_a.join(DOT)]]
187
+ end
188
+ when Array
189
+ [[key, Schema::Path[key].to_a.join(DOT)]]
190
+ else
191
+ [[key, key.to_s]]
192
+ end
193
+ }.flatten(1)
194
+ end
195
+
196
+ # @api private
197
+ def key_map
198
+ __schema__.key_map
199
+ end
200
+
201
+ # @api private
202
+ def core_schema_opts
203
+ {parent: superclass&.__schema__, config: config}
204
+ end
205
+
206
+ # @api private
207
+ def define(method_name, external_schemas, &block)
208
+ return __schema__ if external_schemas.empty? && block.nil?
209
+
210
+ unless __schema__.nil?
211
+ raise ::Dry::Validation::DuplicateSchemaError, "Schema has already been defined"
212
+ end
213
+
214
+ schema_opts = core_schema_opts
215
+
216
+ schema_opts.update(parent: external_schemas) if external_schemas.any?
217
+
218
+ case method_name
219
+ when :schema
220
+ @__schema__ = Schema.define(**schema_opts, &block)
221
+ when :Params
222
+ @__schema__ = Schema.Params(**schema_opts, &block)
223
+ when :JSON
224
+ @__schema__ = Schema.JSON(**schema_opts, &block)
225
+ end
226
+ end
227
+ end
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent/map"
4
+
5
+ require "dry/core/equalizer"
6
+ require "dry/initializer"
7
+ require "dry/schema/path"
8
+
9
+ require "dry/validation/config"
10
+ require "dry/validation/constants"
11
+ require "dry/validation/rule"
12
+ require "dry/validation/evaluator"
13
+ require "dry/validation/messages/resolver"
14
+ require "dry/validation/result"
15
+ require "dry/validation/contract/class_interface"
16
+
17
+ module Dry
18
+ module Validation
19
+ # Contract objects apply rules to input
20
+ #
21
+ # A contract consists of a schema and rules. The schema is applied to the
22
+ # input before rules are applied, this way you can be sure that your rules
23
+ # won't be applied to values that didn't pass schema checks.
24
+ #
25
+ # It's up to you how exactly you're going to separate schema checks from
26
+ # your rules.
27
+ #
28
+ # @example
29
+ # class NewUserContract < Dry::Validation::Contract
30
+ # params do
31
+ # required(:email).filled(:string)
32
+ # required(:age).filled(:integer)
33
+ # optional(:login).maybe(:string, :filled?)
34
+ # optional(:password).maybe(:string, min_size?: 10)
35
+ # optional(:password_confirmation).maybe(:string)
36
+ # end
37
+ #
38
+ # rule(:password) do
39
+ # key.failure('is required') if values[:login] && !values[:password]
40
+ # end
41
+ #
42
+ # rule(:age) do
43
+ # key.failure('must be greater or equal 18') if values[:age] < 18
44
+ # end
45
+ # end
46
+ #
47
+ # new_user_contract = NewUserContract.new
48
+ # new_user_contract.call(email: 'jane@doe.org', age: 21)
49
+ #
50
+ # @api public
51
+ class Contract
52
+ include Dry::Equalizer(:schema, :rules, :messages, inspect: false)
53
+
54
+ extend Dry::Initializer
55
+ extend ClassInterface
56
+
57
+ config.messages.top_namespace = DEFAULT_ERRORS_NAMESPACE
58
+ config.messages.load_paths << DEFAULT_ERRORS_PATH
59
+
60
+ # @!attribute [r] config
61
+ # @return [Config] Contract's configuration object
62
+ # @api public
63
+ option :config, default: -> { self.class.config }
64
+
65
+ # @!attribute [r] macros
66
+ # @return [Macros::Container] Configured macros
67
+ # @see Macros::Container#register
68
+ # @api public
69
+ option :macros, default: -> { config.macros }
70
+
71
+ # @!attribute [r] default_context
72
+ # @return [Hash] Default context for rules
73
+ # @api public
74
+ option :default_context, default: -> { EMPTY_HASH }
75
+
76
+ # @!attribute [r] schema
77
+ # @return [Dry::Schema::Params, Dry::Schema::JSON, Dry::Schema::Processor]
78
+ # @api private
79
+ option :schema, default: -> { self.class.__schema__ || raise(SchemaMissingError, self.class) }
80
+
81
+ # @!attribute [r] rules
82
+ # @return [Hash]
83
+ # @api private
84
+ option :rules, default: -> { self.class.rules }
85
+
86
+ # @!attribute [r] message_resolver
87
+ # @return [Messages::Resolver]
88
+ # @api private
89
+ option :message_resolver, default: -> { Messages::Resolver.new(messages) }
90
+
91
+ # Apply the contract to an input
92
+ #
93
+ # @param [Hash] input The input to validate
94
+ # @param [Hash] context Initial context for rules
95
+ #
96
+ # @return [Result]
97
+ #
98
+ # @api public
99
+ def call(input, context = EMPTY_HASH)
100
+ context_map = Concurrent::Map.new.tap do |map|
101
+ default_context.each { |key, value| map[key] = value }
102
+ context.each { |key, value| map[key] = value }
103
+ end
104
+
105
+ Result.new(schema.(input), context_map) do |result|
106
+ rules.each do |rule|
107
+ next if rule.keys.any? { |key| error?(result, key) }
108
+
109
+ rule_result = rule.(self, result)
110
+
111
+ rule_result.failures.each do |failure|
112
+ result.add_error(message_resolver.(**failure))
113
+ end
114
+ end
115
+ end
116
+ end
117
+
118
+ # Return a nice string representation
119
+ #
120
+ # @return [String]
121
+ #
122
+ # @api public
123
+ def inspect
124
+ %(#<#{self.class} schema=#{schema.inspect} rules=#{rules.inspect}>)
125
+ end
126
+
127
+ private
128
+
129
+ # @api private
130
+ def error?(result, spec)
131
+ path = Schema::Path[spec]
132
+
133
+ if path.multi_value?
134
+ return path.expand.any? { |nested_path| error?(result, nested_path) }
135
+ end
136
+
137
+ return true if result.schema_error?(path)
138
+
139
+ path
140
+ .to_a[0..-2]
141
+ .any? { |key|
142
+ curr_path = Schema::Path[path.keys[0..path.keys.index(key)]]
143
+
144
+ return false unless result.schema_error?(curr_path)
145
+
146
+ result.errors.any? { |err|
147
+ (other = Schema::Path[err.path]).same_root?(curr_path) && other == curr_path
148
+ }
149
+ }
150
+ end
151
+
152
+ # Get a registered macro
153
+ #
154
+ # @return [Proc,#to_proc]
155
+ #
156
+ # @api private
157
+ def macro(name, *args)
158
+ (macros.key?(name) ? macros[name] : Macros[name]).with(args)
159
+ end
160
+
161
+ # Return configured messages backend
162
+ #
163
+ # @return [Dry::Schema::Messages::YAML, Dry::Schema::Messages::I18n]
164
+ #
165
+ # @api private
166
+ def messages
167
+ self.class.messages
168
+ end
169
+ end
170
+ end
171
+ end