hanami-ruby3 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8aaa6b22733000fa1ba8e1e678fad8a21f3efa1a8c03a0860f8ce49aceb57b0b
4
- data.tar.gz: 3068aeb3c3448d1b547d1637a2818839813deaaf5ef6d50c698bd4170c436c76
3
+ metadata.gz: 49e5e9e5b246dd74297750a1be4a0dff8f27f5bfd0cdac95dbeb3bbda1815c21
4
+ data.tar.gz: a36ed7e9847c8395e14a45d5a8214f9f0b890eeb261e88538f4d65077a94b86e
5
5
  SHA512:
6
- metadata.gz: 526813c24d0db9a2e2b3d4d1283d2cf3980a6b4b15169a31b63736b6795148794fd48e73d8527e722145c45eba8d12a2b0d966b57087b08b6f6fb1eca636444a
7
- data.tar.gz: 460a0b14e02b2301bb2ed4b9cbad698c1b7de49220d562082b8bd7454721ebd8f85703008e444ebbcfee4795a7df4d8397225f52a0feef441365a21d6f601651
6
+ metadata.gz: a22e0224fcd2d30defc87d8ff415aef7c6b7a8968354e139722f285019d5bfcf9948121e40ff2d615e27e07d20d8e536622b8306bb031eb3ebfe6f7657c566d3
7
+ data.tar.gz: 73264f585ebdac7972fd32c0fa7458b269650e715ae94a06035240ba443836088b0b6f08cb737aadfbb681cd5bc0c8220d5bc5c1978f337daa510ba64e1ea6c4
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- hanami-ruby3 (0.0.3)
4
+ hanami-ruby3 (0.1.0)
5
5
  hanami (~> 1.3.4)
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # Hanami Ruby 3.0
2
2
 
3
- Monkey patches Hanami components to support Ruby 3.0
3
+ Monkey patches Hanami components to support Ruby 3.0. This includes
4
+ 1. Copying Hanami 1.3.X files to support Ruby 3 syntax.
5
+ 2. Copying Hanami 1.3.X files and getting rid of deprecated DRY libraries to allow upgrade of those gems.
4
6
 
5
7
  ## Overview
6
8
 
@@ -23,6 +25,12 @@ In your project, install overrides for the given classes
23
25
  ```ruby
24
26
  # custom-hanami-utils is required to allow installing Hanami 1.3.5 with ruby 3
25
27
  gem 'hanami-utils', github: 'swilgosz/hanami-utils', branch: '1.3.x-support-ruby-3.0'
28
+
29
+ # Deprecated DRY libraries are renamed to RDY - we install those so we can update our DRY libraries freely.
30
+ %w[core container configurable equalizer monads logic types struct validation].each do |name|
31
+ gem "rdy-#{name}", ascenda_private: "Kaligo/rdy", glob: "lib/rdy/#{name}/rdy-#{name}.gemspec"
32
+ end
33
+
26
34
  gem 'hanami-ruby3'
27
35
  ```
28
36
 
@@ -1 +1,3 @@
1
1
  require_relative "./hanami/ruby3"
2
+
3
+ require_relative "./hanami/v1"
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Hanami
4
4
  module Ruby3
5
- VERSION = "0.0.3"
5
+ VERSION = "0.1.0"
6
6
  end
7
7
  end
data/lib/hanami/ruby3.rb CHANGED
@@ -2,7 +2,10 @@ require_relative "./ruby3/version"
2
2
 
3
3
  require "hanami"
4
4
 
5
- Dir["#{__dir__}/ruby3/hanami/**/*.rb"].each do |f|
5
+ Dir[
6
+ "#{__dir__}/ruby3/hanami/**/*.rb",
7
+ "#{__dir__}/v1/**/*.rb"
8
+ ].each do |f|
6
9
  require_relative f
7
10
  end
8
11
 
