phlex-hanami 0.2.1 → 0.2.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 301d34d14bd2989ab0ab899fd6f76e38b5a8e8b1a3fcffa182f6ece292652e81
4
- data.tar.gz: 74e00aebe59f62800ad37bd576ab97f71efbfbabb536d5fa115e8b2ce5789509
3
+ metadata.gz: 2bb01643b7c075e6b220c2d7ad70e885caba136346380f4236f716350ff7de98
4
+ data.tar.gz: fc0766f20a6709feae68138171b2f0e15723c59c30adf90fcfaa4dfef8bc8986
5
5
  SHA512:
6
- metadata.gz: 84d0aec75d54b2e1e7be95d57c9d2c7e9afb560abd4ec7b86065136618775988a33776da0195c71222f9921c628c7b3b6168ca8cc84710f8a18709114210d6a6
7
- data.tar.gz: 8ab7a57fac797d8daa565d6335ed9efbe2b1cfde7c0f67f30ab98a5c8c01341ee6a2b46ebb0eced44807084080fc6f5d73bec15914f16780163143f4b1b13cc3
6
+ metadata.gz: d30170e3f93a92fdde6d70db733abc97a3384673e9bffa8629737a972aacf2194f1791fe8335a1889c8ed69ec05796c87e8c503e248bc77f3ff2bffb8c46d213
7
+ data.tar.gz: 12d33fe3ddf6952ea5714be91db82b954db1848d2bad93e74bbaa8c5b1643c66ec68bf5ad9e3e9c1db0a02ca7f5ee4822c3863c899b6d2d9f1345dbf2f7457e2
data/CHANGELOG.md CHANGED
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [v0.2.2] - 2026-09-26
11
+
12
+ ### Added
13
+
14
+ - A kind for `prop` in `Phlex::Hanami::Props`, as its third argument: `:positional`, `:*` for the leftover
15
+ positional arguments as an Array, or `:**` for the leftover keywords as a Hash. A component can now take
16
+ `prop :attributes, Types::Hash, :**` and pass HTML attributes through to its tag. A view with a `:**` prop receives
17
+ every exposure and request param.
18
+ - `prop?`, for a prop the caller can leave out. When they do, it holds `Phlex::Hanami::Props::UNSET` rather than
19
+ `nil`, so a form can use the session's CSRF token by default and none when passed `token: nil`.
20
+ - A block on `prop`, which runs on the new instance and coerces the value before the type sees it.
21
+ - `reader:`, `writer:` and `predicate:` options on `prop`, each taking `:public`, `:protected` or `:private`. The
22
+ writer checks the value against the type.
23
+ - `after_initialize`, which a class can define to run once every prop is set.
24
+
25
+ ### Changed
26
+
27
+ - A prop whose type accepts `nil`, such as `Types::String.optional` or `NilClass`, is now optional and `nil` when
28
+ left out. It used to be required.
29
+ - A default proc now runs on the new instance rather than on its own, so it can read the props declared above it. A
30
+ proc that relied on `self` being the class it was written in needs to change.
31
+
10
32
  ## [v0.2.1] - 2026-09-26
11
33
 
12
34
  ### Added
@@ -108,7 +130,9 @@ Initial alpha release by the new maintainer [@aaronmallen](https://github.com/aa
108
130
 
109
131
  Initial release, by the previous maintainer [@stephannv](https://github.com/stephannv).
110
132
 
111
- [Unreleased]: https://github.com/aaronmallen/phlex-hanami/compare/0.2.0...HEAD
133
+ [Unreleased]: https://github.com/aaronmallen/phlex-hanami/compare/0.2.2...HEAD
134
+ [v0.2.2]: https://github.com/aaronmallen/phlex-hanami/compare/0.2.1...0.2.2
135
+ [v0.2.1]: https://github.com/aaronmallen/phlex-hanami/compare/0.2.0...0.2.1
112
136
  [v0.2.0]: https://github.com/aaronmallen/phlex-hanami/compare/0.2.0-alpha.3...0.2.0
113
137
  [v0.2.0-alpha.3]: https://github.com/aaronmallen/phlex-hanami/compare/0.2.0-alpha.2...0.2.0-alpha.3
114
138
  [v0.2.0-alpha.2]: https://github.com/aaronmallen/phlex-hanami/compare/0.2.0-alpha.1...0.2.0-alpha.2
@@ -7,10 +7,10 @@ module Phlex
7
7
  # The type's own error, when there is one, is kept as the `cause`.
8
8
  #
9
9
  # @api public
10
- # @since 0.3.0
10
+ # @since 0.2.1
11
11
  class InvalidPropError < Error
12
12
  # @api private
13
- # @since 0.3.0
13
+ # @since 0.2.1
14
14
  #: (Module, Symbol, String) -> void
15
15
  def initialize(view_class, name, reason)
16
16
  super(<<~MESSAGE)
@@ -4,14 +4,15 @@ module Phlex
4
4
  module Hanami
5
5
  # Declared props, typed with dry-types.
6
6
  #
7
- # Opt in per class. Each `prop` names a keyword `initialize` takes and the type its value goes
7
+ # Opt in per class. Each `prop` names an argument `initialize` takes and the type its value goes
8
8
  # through, and the value lands in an instance variable of the same name. A dry type is called,
9
9
  # so it coerces as well as checks: `Types::Params::Integer` turns the string a request param
10
10
  # arrives as into an Integer. Anything else that answers `call` is called the same way, and
11
11
  # anything that does not, such as a plain class, is matched with `===`.
12
12
  #
13
13
  # The initializer it builds takes real keywords, so auto render hands a view the props it
14
- # declares and drops every other param, the same as it does for a hand written `initialize`.
14
+ # declares and drops every other param, the same as it does for a hand written `initialize`. A
15
+ # `:**` prop takes them all, request params included.
15
16
  #
16
17
  # Literal needs none of this. Extend `Literal::Properties` instead if you would rather use it.
17
18
  #
@@ -23,85 +24,153 @@ module Phlex
23
24
  # prop :count, Types::Params::Integer
24
25
  # prop :compact, Types::Bool, default: false
25
26
  # prop :tags, Types::Array.of(Types::String), default: -> { [] }
27
+ # prop :attributes, Types::Hash, :**
26
28
  #
27
29
  # def view_template
28
- # article(class: ("compact" if @compact)) { h2 { @post.title } }
30
+ # article(class: ("compact" if @compact), **@attributes) { h2 { @post.title } }
29
31
  # end
30
32
  # end
31
33
  #
32
34
  # @api public
33
- # @since 0.3.0
35
+ # @since 0.2.1
34
36
  module Props
35
- # Stands in for a keyword the caller left out, since `nil` can be a real value.
37
+ # The value of a prop declared with `prop?` that the caller left out, so a view can tell it
38
+ # from `nil`.
36
39
  #
37
- # @api private
38
- # @since 0.3.0
39
- UNSET = ::Object.new.freeze #: Object
40
+ # @example
41
+ # prop? :token, Types::String.optional
42
+ #
43
+ # def token_given? = !Phlex::Hanami::Props::UNSET.equal?(@token)
44
+ #
45
+ # @api public
46
+ # @since 0.2.1
47
+ UNSET = ::Object.new.tap { |unset| def unset.inspect = "Phlex::Hanami::Props::UNSET" }.freeze #: Object
40
48
 
41
49
  # How the generated initializer names {UNSET}.
42
50
  #
43
51
  # @api private
44
- # @since 0.3.0
52
+ # @since 0.2.1
45
53
  UNSET_PATH = "::Phlex::Hanami::Props::UNSET" #: String
46
54
 
47
- # A prop name has to be a Ruby identifier, because it becomes a keyword and an instance
55
+ # A prop name has to be a Ruby identifier, because it becomes an argument and an instance
48
56
  # variable.
49
57
  #
50
58
  # @api private
51
- # @since 0.3.0
59
+ # @since 0.2.1
52
60
  NAME_FORMAT = /\A[a-z_][a-zA-Z0-9_]*\z/ #: Regexp
53
61
 
54
- # Sets each prop's instance variable from the keywords the generated initializer received.
62
+ # The argument kinds a prop can take, in the order `initialize` declares them.
55
63
  #
56
64
  # @api private
57
65
  # @since 0.3.0
66
+ KINDS = %i[positional * keyword **].freeze #: Array[Symbol]
67
+
68
+ # What `reader`, `writer` and `predicate` accept.
69
+ #
70
+ # @api private
71
+ # @since 0.3.0
72
+ VISIBILITIES = [false, :public, :protected, :private].freeze #: Array[Symbol | false]
73
+
74
+ # Sets each prop's instance variable from the arguments the generated initializer received.
75
+ #
76
+ # @api private
77
+ # @since 0.2.1
58
78
  #: (untyped, Binding) -> void
59
79
  def self.assign(view, arguments)
60
80
  view.class.props.each_value do |prop|
61
- value = prop.resolve(view.class, arguments.local_variable_get(prop.name))
81
+ value = prop.resolve(view, arguments.local_variable_get(prop.variable))
62
82
  view.instance_variable_set(:"@#{prop.name}", value)
63
83
  end
64
84
  end
65
85
 
66
86
  # @api private
67
- # @since 0.3.0
87
+ # @since 0.2.1
68
88
  #: (Module) -> void
69
89
  def self.included(view_class)
70
90
  view_class.extend(ClassMethods)
71
91
  end
72
92
 
73
93
  # @api public
74
- # @since 0.3.0
94
+ # @since 0.2.1
75
95
  module ClassMethods
76
96
  # Declares a prop.
77
97
  #
78
- # A prop with a default is optional. So is one whose dry type carries its own, as
79
- # `Types::Bool.default(false)` does. A default goes through the type like any other value.
80
- # Pass a proc for anything mutable, so each instance gets its own.
98
+ # A prop with a default is optional. So is one whose type accepts nil, which it gets when
99
+ # left out, and one whose dry type carries its own default, as `Types::Bool.default(false)`
100
+ # does. A default goes through the type like any other value. Pass a proc for anything
101
+ # mutable, so each instance gets its own. The proc runs on the new instance, so it can read
102
+ # the props declared above it.
103
+ #
104
+ # A block, when given, runs on the new instance with the value before the type sees it.
105
+ #
106
+ # @param name [Symbol] the argument, and the instance variable the value lands in
107
+ # @param type [#call, #===] a dry type, or anything that answers `call` or `===`
108
+ # @param kind [Symbol] `:keyword`, `:positional`, `:*` for the rest of the positional
109
+ # arguments as an Array, or `:**` for the rest of the keywords as a Hash
110
+ # @param default [Object, Proc] the value when the argument is left out
111
+ # @param reader [false, Symbol] the visibility of a reader method, or false for none
112
+ # @param writer [false, Symbol] the visibility of a writer method that checks the type
113
+ # @param predicate [false, Symbol] the visibility of a `name?` method
114
+ #
115
+ # @return [Symbol] the name
116
+ #
117
+ # @raise [ArgumentError] if an option is not valid, or the argument would not fit the
118
+ # ones declared before it
119
+ #
120
+ # @api public
121
+ # @since 0.2.1
122
+ # @rbs name: Symbol
123
+ # @rbs type: untyped
124
+ # @rbs kind: Symbol
125
+ # @rbs default: untyped
126
+ # @rbs reader: Symbol | false
127
+ # @rbs writer: Symbol | false
128
+ # @rbs predicate: Symbol | false
129
+ # @rbs &coercion: ? (untyped) -> untyped
130
+ # @rbs return: Symbol
131
+ def prop(name, type, kind = :keyword, default: UNSET, reader: false, writer: false, predicate: false, &coercion)
132
+ prop = Prop.new(name, type, kind, default:, omittable: false, coercion:)
133
+ declare_prop(prop, { reader:, writer:, predicate: })
134
+ end
135
+
136
+ # Declares a prop the caller can leave out, and that is set to {UNSET} when they do. A view
137
+ # can then tell an argument left out from one passed as `nil`.
138
+ #
139
+ # The type does not see {UNSET}, so it only has to accept what a caller passes.
81
140
  #
82
- # @param name [Symbol] the keyword, and the instance variable the value lands in
141
+ # @param name [Symbol] the argument, and the instance variable the value lands in
83
142
  # @param type [#call, #===] a dry type, or anything that answers `call` or `===`
84
- # @param default [Object, Proc] the value when the keyword is left out
143
+ # @param kind [Symbol] `:keyword` or `:positional`
144
+ # @param reader [false, Symbol] the visibility of a reader method, or false for none
145
+ # @param writer [false, Symbol] the visibility of a writer method that checks the type
146
+ # @param predicate [false, Symbol] the visibility of a `name?` method
85
147
  #
86
148
  # @return [Symbol] the name
87
149
  #
88
- # @raise [ArgumentError] if the name is not a Ruby identifier
150
+ # @raise [ArgumentError] if an option is not valid, or the argument would not fit the
151
+ # ones declared before it
89
152
  #
90
153
  # @api public
91
154
  # @since 0.3.0
92
- #: (Symbol, untyped, ?default: untyped) -> Symbol
93
- def prop(name, type, default: UNSET)
94
- raise ArgumentError, "#{name.inspect} is not a valid prop name" unless NAME_FORMAT.match?(name.to_s)
155
+ # @rbs name: Symbol
156
+ # @rbs type: untyped
157
+ # @rbs kind: Symbol
158
+ # @rbs reader: Symbol | false
159
+ # @rbs writer: Symbol | false
160
+ # @rbs predicate: Symbol | false
161
+ # @rbs &coercion: ? (untyped) -> untyped
162
+ # @rbs return: Symbol
163
+ def prop?(name, type, kind = :keyword, reader: false, writer: false, predicate: false, &coercion)
164
+ raise ArgumentError, "prop? takes a :keyword or :positional kind" unless %i[keyword positional].include?(kind)
95
165
 
96
- own_props[name] = Prop.new(name, type, default)
97
- define_props_initializer
98
- name
166
+ prop = Prop.new(name, type, kind, default: UNSET, omittable: true, coercion:)
167
+ declare_prop(prop, { reader:, writer:, predicate: })
99
168
  end
100
169
 
101
170
  # Every prop this class declares, its superclasses' first.
102
171
  #
103
172
  # @api public
104
- # @since 0.3.0
173
+ # @since 0.2.1
105
174
  #: () -> Hash[Symbol, Prop]
106
175
  def props
107
176
  inherited = superclass.respond_to?(:props) ? superclass.props : {} #: Hash[Symbol, Prop]
@@ -110,20 +179,90 @@ module Phlex
110
179
 
111
180
  private
112
181
 
182
+ #: (Prop, Hash[Symbol, Symbol | false]) -> void
183
+ def check_methods(prop, methods)
184
+ methods.each do |option, visibility|
185
+ next if VISIBILITIES.include?(visibility)
186
+
187
+ raise ArgumentError, "#{option} must be one of #{VISIBILITIES.map(&:inspect).join(', ')}"
188
+ end
189
+
190
+ # A reader would replace `Object#class`, which Phlex and Hanami both call.
191
+ raise ArgumentError, "the :class prop cannot have a reader" if methods[:reader] && prop.name == :class
192
+ end
193
+
194
+ # Ruby allows no required positional argument after an optional one once there is a `*`,
195
+ # and the order would surprise a caller anyway.
196
+ #: (Prop, Array[Prop]) -> void
197
+ def check_positional_order(prop, others)
198
+ return unless prop.kind == :positional && !prop.optional?
199
+ return unless others.any? { |other| other.kind == :positional && other.optional? }
200
+
201
+ raise ArgumentError, "the required positional prop #{prop.name.inspect} follows an optional one"
202
+ end
203
+
204
+ #: (Prop, Array[Prop]) -> void
205
+ def check_splat(prop, others)
206
+ return unless %i[* **].include?(prop.kind) && others.any? { |other| other.kind == prop.kind }
207
+
208
+ raise ArgumentError, "#{self} already has a #{prop.kind.inspect} prop"
209
+ end
210
+
211
+ #: (Prop, Hash[Symbol, Symbol | false]) -> Symbol
212
+ def declare_prop(prop, methods)
213
+ others = props.reject { |name, _| name == prop.name }.values
214
+ check_methods(prop, methods)
215
+ check_splat(prop, others)
216
+ check_positional_order(prop, others)
217
+
218
+ own_props[prop.name] = prop
219
+ define_props_initializer
220
+ define_prop_methods(prop, methods)
221
+ prop.name
222
+ end
223
+
224
+ #: (Prop, Symbol) -> Symbol
225
+ def define_prop_method(prop, option)
226
+ ivar = :"@#{prop.name}"
227
+
228
+ case option
229
+ when :reader then props_module.define_method(prop.name) { instance_variable_get(ivar) }
230
+ when :predicate then props_module.define_method(:"#{prop.name}?") { !!instance_variable_get(ivar) }
231
+ else define_prop_writer(prop)
232
+ end
233
+ end
234
+
235
+ # Defines the reader, writer and predicate asked for, each with its own visibility.
236
+ #: (Prop, Hash[Symbol, Symbol | false]) -> void
237
+ def define_prop_methods(prop, methods)
238
+ methods.each do |option, visibility|
239
+ props_module.send(visibility, define_prop_method(prop, option)) if visibility
240
+ end
241
+ end
242
+
243
+ # A writer runs the value through the type, as `initialize` does.
244
+ #: (Prop) -> Symbol
245
+ def define_prop_writer(prop)
246
+ ivar = :"@#{prop.name}"
247
+
248
+ props_module.define_method(:"#{prop.name}=") do |value|
249
+ instance_variable_set(ivar, prop.cast(self.class, value))
250
+ end
251
+ end
252
+
113
253
  # Writes `initialize` into a module of its own rather than onto the class, so a class can
114
254
  # still define `initialize` and call `super`.
115
255
  #: () -> void
116
256
  def define_props_initializer
117
- @props_initializer ||= ::Module.new.tap { |initializer| include(initializer) }
118
- keywords = props.each_value.map { |prop| prop.optional? ? "#{prop.name}: #{UNSET_PATH}" : "#{prop.name}:" }
119
- signature = keywords.join(", ")
257
+ signature = props.values.sort_by.with_index { |prop, index| [KINDS.index(prop.kind), index] }.map(&:parameter)
120
258
 
121
- # `binding` rather than the names themselves, because a prop may be named after a
259
+ # `binding` rather than the names themselves, because a keyword may be named after a
122
260
  # reserved word such as `class`, which is a valid keyword but not a readable variable.
123
- @props_initializer.module_eval(<<~RUBY, __FILE__, __LINE__ + 1)
124
- def initialize(#{signature}) # def initialize(post:, compact: ::Phlex::Hanami::Props::UNSET)
125
- ::Phlex::Hanami::Props.assign(self, binding) # ::Phlex::Hanami::Props.assign(self, binding)
126
- end # end
261
+ props_module.module_eval(<<~RUBY, __FILE__, __LINE__ + 1)
262
+ def initialize(#{signature.join(', ')}) # def initialize(post:, compact: ::Phlex::Hanami::Props::UNSET)
263
+ ::Phlex::Hanami::Props.assign(self, binding) # ::Phlex::Hanami::Props.assign(self, binding)
264
+ after_initialize if respond_to?(:after_initialize, true) # after_initialize if respond_to?(:after_initialize, true)
265
+ end # end
127
266
  RUBY
128
267
  end
129
268
 
@@ -131,59 +270,122 @@ module Phlex
131
270
  def own_props
132
271
  @own_props ||= {}
133
272
  end
273
+
274
+ #: () -> Module
275
+ def props_module
276
+ @props_module ||= ::Module.new.tap { |props_module| include(props_module) }
277
+ end
134
278
  end
135
279
 
136
280
  # One declared prop.
137
281
  #
138
282
  # @api private
139
- # @since 0.3.0
283
+ # @since 0.2.1
140
284
  class Prop
141
285
  attr_reader :name #: Symbol
142
286
 
143
- #: (Symbol, untyped, untyped) -> void
144
- def initialize(name, type, default)
287
+ attr_reader :kind #: Symbol
288
+
289
+ #: (Symbol, untyped, Symbol, default: untyped, omittable: bool, coercion: Proc?) -> void
290
+ def initialize(name, type, kind, default:, omittable:, coercion:)
291
+ raise ArgumentError, "#{name.inspect} is not a valid prop name" unless NAME_FORMAT.match?(name.to_s)
292
+ raise ArgumentError, "kind must be one of #{KINDS.map(&:inspect).join(', ')}" unless KINDS.include?(kind)
293
+ raise ArgumentError, "a #{kind.inspect} prop cannot have a default" if splat?(kind) && !UNSET.equal?(default)
294
+
145
295
  @name = name
146
296
  @type = type
297
+ @kind = kind
147
298
  @default = default
299
+ @omittable = omittable
300
+ @coercion = coercion
301
+ end
302
+
303
+ # Runs a value through the type.
304
+ #
305
+ # @raise [InvalidPropError] if the type rejects the value
306
+ #
307
+ #: (Module, untyped) -> untyped
308
+ def cast(view_class, value)
309
+ if @type.respond_to?(:call)
310
+ begin
311
+ return @type.call(value)
312
+ rescue StandardError => e
313
+ raise InvalidPropError.new(view_class, @name, e.message)
314
+ end
315
+ end
316
+
317
+ return value if @type === value # rubocop:disable Style/CaseEquality
318
+
319
+ raise InvalidPropError.new(view_class, @name, "#{value.inspect} is not a #{@type.inspect}")
148
320
  end
149
321
 
150
- # Whether the keyword can be left out.
322
+ # Whether the argument can be left out.
151
323
  #
152
324
  #: () -> bool
153
325
  def optional?
154
- !UNSET.equal?(@default) || type_default?
326
+ @omittable || splat?(@kind) || !UNSET.equal?(@default) || type_default? || nilable?
327
+ end
328
+
329
+ # This prop's part of the generated initializer's signature.
330
+ #
331
+ #: () -> String
332
+ def parameter
333
+ case @kind
334
+ when :keyword then optional? ? "#{@name}: #{UNSET_PATH}" : "#{@name}:"
335
+ when :positional then optional? ? "#{variable} = #{UNSET_PATH}" : variable.to_s
336
+ else "#{@kind}#{variable}"
337
+ end
155
338
  end
156
339
 
157
340
  # The value to assign, given what the caller passed or {UNSET}.
158
341
  #
159
342
  # @raise [InvalidPropError] if the type rejects the value
160
343
  #
161
- #: (Module, untyped) -> untyped
162
- def resolve(view_class, value)
344
+ #: (untyped, untyped) -> untyped
345
+ def resolve(view, value)
163
346
  if UNSET.equal?(value)
164
- return @type.call if UNSET.equal?(@default)
347
+ return UNSET if @omittable
348
+ return @type.call if UNSET.equal?(@default) && type_default?
165
349
 
166
- value = @default.is_a?(::Proc) ? @default.call : @default
350
+ value = default_value(view)
167
351
  end
168
352
 
169
- cast(view_class, value)
353
+ value = view.instance_exec(value, &@coercion) if @coercion
354
+ cast(view.class, value)
355
+ end
356
+
357
+ # The local variable the generated initializer receives the value in. A keyword keeps its
358
+ # name, since auto render reads it from the signature. Any other argument gets one that
359
+ # cannot clash with a reserved word.
360
+ #
361
+ #: () -> Symbol
362
+ def variable
363
+ @kind == :keyword ? @name : :"__#{@name}__"
170
364
  end
171
365
 
172
366
  private
173
367
 
174
- #: (Module, untyped) -> untyped
175
- def cast(view_class, value)
176
- if @type.respond_to?(:call)
177
- begin
178
- return @type.call(value)
179
- rescue StandardError => e
180
- raise InvalidPropError.new(view_class, @name, e.message)
181
- end
182
- end
368
+ # A prop with no default of its own is optional only because its type accepts nil.
369
+ #: (untyped) -> untyped
370
+ def default_value(view)
371
+ return nil if UNSET.equal?(@default)
183
372
 
184
- return value if @type === value # rubocop:disable Style/CaseEquality
373
+ @default.is_a?(::Proc) ? view.instance_exec(&@default) : @default
374
+ end
185
375
 
186
- raise InvalidPropError.new(view_class, @name, "#{value.inspect} is not a #{@type.inspect}")
376
+ # A dry type built with `.optional`, or anything else that matches nil. A callable is left
377
+ # out of the second test, since `===` on a Proc calls it.
378
+ #: () -> bool
379
+ def nilable?
380
+ return @type.optional? if @type.respond_to?(:optional?)
381
+ return false if @type.respond_to?(:call)
382
+
383
+ @type === nil # rubocop:disable Style/CaseEquality, Style/NilComparison
384
+ end
385
+
386
+ #: (Symbol) -> bool
387
+ def splat?(kind)
388
+ %i[* **].include?(kind)
187
389
  end
188
390
 
189
391
  # A dry type built with `.default` fills in a missing value itself.
@@ -9,34 +9,68 @@ module Phlex
9
9
 
10
10
  NAME_FORMAT: Regexp
11
11
 
12
+ KINDS: Array[Symbol]
13
+
14
+ VISIBILITIES: Array[Symbol | false]
15
+
12
16
  def self.assign: (untyped, Binding) -> void
13
17
 
14
18
  def self.included: (Module) -> void
15
19
 
16
20
  module ClassMethods
17
- def prop: (Symbol, untyped, ?default: untyped) -> Symbol
21
+ def prop: (Symbol name, untyped type, ?Symbol kind, ?default: untyped, ?reader: Symbol | false, ?writer: Symbol | false, ?predicate: Symbol | false) ?{ (untyped) -> untyped } -> Symbol
22
+
23
+ def prop?: (Symbol name, untyped type, ?Symbol kind, ?reader: Symbol | false, ?writer: Symbol | false, ?predicate: Symbol | false) ?{ (untyped) -> untyped } -> Symbol
18
24
 
19
25
  def props: () -> Hash[Symbol, Prop]
20
26
 
21
27
  private
22
28
 
29
+ def check_methods: (Prop, Hash[Symbol, Symbol | false]) -> void
30
+
31
+ def check_positional_order: (Prop, Array[Prop]) -> void
32
+
33
+ def check_splat: (Prop, Array[Prop]) -> void
34
+
35
+ def declare_prop: (Prop, Hash[Symbol, Symbol | false]) -> Symbol
36
+
37
+ def define_prop_method: (Prop, Symbol) -> Symbol
38
+
39
+ def define_prop_writer: (Prop) -> Symbol
40
+
41
+ def define_prop_methods: (Prop, Hash[Symbol, Symbol | false]) -> void
42
+
23
43
  def define_props_initializer: () -> void
24
44
 
25
45
  def own_props: () -> Hash[Symbol, Prop]
46
+
47
+ def props_module: () -> Module
26
48
  end
27
49
 
28
50
  class Prop
29
51
  attr_reader name: Symbol
30
52
 
31
- def initialize: (Symbol, untyped, untyped) -> void
53
+ attr_reader kind: Symbol
54
+
55
+ def initialize: (Symbol, untyped, Symbol, default: untyped, omittable: bool, coercion: Proc?) -> void
56
+
57
+ def cast: (Module, untyped) -> untyped
32
58
 
33
59
  def optional?: () -> bool
34
60
 
35
- def resolve: (Module, untyped) -> untyped
61
+ def parameter: () -> String
62
+
63
+ def resolve: (untyped, untyped) -> untyped
64
+
65
+ def variable: () -> Symbol
36
66
 
37
67
  private
38
68
 
39
- def cast: (Module, untyped) -> untyped
69
+ def default_value: (untyped) -> untyped
70
+
71
+ def nilable?: () -> bool
72
+
73
+ def splat?: (Symbol) -> bool
40
74
 
41
75
  def type_default?: () -> bool
42
76
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phlex-hanami
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aaron Allen
@@ -141,7 +141,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
141
141
  - !ruby/object:Gem::Version
142
142
  version: '0'
143
143
  requirements: []
144
- rubygems_version: 4.0.16
144
+ rubygems_version: 4.0.20
145
145
  specification_version: 4
146
146
  summary: A Phlex adapter for Hanami
147
147
  test_files: []