activefunction-core 0.0.1 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a64c7260d306142ff9c0b5e48d0cd34c9258e65af3e19bbd7aa6b1121a8a3eff
4
- data.tar.gz: 14514dbbad34d5f8b64fadd0b146c5a1f90a955578cbf5e51fe161af4d5939ad
3
+ metadata.gz: 0b2cfe13205db0147f98083ea9d83b4f6ee0a1a1baaec08f848bf7a650a6e844
4
+ data.tar.gz: 136a5559b4c123ba50c384cfaf4beb05a5a9dca14ae398866e9a98a32d72338c
5
5
  SHA512:
6
- metadata.gz: 13038598288b048965d93d6299bc7fe1feda28f28bcee220c87d5361f59060e43c07d44089279840da2c3877dd8a100aacad5b32216a692b9497f431ebab0cdc
7
- data.tar.gz: 3522dd0d9db78ac518093fc9b7f4bf9be48da2593ef63fe0766e7a51b04ab729b63ef521fdc5a0f115fabac0f7207268d02cb586f3aabd50561408adabd47464
6
+ metadata.gz: 5b2a5470029dc54aab1677f6d08af01d1eaca7673d38003745630a82355e44d810085a66c4f4dbafe50409fb4f2e730e3cacc17384883093943e97169ed4ddfe
7
+ data.tar.gz: 8865302a689494285e6ffc23ba6197aca98b2ae6ee3049364c8c0084bfeb3a5507df4e5d3d674277037e5b0621b1a42afacad08d94d2604b13c30a7f496af26c
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
1
  ## [0.1.0]
2
2
 
3
3
  - Introduce ruby-next integration
4
+
5
+ ## [0.1.1]
6
+
7
+ - Bump ruby-next to 1.0.0
8
+
9
+ ## [0.2.0]
10
+
11
+ - Added Plugins support
12
+ - Added Hooks plugin - refactored ActiveFunction::Functions::Callbacks implementation.
13
+
data/README.md CHANGED
@@ -1,143 +1,112 @@
1
- # ActiveFunction
1
+ # ActiveFunction Core
2
2
 
3
- rails/action_controller like gem which provides lightweight callbacks, strong parameters & rendering features. It's designed to be used with AWS Lambda functions, but can be also used with any Ruby application.
3
+ Inspired by the structure of the AWS SDK gem, `activefunction-core` seamlessly integrates with the `activefunction` library family, offering a unified interface. It's also designed to operate as a standalone solution.
4
4
 
