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 +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +71 -102
- data/lib/.rbnext/2.7/plugins/hooks.rb +130 -0
- data/lib/.rbnext/3.0/plugins/hooks.rb +130 -0
- data/lib/.rbnext/3.1/plugins/hooks.rb +130 -0
- data/lib/.rbnext/3.2/plugins/hooks.rb +130 -0
- data/lib/active_function_core/version.rb +1 -1
- data/lib/active_function_core.rb +3 -1
- data/lib/plugins/hooks.rb +130 -0
- metadata +10 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0b2cfe13205db0147f98083ea9d83b4f6ee0a1a1baaec08f848bf7a650a6e844
|
4
|
+
data.tar.gz: 136a5559b4c123ba50c384cfaf4beb05a5a9dca14ae398866e9a98a32d72338c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5b2a5470029dc54aab1677f6d08af01d1eaca7673d38003745630a82355e44d810085a66c4f4dbafe50409fb4f2e730e3cacc17384883093943e97169ed4ddfe
|
7
|
+
data.tar.gz: 8865302a689494285e6ffc23ba6197aca98b2ae6ee3049364c8c0084bfeb3a5507df4e5d3d674277037e5b0621b1a42afacad08d94d2604b13c30a7f496af26c
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -1,143 +1,112 @@
|
|
1
|
-
# ActiveFunction
|
1
|
+
# ActiveFunction Core
|
2
2
|
|
3
|
-
|
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
|
-
|
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
|
-
|
11
|
+
## Plugins
|
11
12
|
|
12
|
-
|
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
|
-
|
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
|
-
|
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
|
33
|
-
|
34
|
-
after_action :log_response
|
35
|
-
|
36
|
-
# some action ...
|
20
|
+
class YourClass
|
21
|
+
include ActiveFunction::Core::Plugins::Hooks
|
37
22
|
|
38
|
-
|
23
|
+
define_hooks_for :your_method
|
39
24
|
|
40
|
-
|
41
|
-
|
42
|
-
end
|
25
|
+
before_your_method :do_something_before
|
26
|
+
after_your_method :do_something_after
|
43
27
|
|
44
|
-
def
|
45
|
-
|
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
|
-
|
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
|
-
|
54
|
-
|
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
|
-
|
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
|
-
|
68
|
-
|
69
|
-
end
|
70
|
-
end
|
59
|
+
class YourClass
|
60
|
+
include ActiveFunction::Core::Plugins::Hooks
|
71
61
|
|
72
|
-
|
73
|
-
|
74
|
-
|
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
|
-
|
82
|
-
PostFunction.process(:index, data: { id: 1, name: "Pupa" })
|
83
|
-
```
|
71
|
+
private
|
84
72
|
|
85
|
-
|
86
|
-
|
87
|
-
|
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
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
def
|
108
|
-
|
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
|
-
|
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
|
123
|
-
|
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
|
-
|
95
|
+
define_hooks_for :your_method
|
131
96
|
|
132
|
-
|
97
|
+
before_your_method :do_something_before, only: %[foo bar]
|
133
98
|
|
134
|
-
|
135
|
-
|
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
|
data/lib/active_function_core.rb
CHANGED
@@ -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
|
-
|
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
|
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:
|
11
|
+
date: 2024-01-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name: ruby-next
|
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
|
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
|
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
|