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 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,3 @@
1
+ # alias_callable Ruby gem
2
+
3
+ > **Keywords:** #alias #method #function #callable #ruby #gem #p20240728a #dependency #inclusion
@@ -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,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AliasCallable
4
+
5
+ UNDEFINED = Class.new do
6
+
7
+ def inspect
8
+ "UNDEFINED"
9
+ end
10
+
11
+ alias_method :to_s, :inspect
12
+
13
+ end.new
14
+
15
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AliasCallable
4
+
5
+ UnknownCallableError = Class.new(StandardError)
6
+
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AliasCallable
4
+
5
+ VERSION = "0.1.0"
6
+
7
+ 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: []