activefunction-core 0.0.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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