marameters 3.12.0 → 4.0.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: bc3c9fb9d29b4864125a1cdd08516fe3092bc36de4f0122298bbbd9b8f7e7490
4
- data.tar.gz: 4efabec65f0782ddb448f72c35c2f4b900fa8d8828c28abc3891a9e7b43ee636
3
+ metadata.gz: b120830ab23db1facffaa30df5ddbc96557d37d9910a24c9b531dd7ad755c1e0
4
+ data.tar.gz: ed8fa0e3aaed0a2ce8c7ad0a5954a8574eaa4242206c5aeff81fcfb5a2b5321b
5
5
  SHA512:
6
- metadata.gz: 6fa293cff9a15f7cd3fc10be82ddad97aff22e1e1fb33748dca1db035466ed764082397c50174d6851b5538f571310539baec240a288a8ca9f9bc6735ab8cb21
7
- data.tar.gz: 3ca85f9fdec13a3b7c04b13a73103d8ea0944890df467fbcd3d5dcfbb35a7c85197cd0aafea41facfebdcd5aca40f7239f92119b52d35ea1086d5c988281ec4a
6
+ metadata.gz: 959d5a7edfafad4f1da16a02ea1a54fbfd23e3cb165230eb6239bd811b0d895e18f65a1d9b08fed8971e1cea6b306b87d095e3451e7c4ca4755a520e81722b93
7
+ data.tar.gz: 5932395cedf488a17862bfe5f820972d4d92cce26f19ff384134d44393ac3ea35219160d9dfca6150e9096ae7e9e5b0de9ab2a6d1881441333bd6c959e66aeda
checksums.yaml.gz.sig CHANGED
Binary file
data/README.adoc CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  :amazing_print_link: link:https://github.com/amazing-print/amazing_print[Amazing Print]
6
6
  :article_link: link:https://alchemists.io/articles/ruby_method_parameters_and_arguments[method parameters and arguments]
7
+ :infusible_link: link:/projects/infusible[Infusible]
7
8
 
8
9
  = Marameters
9
10
 
@@ -62,40 +63,66 @@ require "marameters"
62
63
 
63
64
  At a high level, you can use `Marameters` as a single Object API for accessing all capabilities provided by this gem. Here's an overview:
64
65
 
66
+ *Setup*
67
+
65
68
  [source,ruby]
66
69
  ----
67
- # Setup
68
70
  def demo(one, two = 2, three: 3) = puts "One: #{one}, Two: #{two}, Three: #{three}"
69
71
 
70
72
  parameters = method(:demo).parameters
71
73
  arguments = %w[one two]
74
+ ----
72
75
 
73
- # Marameters::Categorizer wrapper
76
+ *Categorize*
74
77
 
78
+ [source,ruby]
79
+ ----
75
80
  Marameters.categorize parameters, arguments
76
- # #<struct Marameters::Splat positionals=["one", "two"], keywords={}, block=nil>
81
+ # #<struct Marameters::Models::Forward positionals=["one", "two"], keywords={}, block=nil>
82
+ ----
77
83
 
78
- # Marameters::Probe wrapper
84
+ *Probe*
79
85
 
86
+ [source,ruby]
87
+ ----
80
88
  Marameters.of self, :demo # []
81
89
 
82
90
  probe = Marameters.for parameters
83
- probe.to_a # [[:req, :one], [:opt, :two], [:key, :three]]
84
91
  probe.positionals # [:one, :two]
85
92
  probe.keywords # [:three]
86
- probe.block # nil
93
+ probe.to_a # [[:req, :one], [:opt, :two], [:key, :three]]
94
+ ----
87
95
 
88
- # Marameters::Signature wrapper
96
+ *Signature*
89
97
 
90
- Marameters.signature({req: :one, opt: [:two, 2], key: [:three, 3]}).to_s
91
- # one, two = 2, three: 3
98
+ [source,ruby]
92
99
  ----
100
+ Marameters.signature([%i[req one], [:opt, :two, 2], [:key, :three, 3]]).to_s
101
+ # "one, two = 2, three: 3"
102
+ ----
103
+
104
+ === Constants
105
+
106
+ The `KINDS` constant allows you to know the kinds of parameters allowed:
93
107
 
94
- Read on to learn more about the details on how each of these methods work and the objects they wrap.
108
+ [source,ruby]
109
+ ----
110
+ Marameters::KINDS
111
+ # [
112
+ # :req,
113
+ # :opt,
114
+ # :rest,
115
+ # :nokey,
116
+ # :keyreq,
117
+ # :key,
118
+ # :keyrest,
119
+ # :block
120
+ # ]
121
+ ----
95
122
 
96
123
  === Probe
97
124
 
98
- The probe allows you to analyze a method's parameters. To understand how, consider the following:
125
+ The probe (`Marameters::Probe`) allows you to analyze a method's parameters. To understand how, consider the following:
99
126
 
100
127
  [source,ruby]
101
128
  ----
@@ -120,58 +147,93 @@ You can then probe the `#all` method's parameters as follows:
120
147
 
121
148
  [source,ruby]
122
149
  ----
123
- probe = Marameters::Probe.new Demo.instance_method(:all).parameters
124
-
125
- probe.block # :seven
126
- probe.block? # true
127
- probe.empty? # false
128
- probe.keywords # [:four, :five]
129
- probe.keywords? # true
130
- probe.kind?(:keyrest) # true
131
- probe.kinds # [:req, :opt, :rest, :keyreq, :key, :keyrest, :block]
132
- probe.name?(:three) # true
133
- probe.names # [:one, :two, :three, :four, :five, :six, :seven]
134
- probe.only_bare_splats? # false
135
- probe.only_double_splats? # false
136
- probe.only_single_splats? # false
137
- probe.positionals # [:one, :two]
138
- probe.positionals? # true
139
- probe.splats # [:three, :six]
140
- probe.splats? # true
141
- probe.to_a # [[:req, :one], [:opt, :two], [:rest, :three], [:keyreq, :four], [:key, :five], [:keyrest, :six], [:block, :seven]]
142
- probe.to_h # {req: :one, opt: :two, rest: :three, keyreq: :four, key: :five, keyrest: :six, block: :seven}
143
- ----
144
-
145
- In contrast the above, we can also probe the `#none` method which has no parameters for a completely
150
+ probe = Marameters.for Demo.instance_method(:all).parameters
151
+
152
+ probe.deconstruct # (same as to_a, see below)
153
+ probe.empty? # false
154
+ probe.include? %i[req one] # true
155
+ probe.keywords # [:four, :five]
156
+ probe.keywords? # true
157
+ probe.keywords_for :four, four: :demo # {four: :demo}
158
+ probe.kind?(:keyrest) # true
159
+
160
+ probe.kinds
161
+ # [:req, :opt, :rest, :keyreq, :key, :keyrest, :block]
162
+
163
+ probe.name?(:three) # true
164
+
165
+ probe.names
166
+ # [:one, :two, :three, :four, :five, :six, :seven]
167
+
168
+ probe.only_bare_splats? # false
169
+ probe.only_double_splats? # false
170
+ probe.only_single_splats? # false
171
+ probe.positionals # [:one, :two]
172
+ probe.positionals? # true
173
+ probe.positionals_and_maybe_keywords? # true
174
+
175
+ probe.to_a
176
+ # [
177
+ # [:req, :one],
178
+ # [:opt, :two],
179
+ # [:rest, :three],
180
+ # [:keyreq, :four],
181
+ # [:key, :five],
182
+ # [:keyrest, :six],
183
+ # [:block, :seven]
184
+ # ]
185
+ ----
186
+
187
+ In contrast to the above, we can probe the `#none` method which has no parameters for a completely
146
188
  different result:
147
189
 
148
190
  [source,ruby]
149
191
  ----
150
- probe = Marameters::Probe.new Demo.instance_method(:none).parameters
192
+ probe = Marameters.for Demo.instance_method(:none).parameters
151
193
 
152
- probe.block # nil
153
- probe.block? # false
154
- probe.empty? # true
155
- probe.keywords # []
156
- probe.keywords? # false
157
- probe.kind?(:req) # true
158
- probe.kinds # []
159
- probe.name?(:three) # false
160
- probe.names # []
161
- probe.only_bare_splats? # false
162
- probe.only_double_splats? # false
163
- probe.only_single_splats? # false
164
- probe.positionals # []
165
- probe.positionals? # false
166
- probe.splats # []
167
- probe.splats? # false
168
- probe.to_a # []
169
- probe.to_h # {}
194
+ probe.deconstruct # (same as to_a, see below)
195
+ probe.empty? # true
196
+ probe.include? %i[req one] # false
197
+ probe.keywords # []
198
+ probe.keywords? # false
199
+ probe.keywords_for :four, four: :demo # {}
200
+ probe.kind?(:req) # true
201
+ probe.kinds # []
202
+ probe.name?(:three) # false
203
+ probe.names # []
204
+ probe.only_bare_splats? # false
205
+ probe.only_double_splats? # false
206
+ probe.only_single_splats? # false
207
+ probe.positionals # []
208
+ probe.positionals? # false
209
+ probe.positionals_and_maybe_keywords? # false
210
+ probe.to_a # []
170
211
  ----
171
212
 
172
- === Categorizer
213
+ The `#keywords_for` method might need additional explaining because it's meant for selecting keywords which adhere to _either_ of the following criteria:
173
214
 
174
- The categorizer allows you to dynamically build positional, keyword, and block arguments for message passing. This is most valuable when you know the object and method while needing to align the arguments in the right order. Here's a demonstration where {amazing_print_link} (i.e. `ap`) is used to format the output:
215
+ * The given keys don't match any key in the given attributes.
216
+ * The given keys match the parameter keywords.
217
+
218
+ [source,ruby]
219
+ ----
220
+ module Demo
221
+ def self.keywords(four:, five: 5, **six) = puts "Four: #{four}, Five: #{five}, Six: #{six}"
222
+ end
223
+
224
+ probe = Marameters.for Demo.method(:keywords).parameters
225
+
226
+ probe.keywords_for :a, a: 1, four: 4 # {four: 4}
227
+ probe.keywords_for :four, a: 1 # {a: 1}
228
+ probe.keywords_for :a, four: 4, five: :five # {four: 4, five: :five}
229
+ probe.keywords_for :a, six: {name: :test} # {six: {name: :test}}
230
+ ----
231
+
232
+ This useful in gems, like {infusible_link}, when determining which keyword arguments to pass up to the superclass.
233
+
234
+ === Categorize
235
+
236
+ Categorization (`Marameters::Categorizer`) allows you to dynamically build positional, keyword, and block arguments for message passing. This is most valuable when you know the object and method while needing to align the arguments in the right order. Here's a demonstration where {amazing_print_link} (i.e. `ap`) is used to format the output:
175
237
 
176
238
  [source,ruby]
177
239
  ----
@@ -191,18 +253,18 @@ end
191
253
 
192
254
  module Inspector
193
255
  def self.call arguments
194
- Marameters::Categorizer.new(Demo.method(:test).parameters)
195
- .call(arguments).then do |splat|
196
- ap splat
197
- puts
198
- Demo.test(*splat.positionals, **splat.keywords, &splat.block)
199
- end
256
+ Marameters.categorize(Demo.method(:test).parameters, arguments)
257
+ .then do |record|
258
+ ap record
259
+ puts
260
+ Demo.test(*record.positionals, **record.keywords, &record.block)
261
+ end
200
262
  end
201
263
  end
202
264
 
203
265
  Inspector.call [1, nil, nil, {four: 4}]
204
266
 
205
- # #<Struct:Marameters::Splat:0x00021930
267
+ # #<Struct:Marameters::Models::Forward:0x00021930
206
268
  # block = nil,
207
269
  # keywords = {
208
270
  # :four => 4
@@ -226,7 +288,7 @@ Inspector.call [1, nil, nil, {four: 4}]
226
288
  When we step through the above implementation and output, we see the following unfold:
227
289
 
228
290
  . The `Demo` module allows us to define a maximum set of parameters and then print the arguments received for inspection purposes.
229
- . The `Inspector` module provides a wrapper around the `Categorizer` so we can conveniently pass in different arguments for experimentation purposes.
291
+ . The `Inspector` module provides a wrapper around the categorization so we can conveniently pass in different arguments for experimentation purposes.
230
292
  . We pass in our arguments to `Inspector.call` where `nil` is used for optional arguments and hashes for keyword arguments.
231
293
  . Once inside `Inspector.call`, the `Categorizer` is initialized with the `Demo.test` method parameters.
232
294
  . Then the `splat` (i.e. Struct) is printed out so you can see the categorized positional, keyword, and block arguments.
@@ -240,7 +302,7 @@ Inspector.call [1, 2, [98, 99], {four: 4}, {five: 5}, {twenty: 20, thirty: 30},
240
302
 
241
303
  # Output
242
304
 
243
- # #<Struct:Marameters::Splat:0x00029cc0
305
+ # #<Struct:Marameters::Models::Forward:0x00029cc0
244
306
  # block = #<Proc:0x000000010a88cec0 (irb):1>,
245
307
  # keywords = {
246
308
  # :four => 4,
@@ -296,109 +358,184 @@ ap arguments
296
358
  | `%i[block seven]` | `#<Proc:0x0000000108edc778>`
297
359
  |===
298
360
 
299
- This also means that:
361
+ This also means:
300
362
 
301
- * All positions must be filled if you want to supply arguments beyond the first couple of positions because everything is positional due to the nature of how link:https://rubyapi.org/o/method#method-i-parameters[Method#parameters] works. Use `nil` to fill an optional argument when you don't need it.
363
+ * All positions must be filled if you want to supply arguments beyond the first couple of positions because everything is positional due to the nature of how link:https://docs.ruby-lang.org/en/master/Method.html#method-i-parameters[Method#parameters] works. Use `nil` to fill an optional argument when you don't need it.
302
364
  * The `:rest` (single splat) argument must be an array or `nil` if not present because even though it is _optional_, it is still _positional_.
303
365
  * The `:keyrest` (double splat) argument -- much like the `:rest` argument -- must be a hash or `nil` if not present.
304
366
 
305
- Lastly, in all of the above examples, only an array of arguments has been used but you can pass in a single argument too (i.e. non-array). This is handy for method signatures which have only a single parameter or only use splats. Having to remember to wrap your argument in an array each time can get tedious so when _only_ a single argument is supplied, the categorizer will automatically cast the argument as an array. A good example of this use case is when using structs. Example:
367
+ Lastly, in all of the above examples, only an array of arguments has been used but you can pass in a single argument too (i.e. non-array). This is handy for method signatures which have only a single parameter or only use splats.
368
+
369
+ For C-based primitives, like `Struct`, `Data`, etc., you'll want to provide a conversion method. Example:
306
370
 
307
371
  [source,ruby]
308
372
  ----
309
- url = Struct.new :label, :url, keyword_init: true
373
+ url = Struct.new(:label, :url) do
374
+ def self.for(**) = new(**)
375
+ end
310
376
 
311
- Marameters.categorize(url.method(:new).parameters, {label: "Eaxmple", url: "https://example.com"})
312
- .then { |splat| url.new(*splat.positionals, **splat.keywords) }
377
+ Marameters.categorize(url.method(:for).parameters, label: "Example", url: "https://example.com")
378
+ .then { |record| url.for(**record.keywords) }
313
379
 
314
- # Yields: #<struct label="Eaxmple", url="https://example.com">
380
+ # Yields: #<struct label="Example", url="https://example.com">
315
381
  ----
316
382
 
317
- For further details, please refer back to my {article_link} article mentioned in the _Requirements_ section.
383
+ For further details, please refer back to my {article_link} article mentioned in the xref:_requirements[Requirements] section.
318
384
 
319
385
  === Signature
320
386
 
321
- The signature class is the inverse of the probe class in that you want to feed it parameters for turning into a method signature. This is useful when dynamically building method signatures or using the same signature when metaprogramming multiple methods.
387
+ The signature (`Marameters::Signature`) is the opposite of the probe class which allows you to turn a raw array of parameters into a method signature. This is most useful when metaprogramming and needing to dynamically build method signatures. Example:
388
+
389
+ [source,ruby]
390
+ ----
391
+ signature = Marameters.signature [[:opt, :text, "This is a test."]]
392
+
393
+ Example = Module.new do
394
+ module_eval <<~METHOD, __FILE__, __LINE__ + 1
395
+ def self.say(#{signature}) = text
396
+ METHOD
397
+ end
398
+
399
+ puts Example.say # "This is a test."
400
+ puts Example.say("Hello") # "Hello"
401
+ ----
402
+
403
+ ==== Keys
322
404
 
323
- The following demonstrates how you might construct a method signature with all possible parameters:
405
+ The following demonstrates how you can construct a method signature with all possible parameters using the same keys as used by `Method#parameters`:
324
406
 
325
407
  [source,ruby]
326
408
  ----
327
- signature = Marameters::Signature.new(
328
- {
329
- req: :one,
330
- opt: [:two, 2],
331
- rest: :three,
332
- keyreq: :four,
333
- key: [:five, 5],
334
- keyrest: :six,
335
- block: :seven
336
- }
337
- )
409
+ signature = Marameters.signature [
410
+ %i[req one],
411
+ %i[opt two],
412
+ %i[rest three],
413
+ %i[keyreq four],
414
+ %i[key five],
415
+ %i[keyrest six],
416
+ %i[block seven]
417
+ ]
338
418
 
339
419
  puts signature
340
- # one, two = 2, *three, four:, five: 5, **six, &seven
420
+ # "one, two = nil, *three, four:, five: nil, **six, &seven"
421
+ ----
422
+
423
+ ==== Values
424
+
425
+ With the above examples, each sub-array uses a simple key/value pair to map the kind of parameter with the corresponding name. You can also provide a _third_ value when needing to provide a default value for _optional_ parameters. Example:
426
+
427
+ [source,ruby]
428
+ ----
429
+ puts Marameters.signature([[:opt, :one, 1], [:key, :two, 2]])
430
+ # one = 1, two: 2
341
431
  ----
342
432
 
343
- You'll notice that the parameters are a hash _and_ some values can be tuples. The reason is that
344
- it's easier to write a hash than a double nested array as normally produced by the probe or directly
345
- from `Method#parameters`. The optional positional and keyword parameters use tuples because you
346
- might want to supply a default value and this provides a way for you to do that with minimal syntax.
347
433
  This can be demonstrated further by using optional keywords (same applies for optional positionals):
348
434
 
349
435
  [source,ruby]
350
436
  ----
351
- # With no default
352
- puts Marameters::Signature.new({key: :demo})
353
- # demo: nil
437
+ # With implicit nil.
438
+ puts Marameters.signature([%i[key demo]])
439
+ # "demo: nil"
440
+
441
+ # With explicit nil.
442
+ puts Marameters.signature([[:key, :demo, nil]])
443
+ # "demo: nil"
354
444
 
355
- # With explicit nil as default
356
- puts Marameters::Signature.new({key: [:demo, nil]})
357
- # demo: nil
445
+ # With any primitive.
446
+ puts Marameters.signature([[:key, :demo, :test]])
447
+ # "demo: :test"
358
448
 
359
- # With string as default
360
- puts Marameters::Signature.new({key: [:demo, "test"]})
361
- # demo: "test"
449
+ # With proc (no parameters).
450
+ puts Marameters.signature([[:key, :demo, proc { Object.new }]])
451
+ # "demo: Object.new"
452
+ # ⚠️ The above will answer "demo: nil" if used in IRB since the source can't be found.
362
453
 
363
- # With symbol as default
364
- puts Marameters::Signature.new({key: [:demo, :test]})
365
- # demo: :test
454
+ # With proc (with parameters).
455
+ puts Marameters.signature([[:key, :demo, proc { |no| no }]])
456
+ # Avoid using parameters for proc defaults. (ArgumentError)
366
457
 
367
- # With object(dependency) as default
368
- puts Marameters::Signature.new({key: [:demo, "*Object.new"]})
369
- # demo: Object.new
458
+ # With lambda.
459
+ puts Marameters.signature([[:key, :demo, -> { Object.new }]])
460
+ # Use procs instead of lambdas for defaults. (TypeError)
370
461
  ----
371
462
 
372
- In the case of object dependencies, you need to wrap these in a string _and_ prefix them with a star
373
- (`*`) so the signature builder won't confuse them as normal strings. There are two reasons why this
374
- is important:
463
+ You can use any primitive, custom object, etc. as a default despite the limited examples shown above.
375
464
 
376
- * The star (`*`) signifies you want an object to be passed through without further processing while
377
- also not being confused as a normal string.
378
- * Objects wrapped as strings allows your dependency to be lazy loaded. Otherwise, if `Object.new`
379
- was pass in directly, you'd be passing the evaluated instance (i.e.
380
- `#<Object:0x0000000107df4028>`) which is not what you want until much later when your method is
381
- defined.
465
+ Procs _must_ be used when supplying complex objects as default values. _Avoid_ using parameters when using procs because only the source (body) of your proc will be used as a _literal_ string when building the method signature in order to ensure lazy evaluation.
382
466
 
383
- When you put all of this together, you can dynamically build a method as follows:
467
+ Lastly, you can use anonymous splats/blocks by only supplying their kind. Example:
384
468
 
385
469
  [source,ruby]
386
470
  ----
387
- signature = Marameters::Signature.new({opt: [:text, "This is a test."]})
471
+ puts Marameters.signature([[:rest], [:keyrest], [:block]])
472
+ # "*, **, &"
473
+ ----
388
474
 
389
- Example = Module.new do
390
- module_eval <<~DEFINITION, __FILE__, __LINE__ + 1
391
- def self.say(#{signature}) = text
392
- DEFINITION
475
+ You can supply `nil` as a second element (i.e. the name) for each kind but that is the equivalent of the above.
476
+
477
+ ==== Argument Forwarding
478
+
479
+ Use `:all` for building a method signature with argument forwarding. Example:
480
+
481
+ [source, ruby]
482
+ ----
483
+ puts Marameters.signature(:all)
484
+ # "..."
485
+ ----
486
+
487
+ Use of `:all` is special in that you must _only_ supply `:all` with no other keys/values or you'll get an `ArgumentError`.
488
+
489
+ 💡 This is only provided for convenience and completeness. In truth, you're better off writing `my_method(+...+)`, for example, than using this class.
490
+
491
+ ==== Bare
492
+
493
+ Use an empty array when you need a bare method signature. Example:
494
+
495
+ [source,ruby]
496
+ ----
497
+ puts Marameters.signature []
498
+ # ""
499
+ ----
500
+
501
+ 💡 This is only provided for convenience and completeness. In truth, if you need a bare method, then you don't need to use this class.
502
+
503
+ ==== Inheritance
504
+
505
+ Object/method inheritance is more complicated than building a signature for a single method because you need to blend the super and sub parameters as a unified set of parameters. Additionally, you have to account for the arguments that need to be forwarded to the super method via the `super` keyword. To aid in this endeavor, the following objects are available to help you build these more complex method parameters and arguments:
506
+
507
+ * `Marameters::Signatures::Inheritor`: Blends super and sub parameters to produce a unified set of parameters you can turn into a method signature.
508
+ * `Marameters::Signatures::Super`: Blends super and sub parameters to produce arguments for forwarding via the `super` keyword. _This does not support disabled block forwarding (i.e. `&nil`) since there is no way to determine this from the super and sub parameters alone._
509
+
510
+ Here's an example which incorporates both of the above:
511
+
512
+ [source,ruby]
513
+ ----
514
+ module Demo
515
+ def self.parent(one, two = 2, *three, &block) = nil
393
516
  end
394
517
 
395
- puts Example.say
396
- # This is a test.
518
+ super_parameters = Marameters.for Demo.method(:parent).parameters
519
+
520
+ sub_parameters = Marameters.for [
521
+ [:opt, :two, 22],
522
+ %i[keyreq four],
523
+ [:key, :five, 5],
524
+ %i[keyrest six]
525
+ ]
526
+
527
+ inheritor = Marameters::Signatures::Inheritor.new
528
+ forwarder = Marameters::Signatures::Super.new
397
529
 
398
- puts Example.say "Hello"
399
- # Hello
530
+ puts Marameters.signature inheritor.call(super_parameters, sub_parameters)
531
+ # "one, two = 22, *three, four:, five: 5, **six, &block"
532
+
533
+ puts forwarder.call(super_parameters, sub_parameters)
534
+ # "one, two, *three, &block"
400
535
  ----
401
536
 
537
+ As you can see, the above combines the parameters of your super method with the parameters of your sub method in order to produce a method signature -- with no duplicates -- while ensuring you can forward all necessary parameters that the `super` keyword requires. Defaults, if given, will override previously defined defaults as is identical with standard object inheritance.
538
+
402
539
  == Development
403
540
 
404
541
  To contribute, run:
@@ -1,40 +1,33 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "refinements/struct"
4
-
5
3
  module Marameters
6
4
  # Builds the primary argument categories based on method parameters and arguments.
7
5
  class Categorizer
8
- using Refinements::Struct
9
-
10
- def initialize parameters, model: Splat
11
- @parameters = parameters
6
+ def initialize model: Models::Forward
12
7
  @model = model
13
8
  end
14
9
 
15
- def call arguments
10
+ def call parameters, arguments
16
11
  @record = model.new
17
- map arguments.is_a?(Array) ? arguments : [arguments]
18
- record
12
+
13
+ return record if arguments.empty?
14
+
15
+ map parameters, arguments
19
16
  end
20
17
 
21
18
  private
22
19
 
23
- attr_reader :parameters, :model, :record
24
-
25
- def map arguments
26
- return record if arguments.empty?
20
+ attr_reader :model, :record
27
21
 
22
+ def map parameters, arguments
28
23
  size = arguments.size
29
-
30
- parameters.each.with_index do |pair, index|
31
- filter pair, arguments[index] if index < size
32
- end
24
+ parameters.each.with_index { |pair, index| filter pair, arguments[index] if index < size }
25
+ record
33
26
  end
34
27
 
35
28
  def filter pair, value
36
29
  case pair
37
- in [:rest] | [:rest, :*] then splat_positional value
30
+ in [:rest] | [:rest, :*] then to_array value
38
31
  in [:keyrest] | [:keyrest, :**] then record.keywords = Hash value
39
32
  in [:req, *] | [:opt, *] then record.positionals.append value
40
33
  in [:rest, *] then record.positionals.append(*value)
@@ -48,7 +41,7 @@ module Marameters
48
41
  raise TypeError, "#{value.inspect} is an invalid #{pair.first.inspect} value."
49
42
  end
50
43
 
51
- def splat_positional value
44
+ def to_array value
52
45
  return unless value
53
46
 
54
47
  record.positionals = value.is_a?(Array) ? value : [value]
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Marameters
4
+ module Models
5
+ # Models arguments, by category, for forwarding.
6
+ Forward = Struct.new :positionals, :keywords, :block do
7
+ def initialize(positionals: [], keywords: {}, block: nil) = super
8
+ end
9
+ end
10
+ end
@@ -7,11 +7,7 @@ module Marameters
7
7
  class Probe
8
8
  extend Forwardable
9
9
 
10
- CATEGORIES = {
11
- positionals: %i[req opt],
12
- keywords: %i[keyreq key],
13
- splats: %i[rest keyrest]
14
- }.freeze
10
+ CATEGORIES = {positionals: %i[req opt], keywords: %i[keyreq key]}.freeze
15
11
 
16
12
  def self.of klass, name, collection: []
17
13
  method = klass.instance_method name
@@ -22,32 +18,28 @@ module Marameters
22
18
  collection
23
19
  end
24
20
 
25
- delegate %i[deconstruct empty? include? to_a] => :parameters
21
+ delegate %i[any? deconstruct empty? hash include? inspect to_a] => :parameters
26
22
 
27
- attr_reader :keywords, :positionals, :splats
23
+ attr_reader :keywords, :positionals
28
24
 
29
25
  def initialize parameters, categories: CATEGORIES
30
26
  @parameters = parameters
31
27
  categories.each { |category, kinds| define_variable category, kinds }
28
+ freeze
32
29
  end
33
30
 
34
- def block = parameters.find { |kind, name| break name if kind == :block }
31
+ def ==(other) = hash == other.hash
35
32
 
36
- def block? = (parameters in [*, [:block, *]])
33
+ alias eql? ==
37
34
 
38
- def keyword_slice collection, keys:
39
- warn "`#{self.class}##{__method__}` is deprecated, use `#keywords_for` instead.",
40
- category: :deprecated
35
+ def <=>(other) = to_a <=> other.to_a
41
36
 
42
- keywords_for(*keys, **collection)
43
- end
37
+ def keywords? = keywords.any?
44
38
 
45
39
  def keywords_for(*keys, **attributes)
46
40
  attributes.select { |key| !keys.include?(key) || keywords.include?(key) }
47
41
  end
48
42
 
49
- def keywords? = keywords.any?
50
-
51
43
  def kind?(value) = parameters.any? { |kind, _name| kind == value }
52
44
 
53
45
  def kinds = parameters.map { |kind, _name| kind }
@@ -58,10 +50,10 @@ module Marameters
58
50
 
59
51
  def only_bare_splats?
60
52
  parameters in [[:rest]] \
61
- | [[:keyrest]] \
62
- | [[:rest], [:keyrest]] \
63
53
  | [[:rest, :*]] \
54
+ | [[:keyrest]] \
64
55
  | [[:keyrest, :**]] \
56
+ | [[:rest], [:keyrest]] \
65
57
  | [[:rest, :*], [:keyrest, :**]]
66
58
  end
67
59
 
@@ -71,9 +63,9 @@ module Marameters
71
63
 
72
64
  def positionals? = positionals.any?
73
65
 
74
- def splats? = splats.any?
75
-
76
- def to_h = parameters.each.with_object({}) { |(key, name), attributes| attributes[key] = name }
66
+ def positionals_and_maybe_keywords?
67
+ (positionals? && !keywords?) || (positionals? && keywords?)
68
+ end
77
69
 
78
70
  private
79
71
 
@@ -3,12 +3,13 @@
3
3
  module Marameters
4
4
  # Builds a method's parameter signature.
5
5
  class Signature
6
- def initialize parameters, builder: Builder.new
6
+ def initialize parameters, builder: Signatures::Builder.new
7
7
  @parameters = parameters
8
8
  @builder = builder
9
+ freeze
9
10
  end
10
11
 
11
- def to_s = build.join ", "
12
+ def to_s = parameters == :all ? "..." : build.join(", ")
12
13
 
13
14
  alias to_str to_s
14
15
 
@@ -17,7 +18,7 @@ module Marameters
17
18
  attr_reader :parameters, :builder
18
19
 
19
20
  def build
20
- parameters.reduce [] do |signature, (kind, (name, default))|
21
+ parameters.reduce [] do |signature, (kind, name, default)|
21
22
  signature << builder.call(kind, name, default:)
22
23
  end
23
24
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Marameters
4
+ module Signatures
5
+ # Builds a single parameter for a method's signature.
6
+ class Builder
7
+ def initialize defaulter: Defaulter
8
+ @defaulter = defaulter
9
+ freeze
10
+ end
11
+
12
+ def call kind, name = nil, default: nil
13
+ case kind
14
+ when :req then name
15
+ when :opt then "#{name} = #{defaulter.call default}"
16
+ when :rest then "*#{name}"
17
+ when :nokey then "**nil"
18
+ when :keyreq then "#{name}:"
19
+ when :key then "#{name}: #{defaulter.call default}"
20
+ when :keyrest then "**#{name}"
21
+ when :block then "&#{name}"
22
+ else fail ArgumentError, "Wrong kind (#{kind}), name (#{name}), or default (#{default})."
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :defaulter
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Marameters
4
+ module Signatures
5
+ # Computes a method parameter's default value.
6
+ Defaulter = lambda do |value, extractor: Sources::Extractor.new|
7
+ case value
8
+ when Proc
9
+ fail TypeError, "Use procs instead of lambdas for defaults." if value.lambda?
10
+ fail ArgumentError, "Avoid using parameters for proc defaults." if value.arity.nonzero?
11
+
12
+ extractor.call value
13
+ when String then value.dump
14
+ when Symbol then value.inspect
15
+ when nil then "nil"
16
+ else value
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Marameters
4
+ module Signatures
5
+ # Builds single argument for super method's signature when argument forwarding.
6
+ Forwarder = lambda do |kind, name = nil|
7
+ case kind
8
+ when :req, :opt then name
9
+ when :rest then "*#{name}"
10
+ when :nokey then ""
11
+ when :keyreq, :key then "#{name}:"
12
+ when :keyrest then "**#{name}"
13
+ when :block then "&#{name}"
14
+ else fail ArgumentError, "Unable to forward unknown kind: #{kind.inspect}."
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Marameters
4
+ module Signatures
5
+ # Blends ancestor and descendant method parameters together while allowing default overrides.
6
+ class Inheritor
7
+ def initialize key_length: 1, kinds: KINDS
8
+ @key_length = key_length
9
+ @kinds = kinds
10
+ freeze
11
+ end
12
+
13
+ def call ancestor, descendant
14
+ merge(ancestor, descendant).values.sort_by! { |(kind, *)| kinds.index kind }
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :key_length, :kinds
20
+
21
+ def merge ancestor, descendant
22
+ ancestor.to_a.union(descendant.to_a).each.with_object({}) do |parameter, all|
23
+ key = parameter[..key_length]
24
+ kind = key.first
25
+
26
+ case kind
27
+ when :req, :opt then all[key] = parameter if descendant.positionals_and_maybe_keywords?
28
+ when :nokey then all
29
+ when :keyreq, :key
30
+ different = ancestor.keywords? && ancestor.keywords.sort != descendant.keywords.sort
31
+ all[:keyrest] = [:keyrest] if different
32
+ all[key] = parameter if descendant.include? parameter
33
+ else all[kind] = parameter
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Marameters
4
+ module Signatures
5
+ # Blends ancestor and descendant method arguments for forwarding to the super keyword.
6
+ class Super
7
+ def initialize key_length: 1, kinds: KINDS, forwarder: Signatures::Forwarder
8
+ @key_length = key_length
9
+ @kinds = kinds
10
+ @forwarder = forwarder
11
+ freeze
12
+ end
13
+
14
+ def call ancestor, descendant
15
+ return "" if ancestor.empty?
16
+
17
+ merge(ancestor, descendant).values
18
+ .sort_by! { |(kind, *)| kinds.index kind }
19
+ .then { |parameters| build parameters }
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :key_length, :kinds, :forwarder
25
+
26
+ def merge ancestor, descendant
27
+ ancestor.to_a.union(descendant.to_a).each.with_object({}) do |parameter, all|
28
+ key = parameter[..key_length]
29
+ kind, name = key
30
+
31
+ case kind
32
+ when :req, :opt
33
+ if ancestor.positionals? && !descendant.positionals? then all[:rest] = [:rest]
34
+ elsif ancestor.name? name then all[key] = parameter
35
+ else all
36
+ end
37
+ when :nokey then all.delete_if { |key, _| %i[keyreq key keyrest].include? key }
38
+ when :keyreq, :key
39
+ included = ancestor.name?(name) && descendant.name?(name)
40
+ different = ancestor.keywords? && ancestor.keywords.sort != descendant.keywords.sort
41
+
42
+ all[key] = parameter if included
43
+ all[:keyrest] = [:keyrest] if different
44
+ else all[kind] = parameter if ancestor.kind? kind
45
+ end
46
+ end
47
+ end
48
+
49
+ def build parameters
50
+ parameters.filter_map { |kind, name| forwarder.call kind, name }
51
+ .join ", "
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Marameters
4
+ module Sources
5
+ # Extracts the literal source of a Proc's body.
6
+ class Extractor
7
+ PATTERN = /
8
+ proc # Proc statement.
9
+ \s* # Optional space.
10
+ \{ # Block open.
11
+ (?<body>.*?) # Source code body.
12
+ \} # Block close.
13
+ /x
14
+
15
+ def initialize pattern: PATTERN, reader: Reader.new
16
+ @pattern = pattern
17
+ @reader = reader
18
+ @fallback = "nil"
19
+ freeze
20
+ end
21
+
22
+ def call function
23
+ reader.call(function).then do |line|
24
+ line.match?(pattern) ? line.match(pattern)[:body].strip : fallback
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :pattern, :reader, :fallback
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Marameters
4
+ module Sources
5
+ # Reads object source code from memory or file (assumes implementation is a one-liner).
6
+ class Reader
7
+ def initialize offset: 1, parser: RubyVM::InstructionSequence, io: File
8
+ @offset = offset
9
+ @parser = parser
10
+ @io = io
11
+ freeze
12
+ end
13
+
14
+ def call object
15
+ instructions = parser.of object
16
+
17
+ fail StandardError, "Unable to load source for: #{object.inspect}." unless instructions
18
+
19
+ process object, instructions
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :offset, :parser, :io
25
+
26
+ def process object, instructions
27
+ lines = instructions.script_lines
28
+
29
+ return lines.first if lines
30
+ return extract(*object.source_location) if io.readable? instructions.absolute_path
31
+
32
+ fail StandardError, "Unable to load source for: #{object.inspect}."
33
+ end
34
+
35
+ def extract(path, line_number) = io.open(path) { |body| pluck body, line_number }
36
+
37
+ def pluck body, line_number
38
+ body.each_line
39
+ .with_index
40
+ .find { |_line, index| index + offset == line_number }
41
+ .first
42
+ end
43
+ end
44
+ end
45
+ end
data/lib/marameters.rb CHANGED
@@ -16,7 +16,7 @@ module Marameters
16
16
  @loader ||= registry.loaders.find { |loader| loader.tag == File.basename(__FILE__, ".rb") }
17
17
  end
18
18
 
19
- def self.categorize(parameters, arguments) = Categorizer.new(parameters).call(arguments)
19
+ def self.categorize(parameters, arguments) = Categorizer.new.call parameters, arguments
20
20
 
21
21
  def self.of(...) = Probe.of(...)
22
22
 
data/marameters.gemspec CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  Gem::Specification.new do |spec|
4
4
  spec.name = "marameters"
5
- spec.version = "3.12.0"
5
+ spec.version = "4.0.0"
6
6
  spec.authors = ["Brooke Kuhlmann"]
7
7
  spec.email = ["brooke@alchemists.io"]
8
8
  spec.homepage = "https://alchemists.io/projects/marameters"
9
- spec.summary = "A dynamic method parameter inspector."
9
+ spec.summary = "A dynamic method parameter enhancer."
10
10
  spec.license = "Hippocratic-2.1"
11
11
 
12
12
  spec.metadata = {
@@ -22,8 +22,8 @@ Gem::Specification.new do |spec|
22
22
  spec.signing_key = Gem.default_key_path
23
23
  spec.cert_chain = [Gem.default_cert_path]
24
24
 
25
- spec.required_ruby_version = ">= 3.3", "<= 3.4"
26
- spec.add_dependency "refinements", "~> 12.10"
25
+ spec.required_ruby_version = "~> 3.4"
26
+ spec.add_dependency "refinements", "~> 13.0"
27
27
  spec.add_dependency "zeitwerk", "~> 2.7"
28
28
 
29
29
  spec.extra_rdoc_files = Dir["README*", "LICENSE*"]
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,11 +1,10 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: marameters
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.12.0
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brooke Kuhlmann
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain:
11
10
  - |
@@ -35,7 +34,7 @@ cert_chain:
35
34
  3n5C8/6Zh9DYTkpcwPSuIfAga6wf4nXc9m6JAw8AuMLaiWN/r/2s4zJsUHYERJEu
36
35
  gZGm4JqtuSg8pYjPeIJxS960owq+SfuC+jxqmRA54BisFCv/0VOJi7tiJVY=
37
36
  -----END CERTIFICATE-----
38
- date: 2024-11-09 00:00:00.000000000 Z
37
+ date: 2024-12-27 00:00:00.000000000 Z
39
38
  dependencies:
40
39
  - !ruby/object:Gem::Dependency
41
40
  name: refinements
@@ -43,14 +42,14 @@ dependencies:
43
42
  requirements:
44
43
  - - "~>"
45
44
  - !ruby/object:Gem::Version
46
- version: '12.10'
45
+ version: '13.0'
47
46
  type: :runtime
48
47
  prerelease: false
49
48
  version_requirements: !ruby/object:Gem::Requirement
50
49
  requirements:
51
50
  - - "~>"
52
51
  - !ruby/object:Gem::Version
53
- version: '12.10'
52
+ version: '13.0'
54
53
  - !ruby/object:Gem::Dependency
55
54
  name: zeitwerk
56
55
  requirement: !ruby/object:Gem::Requirement
@@ -65,7 +64,6 @@ dependencies:
65
64
  - - "~>"
66
65
  - !ruby/object:Gem::Version
67
66
  version: '2.7'
68
- description:
69
67
  email:
70
68
  - brooke@alchemists.io
71
69
  executables: []
@@ -77,12 +75,17 @@ files:
77
75
  - LICENSE.adoc
78
76
  - README.adoc
79
77
  - lib/marameters.rb
80
- - lib/marameters/builder.rb
81
78
  - lib/marameters/categorizer.rb
82
- - lib/marameters/defaulter.rb
79
+ - lib/marameters/models/forward.rb
83
80
  - lib/marameters/probe.rb
84
81
  - lib/marameters/signature.rb
85
- - lib/marameters/splat.rb
82
+ - lib/marameters/signatures/builder.rb
83
+ - lib/marameters/signatures/defaulter.rb
84
+ - lib/marameters/signatures/forwarder.rb
85
+ - lib/marameters/signatures/inheritor.rb
86
+ - lib/marameters/signatures/super.rb
87
+ - lib/marameters/sources/extractor.rb
88
+ - lib/marameters/sources/reader.rb
86
89
  - marameters.gemspec
87
90
  homepage: https://alchemists.io/projects/marameters
88
91
  licenses:
@@ -95,16 +98,12 @@ metadata:
95
98
  label: Marameters
96
99
  rubygems_mfa_required: 'true'
97
100
  source_code_uri: https://github.com/bkuhlmann/marameters
98
- post_install_message:
99
101
  rdoc_options: []
100
102
  require_paths:
101
103
  - lib
102
104
  required_ruby_version: !ruby/object:Gem::Requirement
103
105
  requirements:
104
- - - ">="
105
- - !ruby/object:Gem::Version
106
- version: '3.3'
107
- - - "<="
106
+ - - "~>"
108
107
  - !ruby/object:Gem::Version
109
108
  version: '3.4'
110
109
  required_rubygems_version: !ruby/object:Gem::Requirement
@@ -113,8 +112,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
113
112
  - !ruby/object:Gem::Version
114
113
  version: '0'
115
114
  requirements: []
116
- rubygems_version: 3.5.23
117
- signing_key:
115
+ rubygems_version: 3.6.2
118
116
  specification_version: 4
119
- summary: A dynamic method parameter inspector.
117
+ summary: A dynamic method parameter enhancer.
120
118
  test_files: []
metadata.gz.sig CHANGED
Binary file
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Marameters
4
- # Builds a single parameter of a method's parameter signature.
5
- class Builder
6
- def initialize defaulter: Defaulter
7
- @defaulter = defaulter
8
- end
9
-
10
- def call kind, name = nil, default: nil
11
- case kind
12
- when :req then name
13
- when :opt then "#{name} = #{defaulter.call default}"
14
- when :rest then "*#{name}"
15
- when :nokey then "**nil"
16
- when :keyreq then "#{name}:"
17
- when :key then "#{name}: #{defaulter.call default}"
18
- when :keyrest then "**#{name}"
19
- when :block then "&#{name}"
20
- else fail ArgumentError, "Wrong kind (#{kind}), name (#{name}), or default (#{default})."
21
- end
22
- end
23
-
24
- private
25
-
26
- attr_reader :defaulter
27
- end
28
- end
@@ -1,14 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Marameters
4
- # Computes a method parameter's default value.
5
- Defaulter = lambda do |value, passthrough: "*"|
6
- case value
7
- when /\A#{Regexp.escape passthrough}/ then value.delete_prefix passthrough
8
- when String then value.dump
9
- when Symbol then value.inspect
10
- when nil then "nil"
11
- else value
12
- end
13
- end
14
- end
@@ -1,8 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Marameters
4
- # Captures arguments, by category, for message splatting.
5
- Splat = Struct.new :positionals, :keywords, :block do
6
- def initialize(positionals: [], keywords: {}, block: nil) = super
7
- end
8
- end