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 +4 -4
- data/CHANGELOG.md +25 -1
- data/lib/phlex/hanami/errors/invalid_prop_error.rb +2 -2
- data/lib/phlex/hanami/props.rb +259 -57
- data/sig/phlex/hanami/props.rbs +38 -4
- metadata +2 -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,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.
|
|
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.
|
|
10
|
+
# @since 0.2.1
|
|
11
11
|
class InvalidPropError < Error
|
|
12
12
|
# @api private
|
|
13
|
-
# @since 0.
|
|
13
|
+
# @since 0.2.1
|
|
14
14
|
#: (Module, Symbol, String) -> void
|
|
15
15
|
def initialize(view_class, name, reason)
|
|
16
16
|
super(<<~MESSAGE)
|
data/lib/phlex/hanami/props.rb
CHANGED
|
@@ -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
|
|
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.
|
|
35
|
+
# @since 0.2.1
|
|
34
36
|
module Props
|
|
35
|
-
#
|
|
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
|
-
# @
|
|
38
|
-
#
|
|
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
|
|
40
48
|
|
|
41
49
|
# How the generated initializer names {UNSET}.
|
|
42
50
|
#
|
|
43
51
|
# @api private
|
|
44
|
-
# @since 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
|
|
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.
|
|
59
|
+
# @since 0.2.1
|
|
52
60
|
NAME_FORMAT = /\A[a-z_][a-zA-Z0-9_]*\z/ #: Regexp
|
|
53
61
|
|
|
54
|
-
#
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
79
|
-
#
|
|
80
|
-
#
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
124
|
-
def initialize(#{signature})
|
|
125
|
-
::Phlex::Hanami::Props.assign(self, binding)
|
|
126
|
-
|
|
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.
|
|
283
|
+
# @since 0.2.1
|
|
140
284
|
class Prop
|
|
141
285
|
attr_reader :name #: Symbol
|
|
142
286
|
|
|
143
|
-
#:
|
|
144
|
-
|
|
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
|
|
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
|
-
#: (
|
|
162
|
-
def resolve(
|
|
344
|
+
#: (untyped, untyped) -> untyped
|
|
345
|
+
def resolve(view, value)
|
|
163
346
|
if UNSET.equal?(value)
|
|
164
|
-
return
|
|
347
|
+
return UNSET if @omittable
|
|
348
|
+
return @type.call if UNSET.equal?(@default) && type_default?
|
|
165
349
|
|
|
166
|
-
value =
|
|
350
|
+
value = default_value(view)
|
|
167
351
|
end
|
|
168
352
|
|
|
169
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
373
|
+
@default.is_a?(::Proc) ? view.instance_exec(&@default) : @default
|
|
374
|
+
end
|
|
185
375
|
|
|
186
|
-
|
|
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.
|
data/sig/phlex/hanami/props.rbs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
144
|
+
rubygems_version: 4.0.20
|
|
145
145
|
specification_version: 4
|
|
146
146
|
summary: A Phlex adapter for Hanami
|
|
147
147
|
test_files: []
|