activefunction-core 0.1.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: 7598948c858b823027e2c33a190a17920f466ce55ea96138bc9a01ac75899e74
4
- data.tar.gz: 57b9189b07f384420404db190dcea2cd2b7aa40e714f732713f252fddb34590d
3
+ metadata.gz: 0b2cfe13205db0147f98083ea9d83b4f6ee0a1a1baaec08f848bf7a650a6e844
4
+ data.tar.gz: 136a5559b4c123ba50c384cfaf4beb05a5a9dca14ae398866e9a98a32d72338c
5
5
  SHA512:
6
- metadata.gz: 10c87d5b48dcbb1fc3e0aa0711d4dcc4125f6210bbd0b1f96d081e4aadb6bf0feb6028abc0161fa69ca3fd380b5001ec044bc833cc83f5da9479772da16ad133
7
- data.tar.gz: 124281e35afce65ca63ca07a4e80ac32167b97104f2c22ab70578fdf280989ef75fc1131dbeef003991857f1462dd0f9eb87b5c00197080573c1cd71ed3a7f09
6
+ metadata.gz: 5b2a5470029dc54aab1677f6d08af01d1eaca7673d38003745630a82355e44d810085a66c4f4dbafe50409fb4f2e730e3cacc17384883093943e97169ed4ddfe
7
+ data.tar.gz: 8865302a689494285e6ffc23ba6197aca98b2ae6ee3049364c8c0084bfeb3a5507df4e5d3d674277037e5b0621b1a42afacad08d94d2604b13c30a7f496af26c
data/CHANGELOG.md CHANGED
@@ -5,3 +5,9 @@
5
5
  ## [0.1.1]
6
6
 
7
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
@@ -1,136 +1,128 @@
1
1
  # frozen_string_literal: true
2
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
+
3
12
  module ActiveFunctionCore
4
13
  module Plugins
5
14
  module Hooks
6
- class MissingCallbackContext < Error
7
- MESSAGE_TEMPLATE = "Missing callback context: %s"
8
-
9
- attr_reader :message
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
10
31
 
11
- def initialize(context)
12
- @message = MESSAGE_TEMPLATE % context
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
13
38
  end
14
- end
15
39
 
16
- class MissingHookableMethod < Error
17
- MESSAGE_TEMPLATE = "Method %s is not defined"
18
-
19
- attr_reader :message
20
-
21
- def initialize(method_name)
22
- @message = MESSAGE_TEMPLATE % method_name
40
+ def initialize(callbacks: SUPPORTED_CALLBACKS.dup, **__kwrest__)
41
+ super(callbacks: callbacks.to_h { [_1, []] }, **__kwrest__)
23
42
  end
24
- end
25
43
 
26
- def self.included(base)
27
- base.extend(ClassMethods)
28
- base.include(InstanceMethods)
29
- end
30
-
31
- module InstanceMethods
32
- private
33
-
34
- def with_callbacks(method_name, &block)
35
- callbacks = _callbacks(method_name)
36
-
37
- _run_callbacks(callbacks.before)
38
-
39
- result = yield
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
40
47
 
41
- _run_callbacks(callbacks.after)
42
-
43
- result
44
- end
45
-
46
- def _callbacks(method_name)
47
- self.class.hooks[method_name].callbacks
48
+ raise(ArgumentError, "Callback already defined")
49
+ end
48
50
  end
49
51
 
50
- def _run_callbacks(callbacks)
51
- callbacks.each do |callback|
52
- raise(MissingCallbackContext, callback) unless respond_to?(callback.target, true)
52
+ def run_callbacks(context, &block)
53
+ callbacks[:before].each { |it| it.run(context) }
53
54
 
54
- send(callback.target) if _executable?(callback.options)
55
- end
56
- end
55
+ yield_result = yield
57
56
 
58
- def _executable?(options)
59
- return false if options[:if] && !send(options[:if])
57
+ callbacks[:after].each { |it| it.run(context) }
60
58
 
61
- true
59
+ yield_result
62
60
  end
63
61
  end
64
62
 
63
+ def self.included(base)
64
+ base.extend(ClassMethods)
65
+ end
66
+
65
67
  module ClassMethods
66
- TYPES = %i[before after].freeze
67
- # rubocop:disable Lint/ConstantDefinitionInBlock
68
- Hooks = Data.define(:hooks) do
69
- def initialize(hooks: {}) ; super; end
70
- def deep_dup ; Marshal.load(Marshal.dump(self)); end
71
- def all ; hooks; end
72
- def [](method_name) ; hooks[method_name]; end
73
-
74
- def add_hook(method_name)
75
- hooks[method_name] = Hook[method_name]
76
- end
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)
77
74
  end
