amenable 0.0.2 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 133a9c4b0a84e4325394ededc6826cb7408a9505d1c45bdfdd06674a10fac5ff
4
- data.tar.gz: 1345444593420700a2b84938c64f58343e6f33c8a6399fd955fb09cd717c74c0
3
+ metadata.gz: 0b63301072a15feda94f5c8a68ca8be4ed71ed08a4f33591c8bd50e0e70913d6
4
+ data.tar.gz: d066ce9fc0f6e20510a17715e68bd36ebf333b3e268775dff8d7a38fe8349892
5
5
  SHA512:
6
- metadata.gz: 7fcb746e4541e5360d25b5281d0b407be1b851ba97196d5bae01fec41eae898227bc87aa09dba59d91e22b5045aa14dd93d63856323805cc4944641b79df71b6
7
- data.tar.gz: 9072334b6a123882a656f07db3797064cf726fdaf7900ecddc90e808b27ca6503246c0417e1f953e4941fc9c69e2ddce1a5a04be69e245aada6a3b6cf64bccf1
6
+ metadata.gz: 8ac3cb8c04e8aab1072cd30c0116d4ae6a86784f827b10c515bec6e6c515069c62559604647668b41159b542fe7bb3f4c3ad2b483657b16924baa54c7e07faaa
7
+ data.tar.gz: 2e97dd44eca60f8ea2c16b59176ea9a5861e95de54a01814337fb0cded49f39b160ca6921f04d802825b4bf612d1c2065cf5bd8d4a8c51311ec8dc99730251d0
data/CHANGELOG.md ADDED
@@ -0,0 +1,25 @@
1
+ ### v0.1.1 (2026-11-03)
2
+ - drop Ruby 3.0 and 3.1 (end of life); requires Ruby >= 3.2
3
+ - fix: amenable methods no longer run with the first caller's `self` (instances, classes sharing a module, subclasses calling amenable class methods, concurrent first calls)
4
+ - fix: amenable methods pass keyword arguments through instead of dropping them (`opt(1, b: 3)` returned the default `b`, `**opts` came back empty)
5
+ - fix: keywords sent to a method with no keyword params are passed through as Ruby would bind them (to `*rest` or an open positional slot, e.g. `def m(opts = {})`, and to C methods like `String#encode`) instead of being dropped; they're only stripped when Ruby would reject them
6
+ - fix: in a class or module body, `amenable` wraps the instance method even when a class method of the same name exists; `amenable def initialize` now works; module_function methods are wrapped on both the module and its includers
7
+ - fix: `amenable` at the top level, and on a plain object's private singleton method, no longer crashes
8
+ - fix: `amenable` with an unknown or inherited method raises NoMethodError naming the method and receiver
9
+ - fix: a non-lambda proc given a lone Array now auto-splats it as a direct call does (`Amenable.call(proc { |a, b = 2, k: 1| [a, b] }, [5, 1])` returned `[[5, 1], 2]`)
10
+ - fix: extra positionals and keywords sent to a non-lambda proc are left for the proc to drop, as a direct call does (`proc { |a,| a }` given `([5, 1], :x)` returned 5 instead of `[5, 1]`)
11
+
12
+ ### v0.1.0 (2021-07-05)
13
+ - readme spec
14
+ - test coverage
15
+ - simplify ci
16
+ - remove wrapper and simplify usage around refinement or .call
17
+ - refinement support
18
+ - kwarg bugfix
19
+
20
+ ### v0.0.2 (2021-06-26)
21
+ - upgrade to .call / .wrap
22
+
23
+ ### v0.0.1 (2021-04-07)
24
+ - init
25
+
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021 Daniel Pepper
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,98 @@
1
+ Amenable
2
+ ======
3
+ Removes excessive parameters from function calls.
4
+
5
+ The rule: amenable drops only the arguments Ruby would reject with `ArgumentError`. Any call Ruby already accepts behaves exactly as it would without amenable.
6
+
7
+
8
+ ```ruby
9
+ require 'amenable'
10
+
11
+ class Dog
12
+ using Amenable
13
+
14
+ amenable def bark(n = 1)
15
+ ([ :woof ] * n).join(" ")
16
+ end
17
+ end
18
+
19
+
20
+ Dog.new.bark
21
+ > "woof"
22
+
23
+ Dog.new.bark(2)
24
+ > "woof woof"
25
+
26
+ Dog.new.bark(2, 3, 4, foo: 5)
27
+ > "woof woof"
28
+ ```
29
+
30
+
31
+ ## Usage
32
+
33
+ ```ruby
34
+ gem "amenable" # requires Ruby >= 3.2
35
+ ```
36
+
37
+ Activate the refinement with `using Amenable`, then wrap a method as you define it (`amenable def bark ...`) or afterwards by name (`amenable :bark`). It works on instance, private, protected, and class methods (`amenable def self.build`), on `module_function` methods, and at the top level.
38
+
39
+ For a Method or Proc you already have, skip the refinement and use `Amenable.call(fn, *args, **kwargs, &block)`:
40
+
41
+ ```ruby
42
+ Amenable.call(->(a) { a * 2 }, 3, 4, x: 1)
43
+ > 6
44
+ ```
45
+
46
+
47
+ ## Keywords
48
+
49
+ Extra keywords are dropped when the method can't take them. If it has an open positional slot, they land there as a Hash, as in plain Ruby:
50
+
51
+ ```ruby
52
+ class Greeter
53
+ using Amenable
54
+
55
+ amenable def hello(name, greeting: "hi")
56
+ "#{greeting} #{name}"
57
+ end
58
+
59
+ amenable def configure(opts = {})
60
+ opts
61
+ end
62
+ end
63
+
64
+
65
+ Greeter.new.hello("bob", greeting: "yo", volume: 11)
66
+ > "yo bob"
67
+
68
+ Greeter.new.configure(verbose: true)
69
+ > {verbose: true}
70
+ ```
71
+
72
+ Missing arguments still raise. Amenable removes; it never fills in.
73
+
74
+
75
+ ## Limits
76
+
77
+ - Only wraps methods defined directly on the receiver, not inherited ones (raises `NoMethodError`).
78
+ - When a class method and an instance method share a name, `amenable def foo` in the class body wraps the instance method. Use `class << self` to wrap the class one.
79
+ - Wrapped methods report the wrapper's `parameters`, `arity`, and `source_location`.
80
+ - Curried lambdas and `Symbol#to_proc` can't be stripped: their real signature isn't visible.
81
+
82
+
83
+ ----
84
+ ## Contributing
85
+
86
+ Yes please :)
87
+
88
+ 1. Fork it
89
+ 1. Create your feature branch (`git checkout -b my-feature`)
90
+ 1. Ensure the tests pass (`bundle exec rspec`)
91
+ 1. Commit your changes (`git commit -am 'awesome new feature'`)
92
+ 1. Push your branch (`git push origin my-feature`)
93
+ 1. Create a Pull Request
94
+
95
+
96
+ ----
97
+ ![Gem](https://img.shields.io/gem/dt/amenable?style=plastic)
98
+ [![codecov](https://codecov.io/gh/dpep/amenable_rb/branch/main/graph/badge.svg)](https://codecov.io/gh/dpep/amenable_rb)
data/amenable.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ require_relative "lib/amenable/version"
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'amenable'
5
+ s.version = Amenable::VERSION
6
+ s.authors = ['Daniel Pepper']
7
+ s.summary = 'Amenable'
8
+ s.description = <<~DESCRIPTION
9
+ A refinement that strips excess positional and keyword arguments
10
+ from method calls, so callers can pass extras without raising
11
+ ArgumentError.
12
+ DESCRIPTION
13
+ s.homepage = "https://github.com/dpep/amenable_rb"
14
+ s.license = 'MIT'
15
+ s.files = `git ls-files * ':!:spec' ':!:CLAUDE.md'`.split("\n")
16
+
17
+ s.metadata = {
18
+ "changelog_uri" => "#{s.homepage}/blob/main/CHANGELOG.md",
19
+ "rubygems_mfa_required" => "true",
20
+ "source_code_uri" => s.homepage,
21
+ }
22
+
23
+ s.required_ruby_version = ">= 3.2"
24
+
25
+ s.add_development_dependency 'debug'
26
+ s.add_development_dependency 'rspec'
27
+ s.add_development_dependency 'simplecov'
28
+ end
@@ -1,3 +1,3 @@
1
1
  module Amenable
2
- VERSION = '0.0.2'
2
+ VERSION = '0.1.1'
3
3
  end
data/lib/amenable.rb CHANGED
@@ -4,15 +4,14 @@ module Amenable
4
4
  extend self
5
5
 
6
6
  def call(fn, *args, **kwargs, &block)
7
- wrap(fn).call(*args, **kwargs, &block)
8
- end
9
-
10
- def wrap(fn)
11
- unless fn.respond_to?(:call)
12
- raise ArgumentError, "fn must be callable: #{fn}"
7
+ unless fn.is_a?(Method) || fn.is_a?(Proc)
8
+ raise ArgumentError, "expecting type Method or Proc, found: #{fn.class}"
13
9
  end
14
10
 
15
- rest = keyrest = false
11
+ rest = keyrest = nokey = false
12
+ # a non-lambda proc drops extra positionals itself, and trimming them can
13
+ # make it auto-splat a lone Array
14
+ loose = fn.is_a?(Proc) && !fn.lambda?
16
15
  params = []
17
16
  keys = []
18
17
 
@@ -26,15 +25,76 @@ module Amenable
26
25
  rest = true
27
26
  when :keyrest
28
27
  keyrest = true
28
+ when :nokey
29
+ nokey = true
29
30
  end
30
31
  end
31
32
 
32
- proc do |*args, **kwargs, &block|
33
- # remove unreferenced params
34
- args.slice! params.count unless rest
33
+ # remove excessive args
34
+ args = args.slice(0, params.count) unless rest || loose
35
+
36
+ if nokey
37
+ kwargs = {}
38
+ elsif !keys.empty? || keyrest
39
+ # remove excessive keywords
35
40
  kwargs = kwargs.slice(*keys) unless keyrest
41
+ elsif !(rest || loose || args.size < params.count)
42
+ # no keyword params: Ruby binds keywords to *rest or an open positional
43
+ # slot as a Hash, so strip them only when there's nowhere for them to go
44
+ kwargs = {}
45
+ end
46
+
47
+ # `**{}` is not a no-op: it stops a non-lambda proc auto-splatting
48
+ kwargs.empty? ? fn.call(*args, &block) : fn.call(*args, **kwargs, &block)
49
+ end
50
+
51
+ refine Object do
52
+ def amenable(name)
53
+ defines = ->(owner) do
54
+ owner.method_defined?(name, false) || owner.private_method_defined?(name, false)
55
+ end
56
+
57
+ wrap = ->(owner) do
58
+ fn = owner.instance_method(name)
59
+ visibility =
60
+ if owner.private_method_defined?(name, false) then :private
61
+ elsif owner.protected_method_defined?(name, false) then :protected
62
+ else :public
63
+ end
64
+
65
+ owner.send(:define_method, name) do |*args, **kwargs, &block|
66
+ # bind per call: `fn` is shared by every receiver, so never reassign it
67
+ Amenable.call(fn.bind(self), *args, **kwargs, &block)
68
+ end
69
+ owner.send(visibility, name)
70
+ end
71
+
72
+ # a top-level `def` lands on Object, not on main's singleton class
73
+ mod = TOPLEVEL_BINDING.receiver.equal?(self) ? Object : self
74
+
75
+ # in a class body, `def foo` means the instance method; only fall back
76
+ # to the singleton side when there isn't one
77
+ if mod.is_a?(Module) && defines.call(mod)
78
+ # module_function leaves a public copy on the singleton and makes the
79
+ # instance method private: wrap both. Location alone can't tell a copy
80
+ # from another def on the same line.
81
+ copy = mod.private_method_defined?(name, false) &&
82
+ mod.singleton_class.public_method_defined?(name, false) &&
83
+ mod.singleton_class.instance_method(name).source_location == mod.instance_method(name).source_location
84
+
85
+ wrap.call(mod)
86
+ wrap.call(mod.singleton_class) if copy
87
+ elsif defines.call(singleton_class)
88
+ wrap.call(singleton_class)
89
+ else
90
+ raise NoMethodError.new(
91
+ "amenable: no method '#{name}' defined directly on #{inspect}",
92
+ name,
93
+ receiver: self,
94
+ )
95
+ end
36
96
 
37
- fn.call(*args, **kwargs, &block)
97
+ name
38
98
  end
39
99
  end
40
100
  end
metadata CHANGED
@@ -1,45 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: amenable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Pepper
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2021-06-26 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
- name: byebug
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '0'
20
- type: :development
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: '0'
27
- - !ruby/object:Gem::Dependency
28
- name: codecov
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '0'
41
- - !ruby/object:Gem::Dependency
42
- name: rake
13
+ name: debug
43
14
  requirement: !ruby/object:Gem::Requirement
44
15
  requirements:
45
16
  - - ">="
@@ -80,20 +51,28 @@ dependencies:
80
51
  - - ">="
81
52
  - !ruby/object:Gem::Version
82
53
  version: '0'
83
- description: Flexibility when you need it.
84
- email:
54
+ description: |
55
+ A refinement that strips excess positional and keyword arguments
56
+ from method calls, so callers can pass extras without raising
57
+ ArgumentError.
85
58
  executables: []
86
59
  extensions: []
87
60
  extra_rdoc_files: []
88
61
  files:
62
+ - CHANGELOG.md
63
+ - Gemfile
64
+ - LICENSE.txt
65
+ - README.md
66
+ - amenable.gemspec
89
67
  - lib/amenable.rb
90
68
  - lib/amenable/version.rb
91
- - spec/amenable_spec.rb
92
69
  homepage: https://github.com/dpep/amenable_rb
93
70
  licenses:
94
71
  - MIT
95
- metadata: {}
96
- post_install_message:
72
+ metadata:
73
+ changelog_uri: https://github.com/dpep/amenable_rb/blob/main/CHANGELOG.md
74
+ rubygems_mfa_required: 'true'
75
+ source_code_uri: https://github.com/dpep/amenable_rb
97
76
  rdoc_options: []
98
77
  require_paths:
99
78
  - lib
@@ -101,16 +80,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
101
80
  requirements:
102
81
  - - ">="
103
82
  - !ruby/object:Gem::Version
104
- version: '0'
83
+ version: '3.2'
105
84
  required_rubygems_version: !ruby/object:Gem::Requirement
106
85
  requirements:
107
86
  - - ">="
108
87
  - !ruby/object:Gem::Version
109
88
  version: '0'
110
89
  requirements: []
111
- rubygems_version: 3.0.8
112
- signing_key:
90
+ rubygems_version: 4.0.16
113
91
  specification_version: 4
114
92
  summary: Amenable
115
- test_files:
116
- - spec/amenable_spec.rb
93
+ test_files: []
@@ -1,119 +0,0 @@
1
- describe Amenable do
2
- describe '.wrap' do
3
- let(:fn) { proc {} }
4
-
5
- it 'requires a function-like input' do
6
- expect { Amenable.wrap(nil) }.to raise_error(ArgumentError)
7
- end
8
-
9
- it 'wraps a function with a function' do
10
- res = Amenable.wrap(fn)
11
- expect(res).to be_a Proc
12
- expect(res).not_to be fn
13
- end
14
-
15
- it 'returns a function that takes any input' do
16
- params = Amenable.wrap(fn).parameters.map &:first
17
- expect(params).to eq [ :rest, :keyrest, :block ]
18
- end
19
- end
20
-
21
- describe '.call' do
22
- let(:fn) { proc {|x, y = :y, a:, b: 2| [ [ x, y ], { a: a, b: b } ] } }
23
-
24
- it 'calls .wrap under the hood' do
25
- fn = proc {}
26
- expect(Amenable).to receive(:wrap).with(fn).and_call_original
27
- Amenable.call(fn)
28
- end
29
-
30
- it 'takes args and kwargs as expected' do
31
- expect(fn).to receive(:call).with(:x, :y, a: 1, b: 2)
32
- Amenable.call(fn, :x, :y, a: 1, b: 2)
33
- end
34
-
35
- it 'removes excessive parameters' do
36
- expect(fn).to receive(:call).with(:x, :y, a: 1, b: 2)
37
- Amenable.call(fn, :x, :y, :z, a: 1, b: 2, c: 3)
38
- end
39
-
40
- it 'works with splat operator' do
41
- args = [ :x, :y, :z ]
42
- kwargs = { a: 1, b: 2, c: 3 }
43
-
44
- expect(fn).to receive(:call).with(:x, :y, a: 1, b: 2)
45
- Amenable.call(fn, *args, **kwargs)
46
- end
47
-
48
- it 'works with default args' do
49
- expect(fn).to receive(:call).with(:x, a: 1).and_call_original
50
-
51
- expect(Amenable.call(fn, :x, a: 1)).to eq([
52
- [ :x, :y ],
53
- { a: 1, b: 2 },
54
- ])
55
- end
56
-
57
- it 'raises if required arguments are not passed' do
58
- expect {
59
- Amenable.call(fn, :x)
60
- }.to raise_error(ArgumentError)
61
-
62
- expect {
63
- Amenable.call(fn, :x, :y)
64
- }.to raise_error(ArgumentError)
65
-
66
- expect {
67
- Amenable.call(fn, :x, :y, b: 2)
68
- }.to raise_error(ArgumentError)
69
-
70
- expect {
71
- Amenable.call(fn, a: 1, b: 2)
72
- }.to raise_error(ArgumentError)
73
- end
74
-
75
- context 'with var args' do
76
- let(:fn) { proc {|x, *z, a:, **c| [ x, z, a, c ] } }
77
-
78
- it 'works with just enough args' do
79
- expect(fn).to receive(:call).with(:x, a: 1).and_call_original
80
-
81
- expect(Amenable.call(fn, :x, a: 1)).to eq([
82
- :x, [], 1, {},
83
- ])
84
- end
85
-
86
- it 'fails without enough args' do
87
- expect {
88
- Amenable.call(fn, :x)
89
- }.to raise_error(ArgumentError)
90
-
91
- expect {
92
- Amenable.call(fn, a: 1)
93
- }.to raise_error(ArgumentError)
94
- end
95
-
96
- it 'passes through all args' do
97
- expect(Amenable.call(fn, :x, :y, :z, a: 1, b: 2, c: 3)).to eq([
98
- :x, [ :y, :z ], 1, { b: 2, c: 3 },
99
- ])
100
- end
101
- end
102
-
103
- context 'with a method' do
104
- before do
105
- def test_fn(x, a:, &block)
106
- [ x, a, block ]
107
- end
108
- end
109
-
110
- it 'works all the same' do
111
- test_block = proc {}
112
-
113
- expect(Amenable.call(method(:test_fn), :x, a: 1, &test_block)).to eq([
114
- :x, 1, test_block
115
- ])
116
- end
117
- end
118
- end
119
- end