5
- Implemented with some of ruby 3.x features, but also supports ruby 2.6.x thanks to [RubyNext](https://github.com/ruby-next/ruby-next) transpiler. Type safety achieved by RBS and [Steep](https://github.com/soutaro/steep).
5
+ ## Features
6
6
 
7
+ - **Ruby-Next Integration:** Enables ruby-next auto-transpiling mode. This allows to use latest Ruby syntax while maintaining compatibility with older versions.
8
+ - **Plugins:** Extends functionality via plugin capabilities, including a callbacks DSL for `before_action` and `after_action` implementation within classes.
7
9
 
8
- ## A Short Example
9
10
 
10
- Here's a simple example of a function that uses ActiveFunction:
11
+ ## Plugins
11
12
 
12
- ```ruby
13
- require 'active_function'
14
-
15
- class AppFunction < ActiveFunction::Base
16
- def index
17
- render json: SomeTable.all
18
- end
19
- end
20
- ```
13
+ ### Hooks
21
14
 
22
- Use `#process` method to proceed the request:
15
+ Provides ActiveSupport::Callbacks like DSL for hooks through `::define_hooks_for` to define `before_[method_name]` & `after_[method_name]` callbacks and redefined #method_name to execute callbacks around it.
23
16
 
24
- ```ruby
25
- AppFunction.process(:index) # processes index action of AppFunction instance
26
- ```
27
- Also check extended [example](https://github.com/DanilMaximov/activefunction/tree/master/active_function_example)
28
- ## Callbacks
29
- ActiveFunction supports simple callbacks `:before` and `:after` which runs around provided action in `#process`.
17
+ ### Usage
30
18
 
31
19
  ```ruby
32
- class AppFunction < ActiveFunction::Base
33
- before_action :set_user
34
- after_action :log_response
35
-
36
- # some action ...
20
+ class YourClass
21
+ include ActiveFunction::Core::Plugins::Hooks
37
22
 
38
- private
23
+ define_hooks_for :your_method
39
24
 
40
- def set_user
41
- @user = User.first
42
- end
25
+ before_your_method :do_something_before
26
+ after_your_method :do_something_after
43
27
 
44
- def log_response
45
- Logger.info @response
28
+ def your_method
29
+ # Method implementation here...
30
+ end
31
+
32
+ private
33
+
34
+ def do_something_before
35
+ # Callback logic to execute before your_method
36
+ end
37
+
38
+ def do_something_after
39
+ # Callback logic to execute after your_method
46
40
  end
47
41
  end
48
- ```
42
+ ```
49
43
 
50
- Callbacks also can be user with `only: Array[Symbol]` and `if: Symbol` options.
44
+ ### Hook Method Alias
45
+
46
+ If you need to alias the method name, you can do so by passing the `:name` option.
51
47
 
52
48
  ```ruby
53
- class AppFunction < ActiveFunction::Base
54
- before_action :set_user, only: %i[show update destroy], if: :request_valid?
55
-
56
- # some actions ...
57
-
58
- private def request_valid? = true
59
- end
49
+ define_hooks_for :your_method, name: :your_method_alias
50
+ before_your_method_alias :do_something_before
60
51
  ```
61
52
 
62
- Callbacks are inheritable so all callbacks calls will be inherited from base class
53
+ ### Options
54
+
55
+ Supports options for `before_[method_name]` & `after_[method_name]` callbacks. Each option is a Proc that return a Bool. By default, `:if` & `:unless` options are vailable, accepting method name.
56
+
63
57
  ```ruby
64
- class BaseFunction < ActiveFunction::Base
65
- before_action :set_current_user
66
58
 
67
- def set_current_user
68
- @current_user = User.first
69
- end
70
- end
59
+ class YourClass
60
+ include ActiveFunction::Core::Plugins::Hooks
71
61
 
72
- class PostsFunction < BaseFunction
73
- def index
74
- render json: @current_user
62
+ define_hooks_for :your_method
63
+
64
+ before_your_method :do_something_before, if: :condition_met?
65
+ after_your_method :do_something_after, unless: :condition_met?
66
+
67
+ def your_method
68
+ # Method implementation here...
75
69
  end
76
- end
77
- ```
78
- ## Strong Parameters
79
- ActiveFunction supports strong parameters which can be accessed by `#params` instance method. Strong parameters hash can be passed in `#process` as second argument.
80
70
 
81
- ```ruby
82
- PostFunction.process(:index, data: { id: 1, name: "Pupa" })
83
- ```
71
+ private
84
72
 
85
- Simple usage:
86
- ```ruby
87
- class PostsFunction < ActiveFunction::Base
88
- def index
89
- render json: permitted_params
90
- end
91
-
92
- def permitted_params = params
93
- .require(:data)
94
- .permit(:id, :name)
95
- .to_h
96
- end
97
- ```
98
- Strong params supports nested attributes
99
- ```ruby
100
- params.permit(:id, :name, :address => [:city, :street])
101
- ```
73
+ def condition_met?
74
+ # Condition logic here...
75
+ end
102
76
 
103
- ## Rendering
104
- ActiveFunction supports rendering of JSON. Rendering is obligatory for any function naction and can be done by `#render` method.
105
- ```ruby
106
- class PostsFunction < ActiveFunction::Base
107
- def index
108
- render json: { id: 1, name: "Pupa" }
109
- end
110
- end
111
- ```
112
- default status code is 200, but it can be changed by `:status` option
113
- ```ruby
114
- class PostsFunction < ActiveFunction::Base
115
- def index
116
- render json: { id: 1, name: "Pupa" }, status: 201
117
- end
77
+ def do_something_before
78
+ # Callback logic to execute before your_method
79
+ end
80
+
81
+ def do_something_after
82
+ # Callback logic to execute after your_method
83
+ end
118
84
  end
119
85
  ```
120
- Headers can be passed by `:headers` option. Default headers are `{"Content-Type" => "application/json"}`.
86
+
87
+ Using `::set_callback_options` method, you can define your own options. This method accepts a single attribute Hash where the key is the option name and the value is a Proc that returns a Bool. Specify `context:` keyword argument for proc to access current class instance.
88
+
121
89
  ```ruby
122
- class PostsFunction < ActiveFunction::Base
123
- def index
124
- render json: { id: 1, name: "Pupa" }, headers: { "X-Request-Id" => "123" }
125
- end
126
- end
127
- ```
90
+ class YourClass
91
+ include ActiveFunction::Core::Plugins::Hooks
128
92
 
93
+ set_callback_options only: ->(only_methods, context:) { only_methods.include?(context.action) }
129
94
 
130
- ## Installation
95
+ define_hooks_for :your_method
131
96
 
132
- Add this line to your application's Gemfile:
97
+ before_your_method :do_something_before, only: %[foo bar]
133
98
 
134
- ```ruby
135
- gem 'activefunction', git: "https://github.com/DanilMaximov/activefunction.git"
99
+ def action = "foo"
100
+ end
136
101
  ```
137
102
 
103
+ ### Callbacks Inheritance
104
+
105
+ Callbacks are inheritable so all callbacks calls will be inherited from base class.
106
+
138
107
  ## Development
139
108
 
140
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/rake test` to run the tests and `bin/rake steep` to run type checker.
109
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/rake test:all` to run the tests and `bin/rake steep` to run type checker.
141
110
 
142
111
  To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
143
112
 
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ # TODO: remove with new ruby-next release
6
+ if RUBY_VERSION < "3.2"
7
+ Data.define_singleton_method(:inherited) do |subclass|
8
+ subclass.instance_variable_set(:@members, members)
9
+ end
10
+ end
11
+
12
+ module ActiveFunctionCore
13
+ module Plugins
14
+ module Hooks
15
+ class Hook < Data.define(:method_name, :callbacks)
16
+ DEFAULT_CALLBACK_OPTIONS = {
17
+ if: ->(v, context:) { context.send(v) if context.respond_to?(v, true) },
18
+ unless: ->(v, context:) { !context.send(v) if context.respond_to?(v, true) }
19
+ }.freeze
20
+ SUPPORTED_CALLBACKS = %i[before after].freeze
21
+
22
+ Callback = Data.define(:options, :target) do
23
+ def run(context)
24
+ raise ArgumentError, "Callback target #{target} is not defined" unless context.respond_to?(target, true)
25
+ raise ArgumentError, ":callback_options is not defined in #{context.class}" unless context.class.respond_to?(:callback_options)
26
+
27
+ context.instance_exec(target, normalized_options(options, context)) do |target, options|
28
+ method(target).call if options.all?(&:call)
29
+ end
30
+ end
31
+
32
+ private def normalized_options(options, context)
33
+ options.map do |option|
34
+ name, arg = option.first
35
+ -> { context.class.callback_options[name].call(arg, context: context) }
36
+ end
37
+ end
38
+ end
39
+
40
+ def initialize(callbacks: SUPPORTED_CALLBACKS.dup, **__kwrest__)
41
+ super(callbacks: callbacks.to_h { |_1| [_1, []] }, **__kwrest__)
42
+ end
43
+
44
+ def add_callback(type:, target:, options: {})
45
+ callbacks[type] << Callback[options, target].tap do |callback|
46
+ next unless callbacks[type].map(&:hash).to_set === callback.hash
47
+
48
+ raise(ArgumentError, "Callback already defined")
49
+ end
50
+ end
51
+
52
+ def run_callbacks(context, &block)
53
+ callbacks[:before].each { |it| it.run(context) }
54
+
55
+ yield_result = yield
56
+
57
+ callbacks[:after].each { |it| it.run(context) }
58
+
59
+ yield_result
60
+ end
61
+ end
62
+
63
+ def self.included(base)
64
+ base.extend(ClassMethods)
65
+ end
66
+
67
+ module ClassMethods
68
+ def hooks ; @__hooks ||= {}; end
69
+ def callback_options ; @__callback_options ||= Hook::DEFAULT_CALLBACK_OPTIONS.dup; end
70
+
71
+ def inherited(subclass)
72
+ subclass.instance_variable_set(:@__hooks, Marshal.load(Marshal.dump(hooks)))
73
+ subclass.instance_variable_set(:@__callback_options, callback_options.dup)
74
+ end
75
+
76
+ # Redefines method providing callbacks calls around it.
77
+ # Defines `before_[name]` and `after_[name]` methods for setting callbacks.
78
+ #
79
+ # @param method [Symbol] the name of the callbackable method.
80
+ # @param name [Symbol] alias for hooked method before_[name] & after_[name] methods.
81
+ def define_hooks_for(method, name: method)
82
+ raise(ArgumentError, "Hook for #{method} are already defined") if hooks.key?(method)
83
+ raise(ArgumentError, "Method #{method} is not defined") unless method_defined?(method)
84
+
85
+ hooks[name] = Hook.new(name)
86
+
87
+ define_singleton_method(:"before_#{name}") do |target, options = {}|
88
+ set_callback(:before, name, target, options)
89
+ end
90
+
91
+ define_singleton_method(:"after_#{name}") do |target, options = {}|
92
+ set_callback(:after, name, target, options)
93
+ end
94
+
95
+ define_method(method) do |*args, &block|
96
+ self.class.hooks[name].run_callbacks(self) do
97
+ super(*args, &block)
98
+ end
99
+ end
100
+ end
101
+
102
+ # Sets a callback for an existing hook'ed method.
103
+ #
104
+ # @param type [Symbol] the type of callback, `:before` or `:after`
105
+ # @param method_name [Symbol] the name of the callbackable method.
106
+ # @param target [Symbol] the name of the callback method.
107
+ # @param options [Hash] the options for the callback.
108
+ # @options options [Symbol] :if the name of the method to check before executing the callback.
109
+ def set_callback(type, method_name, target, options = {})
110
+ raise(ArgumentError, "Hook for :#{method_name} is not defined") unless hooks.key?(method_name)
111
+ raise(ArgumentError, "Hook Callback accepts only #{options.keys} options") if (options.keys - callback_options.keys).any?
112
+
113
+ hooks[method_name].add_callback(type: type, target: target, options: options)
114
+ end
115
+
116
+ # Sets a custom callback option.
117
+ #
118
+ # @param name [Symbol] the name of the option.
119
+ # @yield [*attrs, context:] the block to call.
120
+ # @yieldparam attrs [*] the attributes passed to the option.
121
+ # @yieldparam context [Object] the instance context (optional).
122
+ # @yieldreturn [Boolean].
123
+ def set_callback_options(option)
124
+ name, block = option.first
125
+ callback_options[name] = block
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ # TODO: remove with new ruby-next release
6
+ if RUBY_VERSION < "3.2"
7
+ Data.define_singleton_method(:inherited) do |subclass|
8
+ subclass.instance_variable_set(:@members, members)
9
+ end
10
+ end
11
+
12
+ module ActiveFunctionCore
13
+ module Plugins
14
+ module Hooks
15
+ class Hook < Data.define(:method_name, :callbacks)
16
+ DEFAULT_CALLBACK_OPTIONS = {
17
+ if: ->(v, context:) { context.send(v) if context.respond_to?(v, true) },
18
+ unless: ->(v, context:) { !context.send(v) if context.respond_to?(v, true) }
19
+ }.freeze
20
+ SUPPORTED_CALLBACKS = %i[before after].freeze
21
+
22
+ Callback = Data.define(:options, :target) do
23
+ def run(context)
24
+ raise ArgumentError, "Callback target #{target} is not defined" unless context.respond_to?(target, true)
25
+ raise ArgumentError, ":callback_options is not defined in #{context.class}" unless context.class.respond_to?(:callback_options)
26
+
27
+ context.instance_exec(target, normalized_options(options, context)) do |target, options|
28
+ method(target).call if options.all?(&:call)
29
+ end
30
+ end
31
+
32
+ private def normalized_options(options, context)
33
+ options.map do |option|
34
+ name, arg = option.first
35
+ -> { context.class.callback_options[name].call(arg, context: context) }
36
+ end
37
+ end
38
+ end
39
+
40
+ def initialize(callbacks: SUPPORTED_CALLBACKS.dup, **__kwrest__)
41
+ super(callbacks: callbacks.to_h { [_1, []] }, **__kwrest__)
42
+ end
43
+
44
+ def add_callback(type:, target:, options: {})
45
+ callbacks[type] << Callback[options, target].tap do |callback|
46
+ next unless callbacks[type].map(&:hash).to_set === callback.hash
47
+
48
+ raise(ArgumentError, "Callback already defined")
49
+ end
50
+ end
51
+
52
+ def run_callbacks(context, &block)
53
+ callbacks[:before].each { |it| it.run(context) }
54
+
55
+ yield_result = yield
56
+
57
+ callbacks[:after].each { |it| it.run(context) }
58
+
59
+ yield_result
60
+ end
61
+ end
62
+
63
+ def self.included(base)
64
+ base.extend(ClassMethods)
65
+ end
66
+
67
+ module ClassMethods
68
+ def hooks ; @__hooks ||= {}; end
69
+ def callback_options ; @__callback_options ||= Hook::DEFAULT_CALLBACK_OPTIONS.dup; end
70
+
71
+ def inherited(subclass)
72
+ subclass.instance_variable_set(:@__hooks, Marshal.load(Marshal.dump(hooks)))
73
+ subclass.instance_variable_set(:@__callback_options, callback_options.dup)
74
+ end
75
+
76
+ # Redefines method providing callbacks calls around it.
77
+ # Defines `before_[name]` and `after_[name]` methods for setting callbacks.
78
+ #
79
+ # @param method [Symbol] the name of the callbackable method.
80
+ # @param name [Symbol] alias for hooked method before_[name] & after_[name] methods.
81
+ def define_hooks_for(method, name: method)
82
+ raise(ArgumentError, "Hook for #{method} are already defined") if hooks.key?(method)
83
+ raise(ArgumentError, "Method #{method} is not defined") unless method_defined?(method)
84
+
85
+ hooks[name] = Hook.new(name)
86
+
87
+ define_singleton_method(:"before_#{name}") do |target, options = {}|
88
+ set_callback(:before, name, target, options)
89
+ end
90
+
91
+ define_singleton_method(:"after_#{name}") do |target, options = {}|
92
+ set_callback(:after, name, target, options)
93
+ end
94
+
95
+ define_method(method) do |*args, &block|
96
+ self.class.hooks[name].run_callbacks(self) do
97
+ super(*args, &block)
98
+ end
99
+ end
100
+ end
101
+
102
+ # Sets a callback for an existing hook'ed method.
103
+ #
104
+ # @param type [Symbol] the type of callback, `:before` or `:after`
105
+ # @param method_name [Symbol] the name of the callbackable method.
106
+ # @param target [Symbol] the name of the callback method.
107
+ # @param options [Hash] the options for the callback.
108
+ # @options options [Symbol] :if the name of the method to check before executing the callback.
109
+ def set_callback(type, method_name, target, options = {})
110
+ raise(ArgumentError, "Hook for :#{method_name} is not defined") unless hooks.key?(method_name)
111
+ raise(ArgumentError, "Hook Callback accepts only #{options.keys} options") if (options.keys - callback_options.keys).any?
112
+
113
+ hooks[method_name].add_callback(type: type, target: target, options: options)
114
+ end
115
+
116
+ # Sets a custom callback option.
117
+ #
118
+ # @param name [Symbol] the name of the option.
119
+ # @yield [*attrs, context:] the block to call.
120
+ # @yieldparam attrs [*] the attributes passed to the option.
121
+ # @yieldparam context [Object] the instance context (optional).
122
+ # @yieldreturn [Boolean].
123
+ def set_callback_options(option)
124
+ name, block = option.first
125
+ callback_options[name] = block
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ # TODO: remove with new ruby-next release
6
+ if RUBY_VERSION < "3.2"
7
+ Data.define_singleton_method(:inherited) do |subclass|
8
+ subclass.instance_variable_set(:@members, members)
9
+ end
10
+ end
11
+
12
+ module ActiveFunctionCore
13
+ module Plugins
14
+ module Hooks
15
+ class Hook < Data.define(:method_name, :callbacks)
16
+ DEFAULT_CALLBACK_OPTIONS = {
17
+ if: ->(v, context:) { context.send(v) if context.respond_to?(v, true) },
18
+ unless: ->(v, context:) { !context.send(v) if context.respond_to?(v, true) }
19
+ }.freeze
20
+ SUPPORTED_CALLBACKS = %i[before after].freeze
21
+
22
+ Callback = Data.define(:options, :target) do
23
+ def run(context)
24
+ raise ArgumentError, "Callback target #{target} is not defined" unless context.respond_to?(target, true)
25
+ raise ArgumentError, ":callback_options is not defined in #{context.class}" unless context.class.respond_to?(:callback_options)
26
+
27
+ context.instance_exec(target, normalized_options(options, context)) do |target, options|
28
+ method(target).call if options.all?(&:call)
29
+ end
30
+ end
31
+
32
+ private def normalized_options(options, context)
33
+ options.map do |option|
34
+ name, arg = option.first
35
+ -> { context.class.callback_options[name].call(arg, context: context) }
36
+ end
37
+ end
38
+ end
39
+
40
+ def initialize(callbacks: SUPPORTED_CALLBACKS.dup, **__kwrest__)
41
+ super(callbacks: callbacks.to_h { [_1, []] }, **__kwrest__)
42
+ end
43
+
44
+ def add_callback(type:, target:, options: {})
45
+ callbacks[type] << Callback[options, target].tap do |callback|
46
+ next unless callbacks[type].map(&:hash).to_set === callback.hash
47
+
48
+ raise(ArgumentError, "Callback already defined")
49
+ end
50
+ end
51
+
52
+ def run_callbacks(context, &block)
53
+ callbacks[:before].each { |it| it.run(context) }
54
+
55
+ yield_result = yield
56
+
57
+ callbacks[:after].each { |it| it.run(context) }
58
+
59
+ yield_result
60
+ end
61
+ end
62
+
63
+ def self.included(base)
64
+ base.extend(ClassMethods)
65
+ end
66
+
67
+ module ClassMethods
68
+ def hooks = @__hooks ||= {}
69
+ def callback_options = @__callback_options ||= Hook::DEFAULT_CALLBACK_OPTIONS.dup
70
+
71
+ def inherited(subclass)
72
+ subclass.instance_variable_set(:@__hooks, Marshal.load(Marshal.dump(hooks)))
73
+ subclass.instance_variable_set(:@__callback_options, callback_options.dup)
74
+ end
75
+
76
+ # Redefines method providing callbacks calls around it.
77
+ # Defines `before_[name]` and `after_[name]` methods for setting callbacks.
78
+ #
79
+ # @param method [Symbol] the name of the callbackable method.
80
+ # @param name [Symbol] alias for hooked method before_[name] & after_[name] methods.
81
+ def define_hooks_for(method, name: method)
82
+ raise(ArgumentError, "Hook for #{method} are already defined") if hooks.key?(method)
83
+ raise(ArgumentError, "Method #{method} is not defined") unless method_defined?(method)
84
+
85
+ hooks[name] = Hook.new(name)
86
+
87
+ define_singleton_method(:"before_#{name}") do |target, options = {}|
88
+ set_callback(:before, name, target, options)
89
+ end
90
+
91
+ define_singleton_method(:"after_#{name}") do |target, options = {}|
92
+ set_callback(:after, name, target, options)
93
+ end
94
+
95
+ define_method(method) do |*args, &block|
96
+ self.class.hooks[name].run_callbacks(self) do
97
+ super(*args, &block)
98
+ end
99
+ end
100
+ end
101
+
102
+ # Sets a callback for an existing hook'ed method.
103
+ #
104
+ # @param type [Symbol] the type of callback, `:before` or `:after`
105
+ # @param method_name [Symbol] the name of the callbackable method.
106
+ # @param target [Symbol] the name of the callback method.
107
+ # @param options [Hash] the options for the callback.
108
+ # @options options [Symbol] :if the name of the method to check before executing the callback.
109
+ def set_callback(type, method_name, target, options = {})
110
+ raise(ArgumentError, "Hook for :#{method_name} is not defined") unless hooks.key?(method_name)
111
+ raise(ArgumentError, "Hook Callback accepts only #{options.keys} options") if (options.keys - callback_options.keys).any?
112
+
113
+ hooks[method_name].add_callback(type: type, target: target, options: options)
114
+ end
115
+
116
+ # Sets a custom callback option.
117
+ #
118
+ # @param name [Symbol] the name of the option.
119
+ # @yield [*attrs, context:] the block to call.
120
+ # @yieldparam attrs [*] the attributes passed to the option.
121
+ # @yieldparam context [Object] the instance context (optional).
122
+ # @yieldreturn [Boolean].
123
+ def set_callback_options(option)
124
+ name, block = option.first
125
+ callback_options[name] = block
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ # TODO: remove with new ruby-next release
6
+ if RUBY_VERSION < "3.2"
7
+ Data.define_singleton_method(:inherited) do |subclass|
8
+ subclass.instance_variable_set(:@members, members)
9
+ end
10
+ end
11
+
12
+ module ActiveFunctionCore
13
+ module Plugins
14
+ module Hooks
15
+ class Hook < Data.define(:method_name, :callbacks)
16
+ DEFAULT_CALLBACK_OPTIONS = {
17
+ if: ->(v, context:) { context.send(v) if context.respond_to?(v, true) },
18
+ unless: ->(v, context:) { !context.send(v) if context.respond_to?(v, true) }
19
+ }.freeze
20
+ SUPPORTED_CALLBACKS = %i[before after].freeze
21
+
22
+ Callback = Data.define(:options, :target) do
23
+ def run(context)
24
+ raise ArgumentError, "Callback target #{target} is not defined" unless context.respond_to?(target, true)
25
+ raise ArgumentError, ":callback_options is not defined in #{context.class}" unless context.class.respond_to?(:callback_options)
26
+
27
+ context.instance_exec(target, normalized_options(options, context)) do |target, options|
28
+ method(target).call if options.all?(&:call)
29
+ end
30
+ end
31
+
32
+ private def normalized_options(options, context)
33
+ options.map do |option|
34
+ name, arg = option.first
35
+ -> { context.class.callback_options[name].call(arg, context:) }
36
+ end
37
+ end
38
+ end
39
+
40
+ def initialize(callbacks: SUPPORTED_CALLBACKS.dup, **__kwrest__)
41
+ super(callbacks: callbacks.to_h { [_1, []] }, **__kwrest__)
42
+ end
43
+
44
+ def add_callback(type:, target:, options: {})
45
+ callbacks[type] << Callback[options, target].tap do |callback|
46
+ next unless callbacks[type].map(&:hash).to_set === callback.hash
47
+
48
+ raise(ArgumentError, "Callback already defined")
49
+ end
50
+ end
51
+
52
+ def run_callbacks(context, &block)
53
+ callbacks[:before].each { |it| it.run(context) }
54
+
55
+ yield_result = yield
56
+
57
+ callbacks[:after].each { |it| it.run(context) }
58
+
59
+ yield_result
60
+ end
61
+ end
62
+
63
+ def self.included(base)
64
+ base.extend(ClassMethods)
65
+ end
66
+
67
+ module ClassMethods
68
+ def hooks = @__hooks ||= {}
69
+ def callback_options = @__callback_options ||= Hook::DEFAULT_CALLBACK_OPTIONS.dup
70
+
71
+ def inherited(subclass)
72
+ subclass.instance_variable_set(:@__hooks, Marshal.load(Marshal.dump(hooks)))
73
+ subclass.instance_variable_set(:@__callback_options, callback_options.dup)
74
+ end
75
+
76
+ # Redefines method providing callbacks calls around it.
77
+ # Defines `before_[name]` and `after_[name]` methods for setting callbacks.
78
+ #
79
+ # @param method [Symbol] the name of the callbackable method.
80
+ # @param name [Symbol] alias for hooked method before_[name] & after_[name] methods.
81
+ def define_hooks_for(method, name: method)
82
+ raise(ArgumentError, "Hook for #{method} are already defined") if hooks.key?(method)
83
+ raise(ArgumentError, "Method #{method} is not defined") unless method_defined?(method)
84
+
85
+ hooks[name] = Hook.new(name)
86
+
87
+ define_singleton_method(:"before_#{name}") do |target, options = {}|
88
+ set_callback(:before, name, target, options)
89
+ end
90
+
91
+ define_singleton_method(:"after_#{name}") do |target, options = {}|
92
+ set_callback(:after, name, target, options)
93
+ end
94
+
95
+ define_method(method) do |*args, &block|
96
+ self.class.hooks[name].run_callbacks(self) do
97
+ super(*args, &block)
98
+ end
99
+ end
100
+ end
101
+
102
+ # Sets a callback for an existing hook'ed method.
103
+ #
104
+ # @param type [Symbol] the type of callback, `:before` or `:after`
105
+ # @param method_name [Symbol] the name of the callbackable method.
106
+ # @param target [Symbol] the name of the callback method.
107
+ # @param options [Hash] the options for the callback.
108
+ # @options options [Symbol] :if the name of the method to check before executing the callback.
109
+ def set_callback(type, method_name, target, options = {})
110
+ raise(ArgumentError, "Hook for :#{method_name} is not defined") unless hooks.key?(method_name)
111
+ raise(ArgumentError, "Hook Callback accepts only #{options.keys} options") if (options.keys - callback_options.keys).any?
112
+
113
+ hooks[method_name].add_callback(type:, target:, options:)
114
+ end
115
+
116
+ # Sets a custom callback option.
117
+ #
118
+ # @param name [Symbol] the name of the option.
119
+ # @yield [*attrs, context:] the block to call.
120
+ # @yieldparam attrs [*] the attributes passed to the option.
121
+ # @yieldparam context [Object] the instance context (optional).
122
+ # @yieldreturn [Boolean].
123
+ def set_callback_options(option)
124
+ name, block = option.first
125
+ callback_options[name] = block
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveFunctionCore
4
- VERSION = "0.0.1"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -5,7 +5,9 @@ require "ruby-next/language/setup"
5
5
  RubyNext::Language.setup_gem_load_path(transpile: true)
6
6
 
7
7
  module ActiveFunctionCore
8
- class Error < StandardError; end
8
+ Error = Class.new(StandardError)
9
+
10
+ require "plugins/hooks"
9
11
 
10
12
  require "active_function_core/version"
11
13
  end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ # TODO: remove with new ruby-next release
6
+ if RUBY_VERSION < "3.2"
7
+ Data.define_singleton_method(:inherited) do |subclass|
8
+ subclass.instance_variable_set(:@members, members)
9
+ end
10
+ end
11
+
12
+ module ActiveFunctionCore
13
+ module Plugins
14
+ module Hooks
15
+ class Hook < Data.define(:method_name, :callbacks)
16
+ DEFAULT_CALLBACK_OPTIONS = {
17
+ if: ->(v, context:) { context.send(v) if context.respond_to?(v, true) },
18
+ unless: ->(v, context:) { !context.send(v) if context.respond_to?(v, true) }
19
+ }.freeze
20
+ SUPPORTED_CALLBACKS = %i[before after].freeze
21
+
22
+ Callback = Data.define(:options, :target) do
23
+ def run(context)
24
+ raise ArgumentError, "Callback target #{target} is not defined" unless context.respond_to?(target, true)
25
+ raise ArgumentError, ":callback_options is not defined in #{context.class}" unless context.class.respond_to?(:callback_options)
26
+
27
+ context.instance_exec(target, normalized_options(options, context)) do |target, options|
28
+ method(target).call if options.all?(&:call)
29
+ end
30
+ end
31
+
32
+ private def normalized_options(options, context)
33
+ options.map do |option|
34
+ name, arg = option.first
35
+ -> { context.class.callback_options[name].call(arg, context:) }
36
+ end
37
+ end
38
+ end
39
+
40
+ def initialize(callbacks: SUPPORTED_CALLBACKS.dup, **)
41
+ super(callbacks: callbacks.to_h { [_1, []] }, **)
42
+ end
43
+
44
+ def add_callback(type:, target:, options: {})
45
+ callbacks[type] << Callback[options, target].tap do |callback|
46
+ next unless callbacks[type].map(&:hash).to_set === callback.hash
47
+
48
+ raise(ArgumentError, "Callback already defined")
49
+ end
50
+ end
51
+
52
+ def run_callbacks(context, &block)
53
+ callbacks[:before].each { |it| it.run(context) }
54
+
55
+ yield_result = yield
56
+
57
+ callbacks[:after].each { |it| it.run(context) }
58
+
59
+ yield_result
60
+ end
61
+ end
62
+
63
+ def self.included(base)
64
+ base.extend(ClassMethods)
65
+ end
66
+
67
+ module ClassMethods
68
+ def hooks = @__hooks ||= {}
69
+ def callback_options = @__callback_options ||= Hook::DEFAULT_CALLBACK_OPTIONS.dup
70
+
71
+ def inherited(subclass)
72
+ subclass.instance_variable_set(:@__hooks, Marshal.load(Marshal.dump(hooks)))
73
+ subclass.instance_variable_set(:@__callback_options, callback_options.dup)
74
+ end
75
+
76
+ # Redefines method providing callbacks calls around it.
77
+ # Defines `before_[name]` and `after_[name]` methods for setting callbacks.
78
+ #
79
+ # @param method [Symbol] the name of the callbackable method.
80
+ # @param name [Symbol] alias for hooked method before_[name] & after_[name] methods.
81
+ def define_hooks_for(method, name: method)
82
+ raise(ArgumentError, "Hook for #{method} are already defined") if hooks.key?(method)
83
+ raise(ArgumentError, "Method #{method} is not defined") unless method_defined?(method)
84
+
85
+ hooks[name] = Hook.new(name)
86
+
87
+ define_singleton_method(:"before_#{name}") do |target, options = {}|
88
+ set_callback(:before, name, target, options)
89
+ end
90
+
91
+ define_singleton_method(:"after_#{name}") do |target, options = {}|
92
+ set_callback(:after, name, target, options)
93
+ end
94
+
95
+ define_method(method) do |*args, &block|
96
+ self.class.hooks[name].run_callbacks(self) do
97
+ super(*args, &block)
98
+ end
99
+ end
100
+ end
101
+
102
+ # Sets a callback for an existing hook'ed method.
103
+ #
104
+ # @param type [Symbol] the type of callback, `:before` or `:after`
105
+ # @param method_name [Symbol] the name of the callbackable method.
106
+ # @param target [Symbol] the name of the callback method.
107
+ # @param options [Hash] the options for the callback.
108
+ # @options options [Symbol] :if the name of the method to check before executing the callback.
109
+ def set_callback(type, method_name, target, options = {})
110
+ raise(ArgumentError, "Hook for :#{method_name} is not defined") unless hooks.key?(method_name)
111
+ raise(ArgumentError, "Hook Callback accepts only #{options.keys} options") if (options.keys - callback_options.keys).any?
112
+
113
+ hooks[method_name].add_callback(type:, target:, options:)
114
+ end
115
+
116
+ # Sets a custom callback option.
117
+ #
118
+ # @param name [Symbol] the name of the option.
119
+ # @yield [*attrs, context:] the block to call.
120
+ # @yieldparam attrs [*] the attributes passed to the option.
121
+ # @yieldparam context [Object] the instance context (optional).
122
+ # @yieldreturn [Boolean].
123
+ def set_callback_options(option)
124
+ name, block = option.first
125
+ callback_options[name] = block
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
metadata CHANGED
@@ -1,35 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activefunction-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nerbyk
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-08-20 00:00:00.000000000 Z
11
+ date: 2024-01-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: ruby-next-core
14
+ name: ruby-next
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '0.15'
20
- - - ">="
21
- - !ruby/object:Gem::Version
22
- version: 0.15.3
19
+ version: '1.0'
23
20
  type: :runtime
24
21
  prerelease: false
25
22
  version_requirements: !ruby/object:Gem::Requirement
26
23
  requirements:
27
24
  - - "~>"
28
25
  - !ruby/object:Gem::Version
29
- version: '0.15'
30
- - - ">="
31
- - !ruby/object:Gem::Version
32
- version: 0.15.3
26
+ version: '1.0'
33
27
  description: Provides core functionality, plugins and ruby-next integration for ActiveFunction
34
28
  email:
35
29
  - danil.maximov2000@gmail.com
@@ -40,8 +34,13 @@ files:
40
34
  - CHANGELOG.md
41
35
  - LICENSE.txt
42
36
  - README.md
37
+ - lib/.rbnext/2.7/plugins/hooks.rb
38
+ - lib/.rbnext/3.0/plugins/hooks.rb
39
+ - lib/.rbnext/3.1/plugins/hooks.rb
40
+ - lib/.rbnext/3.2/plugins/hooks.rb
43
41
  - lib/active_function_core.rb
44
42
  - lib/active_function_core/version.rb
43
+ - lib/plugins/hooks.rb
45
44
  - sig/active_function_core.rbs
46
45
  - sig/manifest.yml
47
46
  homepage: https://github.com/DanilMaximov/activefunction