78
75
 
79
- Hook = Data.define(:method_name, :callbacks) do
80
- Callbacks = Data.define(:before, :after) do
81
- def initialize(before: [], after: []) ; super; end
82
- def [](type) ; public_send(type); end
83
- end
84
- Callback = Data.define(:target, :options)
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)
85
84
 
86
- def initialize(method_name:, callbacks: Callbacks.new) ; super; end
87
- def hashes(type) ; Set.new callbacks[type].map(&:hash); end
85
+ hooks[name] = Hook.new(name)
88
86
 
89
- def add_callback(type:, target:, options: {})
90
- new_callbacks = Callback[target, options]
91
- callbacks[type] << new_callbacks unless hashes(type) === new_callbacks.hash
87
+ define_singleton_method(:"before_#{name}") do |target, options = {}|
88
+ set_callback(:before, name, target, options)
92
89
  end
93
- end
94
- # rubocop:enable Lint/ConstantDefinitionInBlock
95
-
96
- def define_hooks_for(*method_names)
97
- method_names.each do |method_name|
98
- override_hookable_method(method_name)
99
90
 
100
- hooks.add_hook(method_name)
91
+ define_singleton_method(:"after_#{name}") do |target, options = {}|
92
+ set_callback(:after, name, target, options)
93
+ end
101
94
 
102
- define_callback_methods_for(method_name)
95
+ define_method(method) do |*args, &block|
96
+ self.class.hooks[name].run_callbacks(self) do
97
+ super(*args, &block)
98
+ end
103
99
  end
104
100
  end
105
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.
106
109
  def set_callback(type, method_name, target, options = {})
107
- hooks[method_name].add_callback(type: type, target: target, options: options)
108
- end
109
-
110
- def hooks
111
- @__hooks ||= Hooks.new
112
- end
113
-
114
- private
115
-
116
- def inherited(subclass)
117
- subclass.instance_variable_set(:@__hooks, @__hooks.deep_dup)
118
- end
119
-
120
- def override_hookable_method(method_name)
121
- raise(MissingHookableMethod, method_name) unless method_defined?(method_name)
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?
122
112
 
123
- define_method(method_name) do |*args|
124
- with_callbacks(method_name) { super(*args) }
125
- end
113
+ hooks[method_name].add_callback(type: type, target: target, options: options)
126
114
  end
127
115
 
128
- def define_callback_methods_for(method_name)
129
- TYPES.each do |type|
130
- define_singleton_method("#{type}_#{method_name}") do |target, options = {}|
131
- set_callback(type, method_name, target, options)
132
- end
133
- end
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
134
126
  end
135
127
  end
136
128
  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.1.1"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -5,7 +5,7 @@ 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
9
 
10
10
  require "plugins/hooks"
11
11
 
data/lib/plugins/hooks.rb CHANGED
@@ -1,136 +1,128 @@
1
1
  # frozen_string_literal: true
2
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
+
3
12
  module ActiveFunctionCore
4
13
  module Plugins
5
14
  module Hooks
6
- class MissingCallbackContext < Error
7
- MESSAGE_TEMPLATE = "Missing callback context: %s"
8
-
9
- attr_reader :message
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
10
31
 
11
- def initialize(context)
12
- @message = MESSAGE_TEMPLATE % context
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
13
38
  end
14
- end
15
39
 
16
- class MissingHookableMethod < Error
17
- MESSAGE_TEMPLATE = "Method %s is not defined"
18
-
19
- attr_reader :message
20
-
21
- def initialize(method_name)
22
- @message = MESSAGE_TEMPLATE % method_name
40
+ def initialize(callbacks: SUPPORTED_CALLBACKS.dup, **)
41
+ super(callbacks: callbacks.to_h { [_1, []] }, **)
23
42
  end
24
- end
25
43
 
26
- def self.included(base)
27
- base.extend(ClassMethods)
28
- base.include(InstanceMethods)
29
- end
30
-
31
- module InstanceMethods
32
- private
33
-
34
- def with_callbacks(method_name, &block)
35
- callbacks = _callbacks(method_name)
36
-
37
- _run_callbacks(callbacks.before)
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
38
47
 
39
- result = yield
40
-
41
- _run_callbacks(callbacks.after)
42
-
43
- result
48
+ raise(ArgumentError, "Callback already defined")
49
+ end
44
50
  end
45
51
 
46
- def _callbacks(method_name)
47
- self.class.hooks[method_name].callbacks
48
- end
52
+ def run_callbacks(context, &block)
53
+ callbacks[:before].each { |it| it.run(context) }
49
54
 