@@ -0,0 +1,11 @@
1
+ ## Collection of overwritten Hanami 1.3.5 files
2
+
3
+ Ruby 3 folder contains Hanami 1.3.5 files tweaked to support Ruby 3 syntax.
4
+ This is different in the sense that we need to keep some Hanami 1.3 files in parallel
5
+ of updated versions of them.
6
+
7
+ We have renamed old DRY -> `RDY` and stored in `Kaligo/rdy` repository, so we can install newer versions of those libs in parallel. In order to do so, we need to kopy some of the Hanami files that depend on deprecated DRY gems and change them to use `RDY`
8
+
9
+ ### Examples
10
+
11
+ - Hanami::Action::Params uses hanami-validations which uses old Dry-Validations with all the old dry-gems preventing us from gradual update. These files are updated to use RDY instead.
@@ -0,0 +1,186 @@
1
+ require 'rack/request'
2
+ require 'hanami/utils/hash'
3
+
4
+ module Hanami
5
+ module V1
6
+ module Action
7
+ class BaseParams
8
+ # The key that returns raw input from the Rack env
9
+ #
10
+ # @since 0.7.0
11
+ # @api private
12
+ RACK_INPUT = 'rack.input'.freeze
13
+
14
+ # The key that returns router params from the Rack env
15
+ # This is a builtin integration for Hanami::Router
16
+ #
17
+ # @since 0.7.0
18
+ # @api private
19
+ ROUTER_PARAMS = 'router.params'.freeze
20
+
21
+ # The key that returns Rack session params from the Rack env
22
+ # Please note that this is used only when an action is unit tested.
23
+ #
24
+ # @since 1.0.0
25
+ # @api private
26
+ #
27
+ # @example
28
+ # # action unit test
29
+ # action.call('rack.session' => { 'foo' => 'bar' })
30
+ # action.session[:foo] # => "bar"
31
+ RACK_SESSION = 'rack.session'.freeze
32
+
33
+ # HTTP request method for Rack env
34
+ #
35
+ # @since 1.1.1
36
+ # @api private
37
+ REQUEST_METHOD = 'REQUEST_METHOD'.freeze
38
+
39
+ # Default HTTP request method for Rack env
40
+ #
41
+ # @since 1.1.1
42
+ # @api private
43
+ DEFAULT_REQUEST_METHOD = 'GET'.freeze
44
+
45
+ # @attr_reader env [Hash] the Rack env
46
+ #
47
+ # @since 0.7.0
48
+ # @api private
49
+ attr_reader :env
50
+
51
+ # @attr_reader raw [Hash] the raw params from the request
52
+ #
53
+ # @since 0.7.0
54
+ # @api private
55
+ attr_reader :raw
56
+
57
+ # Initialize the params and freeze them.
58
+ #
59
+ # @param env [Hash] a Rack env or an hash of params.
60
+ #
61
+ # @return [Params]
62
+ #
63
+ # @since 0.7.0
64
+ # @api private
65
+ def initialize(env)
66
+ @env = env
67
+ @raw = _extract_params
68
+ @params = Hanami::Utils::Hash.deep_symbolize(@raw)
69
+ freeze
70
+ end
71
+
72
+ # Returns the object associated with the given key
73
+ #
74
+ # @param key [Symbol] the key
75
+ #
76
+ # @return [Object,nil] return the associated object, if found
77
+ #
78
+ # @since 0.7.0
79
+ def [](key)
80
+ @params[key]
81
+ end
82
+
83
+ # Get an attribute value associated with the given key.
84
+ # Nested attributes are reached by listing all the keys to get to the value.
85
+ #
86
+ # @param keys [Array<Symbol,Integer>] the key
87
+ #
88
+ # @return [Object,NilClass] return the associated value, if found
89
+ #
90
+ # @since 0.7.0
91
+ #
92
+ # @example
93
+ # require 'hanami/controller'
94
+ #
95
+ # module Deliveries
96
+ # class Create
97
+ # include Hanami::Action
98
+ #
99
+ # def call(params)
100
+ # params.get(:customer_name) # => "Luca"
101
+ # params.get(:uknown) # => nil
102
+ #
103
+ # params.get(:address, :city) # => "Rome"
104
+ # params.get(:address, :unknown) # => nil
105
+ #
106
+ # params.get(:tags, 0) # => "foo"
107
+ # params.get(:tags, 1) # => "bar"
108
+ # params.get(:tags, 999) # => nil
109
+ #
110
+ # params.get(nil) # => nil
111
+ # end
112
+ # end
113
+ # end
114
+ def get(*keys)
115
+ @params.dig(*keys)
116
+ end
117
+
118
+ # This is for compatibility with Hanami::Helpers::FormHelper::Values
119
+ #
120
+ # @api private
121
+ # @since 0.8.0
122
+ alias dig get
123
+
124
+ # Provide a common interface with Params
125
+ #
126
+ # @return [TrueClass] always returns true
127
+ #
128
+ # @since 0.7.0
129
+ #
130
+ # @see Hanami::Action::Params#valid?
131
+ def valid?
132
+ true
133
+ end
134
+
135
+ # Serialize params to Hash
136
+ #
137
+ # @return [::Hash]
138
+ #
139
+ # @since 0.7.0
140
+ def to_h
141
+ @params
142
+ end
143
+ alias_method :to_hash, :to_h
144
+
145
+ # Iterates through params
146
+ #
147
+ # @param blk [Proc]
148
+ #
149
+ # @since 0.7.1
150
+ def each(&blk)
151
+ to_h.each(&blk)
152
+ end
153
+
154
+ private
155
+
156
+ # @since 0.7.0
157
+ # @api private
158
+ def _extract_params
159
+ result = {}
160
+
161
+ if env.key?(RACK_INPUT)
162
+ result.merge! ::Rack::Request.new(env).params
163
+ result.merge! _router_params
164
+ else
165
+ result.merge! _router_params(env)
166
+ env[REQUEST_METHOD] ||= DEFAULT_REQUEST_METHOD
167
+ end
168
+
169
+ result
170
+ end
171
+
172
+ # @since 0.7.0
173
+ # @api private
174
+ def _router_params(fallback = {})
175
+ env.fetch(ROUTER_PARAMS) do
176
+ if session = fallback.delete(RACK_SESSION) # rubocop:disable Lint/AssignmentInCondition
177
+ fallback[RACK_SESSION] = Hanami::Utils::Hash.new(session).symbolize!.to_hash
178
+ end
179
+
180
+ fallback
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,248 @@
1
+ require_relative './base_params'
2
+ require_relative '../validations/form'
3
+
4
+ module Hanami
5
+ module V1
6
+ module Action
7
+ # A set of params requested by the client
8
+ #
9
+ # It's able to extract the relevant params from a Rack env of from an Hash.
10
+ #
11
+ # There are three scenarios:
12
+ # * When used with Hanami::Router: it contains only the params from the request
13
+ # * When used standalone: it contains all the Rack env
14
+ # * Default: it returns the given hash as it is. It's useful for testing purposes.
15
+ #
16
+ # @since 0.1.0
17
+ class Params < BaseParams
18
+ include Hanami::V1::Validations::Form
19
+
20
+ # Params errors
21
+ #
22
+ # @since 1.1.0
23
+ class Errors < SimpleDelegator
24
+ # @since 1.1.0
25
+ # @api private
26
+ def initialize(errors = {})
27
+ super(errors)
28
+ end
29
+
30
+ # Add an error to the param validations
31
+ #
32
+ # This has a semantic similar to `Hash#dig` where you use a set of keys
33
+ # to get a nested value, here you use a set of keys to set a nested
34
+ # value.
35
+ #
36
+ # @param args [Array<Symbol, String>] an array of arguments: the last
37
+ # one is the message to add (String), while the beginning of the array
38
+ # is made of keys to reach the attribute.
39
+ #
40
+ # @raise [ArgumentError] when try to add a message for a key that is
41
+ # already filled with incompatible message type.
42
+ # This usually happens with nested attributes: if you have a `:book`
43
+ # schema and the input doesn't include data for `:book`, the messages
44
+ # will be `["is missing"]`. In that case you can't add an error for a
45
+ # key nested under `:book`.
46
+ #
47
+ # @since 1.1.0
48
+ #
49
+ # @example Basic usage
50
+ # require "hanami/controller"
51
+ #
52
+ # class MyAction
53
+ # include Hanami::Action
54
+ #
55
+ # params do
56
+ # required(:book).schema do
57
+ # required(:isbn).filled(:str?)
58
+ # end
59
+ # end
60
+ #
61
+ # def call(params)
62
+ # # 1. Don't try to save the record if the params aren't valid
63
+ # return unless params.valid?
64
+ #
65
+ # BookRepository.new.create(params[:book])
66
+ # rescue Hanami::Model::UniqueConstraintViolationError
67
+ # # 2. Add an error in case the record wasn't unique
68
+ # params.errors.add(:book, :isbn, "is not unique")
69
+ # end
70
+ # end
71
+ #
72
+ # @example Invalid argument
73
+ # require "hanami/controller"
74
+ #
75
+ # class MyAction
76
+ # include Hanami::Action
77
+ #
78
+ # params do
79
+ # required(:book).schema do
80
+ # required(:title).filled(:str?)
81
+ # end
82
+ # end
83
+ #
84
+ # def call(params)
85
+ # puts params.to_h # => {}
86
+ # puts params.valid? # => false
87
+ # puts params.error_messages # => ["Book is missing"]
88
+ # puts params.errors # => {:book=>["is missing"]}
89
+ #
90
+ # params.errors.add(:book, :isbn, "is not unique") # => ArgumentError
91
+ # end
92
+ # end
93
+ def add(*args)
94
+ *keys, key, error = args
95
+ _nested_attribute(keys, key) << error
96
+ rescue TypeError
97
+ raise ArgumentError.new("Can't add #{args.map(&:inspect).join(', ')} to #{inspect}")
98
+ end
99
+
100
+ private
101
+
102
+ # @since 1.1.0
103
+ # @api private
104
+ def _nested_attribute(keys, key)
105
+ if keys.empty?
106
+ self
107
+ else
108
+ keys.inject(self) { |result, k| result[k] ||= {} }
109
+ dig(*keys)
110
+ end[key] ||= []
111
+ end
112
+ end
113
+
114
+ # This is a Hanami::Validations extension point
115
+ #
116
+ # @since 0.7.0
117
+ # @api private
118
+ def self._base_rules
119
+ lambda do
120
+ optional(:_csrf_token).filled(:str?)
121
+ end
122
+ end
123
+
124
+ # Define params validations
125
+ #
126
+ # @param blk [Proc] the validations definitions
127
+ #
128
+ # @since 0.7.0
129
+ #
130
+ # @see https://guides.hanamirb.org/validations/overview
131
+ #
132
+ # @example
133
+ # class Signup
134
+ # MEGABYTE = 1024 ** 2
135
+ # include Hanami::Action
136
+ #
137
+ # params do
138
+ # required(:first_name).filled(:str?)
139
+ # required(:last_name).filled(:str?)
140
+ # required(:email).filled?(:str?, format?: /\A.+@.+\z/)
141
+ # required(:password).filled(:str?).confirmation
142
+ # required(:terms_of_service).filled(:bool?)
143
+ # required(:age).filled(:int?, included_in?: 18..99)
144
+ # optional(:avatar).filled(size?: 1..(MEGABYTE * 3))
145
+ # end
146
+ #
147
+ # def call(params)
148
+ # halt 400 unless params.valid?
149
+ # # ...
150
+ # end
151
+ # end
152
+ def self.params(&blk)
153
+ validations(&blk || ->() {})
154
+ end
155
+
156
+ # Initialize the params and freeze them.
157
+ #
158
+ # @param env [Hash] a Rack env or an hash of params.
159
+ #
160
+ # @return [Params]
161
+ #
162
+ # @since 0.1.0
163
+ # @api private
164
+ def initialize(env)
165
+ @env = env
166
+ super(_extract_params)
167
+ @result = validate
168
+ @params = _params
169
+ @errors = Errors.new(@result.messages)
170
+ freeze
171
+ end
172
+
173
+ # Returns raw params from Rack env
174
+ #
175
+ # @return [Hash]
176
+ #
177
+ # @since 0.3.2
178
+ def raw
179
+ @input
180
+ end
181
+
182
+ # Returns structured error messages
183
+ #
184
+ # @return [Hash]
185
+ #
186
+ # @since 0.7.0
187
+ #
188
+ # @example
189
+ # params.errors
190
+ # # => {:email=>["is missing", "is in invalid format"], :name=>["is missing"], :tos=>["is missing"], :age=>["is missing"], :address=>["is missing"]}
191
+ attr_reader :errors
192
+
193
+ # Returns flat collection of full error messages
194
+ #
195
+ # @return [Array]
196
+ #
197
+ # @since 0.7.0
198
+ #
199
+ # @example
200
+ # params.error_messages
201
+ # # => ["Email is missing", "Email is in invalid format", "Name is missing", "Tos is missing", "Age is missing", "Address is missing"]
202
+ def error_messages(error_set = errors)
203
+ error_set.each_with_object([]) do |(key, messages), result|
204
+ k = Utils::String.titleize(key)
205
+
206
+ _messages = if messages.is_a?(Hash)
207
+ error_messages(messages)
208
+ else
209
+ messages.map { |message| "#{k} #{message}" }
210
+ end
211
+
212
+ result.concat(_messages)
213
+ end
214
+ end
215
+
216
+ # Returns true if no validation errors are found,
217
+ # false otherwise.
218
+ #
219
+ # @return [TrueClass, FalseClass]
220
+ #
221
+ # @since 0.7.0
222
+ #
223
+ # @example
224
+ # params.valid? # => true
225
+ def valid?
226
+ errors.empty?
227
+ end
228
+
229
+ # Serialize params to Hash
230
+ #
231
+ # @return [::Hash]
232
+ #
233
+ # @since 0.3.0
234
+ def to_h
235
+ @params
236
+ end
237
+ alias_method :to_hash, :to_h
238
+
239
+ private
240
+
241
+ # @api private
242
+ def _params
243
+ _router_params.merge(@result.output)
244
+ end
245
+ end
246
+ end
247
+ end
248
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../validations"
4
+
5
+ module Hanami
6
+ module V1
7
+ module Validations
8
+ # Validations mixin for forms/HTTP params.
9
+ #
10
+ # This must be used when the input comes from a browser or an HTTP endpoint.
11
+ # It knows how to deal with common data types, and common edge cases like blank strings.
12
+ #
13
+ # @since 0.6.0
14
+ #
15
+ # @example
16
+ # require 'hanami/validations/form'
17
+ #
18
+ # class Signup
19
+ # include Hanami::Validations::Form
20
+ #
21
+ # validations do
22
+ # required(:name).filled(:str?)
23
+ # optional(:location).filled(:str?)
24
+ # end
25
+ # end
26
+ #
27
+ # result = Signup.new('location' => 'Rome').validate
28
+ # result.success? # => false
29
+ #
30
+ # result = Signup.new('name' => 'Luca').validate
31
+ # result.success? # => true
32
+ #
33
+ # # it works with symbol keys too
34
+ # result = Signup.new(location: 'Rome').validate
35
+ # result.success? # => false
36
+ #
37
+ # result = Signup.new(name: 'Luca').validate
38
+ # result.success? # => true
39
+ #
40
+ # result = Signup.new(name: 'Luca', location: 'Rome').validate
41
+ # result.success? # => true
42
+ module Form
43
+ # Override Ruby's hook for modules.
44
+ #
45
+ # @param base [Class] the target action
46
+ #
47
+ # @since 0.6.0
48
+ # @api private
49
+ #
50
+ # @see http://www.ruby-doc.org/core/Module.html#method-i-included
51
+ def self.included(base)
52
+ base.class_eval do
53
+ include Hanami::V1::Validations
54
+ extend ClassMethods
55
+ end
56
+ end
57
+
58
+ # @since 0.6.0
59
+ # @api private
60
+ module ClassMethods
61
+ private
62
+
63
+ # @since 0.6.0
64
+ # @api private
65
+ def _schema_type
66
+ :Form
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ module V1
5
+ module Validations
6
+ # Inline predicate
7
+ #
8
+ # @since 0.6.0
9
+ # @api private
10
+ class InlinePredicate
11
+ # @since 0.6.0
12
+ # @api private
13
+ attr_reader :name
14
+
15
+ # @since 0.6.0
16
+ # @api private
17
+ attr_reader :message
18
+
19
+ # Return a new instance.
20
+ #
21
+ # @param name [Symbol] inline predicate name
22
+ # @param message [String] optional failure message
23
+ # @param blk [Proc] predicate implementation
24
+ #
25
+ # @return [Hanami::Validations::InlinePredicate] a new instance
26
+ #
27
+ # @since 0.6.0
28
+ # @api private
29
+ def initialize(name, message, &blk)
30
+ @name = name
31
+ @message = message
32
+ @blk = blk
33
+ end
34
+
35
+ # @since 0.6.0
36
+ # @api private
37
+ def to_proc
38
+ @blk
39
+ end
40
+
41
+ # @since 0.6.0
42
+ # @api private
43
+ def ==(other)
44
+ self.class == other.class &&
45
+ name == other.name
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hanami/utils/string"
4
+
5
+ module Hanami
6
+ module V1
7
+ module Validations
8
+ # Validations message namespace.
9
+ #
10
+ # For a given `FooValidator` class, it will look for I18n messages within
11
+ # the `foo` group.
12
+ #
13
+ # @since 0.6.0
14
+ # @api private
15
+ class Namespace
16
+ # @since 0.6.0
17
+ # @api private
18
+ SUFFIX = "Validator"
19
+
20
+ # @since 0.6.0
21
+ # @api private
22
+ SUFFIX_REPLACEMENT = ""
23
+
24
+ # @since 0.6.0
25
+ # @api private
26
+ RUBY_NAMESPACE_SEPARATOR = "/"
27
+
28
+ # @since 0.6.0
29
+ # @api private
30
+ RUBY_NAMESPACE_REPLACEMENT = "."
31
+
32
+ # @since 0.6.0
33
+ # @api private
34
+ def self.new(name, klass)
35
+ result = name || klass.name
36
+ return nil if result.nil?
37
+
38
+ super(result)
39
+ end
40
+
41
+ # @since 0.6.0
42
+ # @api private
43
+ def initialize(name)
44
+ @name = name
45
+ end
46
+
47
+ # @since 0.6.0
48
+ # @api private
49
+ def to_s
50
+ underscored_name.gsub(RUBY_NAMESPACE_SEPARATOR, RUBY_NAMESPACE_REPLACEMENT)
51
+ end
52
+
53
+ private
54
+
55
+ # @since 0.6.0
56
+ # @api private
57
+ def underscored_name
58
+ Hanami::Utils::String.underscore(name_without_suffix)
59
+ end
60
+
61
+ # @since 0.6.0
62
+ # @api private
63
+ def name_without_suffix
64
+ @name.sub(SUFFIX, SUFFIX_REPLACEMENT)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rdy/logic/predicates"
4
+ require "hanami/utils/class_attribute"
5
+
6
+ module Hanami
7
+ module Validations
8
+ # Mixin to include when defining shared predicates
9
+ #
10
+ # @since 0.6.0
11
+ #
12
+ # @see Hanami::Validations::ClassMethods#predicates
13
+ #
14
+ # @example Inline Predicate
15
+ # require 'hanami/validations'
16
+ #
17
+ # module MySharedPredicates
18
+ # include Hanami::Validations::Predicates
19
+ #
20
+ # predicate :foo? do |actual|
21
+ # actual == 'foo'
22
+ # end
23
+ # end
24
+ #
25
+ # class MyValidator
26
+ # include Hanami::Validations
27
+ # predicates MySharedPredicates
28
+ #
29
+ # validations do
30
+ # required(:name).filled(:foo?)
31
+ # end
32
+ # end
33
+ module Predicates
34
+ # @since 0.6.0
35
+ # @api private
36
+ def self.included(base)
37
+ base.class_eval do
38
+ include Rdy::Logic::Predicates
39
+ include Hanami::Utils::ClassAttribute
40
+
41
+ class_attribute :messages
42
+ class_attribute :messages_path
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,388 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rdy-validation"
4
+ require "hanami/utils/class_attribute"
5
+ require_relative "./validations/namespace"
6
+ require_relative "./validations/predicates"
7
+ require_relative "./validations/inline_predicate"
8
+ require "set"
9
+
10
+ Rdy::Validation::Messages::Namespaced.configure do |config|
11
+ # rubocop:disable Lint/NestedPercentLiteral
12
+ #
13
+ # This is probably a false positive.
14
+ # See: https://github.com/bbatsov/rubocop/issues/5314
15
+ config.lookup_paths = config.lookup_paths + %w[
16
+ %<root>s.%<rule>s.%<predicate>s
17
+ ].freeze
18
+ # rubocop:enable Lint/NestedPercentLiteral
19
+ end
20
+
21
+ # @since 0.1.0
22
+ module Hanami
23
+ module V1
24
+ # Hanami::Validations is a set of lightweight validations for Ruby objects.
25
+ #
26
+ # @since 0.1.0
27
+ #
28
+ # @example
29
+ # require 'hanami/validations'
30
+ #
31
+ # class Signup
32
+ # include Hanami::Validations
33
+ #
34
+ # validations do
35
+ # # ...
36
+ # end
37
+ # end
38
+ module Validations
39
+ # @since 0.6.0
40
+ # @api private
41
+ DEFAULT_MESSAGES_ENGINE = :yaml
42
+
43
+ # Override Ruby's hook for modules.
44
+ #
45
+ # @param base [Class] the target action
46
+ #
47
+ # @since 0.1.0
48
+ # @api private
49
+ #
50
+ # @see http://www.ruby-doc.org/core/Module.html#method-i-included
51
+ def self.included(base)
52
+ base.class_eval do
53
+ extend ClassMethods
54
+
55
+ include Hanami::Utils::ClassAttribute
56
+ class_attribute :schema
57
+ class_attribute :_messages
58
+ class_attribute :_messages_path
59
+ class_attribute :_namespace
60
+ class_attribute :_predicates_module
61
+
62
+ class_attribute :_predicates
63
+ self._predicates = Set.new
64
+ end
65
+ end
66
+
67
+ # Validations DSL
68
+ #
69
+ # @since 0.1.0
70
+ module ClassMethods
71
+ # Define validation rules from the given block.
72
+ #
73
+ # @param blk [Proc] validation rules
74
+ #
75
+ # @since 0.6.0
76
+ #
77
+ # @see https://guides.hanamirb.org/validations/overview
78
+ #
79
+ # @example Basic Example
80
+ # require 'hanami/validations'
81
+ #
82
+ # class Signup
83
+ # include Hanami::Validations
84
+ #
85
+ # validations do
86
+ # required(:name).filled
87
+ # end
88
+ # end
89
+ #
90
+ # result = Signup.new(name: "Luca").validate
91
+ #
92
+ # result.success? # => true
93
+ # result.messages # => []
94
+ # result.output # => {:name=>""}
95
+ #
96
+ # result = Signup.new(name: "").validate
97
+ #
98
+ # result.success? # => false
99
+ # result.messages # => {:name=>["must be filled"]}
100
+ # result.output # => {:name=>""}
101
+ def validations(&blk)
102
+ schema_predicates = __predicates
103
+
104
+ base = _build(predicates: schema_predicates, &_base_rules)
105
+ schema = _build(predicates: schema_predicates, rules: base.rules, &blk)
106
+ schema.configure(&_schema_config)
107
+ schema.configure(&_schema_predicates)
108
+ schema.extend(__messages) unless _predicates.empty?
109
+
110
+ self.schema = schema.new
111
+ end
112
+
113
+ # Define an inline predicate
114
+ #
115
+ # @param name [Symbol] inline predicate name
116
+ # @param message [String] optional error message
117
+ # @param blk [Proc] predicate implementation
118
+ #
119
+ # @return nil
120
+ #
121
+ # @since 0.6.0
122
+ #
123
+ # @example Without Custom Message
124
+ # require 'hanami/validations'
125
+ #
126
+ # class Signup
127
+ # include Hanami::Validations
128
+ #
129
+ # predicate :foo? do |actual|
130
+ # actual == 'foo'
131
+ # end
132
+ #
133
+ # validations do
134
+ # required(:name).filled(:foo?)
135
+ # end
136
+ # end
137
+ #
138
+ # result = Signup.new(name: nil).call
139
+ # result.messages # => { :name => ['is invalid'] }
140
+ #
141
+ # @example With Custom Message
142
+ # require 'hanami/validations'
143
+ #
144
+ # class Signup
145
+ # include Hanami::Validations
146
+ #
147
+ # predicate :foo?, message: 'must be foo' do |actual|
148
+ # actual == 'foo'
149
+ # end
150
+ #
151
+ # validations do
152
+ # required(:name).filled(:foo?)
153
+ # end
154
+ # end
155
+ #
156
+ # result = Signup.new(name: nil).call
157
+ # result.messages # => { :name => ['must be foo'] }
158
+ def predicate(name, message: "is invalid", &blk)
159
+ _predicates << InlinePredicate.new(name, message, &blk)
160
+ end
161
+
162
+ # Assign a set of shared predicates wrapped in a module
163
+ #
164
+ # @param mod [Module] a module with shared predicates
165
+ #
166
+ # @since 0.6.0
167
+ #
168
+ # @see Hanami::Validations::Predicates
169
+ #
170
+ # @example
171
+ # require 'hanami/validations'
172
+ #
173
+ # module MySharedPredicates
174
+ # include Hanami::Validations::Predicates
175
+ #
176
+ # predicate :foo? fo |actual|
177
+ # actual == 'foo'
178
+ # end
179
+ # end
180
+ #
181
+ # class MyValidator
182
+ # include Hanami::Validations
183
+ # predicates MySharedPredicates
184
+ #
185
+ # validations do
186
+ # required(:name).filled(:foo?)
187
+ # end
188
+ # end
189
+ def predicates(mod)
190
+ self._predicates_module = mod
191
+ end
192
+
193
+ # Define the type of engine for error messages.
194
+ #
195
+ # Accepted values are `:yaml` (default), `:i18n`.
196
+ #
197
+ # @param type [Symbol] the preferred engine
198
+ #
199
+ # @since 0.6.0
200
+ #
201
+ # @example
202
+ # require 'hanami/validations'
203
+ #
204
+ # class Signup
205
+ # include Hanami::Validations
206
+ #
207
+ # messages :i18n
208
+ # end
209
+ def messages(type)
210
+ self._messages = type
211
+ end
212
+
213
+ # Define the path where to find translation file
214
+ #
215
+ # @param path [String] path to translation file
216
+ #
217
+ # @since 0.6.0
218
+ #
219
+ # @example
220
+ # require 'hanami/validations'
221
+ #
222
+ # class Signup
223
+ # include Hanami::Validations
224
+ #
225
+ # messages_path 'config/messages.yml'
226
+ # end
227
+ def messages_path(path)
228
+ self._messages_path = path
229
+ end
230
+
231
+ # Namespace for error messages.
232
+ #
233
+ # @param name [String] namespace
234
+ #
235
+ # @since 0.6.0
236
+ #
237
+ # @example
238
+ # require 'hanami/validations'
239
+ #
240
+ # module MyApp
241
+ # module Validators
242
+ # class Signup
243
+ # include Hanami::Validations
244
+ #
245
+ # namespace 'signup'
246
+ # end
247
+ # end
248
+ # end
249
+ #
250
+ # # Instead of looking for error messages under the `my_app.validator.signup`
251
+ # # namespace, it will look just for `signup`.
252
+ # #
253
+ # # This helps to simplify YAML files where are stored error messages
254
+ def namespace(name = nil)
255
+ if name.nil?
256
+ Namespace.new(_namespace, self)
257
+ else
258
+ self._namespace = name.to_s
259
+ end
260
+ end
261
+
262
+ private
263
+
264
+ # @since 0.6.0
265
+ # @api private
266
+ def _build(options = {}, &blk)
267
+ options = {build: false}.merge(options)
268
+ Rdy::Validation.__send__(_schema_type, options, &blk)
269
+ end
270
+
271
+ # @since 0.6.0
272
+ # @api private
273
+ def _schema_type
274
+ :Schema
275
+ end
276
+
277
+ # @since 0.6.0
278
+ # @api private
279
+ def _base_rules
280
+ lambda do
281
+ end
282
+ end
283
+
284
+ # @since 0.6.0
285
+ # @api private
286
+ def _schema_config
287
+ lambda do |config|
288
+ config.messages = _messages unless _messages.nil?
289
+ config.messages_file = _messages_path unless _messages_path.nil?
290
+ config.namespace = namespace
291
+
292
+ require "rdy/validation/messages/i18n" if config.messages == :i18n
293
+ end
294
+ end
295
+
296
+ # @since 0.6.0
297
+ # @api private
298
+ def _schema_predicates
299
+ return if _predicates_module.nil? && _predicates.empty?
300
+
301
+ lambda do |config|
302
+ config.messages = _predicates_module&.messages || @_messages || DEFAULT_MESSAGES_ENGINE
303
+ config.messages_file = _predicates_module.messages_path unless _predicates_module.nil?
304
+
305
+ require "rdy/validation/messages/i18n" if config.messages == :i18n
306
+ end
307
+ end
308
+
309
+ # @since 0.6.0
310
+ # @api private
311
+ def __predicates
312
+ mod = _predicates_module || Module.new { include Hanami::Validations::Predicates }
313
+
314
+ _predicates.each do |p|
315
+ mod.module_eval do
316
+ predicate(p.name, &p.to_proc)
317
+ end
318
+ end
319
+
320
+ mod
321
+ end
322
+
323
+ # @since 0.6.0
324
+ # @api private
325
+ def __messages
326
+ result = _predicates.each_with_object({}) do |p, ret|
327
+ ret[p.name] = p.message
328
+ end
329
+
330
+ # @api private
331
+ Module.new do
332
+ @@__messages = result # rubocop:disable Style/ClassVars
333
+
334
+ # @api private
335
+ def self.extended(base)
336
+ base.instance_eval do
337
+ def __messages
338
+ Hash[en: {errors: @@__messages}]
339
+ end
340
+ end
341
+ end
342
+
343
+ # @api private
344
+ def messages
345
+ engine = super
346
+
347
+ messages = if engine.respond_to?(:merge)
348
+ engine
349
+ else
350
+ engine.messages
351
+ end
352
+
353
+ config.messages == :i18n ? messages : messages.merge(__messages)
354
+ end
355
+ end
356
+ end
357
+ end
358
+
359
+ # Initialize a new instance of a validator
360
+ #
361
+ # @param input [#to_h] a set of input data
362
+ #
363
+ # @since 0.6.0
364
+ def initialize(input = {})
365
+ @input = input.to_h
366
+ end
367
+
368
+ # Validates the object.
369
+ #
370
+ # @return [Rdy::Validations::Result]
371
+ #
372
+ # @since 0.2.4
373
+ def validate
374
+ self.class.schema.call(@input)
375
+ end
376
+
377
+ # Returns a Hash with the defined attributes as symbolized keys, and their
378
+ # relative values.
379
+ #
380
+ # @return [Hash]
381
+ #
382
+ # @since 0.1.0
383
+ def to_h
384
+ validate.output
385
+ end
386
+ end
387
+ end
388
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hanami-ruby3
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ascenda Developers
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-02-08 00:00:00.000000000 Z
11
+ date: 2023-03-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: hanami
@@ -53,6 +53,14 @@ files:
53
53
  - lib/hanami/ruby3/hanami/view/rendering/scope.rb
54
54
  - lib/hanami/ruby3/hanami/views/default.rb
55
55
  - lib/hanami/ruby3/version.rb
56
+ - lib/hanami/v1/README.md
57
+ - lib/hanami/v1/action/base_params.rb
58
+ - lib/hanami/v1/action/params.rb
59
+ - lib/hanami/v1/validations.rb
60
+ - lib/hanami/v1/validations/form.rb
61
+ - lib/hanami/v1/validations/inline_predicate.rb
62
+ - lib/hanami/v1/validations/namespace.rb
63
+ - lib/hanami/v1/validations/predicates.rb
56
64
  homepage: http://github.com/Kaligo/hanami-ruby3
57
65
  licenses: []
58
66
  metadata: