phlex-hanami 0.2.0 → 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 +4 -4
- data/CHANGELOG.md +32 -1
- data/lib/phlex/hanami/errors/invalid_prop_error.rb +24 -0
- data/lib/phlex/hanami/props.rb +399 -0
- data/sig/phlex/hanami/errors/invalid_prop_error.rbs +9 -0
- data/sig/phlex/hanami/props.rbs +79 -0
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2bb01643b7c075e6b220c2d7ad70e885caba136346380f4236f716350ff7de98
|
|
4
|
+
data.tar.gz: fc0766f20a6709feae68138171b2f0e15723c59c30adf90fcfaa4dfef8bc8986
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d30170e3f93a92fdde6d70db733abc97a3384673e9bffa8629737a972aacf2194f1791fe8335a1889c8ed69ec05796c87e8c503e248bc77f3ff2bffb8c46d213
|
|
7
|
+
data.tar.gz: 12d33fe3ddf6952ea5714be91db82b954db1848d2bad93e74bbaa8c5b1643c66ec68bf5ad9e3e9c1db0a02ca7f5ee4822c3863c899b6d2d9f1345dbf2f7457e2
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,35 @@ 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
|
+
|
|
32
|
+
## [v0.2.1] - 2026-09-26
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- `Phlex::Hanami::Props`, an opt in `prop` for views and components typed with dry-types. The type coerces as
|
|
37
|
+
well as checks, so a view can turn a request param into an Integer. Literal still works as before.
|
|
38
|
+
|
|
10
39
|
## [v0.2.0] - 2026-09-10
|
|
11
40
|
|
|
12
41
|
First stable release by the new maintainer. It shares no code with 0.1.0, so treat an upgrade from 0.1.0 as a move
|
|
@@ -101,7 +130,9 @@ Initial alpha release by the new maintainer [@aaronmallen](https://github.com/aa
|
|
|
101
130
|
|
|
102
131
|
Initial release, by the previous maintainer [@stephannv](https://github.com/stephannv).
|
|
103
132
|
|
|
104
|
-
[Unreleased]: https://github.com/aaronmallen/phlex-hanami/compare/0.2.
|
|
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
|
|
105
136
|
[v0.2.0]: https://github.com/aaronmallen/phlex-hanami/compare/0.2.0-alpha.3...0.2.0
|
|
106
137
|
[v0.2.0-alpha.3]: https://github.com/aaronmallen/phlex-hanami/compare/0.2.0-alpha.2...0.2.0-alpha.3
|
|
107
138
|
[v0.2.0-alpha.2]: https://github.com/aaronmallen/phlex-hanami/compare/0.2.0-alpha.1...0.2.0-alpha.2
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phlex
|
|
4
|
+
module Hanami
|
|
5
|
+
# Raised when a prop's type rejects the value it was given.
|
|
6
|
+
#
|
|
7
|
+
# The type's own error, when there is one, is kept as the `cause`.
|
|
8
|
+
#
|
|
9
|
+
# @api public
|
|
10
|
+
# @since 0.2.1
|
|
11
|
+
class InvalidPropError < Error
|
|
12
|
+
# @api private
|
|
13
|
+
# @since 0.2.1
|
|
14
|
+
#: (Module, Symbol, String) -> void
|
|
15
|
+
def initialize(view_class, name, reason)
|
|
16
|
+
super(<<~MESSAGE)
|
|
17
|
+
#{view_class} was given an invalid #{name.inspect} prop.
|
|
18
|
+
|
|
19
|
+
#{reason}
|
|
20
|
+
MESSAGE
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phlex
|
|
4
|
+
module Hanami
|
|
5
|
+
# Declared props, typed with dry-types.
|
|
6
|
+
#
|
|
7
|
+
# Opt in per class. Each `prop` names an argument `initialize` takes and the type its value goes
|
|
8
|
+
# through, and the value lands in an instance variable of the same name. A dry type is called,
|
|
9
|
+
# so it coerces as well as checks: `Types::Params::Integer` turns the string a request param
|
|
10
|
+
# arrives as into an Integer. Anything else that answers `call` is called the same way, and
|
|
11
|
+
# anything that does not, such as a plain class, is matched with `===`.
|
|
12
|
+
#
|
|
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`. A
|
|
15
|
+
# `:**` prop takes them all, request params included.
|
|
16
|
+
#
|
|
17
|
+
# Literal needs none of this. Extend `Literal::Properties` instead if you would rather use it.
|
|
18
|
+
#
|
|
19
|
+
# @example
|
|
20
|
+
# class Card < Phlex::Hanami::Component
|
|
21
|
+
# include Phlex::Hanami::Props
|
|
22
|
+
#
|
|
23
|
+
# prop :post, Types::Instance(Post)
|
|
24
|
+
# prop :count, Types::Params::Integer
|
|
25
|
+
# prop :compact, Types::Bool, default: false
|
|
26
|
+
# prop :tags, Types::Array.of(Types::String), default: -> { [] }
|
|
27
|
+
# prop :attributes, Types::Hash, :**
|
|
28
|
+
#
|
|
29
|
+
# def view_template
|
|
30
|
+
# article(class: ("compact" if @compact), **@attributes) { h2 { @post.title } }
|
|
31
|
+
# end
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
# @api public
|
|
35
|
+
# @since 0.2.1
|
|
36
|
+
module Props
|
|
37
|
+
# The value of a prop declared with `prop?` that the caller left out, so a view can tell it
|
|
38
|
+
# from `nil`.
|
|
39
|
+
#
|
|
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
|
|
48
|
+
|
|
49
|
+
# How the generated initializer names {UNSET}.
|
|
50
|
+
#
|
|
51
|
+
# @api private
|
|
52
|
+
# @since 0.2.1
|
|
53
|
+
UNSET_PATH = "::Phlex::Hanami::Props::UNSET" #: String
|
|
54
|
+
|
|
55
|
+
# A prop name has to be a Ruby identifier, because it becomes an argument and an instance
|
|
56
|
+
# variable.
|
|
57
|
+
#
|
|
58
|
+
# @api private
|
|
59
|
+
# @since 0.2.1
|
|
60
|
+
NAME_FORMAT = /\A[a-z_][a-zA-Z0-9_]*\z/ #: Regexp
|
|
61
|
+
|
|
62
|
+
# The argument kinds a prop can take, in the order `initialize` declares them.
|
|
63
|
+
#
|
|
64
|
+
# @api private
|
|
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
|
|
78
|
+
#: (untyped, Binding) -> void
|
|
79
|
+
def self.assign(view, arguments)
|
|
80
|
+
view.class.props.each_value do |prop|
|
|
81
|
+
value = prop.resolve(view, arguments.local_variable_get(prop.variable))
|
|
82
|
+
view.instance_variable_set(:"@#{prop.name}", value)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# @api private
|
|
87
|
+
# @since 0.2.1
|
|
88
|
+
#: (Module) -> void
|
|
89
|
+
def self.included(view_class)
|
|
90
|
+
view_class.extend(ClassMethods)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# @api public
|
|
94
|
+
# @since 0.2.1
|
|
95
|
+
module ClassMethods
|
|
96
|
+
# Declares a prop.
|
|
97
|
+
#
|
|
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.
|
|
140
|
+
#
|
|
141
|
+
# @param name [Symbol] the argument, and the instance variable the value lands in
|
|
142
|
+
# @param type [#call, #===] a dry type, or anything that answers `call` or `===`
|
|
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
|
|
147
|
+
#
|
|
148
|
+
# @return [Symbol] the name
|
|
149
|
+
#
|
|
150
|
+
# @raise [ArgumentError] if an option is not valid, or the argument would not fit the
|
|
151
|
+
# ones declared before it
|
|
152
|
+
#
|
|
153
|
+
# @api public
|
|
154
|
+
# @since 0.3.0
|
|
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)
|
|
165
|
+
|
|
166
|
+
prop = Prop.new(name, type, kind, default: UNSET, omittable: true, coercion:)
|
|
167
|
+
declare_prop(prop, { reader:, writer:, predicate: })
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Every prop this class declares, its superclasses' first.
|
|
171
|
+
#
|
|
172
|
+
# @api public
|
|
173
|
+
# @since 0.2.1
|
|
174
|
+
#: () -> Hash[Symbol, Prop]
|
|
175
|
+
def props
|
|
176
|
+
inherited = superclass.respond_to?(:props) ? superclass.props : {} #: Hash[Symbol, Prop]
|
|
177
|
+
inherited.merge(own_props)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
private
|
|
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
|
+
|
|
253
|
+
# Writes `initialize` into a module of its own rather than onto the class, so a class can
|
|
254
|
+
# still define `initialize` and call `super`.
|
|
255
|
+
#: () -> void
|
|
256
|
+
def define_props_initializer
|
|
257
|
+
signature = props.values.sort_by.with_index { |prop, index| [KINDS.index(prop.kind), index] }.map(&:parameter)
|
|
258
|
+
|
|
259
|
+
# `binding` rather than the names themselves, because a keyword may be named after a
|
|
260
|
+
# reserved word such as `class`, which is a valid keyword but not a readable variable.
|
|
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
|
|
266
|
+
RUBY
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
#: () -> Hash[Symbol, Prop]
|
|
270
|
+
def own_props
|
|
271
|
+
@own_props ||= {}
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
#: () -> Module
|
|
275
|
+
def props_module
|
|
276
|
+
@props_module ||= ::Module.new.tap { |props_module| include(props_module) }
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# One declared prop.
|
|
281
|
+
#
|
|
282
|
+
# @api private
|
|
283
|
+
# @since 0.2.1
|
|
284
|
+
class Prop
|
|
285
|
+
attr_reader :name #: Symbol
|
|
286
|
+
|
|
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
|
+
|
|
295
|
+
@name = name
|
|
296
|
+
@type = type
|
|
297
|
+
@kind = kind
|
|
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}")
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Whether the argument can be left out.
|
|
323
|
+
#
|
|
324
|
+
#: () -> bool
|
|
325
|
+
def optional?
|
|
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
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# The value to assign, given what the caller passed or {UNSET}.
|
|
341
|
+
#
|
|
342
|
+
# @raise [InvalidPropError] if the type rejects the value
|
|
343
|
+
#
|
|
344
|
+
#: (untyped, untyped) -> untyped
|
|
345
|
+
def resolve(view, value)
|
|
346
|
+
if UNSET.equal?(value)
|
|
347
|
+
return UNSET if @omittable
|
|
348
|
+
return @type.call if UNSET.equal?(@default) && type_default?
|
|
349
|
+
|
|
350
|
+
value = default_value(view)
|
|
351
|
+
end
|
|
352
|
+
|
|
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}__"
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
private
|
|
367
|
+
|
|
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)
|
|
372
|
+
|
|
373
|
+
@default.is_a?(::Proc) ? view.instance_exec(&@default) : @default
|
|
374
|
+
end
|
|
375
|
+
|
|
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)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# A dry type built with `.default` fills in a missing value itself.
|
|
392
|
+
#: () -> bool
|
|
393
|
+
def type_default?
|
|
394
|
+
@type.respond_to?(:default?) && @type.default?
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Generated from lib/phlex/hanami/props.rb with RBS::Inline
|
|
2
|
+
|
|
3
|
+
module Phlex
|
|
4
|
+
module Hanami
|
|
5
|
+
module Props
|
|
6
|
+
UNSET: Object
|
|
7
|
+
|
|
8
|
+
UNSET_PATH: String
|
|
9
|
+
|
|
10
|
+
NAME_FORMAT: Regexp
|
|
11
|
+
|
|
12
|
+
KINDS: Array[Symbol]
|
|
13
|
+
|
|
14
|
+
VISIBILITIES: Array[Symbol | false]
|
|
15
|
+
|
|
16
|
+
def self.assign: (untyped, Binding) -> void
|
|
17
|
+
|
|
18
|
+
def self.included: (Module) -> void
|
|
19
|
+
|
|
20
|
+
module ClassMethods
|
|
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
|
|
24
|
+
|
|
25
|
+
def props: () -> Hash[Symbol, Prop]
|
|
26
|
+
|
|
27
|
+
private
|
|
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
|
+
|
|
43
|
+
def define_props_initializer: () -> void
|
|
44
|
+
|
|
45
|
+
def own_props: () -> Hash[Symbol, Prop]
|
|
46
|
+
|
|
47
|
+
def props_module: () -> Module
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
class Prop
|
|
51
|
+
attr_reader name: Symbol
|
|
52
|
+
|
|
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
|
|
58
|
+
|
|
59
|
+
def optional?: () -> bool
|
|
60
|
+
|
|
61
|
+
def parameter: () -> String
|
|
62
|
+
|
|
63
|
+
def resolve: (untyped, untyped) -> untyped
|
|
64
|
+
|
|
65
|
+
def variable: () -> Symbol
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def default_value: (untyped) -> untyped
|
|
70
|
+
|
|
71
|
+
def nilable?: () -> bool
|
|
72
|
+
|
|
73
|
+
def splat?: (Symbol) -> bool
|
|
74
|
+
|
|
75
|
+
def type_default?: () -> bool
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
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.
|
|
4
|
+
version: 0.2.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Aaron Allen
|
|
@@ -68,6 +68,7 @@ files:
|
|
|
68
68
|
- lib/phlex/hanami/context.rb
|
|
69
69
|
- lib/phlex/hanami/contextual.rb
|
|
70
70
|
- lib/phlex/hanami/errors/error.rb
|
|
71
|
+
- lib/phlex/hanami/errors/invalid_prop_error.rb
|
|
71
72
|
- lib/phlex/hanami/errors/mailer_view_error.rb
|
|
72
73
|
- lib/phlex/hanami/errors/missing_context_error.rb
|
|
73
74
|
- lib/phlex/hanami/errors/relative_path_error.rb
|
|
@@ -80,6 +81,7 @@ files:
|
|
|
80
81
|
- lib/phlex/hanami/mailer/renderable.rb
|
|
81
82
|
- lib/phlex/hanami/mailer/text.rb
|
|
82
83
|
- lib/phlex/hanami/mailer/view.rb
|
|
84
|
+
- lib/phlex/hanami/props.rb
|
|
83
85
|
- lib/phlex/hanami/renderable.rb
|
|
84
86
|
- lib/phlex/hanami/rspec.rb
|
|
85
87
|
- lib/phlex/hanami/slice_configured.rb
|
|
@@ -96,6 +98,7 @@ files:
|
|
|
96
98
|
- sig/phlex/hanami/context.rbs
|
|
97
99
|
- sig/phlex/hanami/contextual.rbs
|
|
98
100
|
- sig/phlex/hanami/errors/error.rbs
|
|
101
|
+
- sig/phlex/hanami/errors/invalid_prop_error.rbs
|
|
99
102
|
- sig/phlex/hanami/errors/mailer_view_error.rbs
|
|
100
103
|
- sig/phlex/hanami/errors/missing_context_error.rbs
|
|
101
104
|
- sig/phlex/hanami/errors/relative_path_error.rbs
|
|
@@ -108,6 +111,7 @@ files:
|
|
|
108
111
|
- sig/phlex/hanami/mailer/renderable.rbs
|
|
109
112
|
- sig/phlex/hanami/mailer/text.rbs
|
|
110
113
|
- sig/phlex/hanami/mailer/view.rbs
|
|
114
|
+
- sig/phlex/hanami/props.rbs
|
|
111
115
|
- sig/phlex/hanami/renderable.rbs
|
|
112
116
|
- sig/phlex/hanami/rspec.rbs
|
|
113
117
|
- sig/phlex/hanami/slice_configured.rbs
|
|
@@ -137,7 +141,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
137
141
|
- !ruby/object:Gem::Version
|
|
138
142
|
version: '0'
|
|
139
143
|
requirements: []
|
|
140
|
-
rubygems_version: 4.0.
|
|
144
|
+
rubygems_version: 4.0.20
|
|
141
145
|
specification_version: 4
|
|
142
146
|
summary: A Phlex adapter for Hanami
|
|
143
147
|
test_files: []
|