candor 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c57856ee0508cf6514f57073f0b68462e9cd8fc6f14c22bebe338dd3161558a3
4
+ data.tar.gz: f121e2ff9a2ca75a812f2e24991e10332ce5a81765f842c22572672928d3f32f
5
+ SHA512:
6
+ metadata.gz: cdd277bdb4c503f974ead0e2be03da69361ceee33b586eefd187a761d03654b0084bc42a069d016578d174aa91cbf767f18a5885bdb4a7602a628b5c83f5d4af
7
+ data.tar.gz: e5275d7788db55cb88d258f391c45d10a4807b44616c332ace2f3a28a51a975357632a481d3974406147cd85a39a557145b377253033e23456a178eb5c2c8431
data/.yardopts ADDED
@@ -0,0 +1,6 @@
1
+ --no-private
2
+ --readme README.md
3
+ lib/**/*.rb
4
+ -
5
+ CHANGELOG.md
6
+ LICENSE.txt
data/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) and to [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
6
+
7
+ ## [0.1.0] - 2026-07-10
8
+
9
+ ### Added
10
+ - `Candor.define(target, name, aliases:, via:, parameters:, body:)` — fabricates a real method whose arity, `parameters` kinds and `source_location` are the body's. Bodies may be a block, a `Proc`, a `Method` or an `UnboundMethod`; a `#call` object or any body with a `nil` `source_location` (a curried proc, a C-defined method) raises `TypeError`. With `via:` every call routes to that interceptor, which reaches the body through `Candor.body_name`; without it the wrapper forwards straight to the body. A wrong-arity call or an unknown keyword raises `ArgumentError` at the wrapper, before the interceptor or the body runs. An unpassed optional is dropped from the forwarded arguments, so the body applies its own default.
11
+ - `Candor::Signature` — the low-level compiler, public on its own: a parameter shape, a call-site name and a `source_location` in, a dispatch lambda out. Renders off a parameter's kind, never its name, so `end:`, `it`, `_1`, `**nil` and destructuring parameters all wrap at full fidelity. Both call-site names are validated against a strict method-name pattern, and a malformed `parameters` shape raises `ArgumentError` rather than a `SyntaxError` from inside `eval` — including a shape whose entries are each legal but whose combination Ruby's parser rejects, which is compiled before the target is mutated so a rejected fabrication cannot destroy the method it was replacing.
12
+ - Dispatch allocates nothing for shapes up to `Candor::Signature::KEYWORD_BRANCH_LIMIT` (2) optional keywords; beyond it, exactly one Hash per call. Past the limit a body's keyrest *is* that Hash, so a `**kw` in the signature costs only the capture and re-splat Ruby charges any wrapper. Exact from Ruby 3.4: on earlier Rubies the VM charges a `define_method`-created method for arguments crossing into it, and both the wrapper and the body are ones. A call carrying no keyword allocates nothing on every supported Ruby.
13
+ - Fabrication is thread-safe through one global `Monitor`; call-time dispatch takes no lock and never needs one. Re-fabricating a name overwrites the wrapper and the body in place, so it is warning-free under `ruby -w`, leaves no orphaned body, and a concurrent caller sees the old method or the new one, never a `NoMethodError`. A frozen target, an unbindable `Method`/`UnboundMethod` body, a name under the reserved `Candor::BODY_PREFIX`, an uncallable `via:` and a malformed `parameters:` all raise before the target is touched.
14
+ - `sig/` ships RBS for the public API: `Candor.define`, `Candor.body_name`, `BODY_PREFIX`, and `Candor::Signature` with `KINDS` and `KEYWORD_BRANCH_LIMIT`. Everything else is `private_constant`, which RBS cannot express and therefore does not declare.
15
+
16
+ [0.1.0]: https://github.com/svyatov/candor/releases/tag/v0.1.0
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Leonid Svyatov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,196 @@
1
+ # Candor
2
+
3
+ [![Gem Version](https://img.shields.io/gem/v/candor)](https://rubygems.org/gems/candor)
4
+ [![CI](https://github.com/svyatov/candor/actions/workflows/main.yml/badge.svg)](https://github.com/svyatov/candor/actions/workflows/main.yml)
5
+ [![codecov](https://codecov.io/gh/svyatov/candor/branch/main/graph/badge.svg)](https://codecov.io/gh/svyatov/candor)
6
+
7
+ **Ruby's missing `functools.wraps`.** Turn a block or a callable into a real method that reports the
8
+ *body's* arity, the *body's* `parameters` and the *body's* `source_location` — and rejects a bad call
9
+ before anything of yours runs.
10
+
11
+ Zero runtime dependencies. Ruby >= 3.2.
12
+
13
+ ```ruby
14
+ Candor.define(Greeter, :greet) { |name, greeting: "hi"| "#{greeting}, #{name}" }
15
+
16
+ Greeter.instance_method(:greet).arity # => -2
17
+ Greeter.instance_method(:greet).parameters # => [[:req, :__p0], [:key, :greeting]]
18
+ Greeter.instance_method(:greet).source_location # => ["app/greeter.rb", 3] ← the block, not the gem
19
+
20
+ Greeter.new.greet # => ArgumentError: wrong number of arguments (given 0, expected 1)
21
+ Greeter.new.greet("bob", grating: "yo") # => ArgumentError: unknown keyword: :grating
22
+ ```
23
+
24
+ ## The problem
25
+
26
+ Ruby gives you no way to hand a generated wrapper the signature of the callable it wraps.
27
+ `define_method` accepts only a `Proc`, a `Method` or an `UnboundMethod`, so a wrapper's arity has to
28
+ come from an object that already has it — and the only source of such an object is a literal parameter
29
+ list, hand-typed or generated as text.
30
+
31
+ So libraries that wrap user-supplied callables retreat to `|*args, **kwargs, &block|`. The results are
32
+ right; the reflection lies. `arity` is `-1`, `parameters` reads `[[:rest], [:keyrest], [:block]]`, a
33
+ wrong-arity call blows up one frame too deep — inside the wrapper, where the library's own error
34
+ handling can swallow it — and every call allocates an Array and a Hash.
35
+
36
+ Candor generates the parameter list as source and `eval`s it, once, at definition time. There is no
37
+ degraded fallback: every shape Ruby can express wraps at full fidelity, including `end:`, `it`, `_1`,
38
+ `**nil`, and destructuring parameters.
39
+
40
+ ## Install
41
+
42
+ ```ruby
43
+ gem "candor"
44
+ ```
45
+
46
+ ## Use
47
+
48
+ ### Direct forward
49
+
50
+ With no interceptor, the wrapper forwards straight to the body. An unpassed optional is **dropped** from
51
+ the forwarded arguments, so the body applies its own default.
52
+
53
+ ```ruby
54
+ Candor.define(Formatter, :pad) { |string, width = 8| string.ljust(width) }
55
+
56
+ Formatter.new.pad("x") # => "x " the body's own default
57
+ Formatter.new.pad("x", 3) # => "x "
58
+ ```
59
+
60
+ ### Through an interceptor
61
+
62
+ `via:` names a method on the target, resolved per call. Every call routes to it and to it alone. It
63
+ receives the canonical name and the arguments exactly as passed, and reaches the body through
64
+ `Candor.body_name`, so it can also choose *not* to run it — memoize, instrument, short-circuit.
65
+
66
+ ```ruby
67
+ class Memo
68
+ def initialize = @cache = {}
69
+
70
+ private
71
+
72
+ def __call(name, ...)
73
+ @cache[name] ||= send(Candor.body_name(name), ...)
74
+ end
75
+ end
76
+
77
+ Candor.define(Memo, :catalog, parameters: [], via: :__call) { Catalog.build }
78
+ ```
79
+
80
+ A wrong-arity call raises at the wrapper, before `__call` is ever entered — so an interceptor that
81
+ rescues broadly never sees a caller's mistake as a body's failure. An alias left behind by an *earlier*
82
+ fabrication is the exception: it keeps the wrapper it was built with, so its gate is that fabrication's
83
+ signature and not the current body's. See [Re-fabrication](#the-contract).
84
+
85
+ ### Aliases and shape overrides
86
+
87
+ ```ruby
88
+ Candor.define(App, :configuration, aliases: %i[config c], via: :__call) { |scope| ... }
89
+ Candor.define(App, :catalog, parameters: []) { |page = 1| ... } # advertise zero parameters
90
+ ```
91
+
92
+ `aliases:` share one dispatch, and the interceptor always receives the *canonical* name.
93
+ `parameters:` overrides what the wrapper advertises; it is **not** validated against the body, so a
94
+ mismatch surfaces as the body's own `ArgumentError`.
95
+
96
+ ### The compiler on its own
97
+
98
+ `Candor::Signature` is public for consumers that install methods themselves:
99
+
100
+ ```ruby
101
+ dispatch = Candor::Signature.compile(parameters, name: :greet, via: :__call, source_location: loc)
102
+ klass.define_method(:greet, &dispatch)
103
+ ```
104
+
105
+ ## The contract
106
+
107
+ **Body kinds.** A block, a `Proc`, a `Method` or an `UnboundMethod` — the three things `define_method`
108
+ takes. A `#call` object is rejected. So is any body whose `source_location` is `nil`: a curried proc, a
109
+ `Symbol#to_proc`, a C-defined method. Ruby exposes no `curried?` predicate, and a nil `source_location`
110
+ is the one reliable discriminator — without it the honest-`source_location` guarantee cannot be kept, and
111
+ that guarantee is the product. Inside a block body, `self` is the receiver.
112
+
113
+ **Reserved prefix.** Compiled bodies are private methods named `#{Candor::BODY_PREFIX}#{name}`.
114
+ Fabricating a name that starts with the prefix raises `ArgumentError`.
115
+
116
+ **Allocation.** Dispatch allocates **nothing** for every shape up to two optional keywords. Ruby fills
117
+ optional positionals left to right, so `n` of them have `n + 1` states, enumerated as call sites rather
118
+ than accumulated into an Array. Optional keywords are independent, so `n` of them would cost `2**n` call
119
+ sites; past `Candor::Signature::KEYWORD_BRANCH_LIMIT` (2) they go through one Hash instead, and
120
+ dispatch allocates exactly one Hash per call. That shape — three or more optional keywords — is the one
121
+ case where candor is slower than the variadic wrapper it replaces. It is measured, and accepted.
122
+
123
+ Those counts are what the *wrapper* adds. A body that declares a **keyrest** (`**kw`) costs Ruby one Hash
124
+ to capture it and one to re-splat it into the body, on any wrapper you could write by hand; candor adds
125
+ none of its own, because past the branch limit the captured keyrest *is* the Hash the keywords accumulate
126
+ into.
127
+
128
+ Exactly as stated from **Ruby 3.4**. Below that, Ruby itself charges a `define_method`-created method for
129
+ arguments crossing into it, and both the wrapper and the body are ones: on 3.3 the hashed path costs one
130
+ Hash more, and on 3.2 a keyword-carrying call costs one Hash per hop. Nothing in the gem can reach that,
131
+ and no call that carries no keyword ever allocates, on any supported Ruby.
132
+
133
+ **Thread safety.** Fabrication takes one global `Monitor`; concurrent `Candor.define` calls
134
+ serialize. Call-time dispatch takes no lock at all, and never needs one: re-fabrication replaces a
135
+ method in place rather than removing and reinstalling it, so a caller racing a `Candor.define` gets
136
+ the old method or the new one, never a `NoMethodError`.
137
+
138
+ **Failing fast.** A frozen target raises `FrozenError`, an `UnboundMethod` whose owner is not an
139
+ ancestor of the target raises `TypeError`, and a malformed `parameters:` or an uncallable `via:` raises
140
+ `ArgumentError` — all *before* the target is touched. A rejected fabrication leaves the target exactly
141
+ as it was. A hand-written `parameters:` never passed Ruby's parser, so it is compiled before the first
142
+ mutation: a shape whose *combination* is illegal — two rests, a duplicate keyword — is an `ArgumentError`
143
+ too. Nothing ever surfaces as a `SyntaxError` from inside `eval`, which a `rescue` would not catch.
144
+
145
+ **A Ruby 3.4 wrinkle.** On 3.4 alone, a body written with the implicit `it` parameter receives its
146
+ arguments packed into an Array when it is reached through `(name, ...)` forwarding, `send`, or a splat —
147
+ the shape an interceptor is usually written in. Candor's own generated call sites name every
148
+ argument, so direct-forward mode is unaffected; fixed in Ruby 4.0, and `_1` and named parameters never
149
+ had it.
150
+
151
+ **Re-fabrication.** Redefining a name overwrites candor's own prior wrapper and body in place, so a
152
+ second `Candor.define` is warning-free under `ruby -w` and orphans nothing. It rewrites only the
153
+ names passed to the *current* call: an alias installed by an earlier fabrication stays installed and
154
+ rebinds to the replacement body while still advertising its original signature. Last write wins. A
155
+ hand-written method of the same name is replaced cleanly; an inherited one is shadowed, not removed.
156
+
157
+ ## Benchmarks
158
+
159
+ `bundle exec rake bench`, on ruby 4.0.5 (arm64-darwin23). Nanoseconds per call, lower is better; the
160
+ optional-argument rows carry a few percent of noise.
161
+
162
+ | shape | candor | hand-written wrapper | bare method | variadic wrapper |
163
+ |---|---|---|---|---|
164
+ | no arguments | 52 ns · 0 alloc | 47 ns · 0 | 27 ns · 0 | 122 ns · 2 |
165
+ | required + optional, omitted | 107 ns · 0 alloc | 82 ns · 0 | 43 ns · 0 | 140 ns · 2 |
166
+ | required + optional, passed | 81 ns · 0 alloc | 63 ns · 0 | 36 ns · 0 | 149 ns · 2 |
167
+ | two optional keywords | 112 ns · 0 alloc | 79 ns · 0 | 46 ns · 0 | 170 ns · 2 |
168
+ | three optional keywords | 251 ns · 1 alloc | 99 ns · 0 | 46 ns · 0 | 185 ns · 2 |
169
+
170
+ - **hand-written wrapper** is the ceiling: a real `def` with the same signature forwarding to the same
171
+ private body, with the defaults hard-coded — what you would write by hand if you knew the shape *and*
172
+ the default expressions. Candor is within ~10–40% of it, and allocates the same nothing.
173
+ - **bare method** is a single `def` doing the work inline. It is roughly twice as fast as any wrapper,
174
+ because it is one method call rather than two. That is the price of wrapping at all, not of candor.
175
+ - **variadic wrapper** is the `|*args, **kwargs, &block|` retreat: slower than candor on every shape
176
+ below the keyword branch limit, faster on the hashed one, and dishonest about its signature everywhere.
177
+
178
+ Definition time is ~40 µs per fabricated method — one `eval` — against ~1.3 µs for a bare
179
+ `define_method`. It runs once, at boot.
180
+
181
+ ## What it is not
182
+
183
+ Candor routes calls. It does not rescue, memoize, delegate or instrument; it is the method-fabrication
184
+ layer those features stand on. It does not recover an optional's default expression — that is
185
+ [permanently closed upstream](https://bugs.ruby-lang.org/issues/8629), and dropping the unpassed optional
186
+ so the body defaults is the semantics that replaces it. Fabricated methods are not Ractor-shareable, and
187
+ the gem needs `eval`, so it does not run on eval-restricted platforms.
188
+
189
+ ## Contributing
190
+
191
+ Bug reports and pull requests are welcome on [GitHub](https://github.com/svyatov/candor). See
192
+ [CONTRIBUTING.md](CONTRIBUTING.md).
193
+
194
+ ## License
195
+
196
+ MIT. See [LICENSE.txt](LICENSE.txt).
data/candor.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/candor/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "candor"
7
+ spec.version = Candor::VERSION
8
+ spec.authors = ["Leonid Svyatov"]
9
+ spec.email = ["leonid@svyatov.com"]
10
+
11
+ spec.summary = "Turn a block or a callable into a real method with an honest signature."
12
+ spec.description = "Ruby's missing functools.wraps. Candor fabricates real methods from blocks and " \
13
+ "callables: same arity, same parameters, source_location pointing at your code, and " \
14
+ "allocation-free dispatch. Zero runtime dependencies."
15
+ spec.homepage = "https://github.com/svyatov/candor"
16
+ spec.license = "MIT"
17
+
18
+ spec.required_ruby_version = ">= 3.2.0"
19
+
20
+ spec.require_paths = ["lib"]
21
+ spec.files = Dir["lib/**/*.rb"] + Dir["sig/**/*"] +
22
+ %w[.yardopts CHANGELOG.md LICENSE.txt README.md candor.gemspec]
23
+
24
+ spec.metadata["rubygems_mfa_required"] = "true"
25
+ spec.metadata["documentation_uri"] = "https://rubydoc.info/gems/candor"
26
+ spec.metadata["source_code_uri"] = "https://github.com/svyatov/candor"
27
+ spec.metadata["changelog_uri"] = "https://github.com/svyatov/candor/blob/main/CHANGELOG.md"
28
+ spec.metadata["bug_tracker_uri"] = "https://github.com/svyatov/candor/issues"
29
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Candor
4
+ # Fabricates a real method from a block or a callable: compile the body strictly, read its true
5
+ # parameter shape, render a signature-identical wrapper, install it.
6
+ #
7
+ # The pipeline is an order, not a sequence of conveniences. Every check runs before the first
8
+ # mutation, so a rejected fabrication leaves the target exactly as it was — a frozen target, an
9
+ # unbindable +Method+, a name under the reserved prefix and a malformed shape all raise before the
10
+ # prior wrapper is replaced. The parameter shape is then read from the *compiled* method, never from
11
+ # the Proc, which reports every positional as +:opt+ and would silently destroy arity strictness.
12
+ #
13
+ # @api private
14
+ class Definer
15
+ # The only bodies +define_method+ accepts. A +#call+ object is not one of them, and converting it
16
+ # would forge a +source_location+ nobody wrote.
17
+ BODY_KINDS = [Proc, Method, UnboundMethod].freeze
18
+
19
+ # @param target [Module] the module the method is installed onto
20
+ # @param name [Symbol] the canonical name
21
+ # @param aliases [Array<Symbol>] further names sharing the one dispatch
22
+ # @param via [Symbol, nil] an interceptor method on +target+, resolved per call
23
+ # @param parameters [Array<Array>, nil] an explicit shape, overriding the body's
24
+ # @param body [Proc, Method, UnboundMethod]
25
+ def initialize(target, name, aliases:, via:, parameters:, body:)
26
+ @target = target
27
+ @canonical = name.to_sym
28
+ @names = [@canonical, *aliases.map(&:to_sym)].uniq
29
+ @via = via
30
+ @parameters = parameters
31
+ @body = body
32
+ @body_name = Candor.body_name(@canonical)
33
+ end
34
+
35
+ # @return [Symbol] the canonical name
36
+ def call
37
+ validate!
38
+ # An overridden shape never passed Ruby's parser, so it is compiled before the first mutation: a
39
+ # combination {Signature.parameters!} cannot see — two rests, a duplicate keyword — must not fail once
40
+ # the body is already installed. A shape read from the compiled body always renders.
41
+ dispatch = compile(@parameters) if @parameters
42
+ # Definition is a boot-time operation, so one global lock is the whole answer to concurrency;
43
+ # the dispatch it installs takes no lock at all.
44
+ MONITOR.synchronize do
45
+ install_body
46
+ install(dispatch || compile(@target.instance_method(@body_name).parameters))
47
+ end
48
+ @canonical
49
+ end
50
+
51
+ private
52
+
53
+ # @return [void]
54
+ # @raise [TypeError, ArgumentError, FrozenError]
55
+ def validate!
56
+ raise TypeError, "target must be a Module, got #{@target.inspect}" unless @target.is_a?(Module)
57
+
58
+ validate_body!
59
+ validate_names!
60
+ # Both call-site names are interpolated into `eval`'d source. Without `via` the body's own name is
61
+ # the call site, so the canonical name has to survive being one.
62
+ Signature.method_name!(@via || @body_name)
63
+ Signature.parameters!(@parameters) if @parameters
64
+ raise FrozenError.new("can't fabricate a method on frozen #{@target}", receiver: @target) if @target.frozen?
65
+ end
66
+
67
+ # +source_location+ is the one reliable discriminator: Ruby exposes no +curried?+ predicate, and a
68
+ # curried proc, a symbol-to-proc and a C-defined method all report +nil+. R5 cannot be honoured for
69
+ # any of them, and honouring R5 is the product.
70
+ #
71
+ # @return [void]
72
+ # @raise [TypeError]
73
+ def validate_body!
74
+ raise TypeError, body_error unless BODY_KINDS.any? { |kind| @body.is_a?(kind) }
75
+ raise TypeError, body_error if @body.source_location.nil?
76
+ return unless @body.is_a?(Method) || @body.is_a?(UnboundMethod)
77
+ # `define_method` would raise this itself — after the prior wrapper was already removed.
78
+ raise TypeError, "#{@body.owner} is not an ancestor of #{@target}" unless @target <= @body.owner
79
+ end
80
+
81
+ # @return [void]
82
+ # @raise [ArgumentError]
83
+ def validate_names!
84
+ offender = @names.find { |name| name.to_s.start_with?(BODY_PREFIX) }
85
+ return unless offender
86
+
87
+ raise ArgumentError, "#{offender.inspect} starts with the reserved prefix #{BODY_PREFIX.inspect}"
88
+ end
89
+
90
+ # @return [String]
91
+ def body_error
92
+ "body must be a block, a Proc, a Method or an UnboundMethod with a source_location, " \
93
+ "got #{@body.inspect}; curried procs, `#call` objects and C-defined methods have none"
94
+ end
95
+
96
+ # +define_method+ replaces a method in place, so a re-fabrication is never observable as a missing
97
+ # one. Calling +remove_method+ first — the obvious way to reinstall a name — leaves it undefined for
98
+ # as long as the install takes, and a concurrent caller sees +NoMethodError+. Removal only ever bought
99
+ # silence from the +-w+ redefinition warning, so buy that here instead.
100
+ #
101
+ # +$VERBOSE+ is process-global, and that is the ceiling: {MONITOR} serializes fabricators, so two of
102
+ # them cannot interleave these assignments, but a thread warning about something else while one holds
103
+ # the lock loses that warning. An install is microseconds, and this runs at boot.
104
+ #
105
+ # @return [void]
106
+ def silently
107
+ verbose = $VERBOSE
108
+ $VERBOSE = nil
109
+ yield
110
+ ensure
111
+ $VERBOSE = verbose
112
+ end
113
+
114
+ # An inherited method of the same name is shadowed by the install, never removed.
115
+ #
116
+ # @return [void]
117
+ def install_body
118
+ silently { @target.define_method(@body_name, @body) }
119
+ @target.send(:private, @body_name)
120
+ end
121
+
122
+ # @param shape [Array<Array>]
123
+ # @return [Proc] the dispatch lambda
124
+ # @raise [ArgumentError] if the shape does not render to legal source
125
+ def compile(shape)
126
+ Signature.compile(shape, name: @via ? @canonical : @body_name, via: @via,
127
+ source_location: @body.source_location)
128
+ end
129
+
130
+ # @param dispatch [Proc]
131
+ # @return [void]
132
+ def install(dispatch) = silently { @names.each { |name| @target.define_method(name, &dispatch) } }
133
+ end
134
+ end
@@ -0,0 +1,332 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Candor
4
+ # Compiles a method's +parameters+ into a dispatch lambda carrying the same parameter kinds, in the
5
+ # same order, and therefore the same +arity+.
6
+ #
7
+ # Rendering switches on a parameter's *kind*, never its name. +Method#parameters+ reports names that
8
+ # cannot be used as parameters (+:_1+), names that are reserved words (+:end+), and entries with no
9
+ # name at all — +[[:req]]+, for both +it+ and a destructuring parameter. A renderer that echoed names
10
+ # would die on most real shapes, and on +it+ it would fail *silently*, emitting arity 0. So every
11
+ # positional, rest, keyrest and block parameter gets a generated name; only keyword names survive,
12
+ # because a keyword name is the calling convention.
13
+ #
14
+ # Those surviving names are the only user input in the generated source, and Ruby lets a parameter
15
+ # shadow anything the source would otherwise reach. So the lambda closes over nothing it could lose:
16
+ # the canonical name is a Symbol literal, +binding()+ is called with parentheses a local cannot
17
+ # shadow, and every generated local is prefixed with a run of underscores no keyword name starts
18
+ # with. Only the sentinel survives as a free variable, and it is created by the rendered source
19
+ # itself, under that same collision-free prefix.
20
+ #
21
+ # The lambda is rendered on one line, so +eval+'s line forges its +source_location+ exactly.
22
+ #
23
+ # The call site it forwards to is one of two shapes. With +via+ it is +via(:name, args…)+, an
24
+ # interceptor that receives the canonical name; without, it is +name(args…)+, calling the body
25
+ # directly. Either way the target is a method name interpolated into +eval+'d source, so
26
+ # {method_name!} gates it first.
27
+ #
28
+ # Candor::Signature.compile([%i[req a]], name: :greet, via: :__call, source_location: loc)
29
+ # # => ->(__p0) { __call(:greet, __p0) }
30
+ class Signature
31
+ # Reserved words that are legal keyword parameter names but illegal as a bare reference:
32
+ # +__k[:end] = end+ is a +SyntaxError+.
33
+ RESERVED_WORDS = %w[
34
+ __ENCODING__ __FILE__ __LINE__ alias and begin break case class def do else elsif end ensure
35
+ false for if in module next nil not or redo rescue retry return self super then true undef
36
+ unless until when while yield
37
+ ].freeze
38
+
39
+ # Kinds whose reported name is the calling convention and must be preserved.
40
+ NAMED_KINDS = %i[keyreq key].freeze
41
+
42
+ # Kinds that may have to be dropped from the forwarded arguments, so the body applies its own default.
43
+ OPTIONAL_KINDS = %i[opt key].freeze
44
+
45
+ # The generated local's name, after the collision-free prefix and before the parameter's index.
46
+ SUFFIXES = { req: "p", opt: "p", rest: "r", keyrest: "kr", block: "b" }.freeze
47
+
48
+ # How many optional keywords still dispatch through call sites rather than a Hash. Each one doubles
49
+ # the sites, so four is where a shape nobody writes stops paying for one everybody does.
50
+ KEYWORD_BRANCH_LIMIT = 2
51
+
52
+ # The lambda's parameter declaration, per kind: the name, then the sentinel. Filled with `sub`, not
53
+ # `%`: +:nokey+ takes neither, and `format` warns about the unused arguments under +-w+.
54
+ DECLARATIONS = {
55
+ req: "%s", opt: "%s = %s", rest: "*%s", keyreq: "%s:", key: "%s: %s",
56
+ keyrest: "**%s", nokey: "**nil", block: "&%s"
57
+ }.freeze
58
+
59
+ # Every kind +Method#parameters+ can emit, and the only ones {render} accepts.
60
+ KINDS = DECLARATIONS.keys.freeze
61
+
62
+ # A name the generated source may call with parentheses. Operators, spaces and everything else a
63
+ # +Symbol+ can hold are rejected before they reach +eval+.
64
+ METHOD_NAME = /\A[a-zA-Z_][a-zA-Z0-9_]*[?!]?\z/
65
+
66
+ # A name the generated source may declare as a keyword parameter. Reserved words qualify — the
67
+ # source reads them back through +binding()+ — but a numbered parameter does not.
68
+ KEYWORD_NAME = /\A[a-zA-Z_][a-zA-Z0-9_]*\z/
69
+
70
+ # Names Ruby reserves for the numbered block parameters; +->(_1: 1) {}+ is a +SyntaxError+.
71
+ NUMBERED_PARAMETERS = (1..9).map { |i| "_#{i}" }.freeze
72
+
73
+ # How the source is spelled, and what the two name gates happen to be spelled as. A consumer builds
74
+ # against {compile}, {method_name!} and {parameters!} — which raise — not against the patterns and
75
+ # tables they are written in terms of. Only {KINDS} and {KEYWORD_BRANCH_LIMIT} name something a
76
+ # caller has to know to use the compiler, so only those two stay public.
77
+ private_constant :RESERVED_WORDS, :NAMED_KINDS, :OPTIONAL_KINDS, :SUFFIXES, :DECLARATIONS,
78
+ :METHOD_NAME, :KEYWORD_NAME, :NUMBERED_PARAMETERS
79
+
80
+ class << self
81
+ # +eval+'s file and line forge the lambda's — and therefore the compiled method's —
82
+ # +source_location+ onto the body the caller supplied.
83
+ #
84
+ # @param parameters [Array<Array>] a *compiled* method's +parameters+; a Proc's would report every
85
+ # positional as +:opt+ and silently destroy arity strictness
86
+ # @param name [Symbol] the method to call, or — with +via+ — the canonical name handed to it
87
+ # @param source_location [Array(String, Integer)] the body's
88
+ # @param via [Symbol, nil] an interceptor method taking +(name, ...)+; omit to call +name+ directly
89
+ # @return [Proc] a lambda taking the body's parameter kinds and forwarding to the call site
90
+ # @raise [ArgumentError] if the call site's name or the parameter shape is malformed
91
+ def compile(parameters, name:, source_location:, via: nil)
92
+ eval(render(parameters, name: name, via: via), binding, *source_location) # rubocop:disable Security/Eval
93
+ rescue SyntaxError => e
94
+ # {parameters!} sees one entry at a time; only the parser sees the combination — two rests, a
95
+ # duplicate keyword, a block before a positional. A +SyntaxError+ is a +ScriptError+, which a
96
+ # consumer's +rescue+ never catches.
97
+ raise ArgumentError, "malformed parameters: #{parameters.inspect} (#{e.message.lines.first.strip})"
98
+ end
99
+
100
+ # @param parameters [Array<Array>]
101
+ # @param name [Symbol]
102
+ # @param via [Symbol, nil]
103
+ # @return [String] single-line source for a lambda forwarding to the call site
104
+ # @raise [ArgumentError] if the call site's name or the parameter shape is malformed
105
+ def render(parameters, name:, via: nil) = new(parameters, name, via).source
106
+
107
+ # The call site is interpolated into +eval+'d source, so its name is a trust boundary rather than
108
+ # a typo check.
109
+ #
110
+ # @param name [Symbol, String]
111
+ # @return [Symbol] the name
112
+ # @raise [ArgumentError] unless the name can be called with parentheses
113
+ def method_name!(name)
114
+ string = name.to_s
115
+ # +:class+ is callable on a receiver and useless here: the generated source calls the site bare,
116
+ # and a bare +class(…)+ is a +SyntaxError+. Say that, rather than "not a callable method name".
117
+ if RESERVED_WORDS.include?(string)
118
+ raise ArgumentError, "#{name.inspect} is a reserved word: the generated source would call it bare"
119
+ end
120
+ raise ArgumentError, "not a callable method name: #{name.inspect}" unless METHOD_NAME.match?(string)
121
+
122
+ string.to_sym
123
+ end
124
+
125
+ # A parameter shape reaching {render} from a consumer's override never passed Ruby's parser, so
126
+ # every kind and every keyword name is checked here rather than discovered as a +SyntaxError+.
127
+ #
128
+ # @param parameters [Array<Array>]
129
+ # @return [Array<Array>] the shape
130
+ # @raise [ArgumentError] unless every entry is a known kind, with a usable name where one is required
131
+ def parameters!(parameters)
132
+ raise ArgumentError, "malformed parameters: #{parameters.inspect}" unless parameters.is_a?(Array)
133
+
134
+ # +index+, not +find+: a +nil+ entry is malformed, and +find+ would hand back the same +nil+ it
135
+ # returns when every entry is fine. The caller passed the Array; naming the offender saves a bisect.
136
+ index = parameters.index { |entry| !valid_entry?(entry) }
137
+ raise ArgumentError, "malformed parameters: entry #{index} is #{parameters[index].inspect}" if index
138
+
139
+ parameters
140
+ end
141
+
142
+ private
143
+
144
+ # @param entry [Object]
145
+ # @return [Boolean]
146
+ def valid_entry?(entry)
147
+ return false unless entry.is_a?(Array) && KINDS.include?(entry.first)
148
+
149
+ case entry.size
150
+ when 1 then !NAMED_KINDS.include?(entry.first)
151
+ when 2 then valid_name?(entry.first, entry[1])
152
+ else false
153
+ end
154
+ end
155
+
156
+ # A keyword name is the one piece of the shape the source carries verbatim; every other name is
157
+ # replaced by a generated one and so may be anything, or nothing.
158
+ #
159
+ # @param kind [Symbol]
160
+ # @param name [Object]
161
+ # @return [Boolean]
162
+ def valid_name?(kind, name)
163
+ return false unless name.is_a?(Symbol)
164
+ return true unless NAMED_KINDS.include?(kind)
165
+
166
+ string = name.to_s
167
+ KEYWORD_NAME.match?(string) && !NUMBERED_PARAMETERS.include?(string)
168
+ end
169
+ end
170
+
171
+ # @param parameters [Array<Array>]
172
+ # @param name [Symbol]
173
+ # @param via [Symbol, nil]
174
+ def initialize(parameters, name, via = nil)
175
+ self.class.parameters!(parameters)
176
+ @target = self.class.method_name!(via || name)
177
+ # With an interceptor the canonical name leads the argument list, as a literal rather than a free
178
+ # variable a keyword could capture.
179
+ @literal = via ? name.to_sym.inspect : nil
180
+ @prefix = collision_free_prefix(parameters)
181
+ @unset = "#{@prefix}u"
182
+ @hash = "#{@prefix}k"
183
+ @entries = entries(parameters)
184
+ @optionals = @entries.select { |entry| OPTIONAL_KINDS.include?(entry.first) }
185
+ end
186
+
187
+ # The sentinel marking an unpassed optional is a local, not a constant: +Module#const_get+ pierces
188
+ # +private_constant+, so a caller could obtain the sentinel and pass it as an argument, silently
189
+ # defeating an optional's default.
190
+ #
191
+ # @return [String]
192
+ def source = "#{@unset} = ::Object.new.freeze; ->(#{declarations}) { #{dispatch} }"
193
+
194
+ private
195
+
196
+ # @param parameters [Array<Array>]
197
+ # @return [Array<Array(Symbol, String)>] each kind paired with the local the lambda gives it
198
+ def entries(parameters)
199
+ parameters.each_with_index.map { |(kind, given), index| [kind, local(kind, given, index)] }
200
+ end
201
+
202
+ # No keyword name starts with the returned prefix, so no generated local can be shadowed by one.
203
+ #
204
+ # @param parameters [Array<Array>]
205
+ # @return [String]
206
+ def collision_free_prefix(parameters)
207
+ keywords = parameters.filter_map { |kind, given| given.to_s if NAMED_KINDS.include?(kind) }
208
+ prefix = "__"
209
+ prefix += "_" while keywords.any? { |keyword| keyword.start_with?(prefix) }
210
+ prefix
211
+ end
212
+
213
+ # @param kind [Symbol]
214
+ # @param given [Symbol, nil]
215
+ # @param index [Integer]
216
+ # @return [String]
217
+ def local(kind, given, index) = NAMED_KINDS.include?(kind) ? given.to_s : "#{@prefix}#{SUFFIXES[kind]}#{index}"
218
+
219
+ # @return [String] the lambda's parameter list
220
+ def declarations
221
+ @entries.map { |kind, name| DECLARATIONS[kind].sub("%s", name).sub("%s", @unset) }.join(", ")
222
+ end
223
+
224
+ # An optional's default expression is unrecoverable from +parameters+, so an unpassed optional is
225
+ # dropped from the forwarded arguments and the body applies its own default. Which ones were passed
226
+ # is a runtime fact, and {#branch} enumerates it as call sites rather than an Array: nothing is
227
+ # allocated, and every kind around the optionals forwards straight through — a rest as `*r`, a
228
+ # keyrest as `**kw`.
229
+ #
230
+ # Optional *keywords* are independently passed, so enumerating them costs +2**n+ call sites rather
231
+ # than +n + 1+. Past {KEYWORD_BRANCH_LIMIT} of them the source would balloon, so they go through a
232
+ # Hash instead and the branching sees only the positionals.
233
+ #
234
+ # @return [String] the lambda's body
235
+ def dispatch
236
+ keywords = @optionals.select { |entry| entry.first == :key }
237
+ hash = keywords.size > KEYWORD_BRANCH_LIMIT
238
+ statements = hash ? keyword_setup : []
239
+ statements << branch(hash ? @optionals - keywords : @optionals, [], hash)
240
+ statements.join("; ")
241
+ end
242
+
243
+ # Ruby fills optional positionals left to right, so an unpassed one guarantees every optional
244
+ # positional after it went unpassed too. The states are a chain of +n + 1+ call sites, not the
245
+ # +2**n+ a subset would need.
246
+ #
247
+ # @param optionals [Array<Array(Symbol, String)>] those still to be decided
248
+ # @param dropped [Array<Array(Symbol, String)>] those already known unpassed
249
+ # @param hash [Boolean]
250
+ # @return [String] a call site, or a ternary choosing between two of them
251
+ def branch(optionals, dropped, hash)
252
+ return call(@entries - dropped, hash) if optionals.empty?
253
+
254
+ first, *rest = optionals
255
+ trailing = first.first == :opt ? rest.select { |entry| entry.first == :opt } : []
256
+ unpassed = branch(rest - trailing, dropped + [first] + trailing, hash)
257
+ "#{@unset}.equal?(#{value(first)}) ? #{unpassed} : #{branch(rest, dropped, hash)}"
258
+ end
259
+
260
+ # The target takes parentheses and, usually, an argument — neither of which a local of the same name
261
+ # can shadow.
262
+ #
263
+ # @param entries [Array<Array(Symbol, String)>]
264
+ # @param hash [Boolean]
265
+ # @return [String]
266
+ def call(entries, hash) = "#{@target}(#{[*@literal, *forward(entries, hash)].join(", ")})"
267
+
268
+ # @param entry [Array(Symbol, String)]
269
+ # @return [String] source reading that parameter's value
270
+ def value(entry) = entry.first == :key ? reference(entry.last) : entry.last
271
+
272
+ # A keyrest is the accumulator rather than a source to merge in: +**+ capture already allocated a Hash the
273
+ # lambda alone owns, and Ruby routes a declared keyword to its own parameter, never into the keyrest — so
274
+ # seeding from it and assigning after is what +{}+ plus +update+ was, one Hash cheaper.
275
+ #
276
+ # @return [Array<String>]
277
+ def keyword_setup
278
+ keyrest = @entries.find { |kind, _name| kind == :keyrest }
279
+ @entries.filter_map do |kind, name|
280
+ case kind
281
+ when :keyreq then "#{@hash}[:#{name}] = #{reference(name)}"
282
+ when :key then "#{@hash}[:#{name}] = #{reference(name)} unless #{@unset}.equal?(#{reference(name)})"
283
+ end
284
+ end.unshift("#{@hash} = #{keyrest ? keyrest.last : "{}"}")
285
+ end
286
+
287
+ # @param entries [Array<Array(Symbol, String)>]
288
+ # @param hash [Boolean]
289
+ # @return [Array<String>] the forwarded arguments
290
+ def forward(entries, hash)
291
+ args = positional_args(entries)
292
+ args.concat(hash ? ["**#{@hash}"] : keyword_args(entries))
293
+ args.concat(block_arg(entries))
294
+ end
295
+
296
+ # An +:opt+ appears here only on a call site that passes it: {#dispatch} drops it from the entries
297
+ # of the call site that does not.
298
+ #
299
+ # @param entries [Array<Array(Symbol, String)>]
300
+ # @return [Array<String>] in declaration order, so a rest keeps its place among the required
301
+ def positional_args(entries)
302
+ entries.filter_map do |kind, name|
303
+ case kind
304
+ when :req, :opt then name
305
+ when :rest then "*#{name}"
306
+ end
307
+ end
308
+ end
309
+
310
+ # @param entries [Array<Array(Symbol, String)>]
311
+ # @return [Array<String>]
312
+ def keyword_args(entries)
313
+ entries.filter_map do |kind, name|
314
+ case kind
315
+ when :keyreq, :key then "#{name}: #{reference(name)}"
316
+ when :keyrest then "**#{name}"
317
+ end
318
+ end
319
+ end
320
+
321
+ # @param entries [Array<Array(Symbol, String)>]
322
+ # @return [Array<String>] at most one element
323
+ def block_arg(entries) = entries.filter_map { |kind, name| "&#{name}" if kind == :block }
324
+
325
+ # +binding()+, never bare +binding+: a keyword named +binding+ is a local, and a local shadows the
326
+ # very method this reaches for.
327
+ #
328
+ # @param name [String] a keyword parameter's name
329
+ # @return [String] source reading that keyword's value
330
+ def reference(name) = RESERVED_WORDS.include?(name) ? "binding().local_variable_get(:#{name})" : name
331
+ end
332
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Candor
4
+ # The gem version.
5
+ VERSION = "0.1.0"
6
+ end
data/lib/candor.rb ADDED
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+
5
+ require "candor/version"
6
+ require "candor/signature"
7
+ require "candor/definer"
8
+
9
+ # Turns a block or a callable into a real method with an honest signature: the body's arity, the body's
10
+ # +parameters+, the body's +source_location+, and allocation-free dispatch.
11
+ #
12
+ # Candor.define(MyClass, :greet) { |name, greeting: "hi"| "#{greeting}, #{name}" }
13
+ # MyClass.instance_method(:greet).parameters # => [[:req, :__p0], [:key, :greeting]]
14
+ #
15
+ # A wrong-arity call and an unknown keyword raise +ArgumentError+ at the fabricated method, before
16
+ # anything of yours runs.
17
+ module Candor
18
+ # Prefix of the private methods holding compiled bodies. Fabricated names may not start with it, and
19
+ # an interceptor reaches its body through {Candor.body_name}.
20
+ BODY_PREFIX = "__candor_body_"
21
+
22
+ # Fabrication is a boot-time operation, so one global lock serializes it; nothing is taken at call time.
23
+ MONITOR = Monitor.new
24
+ private_constant :MONITOR
25
+
26
+ class << self
27
+ # Defines a real method on +target+ whose signature is the body's.
28
+ #
29
+ # With +via+, every call routes to that method on the target as +via(canonical_name, ...)+, and it
30
+ # alone decides whether to run the body — which it reaches under {body_name}. Without +via+, the
31
+ # wrapper forwards straight to the body.
32
+ #
33
+ # An unpassed optional is dropped from the forwarded arguments, so the body applies its own default.
34
+ #
35
+ # Candor.define(App.singleton_class, :fetch, aliases: [:get], via: :__call) { |id, ttl: 60| ... }
36
+ #
37
+ # @param target [Module] the module — often a +singleton_class+ — to install onto
38
+ # @param name [Symbol] the canonical name, passed to the interceptor and used for the body method
39
+ # @param aliases [Array<Symbol>] further names sharing the one dispatch and the one canonical name
40
+ # @param via [Symbol, nil] an interceptor method on +target+, resolved per call
41
+ # @param parameters [Array<Array>, nil] an explicit shape advertised instead of the body's; not
42
+ # validated against the body, so a mismatch surfaces as the body's own +ArgumentError+
43
+ # @param body [Proc, Method, UnboundMethod, nil] the body, when it is not given as a block
44
+ # @yield the body, when it is not given as +body+; +self+ inside it is the receiver
45
+ # @return [Symbol] the canonical name
46
+ # @raise [TypeError] if +target+ is not a Module, or the body is neither a block, a Proc, a Method
47
+ # nor an UnboundMethod, or its +source_location+ is +nil+, or its owner is not an ancestor of
48
+ # +target+
49
+ # @raise [ArgumentError] if a name starts with {BODY_PREFIX}, or +via+ is not a callable method
50
+ # name, or +parameters+ is malformed
51
+ # @raise [FrozenError] if +target+ is frozen
52
+ def define(target, name, aliases: [], via: nil, parameters: nil, body: nil, &block)
53
+ Definer.new(target, name, aliases: aliases, via: via, parameters: parameters, body: body || block).call
54
+ end
55
+
56
+ # The private method holding a fabricated method's body. An interceptor calls it with +send+.
57
+ #
58
+ # @param name [Symbol] a canonical name
59
+ # @return [Symbol]
60
+ def body_name(name) = :"#{BODY_PREFIX}#{name}"
61
+ end
62
+
63
+ private_constant :Definer
64
+ end
@@ -0,0 +1,22 @@
1
+ module Candor
2
+ # Compiles a method's `parameters` into a dispatch lambda with the same parameter kinds, and
3
+ # therefore the same `arity`. Switches on the kind, never the name.
4
+ class Signature
5
+ # Every kind `Method#parameters` can emit, and the only ones `render` accepts.
6
+ KINDS: Array[Symbol]
7
+
8
+ # How many optional keywords still dispatch through call sites rather than a Hash.
9
+ KEYWORD_BRANCH_LIMIT: Integer
10
+
11
+ # One entry of `Method#parameters`: a kind, and a name the body may not have given.
12
+ type parameter = [ Symbol ] | [ Symbol, Symbol ]
13
+
14
+ def self.compile: (Array[parameter] parameters, name: Symbol, source_location: [ String, Integer ], ?via: Symbol?) -> Proc
15
+ def self.render: (Array[parameter] parameters, name: Symbol, ?via: Symbol?) -> String
16
+ def self.method_name!: (Symbol | String name) -> Symbol
17
+ def self.parameters!: (untyped parameters) -> Array[parameter]
18
+
19
+ def initialize: (Array[parameter] parameters, Symbol name, ?Symbol? via) -> void
20
+ def source: () -> String
21
+ end
22
+ end
data/sig/candor.rbs ADDED
@@ -0,0 +1,28 @@
1
+ # Turns a block or a callable into a real method with an honest signature.
2
+ #
3
+ # Fabricated methods are compiled at runtime, so RBS cannot see them. Declare the ones you rely on in
4
+ # your own `sig/`.
5
+ #
6
+ # These files declare the public API and nothing else. `Candor::Definer` and the compiler's internal
7
+ # constants are `private_constant` in Ruby, and RBS has no syntax for that — declaring them here would
8
+ # advertise a surface that raises NameError on use.
9
+ module Candor
10
+ VERSION: String
11
+
12
+ # Prefix of the private methods holding compiled bodies.
13
+ BODY_PREFIX: String
14
+
15
+ # A body `define_method` accepts, with a `source_location` to forge onto the wrapper.
16
+ type body = Proc | Method | UnboundMethod
17
+
18
+ def self.define: (
19
+ Module target,
20
+ Symbol name,
21
+ ?aliases: Array[Symbol],
22
+ ?via: Symbol?,
23
+ ?parameters: Array[Signature::parameter]?,
24
+ ?body: body?
25
+ ) ?{ (?) -> untyped } -> Symbol
26
+
27
+ def self.body_name: (Symbol name) -> Symbol
28
+ end
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: candor
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Leonid Svyatov
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: 'Ruby''s missing functools.wraps. Candor fabricates real methods from
13
+ blocks and callables: same arity, same parameters, source_location pointing at your
14
+ code, and allocation-free dispatch. Zero runtime dependencies.'
15
+ email:
16
+ - leonid@svyatov.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - ".yardopts"
22
+ - CHANGELOG.md
23
+ - LICENSE.txt
24
+ - README.md
25
+ - candor.gemspec
26
+ - lib/candor.rb
27
+ - lib/candor/definer.rb
28
+ - lib/candor/signature.rb
29
+ - lib/candor/version.rb
30
+ - sig/candor.rbs
31
+ - sig/candor/signature.rbs
32
+ homepage: https://github.com/svyatov/candor
33
+ licenses:
34
+ - MIT
35
+ metadata:
36
+ rubygems_mfa_required: 'true'
37
+ documentation_uri: https://rubydoc.info/gems/candor
38
+ source_code_uri: https://github.com/svyatov/candor
39
+ changelog_uri: https://github.com/svyatov/candor/blob/main/CHANGELOG.md
40
+ bug_tracker_uri: https://github.com/svyatov/candor/issues
41
+ rdoc_options: []
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: 3.2.0
49
+ required_rubygems_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ requirements: []
55
+ rubygems_version: 4.0.12
56
+ specification_version: 4
57
+ summary: Turn a block or a callable into a real method with an honest signature.
58
+ test_files: []