50
- def _run_callbacks(callbacks)
51
- callbacks.each do |callback|
52
- raise(MissingCallbackContext, callback) unless respond_to?(callback.target, true)
55
+ yield_result = yield
53
56
 
54
- send(callback.target) if _executable?(callback.options)
55
- end
56
- end
57
+ callbacks[:after].each { |it| it.run(context) }
57
58
 
58
- def _executable?(options)
59
- return false if options[:if] && !send(options[:if])
60
-
61
- true
59
+ yield_result
62
60
  end
63
61
  end
64
62
 
63
+ def self.included(base)
64
+ base.extend(ClassMethods)
65
+ end
66
+
65
67
  module ClassMethods
66
- TYPES = %i[before after].freeze
67
- # rubocop:disable Lint/ConstantDefinitionInBlock
68
- Hooks = Data.define(:hooks) do
69
- def initialize(hooks: {}) = super
70
- def deep_dup = Marshal.load(Marshal.dump(self))
71
- def all = hooks
72
- def [](method_name) = hooks[method_name]
73
-
74
- def add_hook(method_name)
75
- hooks[method_name] = Hook[method_name]
76
- end
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)
77
74
  end
78
75
 
79
- Hook = Data.define(:method_name, :callbacks) do
80
- Callbacks = Data.define(:before, :after) do
81
- def initialize(before: [], after: []) = super
82
- def [](type) = public_send(type)
83
- end
84
- Callback = Data.define(:target, :options)
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)
85
84
 
86
- def initialize(method_name:, callbacks: Callbacks.new) = super
87
- def hashes(type) = Set.new callbacks[type].map(&:hash)
85
+ hooks[name] = Hook.new(name)
88
86
 
89
- def add_callback(type:, target:, options: {})
90
- new_callbacks = Callback[target, options]
91
- callbacks[type] << new_callbacks unless hashes(type) === new_callbacks.hash
87
+ define_singleton_method(:"before_#{name}") do |target, options = {}|
88
+ set_callback(:before, name, target, options)
92
89
  end
93
- end
94
- # rubocop:enable Lint/ConstantDefinitionInBlock
95
-
96
- def define_hooks_for(*method_names)
97
- method_names.each do |method_name|
98
- override_hookable_method(method_name)
99
90
 
100
- hooks.add_hook(method_name)
91
+ define_singleton_method(:"after_#{name}") do |target, options = {}|
92
+ set_callback(:after, name, target, options)
93
+ end
101
94
 
102
- define_callback_methods_for(method_name)
95
+ define_method(method) do |*args, &block|
96
+ self.class.hooks[name].run_callbacks(self) do
97
+ super(*args, &block)
98
+ end
103
99
  end
104
100
  end
105
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.
106
109
  def set_callback(type, method_name, target, options = {})
107
- hooks[method_name].add_callback(type: type, target: target, options: options)
108
- end
109
-
110
- def hooks
111
- @__hooks ||= Hooks.new
112
- end
113
-
114
- private
115
-
116
- def inherited(subclass)
117
- subclass.instance_variable_set(:@__hooks, @__hooks.deep_dup)
118
- end
119
-
120
- def override_hookable_method(method_name)
121
- raise(MissingHookableMethod, method_name) unless method_defined?(method_name)
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?
122
112
 
123
- define_method(method_name) do |*args|
124
- with_callbacks(method_name) { super(*args) }
125
- end
113
+ hooks[method_name].add_callback(type:, target:, options:)
126
114
  end
127
115
 
128
- def define_callback_methods_for(method_name)
129
- TYPES.each do |type|
130
- define_singleton_method("#{type}_#{method_name}") do |target, options = {}|
131
- set_callback(type, method_name, target, options)
132
- end
133
- end
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
134
126
  end
135
127
  end
136
128
  end
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activefunction-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.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-12-27 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: 1.0.0
19
+ version: '1.0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ">="
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 1.0.0
26
+ version: '1.0'
27
27
  description: Provides core functionality, plugins and ruby-next integration for ActiveFunction
28
28
  email:
29
29
  - danil.maximov2000@gmail.com
@@ -34,7 +34,10 @@ files:
34
34
  - CHANGELOG.md
35
35
  - LICENSE.txt
36
36
  - README.md
37
+ - lib/.rbnext/2.7/plugins/hooks.rb
37
38
  - lib/.rbnext/3.0/plugins/hooks.rb
39
+ - lib/.rbnext/3.1/plugins/hooks.rb
40
+ - lib/.rbnext/3.2/plugins/hooks.rb
38
41
  - lib/active_function_core.rb
39
42
  - lib/active_function_core/version.rb
40
43
  - lib/plugins/hooks.rb