alias_callable 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 +7 -0
- data/CHANGELOG.md +0 -0
- data/LICENSE +21 -0
- data/README.md +3 -0
- data/lib/alias_callable/build_alias_method.rb +113 -0
- data/lib/alias_callable/class_methods.rb +39 -0
- data/lib/alias_callable/extract_callable_parameters.rb +107 -0
- data/lib/alias_callable/parameter_info.rb +16 -0
- data/lib/alias_callable/undefined.rb +15 -0
- data/lib/alias_callable/unknown_callable_error.rb +7 -0
- data/lib/alias_callable/version.rb +7 -0
- data/lib/alias_callable.rb +42 -0
- metadata +54 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 4334c1231c35835f8201a245e9311facbf5955d7586eff0b4f50787640f418c2
|
4
|
+
data.tar.gz: fe35d58d1cf341250766a2aa564ac05045d18c4360852cb50d86b26c92416b94
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 823997bf9df2033c1a3d3e9159d3e9ede6e0dfc3b2b0d855bac9f2647803f5edcb087d53307b715e51c6ed2b99772a227615857bea07099d269264268278c50e
|
7
|
+
data.tar.gz: 226205bb435b18b36c3f6113170d71f67b2493e80b1dd0734fcdb51f333d1ab3fa8fbea368421be1f576abda7eb924820d43223ea23765008221446e98fefe42
|
data/CHANGELOG.md
ADDED
File without changes
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 Vladimir Gorodulin
|
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,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AliasCallable
|
4
|
+
|
5
|
+
class BuildAliasMethod
|
6
|
+
def self.call(**kwargs)
|
7
|
+
new(**kwargs).call
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(alias_name:, callable:, auto_fill: [])
|
11
|
+
@alias_name = alias_name
|
12
|
+
@callable = callable
|
13
|
+
@auto_fill = auto_fill
|
14
|
+
end
|
15
|
+
|
16
|
+
def call
|
17
|
+
ensure_auto_fill_validity! # Ensure that the auto_fill arguments are valid
|
18
|
+
signature = generate_method_signature # Generate method signature with proper parameters
|
19
|
+
kwargs_logic = generate_kwargs_logic # Generate code to handle keyword arguments with auto-fetching
|
20
|
+
call_expression = generate_call_expression # Generate the final method call with all arguments
|
21
|
+
|
22
|
+
<<-RUBY
|
23
|
+
def #{alias_name}(#{signature})
|
24
|
+
kwargs = {}
|
25
|
+
#{kwargs_logic}
|
26
|
+
#{call_expression}
|
27
|
+
end
|
28
|
+
RUBY
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
attr_reader :alias_name, :callable, :auto_fill
|
34
|
+
|
35
|
+
def params
|
36
|
+
@params ||= ::AliasCallable::ExtractCallableParameters.new(callable).call
|
37
|
+
end
|
38
|
+
|
39
|
+
def ensure_auto_fill_validity!
|
40
|
+
unsupported = auto_fill - params.keywords
|
41
|
+
if unsupported.any? # rubocop:disable Style/GuardClause
|
42
|
+
raise ArgumentError, "Unsupported auto_fill arguments: #{unsupported.join(', ')}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def generate_method_signature
|
47
|
+
parts = []
|
48
|
+
|
49
|
+
# Add positional parameters if any
|
50
|
+
if params.positionals.any? || params.rest?
|
51
|
+
parts << "*args"
|
52
|
+
end
|
53
|
+
|
54
|
+
if auto_fill.any?
|
55
|
+
parts << auto_fill.map do |kw|
|
56
|
+
"#{kw}: ::AliasCallable::UNDEFINED"
|
57
|
+
end.join(", ")
|
58
|
+
end
|
59
|
+
|
60
|
+
# Always add keyrest capture to support methods with **kwargs
|
61
|
+
# Even if the callable doesn't have keyrest, this allows for more flexible method calls
|
62
|
+
parts << "**extra_kwargs"
|
63
|
+
|
64
|
+
# Always include block parameter to support both explicit &block and block_given?
|
65
|
+
parts << "&block"
|
66
|
+
|
67
|
+
parts.join(", ")
|
68
|
+
end
|
69
|
+
|
70
|
+
def generate_kwargs_logic
|
71
|
+
logic = []
|
72
|
+
|
73
|
+
auto_fill.each do |kw|
|
74
|
+
logic << <<-RUBY
|
75
|
+
if #{kw} != ::AliasCallable::UNDEFINED
|
76
|
+
kwargs[:#{kw}] = #{kw}
|
77
|
+
elsif respond_to?(:#{kw}, true)
|
78
|
+
kwargs[:#{kw}] = send(:#{kw})
|
79
|
+
elsif instance_variable_defined?("@#{kw}")
|
80
|
+
kwargs[:#{kw}] = instance_variable_get("@#{kw}")
|
81
|
+
end
|
82
|
+
RUBY
|
83
|
+
end
|
84
|
+
|
85
|
+
# Handle extra_kwargs (always include them in kwargs)
|
86
|
+
# Merge any additional keyword arguments
|
87
|
+
logic << <<-RUBY
|
88
|
+
kwargs.merge!(extra_kwargs) unless extra_kwargs.empty?
|
89
|
+
RUBY
|
90
|
+
|
91
|
+
logic.join("\n")
|
92
|
+
end
|
93
|
+
|
94
|
+
def generate_call_expression
|
95
|
+
parts = []
|
96
|
+
|
97
|
+
# Add positional args if needed
|
98
|
+
if params.positionals.any? || params.rest?
|
99
|
+
parts << "*args"
|
100
|
+
end
|
101
|
+
|
102
|
+
# Add keyword args
|
103
|
+
parts << "**kwargs"
|
104
|
+
|
105
|
+
# Always pass the block, regardless of whether the callable has an explicit block parameter
|
106
|
+
# This ensures support for callables that use block_given? internally
|
107
|
+
parts << "&block"
|
108
|
+
|
109
|
+
"_callable__#{alias_name}.call(#{parts.join(', ')})"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AliasCallable
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
def alias_callable(alias_name, callable, auto_fill: [])
|
7
|
+
method_code = ::AliasCallable::BuildAliasMethod
|
8
|
+
.call(alias_name: alias_name, callable: callable, auto_fill: auto_fill)
|
9
|
+
# Define the delegator method with smart argument forwarding
|
10
|
+
class_eval(method_code)
|
11
|
+
# Define the "raw" delegator method and make it private
|
12
|
+
# This is the method that will be called by the delegator (alias) method
|
13
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
14
|
+
def _callable__#{alias_name} # def _callable__do_something
|
15
|
+
#{callable} # ::DoSomething
|
16
|
+
end # end
|
17
|
+
private :_callable__#{alias_name} # private :_callable__do_something
|
18
|
+
RUBY
|
19
|
+
alias_name
|
20
|
+
end
|
21
|
+
|
22
|
+
def aliased_callable(alias_name)
|
23
|
+
get_attached_object = lambda do |sc|
|
24
|
+
if sc.respond_to?(:attached_object)
|
25
|
+
sc.attached_object
|
26
|
+
else # Ruby 3.2 and earlier support. Slow, but better than nothing :(
|
27
|
+
::ObjectSpace.each_object(Object).find { |o| o.is_a?(Module) && o.singleton_class == sc }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
target = self.singleton_class? ? get_attached_object.call(self) : self.allocate # rubocop:disable Style/RedundantSelf
|
31
|
+
helper_name = :"_callable__#{alias_name}"
|
32
|
+
unless target.private_methods.include?(helper_name)
|
33
|
+
raise ::AliasCallable::UnknownCallableError, "No callable registered as `#{alias_name}` in #{self}."
|
34
|
+
end
|
35
|
+
target.send(helper_name)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AliasCallable
|
4
|
+
|
5
|
+
# Extracts parameters from a callable object (class/module)
|
6
|
+
class ExtractCallableParameters
|
7
|
+
def initialize(callable)
|
8
|
+
@callable = callable
|
9
|
+
end
|
10
|
+
|
11
|
+
# Main method to extract parameter information from the callable
|
12
|
+
def call
|
13
|
+
info = ParameterInfo.new
|
14
|
+
|
15
|
+
target_method_parameters.each do |param_type, param_name|
|
16
|
+
case param_type
|
17
|
+
when :key, :keyreq
|
18
|
+
info.keywords << param_name
|
19
|
+
when :req, :opt
|
20
|
+
info.positionals << param_name
|
21
|
+
when :rest
|
22
|
+
info.rest = true
|
23
|
+
when :keyrest
|
24
|
+
info.keyrest = true
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
info
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
# Determine which method's parameters we should extract
|
34
|
+
def target_method_parameters
|
35
|
+
if callable_responds_to_call? && !call_forwards_all_arguments?
|
36
|
+
class_callable_parameters # Parameters of callable.call method
|
37
|
+
elsif instance_initialize_parameters.any?
|
38
|
+
instance_initialize_parameters # Parameters of callable.new method
|
39
|
+
else
|
40
|
+
instance_callable_parameters # Parameters of callable.new.call method
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def callable_responds_to_call?
|
45
|
+
@callable.respond_to?(:call)
|
46
|
+
end
|
47
|
+
|
48
|
+
def call_forwards_all_arguments?
|
49
|
+
return false if @callable.is_a?(Module) # not instantiable, so nowhere to forward
|
50
|
+
return false unless @callable.instance_methods.include?(:call)
|
51
|
+
|
52
|
+
full_argument_forwarding_pattern?(class_callable_parameters)
|
53
|
+
end
|
54
|
+
|
55
|
+
def full_argument_forwarding_pattern?(parameters)
|
56
|
+
# See NOTES.md on full forwarding parameter patterns.
|
57
|
+
# Handles:
|
58
|
+
# - explicit *args, **kwargs, &block style regardless of parameter names
|
59
|
+
# - *args, &block with ruby2_keywords (which lacks visible :keyrest in Ruby 2.7-3.0)
|
60
|
+
# - the ... syntax in both
|
61
|
+
# - Ruby 3.0 (which showed [[:rest, :*], [:block, :&]]) and
|
62
|
+
# - Ruby 3.1+ (which shows [[:rest, :*], [:keyrest, :**], [:block, :&]])
|
63
|
+
|
64
|
+
# Skip methods with specific required, optional or keyword parameters
|
65
|
+
return false if parameters.any? do |param|
|
66
|
+
[:req, :opt, :key, :keyreq].include?(param[0])
|
67
|
+
end
|
68
|
+
|
69
|
+
# Extract the parameter types (ignoring names)
|
70
|
+
param_types = parameters.map { |param| param[0] }
|
71
|
+
|
72
|
+
# Check for the required pattern components
|
73
|
+
has_rest = param_types.include?(:rest)
|
74
|
+
has_block = param_types.include?(:block)
|
75
|
+
|
76
|
+
# Consider keyrest optional because:
|
77
|
+
# 1. Ruby 3.0's `...` syntax initially didn't show keyrest in parameters inspection
|
78
|
+
# 2. Explicit `*args, &block` (without keyrest) can still forward all arguments
|
79
|
+
# in certain Ruby versions with ruby2_keywords applied
|
80
|
+
|
81
|
+
# Must have at minimum rest + block
|
82
|
+
if has_rest && has_block
|
83
|
+
# No other parameter types should exist beyond rest, keyrest, and block
|
84
|
+
extra_params = param_types - [:rest, :keyrest, :block]
|
85
|
+
return extra_params.empty?
|
86
|
+
end
|
87
|
+
|
88
|
+
false
|
89
|
+
end
|
90
|
+
|
91
|
+
def class_callable_parameters
|
92
|
+
@callable.method(:call).parameters # TODO: memoize
|
93
|
+
end
|
94
|
+
|
95
|
+
def instance_initialize_parameters
|
96
|
+
@callable.instance_method(:initialize).parameters # TODO: memoize
|
97
|
+
rescue NameError
|
98
|
+
[]
|
99
|
+
end
|
100
|
+
|
101
|
+
def instance_callable_parameters
|
102
|
+
instance = @callable.allocate rescue nil # rubocop:disable Style/RescueModifier
|
103
|
+
instance&.method(:call)&.parameters || []
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AliasCallable
|
4
|
+
|
5
|
+
ParameterInfo = Struct.new(:keywords, :positionals, :rest, :keyrest) do
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
super([], [], false, false)
|
9
|
+
end
|
10
|
+
|
11
|
+
alias_method :rest?, :rest
|
12
|
+
alias_method :keyrest?, :keyrest
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AliasCallable
|
4
|
+
|
5
|
+
autoload :BuildAliasMethod, "alias_callable/build_alias_method"
|
6
|
+
autoload :ClassMethods, "alias_callable/class_methods"
|
7
|
+
autoload :ExtractCallableParameters, "alias_callable/extract_callable_parameters"
|
8
|
+
autoload :ParameterInfo, "alias_callable/parameter_info"
|
9
|
+
autoload :UNDEFINED, "alias_callable/undefined"
|
10
|
+
autoload :UnknownCallableError, "alias_callable/unknown_callable_error"
|
11
|
+
autoload :VERSION, "alias_callable/version"
|
12
|
+
|
13
|
+
@backtrace_filtering_enabled = false
|
14
|
+
|
15
|
+
def self.enable_globally
|
16
|
+
::Module.include(::AliasCallable::ClassMethods) unless enabled_globally?
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.enabled_globally?
|
20
|
+
::Module.included_modules.include?(::AliasCallable::ClassMethods)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.enable_backtrace_filtering
|
24
|
+
return if backtrace_filtering_enabled?
|
25
|
+
|
26
|
+
::Exception.class_eval do
|
27
|
+
alias_method :original_backtrace, :backtrace
|
28
|
+
|
29
|
+
def backtrace
|
30
|
+
bt = original_backtrace
|
31
|
+
bt&.grep_v(%r{/alias_callable/})
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
@backtrace_filtering_enabled = true
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.backtrace_filtering_enabled?
|
39
|
+
@backtrace_filtering_enabled
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
metadata
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: alias_callable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Vladimir Gorodulin
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 2025-04-11 00:00:00.000000000 Z
|
11
|
+
dependencies: []
|
12
|
+
description: Bring callable classes to your code as methods.
|
13
|
+
email:
|
14
|
+
- ru.hostmaster@gmail.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- CHANGELOG.md
|
20
|
+
- LICENSE
|
21
|
+
- README.md
|
22
|
+
- lib/alias_callable.rb
|
23
|
+
- lib/alias_callable/build_alias_method.rb
|
24
|
+
- lib/alias_callable/class_methods.rb
|
25
|
+
- lib/alias_callable/extract_callable_parameters.rb
|
26
|
+
- lib/alias_callable/parameter_info.rb
|
27
|
+
- lib/alias_callable/undefined.rb
|
28
|
+
- lib/alias_callable/unknown_callable_error.rb
|
29
|
+
- lib/alias_callable/version.rb
|
30
|
+
homepage: https://github.com/gorodulin/alias_callable
|
31
|
+
licenses:
|
32
|
+
- MIT
|
33
|
+
metadata:
|
34
|
+
changelog_uri: https://github.com/gorodulin/alias_callable/blob/main/CHANGELOG.md
|
35
|
+
homepage_uri: https://github.com/gorodulin/alias_callable
|
36
|
+
source_code_uri: https://github.com/gorodulin/alias_callable/tree/main
|
37
|
+
rdoc_options: []
|
38
|
+
require_paths:
|
39
|
+
- lib
|
40
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '3.0'
|
45
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - ">="
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '0'
|
50
|
+
requirements: []
|
51
|
+
rubygems_version: 3.6.2
|
52
|
+
specification_version: 4
|
53
|
+
summary: Call Service Objects with ease. Include them to your classes as methods!
|
54
|
+
test_files: []
|