activefunction 0.3.5 → 0.4.1

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.
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_function_core"
4
+ require "active_function/version"
5
+ require "active_function/base"
6
+
7
+ RubyNext::Language.setup_gem_load_path(transpile: true)
8
+
9
+ module ActiveFunction
10
+ class << self
11
+ # Configure ActiveFunction through DSL method calls.
12
+ # Setups {ActiveFunction::Base} with provided internal and custom plugins.
13
+ # Also freezes plugins and {ActiveFunction::Base}.
14
+ #
15
+ # @example
16
+ # ActiveFunction.config do
17
+ # plugin :callbacks
18
+ # end
19
+ #
20
+ # @param block [Proc] class_eval'ed block in ActiveFunction module.
21
+ # @return [void]
22
+ def config(&block)
23
+ class_eval(&block)
24
+ @_plugins.freeze
25
+ self::Base.freeze
26
+ end
27
+
28
+ # List of registered internal plugins.
29
+ def plugins ; @_plugins ||= {}; end
30
+
31
+ # Register internal Symbol'ed plugin.
32
+ #
33
+ # @param [Symbol] symbol name of internal plugin,
34
+ # should match file name in ./lib/active_function/functions/*.rb
35
+ # @param [Module] mod module to register.
36
+ def register_plugin(symbol, mod)
37
+ plugins[symbol] = mod
38
+ end
39
+
40
+ # Add plugin to ActiveFunction::Base.
41
+ #
42
+ # @example
43
+ # ActiveFunction.plugin :callbacks
44
+ # ActiveFunction.plugin CustomPlugin
45
+ #
46
+ # @param [Symbol, Module] mod
47
+ # @return [void]
48
+ def plugin(mod)
49
+ if mod.is_a? Symbol
50
+ begin
51
+ require "active_function/functions/#{mod}"
52
+ mod = plugins.fetch(mod)
53
+ rescue LoadError
54
+ raise ArgumentError, "Unknown plugin #{mod}"
55
+ end
56
+ end
57
+
58
+ self::Base.include(mod)
59
+ end
60
+ end
61
+
62
+ plugin :response
63
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveFunction
4
+ module Functions
5
+ # The only required plugin for {ActiveFunction::Base} to work.
6
+ # Provides a simple {Response} object to manage response details.
7
+ #
8
+ # @example
9
+ # response = Response.new.tap do |r|
10
+ # r.body = "Hello World!"
11
+ # r.headers = {"Content-Type" => "text/plain"}
12
+ # r.commit!
13
+ # end
14
+ #
15
+ # response.performed? # => true
16
+ # response.to_h # => { statusCode: 200, headers: { "Content-Type" => "text/plain" }, body: "Hello World!" }
17
+ module Response
18
+ ActiveFunction.register_plugin :response, self
19
+
20
+ class Response < Struct.new(:status, :headers, :body, :committed)
21
+ # Initializes a new Response instance with default values.
22
+ #
23
+ # @param status [Integer] HTTP status code.
24
+ # @param headers [Hash] HTTP headers.
25
+ # @param body [Object] Response body.
26
+ # @param committed [Boolean] Indicates whether the response has been committed (default is false).
27
+ def initialize(status: 200, headers: {}, body: nil, committed: false) = super(status, headers, body, committed)
28
+
29
+ # Converts the Response instance to a hash for JSON serialization.
30
+ #
31
+ # @return [Hash{statusCode: Integer, headers: Hash, body: Object}]
32
+ def to_h
33
+ {
34
+ statusCode: status,
35
+ headers: headers,
36
+ body: body
37
+ }
38
+ end
39
+
40
+ # Marks the response as committed.
41
+ def commit!
42
+ self.committed = true
43
+ end
44
+
45
+ alias_method :committed?, :committed
46
+ end
47
+ end
48
+ end
49
+ end
@@ -1,20 +1,58 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveFunction
4
- class Base
5
- require "active_function/functions/core"
6
- require "active_function/functions/callbacks"
7
- require "active_function/functions/strong_parameters"
8
- require "active_function/functions/rendering"
9
- require "active_function/functions/response"
10
-
11
- include Functions::Core
12
- include Functions::Callbacks
13
- include Functions::Rendering
14
- include Functions::StrongParameters
15
-
16
- def self.process(action_name, request = {}, response = Functions::Response.new)
17
- new.dispatch(action_name, request, response)
4
+ # Abstract base class with request processing logic.
5
+ class SuperBase
6
+ attr_reader :action_name, :request, :response
7
+
8
+ def initialize(action_name, request, response)
9
+ @action_name = action_name
10
+ @request = request
11
+ @response = response
12
+ end
13
+
14
+ # Executes specified @action_name instance method and returns Hash'ed response object
15
+ def dispatch
16
+ process(action_name)
17
+
18
+ @response.commit! unless performed?
19
+
20
+ @response.to_h
21
+ end
22
+
23
+ def process(action) = public_send(action)
24
+
25
+ private def performed? = @response.committed?
26
+ end
27
+
28
+ # The main base class for defining functions using the ActiveFunction framework.
29
+ # Public methods of this class are considered as actions and be proceeded on {ActiveFunction::Base.process} call.
30
+ #
31
+ # @example
32
+ # class MyFunction < ActiveFunction::Base
33
+ # def index
34
+ # if user = User.find(@request.dig(:data, :user, :id))
35
+ # @response.body = user.to_h
36
+ # else
37
+ # @response.status = 404
38
+ # end
39
+ # end
40
+ # end
41
+ class Base < SuperBase
42
+ Error = Class.new(StandardError)
43
+
44
+ # Processes specified action and returns Hash'ed {ActiveFunction::Functions::Response::Response} object.
45
+ #
46
+ # @example
47
+ # MyFunction.process :index, { data: { user: { id: 1 } } } # => { statusCode: 200, body: { id: 1, name: "Pupa" }, headers: {} }
48
+ #
49
+ # @param [String, Symbol] action_name - name of method to call
50
+ # @param [Hash] request - request parameters.
51
+ # @param [Response] response - Functions::Response response object.
52
+ def self.process(action_name, request = {}, response = Response.new)
53
+ raise ArgumentError, "Action method #{action_name} is not defined" unless method_defined?(action_name)
54
+
55
+ new(action_name, request, response).dispatch
18
56
  end
19
57
  end
20
58
  end
@@ -1,69 +1,69 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveFunction
4
- class MissingCallbackContext < Error
5
- MESSAGE_TEMPLATE = "Missing callback context: %s"
6
-
7
- attr_reader :message
8
-
9
- def initialize(context)
10
- @message = MESSAGE_TEMPLATE % context
11
- end
12
- end
13
-
14
4
  module Functions
5
+ # Setups {before_action} and {after_action} callbacks around {ActiveFunction::SuperBase#process}
6
+ # using {ActiveFunctionCore::Plugins::Hooks}. Also provides {define_hooks_for} and {set_callback_options} for
7
+ # defining custom hooks & options.
8
+ #
9
+ # @example
10
+ # ActiveFunction.plugin :callbacks
11
+ #
12
+ # class MessagingApp < ActiveFunction::Base
13
+ # set_callback_options retries: ->(times, context:) { context.retry if context.retries < times }
14
+ # define_hooks_for :retry
15
+ #
16
+ # after_action :retry, if: :failed?, only: %i[send_message], retries: 3
17
+ # after_retry :increment_retries
18
+ #
19
+ # def send_message
20
+ # @response.status = 200 if SomeApi.send(@request[:message_content]).success?
21
+ # end
22
+ #
23
+ # def retry
24
+ # @response.committed = false
25
+ # process
26
+ # end
27
+ #
28
+ # private def increment_retries = @response.body[:tries] += 1
29
+ # private def failed? = @response.status != 200
30
+ # private def retries = @response.body[:tries] ||= 0
31
+ # end
32
+ #
33
+ # MessagingApp.process(:send_message, { sender_name: "Alice", message_content: "How are you?" })
34
+ # defining custom hooks & options.
15
35
  module Callbacks
16
- def self.included(base)
17
- base.extend(ClassMethods)
18
- end
19
-
20
- private
21
-
22
- def process(*)
23
- _run_callbacks :before
24
-
25
- super
26
-
27
- _run_callbacks :after
28
- end
29
-
30
- def _run_callbacks(type)
31
- self.class.callbacks[type].each do |callback_method, options|
32
- raise MissingCallbackContext, callback_method unless respond_to?(callback_method, true)
33
-
34
- send(callback_method) if _executable?(options)
35
- end
36
- end
36
+ ActiveFunction.register_plugin :callbacks, self
37
37
 
38
- def _executable?(options)
39
- return false if options[:only] && !options[:only].include?(@action_name)
40
- return false if options[:if] && !send(options[:if])
41
- true
38
+ # Setup callbacks around {ActiveFunction::Base#process} method using {ActiveFunctionCore::Plugins::Hooks}.
39
+ # Also provides :only option for filtering callbacks by action name.
40
+ def self.included(base)
41
+ base.include ActiveFunctionCore::Plugins::Hooks
42
+ base.define_hooks_for :process, name: :action
43
+ base.set_callback_options only: ->(args, context:) { args.to_set === context.action_name }
42
44
  end
43
45
 
44
- module ClassMethods
45
- def inherited(subclass)
46
- subclass.instance_variable_set(:@__callbacks, @__callbacks)
47
- end
48
-
49
- def before_action(method, options = {})
50
- set_callback :before, method, options
51
- end
52
-
53
- def after_action(method, options = {})
54
- set_callback :after, method, options
55
- end
56
-
57
- def set_callback(type, method, options = {})
58
- callbacks[type][method] = options
59
- end
60
-
61
- def callbacks
62
- @__callbacks ||= {before: {}, after: {}}
63
-
64
- @__callbacks
65
- end
66
- end
46
+ # @!method before_action(target, options)
47
+ # @param [Symbol, String] target - method name to call
48
+ # @option options [Symbol, String] :if - method name to check before executing the callback.
49
+ # @option options [Symbol, String] :unless - method name to check before executing the callback.
50
+ # @option options [Array<Symbol, String>] :only - array of action names.
51
+ # @see ActiveFunctionCore::Plugins::Hooks::ClassMethods#set_callback
52
+ # @!method after_action(target, options)
53
+ # @param [Symbol, String] target - method name to call
54
+ # @option options [Symbol, String] :if - method name to check before executing the callback.
55
+ # @option options [Symbol, String] :unless - method name to check before executing the callback.
56
+ # @option options [Array<Symbol, String>] :only - array of action names.
57
+ # @see ActiveFunctionCore::Plugins::Hooks::ClassMethods#set_callback
58
+
59
+ # @!method set_callback(type, hook_name, target, options)
60
+ # @see ActiveFunctionCore::Plugins::Hooks::ClassMethods#set_callback
61
+
62
+ # @!method define_hooks_for(method_name, name: method_name)
63
+ # @see ActiveFunctionCore::Plugins::Hooks::ClassMethods#define_hooks_for
64
+
65
+ # @!method set_callback_options(options)
66
+ # @see ActiveFunctionCore::Plugins::Hooks::ClassMethods#set_callback_options
67
67
  end
68
68
  end
69
69
  end
@@ -3,20 +3,47 @@
3
3
  require "json"
4
4
 
5
5
  module ActiveFunction
6
- class DoubleRenderError < Error
7
- MESSAGE_TEMPLATE = "#render was called multiple times in action: %s"
6
+ module Functions
7
+ # Allows manipulations with {ActiveFunction::SuperBase#response} via {render} instance method.
8
+ #
9
+ # @example
10
+ # require "active_function"
11
+ #
12
+ # ActiveFunction.config do
13
+ # plugin :rendering
14
+ # end
15
+ #
16
+ # class PostsFunction < ActiveFunction::Base
17
+ # def index
18
+ # render json: {id: 1, name: "Pupa"}, status: 200, head: {"Some-Header" => "Some-Value"}
19
+ # end
20
+ # end
21
+ #
22
+ # PostFunction.process(:index) # => { :statusCode=>200, :headers=> {"Content-Type"=>"application/json", "Some-Header" => "Some-Value"}, :body=>"{\"id\":1,\"name\":\"Pupa\"}" }
23
+ module Rendering
24
+ ActiveFunction.register_plugin :rendering, self
8
25
 
9
- attr_reader :message
26
+ Error = Class.new(StandardError)
10
27
 
11
- def initialize(context)
12
- @message = MESSAGE_TEMPLATE % context
13
- end
14
- end
28
+ class DoubleRenderError < Error
29
+ MESSAGE_TEMPLATE = "#render was called multiple times in action: %s"
30
+
31
+ attr_reader :message
32
+
33
+ def initialize(context)
34
+ @message = MESSAGE_TEMPLATE % context
35
+ end
36
+ end
15
37
 
16
- module Functions
17
- module Rendering
18
38
  DEFAULT_HEADER = {"Content-Type" => "application/json"}.freeze
19
39
 
40
+ # Render JSON response.
41
+ #
42
+ # @param status [Integer] HTTP status code (default is 200).
43
+ # @param json [Hash] JSON data to be rendered (default is an empty hash).
44
+ # @param head [Hash] Additional headers to be included in the response (default is an empty hash).
45
+ #
46
+ # @raise [DoubleRenderError] Raised if #render is called multiple times in the same action.
20
47
  def render(status: 200, json: {}, head: {})
21
48
  raise DoubleRenderError, @action_name if performed?
22
49
 
@@ -2,29 +2,48 @@
2
2
 
3
3
  module ActiveFunction
4
4
  module Functions
5
- class Response
6
- attr_accessor :status, :headers, :body
5
+ # The only required plugin for {ActiveFunction::Base} to work.
6
+ # Provides a simple {Response} object to manage response details.
7
+ #
8
+ # @example
9
+ # response = Response.new.tap do |r|
10
+ # r.body = "Hello World!"
11
+ # r.headers = {"Content-Type" => "text/plain"}
12
+ # r.commit!
13
+ # end
14
+ #
15
+ # response.performed? # => true
16
+ # response.to_h # => { statusCode: 200, headers: { "Content-Type" => "text/plain" }, body: "Hello World!" }
17
+ module Response
18
+ ActiveFunction.register_plugin :response, self
7
19
 
8
- def initialize(status: 200, headers: {}, body: nil)
9
- @status = status
10
- @headers = headers
11
- @body = body
12
- @committed = false
13
- end
20
+ class Response < Struct.new(:status, :headers, :body, :committed)
21
+ # Initializes a new Response instance with default values.
22
+ #
23
+ # @param status [Integer] HTTP status code.
24
+ # @param headers [Hash] HTTP headers.
25
+ # @param body [Object] Response body.
26
+ # @param committed [Boolean] Indicates whether the response has been committed (default is false).
27
+ def initialize(status: 200, headers: {}, body: nil, committed: false) = super(status, headers, body, committed)
14
28
 
15
- def to_h
16
- {
17
- statusCode: status,
18
- headers: headers,
19
- body: body
20
- }
21
- end
29
+ # Converts the Response instance to a hash for JSON serialization.
30
+ #
31
+ # @return [Hash{statusCode: Integer, headers: Hash, body: Object}]
32
+ def to_h
33
+ {
34
+ statusCode: status,
35
+ headers:,
36
+ body:
37
+ }
38
+ end
22
39
 
23
- def commit!
24
- @committed = true
25
- end
40
+ # Marks the response as committed.
41
+ def commit!
42
+ self.committed = true
43
+ end
26
44
 
27
- def committed? = @committed
45
+ alias_method :committed?, :committed
46
+ end
28
47
  end
29
48
  end
30
49
  end
@@ -3,54 +3,80 @@
3
3
  require "forwardable"
4
4
 
5
5
  module ActiveFunction
6
- class ParameterMissingError < Error
7
- MESSAGE_TEMPLATE = "Missing parameter: %s"
8
-
9
- attr_reader :message
10
-
11
- def initialize(param)
12
- MESSAGE_TEMPLATE % param
13
- end
14
- end
6
+ module Functions
7
+ # Allows manipulations with {ActiveFunction::SuperBase#request} via {params} instance method and {Parameters} object.
8
+ #
9
+ # @example
10
+ # require "active_function"
11
+ #
12
+ # ActiveFunction.config do
13
+ # plugin :strong_parameters
14
+ # end
15
+ #
16
+ # class PostsFunction < ActiveFunction::Base
17
+ # def index
18
+ # @response.body = permitted_params
19
+ # end
20
+ #
21
+ # def permitted_params
22
+ # params.require(:data).permit(:id, :name).to_h
23
+ # end
24
+ # end
25
+ #
26
+ # PostsFunction.process(:index, data: { id: 1, name: "Pupa" })
27
+ module StrongParameters
28
+ ActiveFunction.register_plugin :strong_parameters, self
15
29
 
16
- class UnpermittedParameterError < Error
17
- MESSAGE_TEMPLATE = "Unpermitted parameter: %s"
30
+ Error = Class.new(StandardError)
31
+ # The Parameters class encapsulates the parameter handling logic.
32
+ class Parameters < Data.define(:params, :permitted)
33
+ class ParameterMissingError < Error
34
+ MESSAGE_TEMPLATE = "Missing parameter: %s"
18
35
 
19
- attr_reader :message
36
+ attr_reader :message
20
37
 
21
- def initialize(param)
22
- MESSAGE_TEMPLATE % param
23
- end
24
- end
38
+ def initialize(param)
39
+ MESSAGE_TEMPLATE % param
40
+ end
41
+ end
25
42
 
26
- module Functions
27
- module StrongParameters
28
- def params
29
- @_params ||= Parameters.new(@request)
30
- end
43
+ class UnpermittedParameterError < Error
44
+ MESSAGE_TEMPLATE = "Unpermitted parameter: %s"
31
45
 
32
- class Parameters
33
- extend Forwardable
34
- def_delegators :@parameters, :each, :map
35
- include Enumerable
46
+ attr_reader :message
36
47
 
37
- def initialize(parameters, permitted: false)
38
- @parameters = parameters
39
- @permitted = permitted
48
+ def initialize(param)
49
+ MESSAGE_TEMPLATE % param
50
+ end
40
51
  end
41
52
 
53
+ protected :params
54
+
55
+ # Allows access to parameters by key.
56
+ #
57
+ # @param attribute [Symbol] The key of the parameter.
58
+ # @return [Parameters, Object] The value of the parameter.
42
59
  def [](attribute)
43
- nested_attribute(parameters[attribute])
60
+ nested_attribute(params[attribute])
44
61
  end
45
62
 
63
+ # Requires the presence of a specific parameter.
64
+ #
65
+ # @param attribute [Symbol] The key of the required parameter.
66
+ # @return [Parameters, Object] The value of the required parameter.
67
+ # @raise [ParameterMissingError] if the required parameter is missing.
46
68
  def require(attribute)
47
- value = self[attribute]
48
-
49
- raise ParameterMissingError, attribute if value.nil?
50
-
51
- value
69
+ if (value = self[attribute])
70
+ value
71
+ else
72
+ raise ParameterMissingError, attribute
73
+ end
52
74
  end
53
75
 
76
+ # Specifies the allowed parameters.
77
+ #
78
+ # @param attributes [Array<Symbol, Hash<Symbol, Array<Symbol>>>] The attributes to permit.
79
+ # @return [Parameters] A new instance with permitted parameters.
54
80
  def permit(*attributes)
55
81
  pparams = {}
56
82
 
@@ -60,28 +86,40 @@ module ActiveFunction
60
86
  pparams[k] = process_nested(self[k], :permit, v)
61
87
  end
62
88
  else
63
- next unless parameters.key?(attribute)
89
+ next unless params.key?(attribute)
64
90
 
65
91
  pparams[attribute] = self[attribute]
66
92
  end
67
93
  end
68
94
 
69
- Parameters.new(pparams, permitted: true)
95
+ with(params: pparams, permitted: true)
70
96
  end
71
97
 
98
+ # Converts parameters to a hash.
99
+ #
100
+ # @return [Hash] The hash representation of the parameters.
101
+ # @raise [UnpermittedParameterError] if any parameters are unpermitted.
72
102
  def to_h
73
- raise UnpermittedParameterError, parameters.keys unless @permitted
103
+ raise UnpermittedParameterError, params.keys unless permitted
74
104
 
75
- parameters.transform_values { process_nested(_1, :to_h) }
105
+ params.transform_values { process_nested(_1, :to_h) }
106
+ end
107
+
108
+ def hash
109
+ @attributes.to_h.hash
110
+ end
111
+
112
+ def with(params:, permitted: false)
113
+ self.class.new(params, permitted)
76
114
  end
77
115
 
78
116
  private
79
117
 
80
118
  def nested_attribute(attribute)
81
119
  if attribute.is_a? Hash
82
- Parameters.new(attribute)
120
+ with(params: attribute)
83
121
  elsif attribute.is_a?(Array) && attribute[0].is_a?(Hash)
84
- attribute.map { Parameters.new(_1) }
122
+ attribute.map { |it| with(params: it) }
85
123
  else
86
124
  attribute
87
125
  end
@@ -96,8 +134,13 @@ module ActiveFunction
96
134
  attribute
97
135
  end
98
136
  end
137
+ end
99
138
 
100
- attr_reader :parameters
139
+ # Return params object with {ActiveFunction::SuperBase#request}.
140
+ #
141
+ # @return [Parameters] instance of {Parameters} class.
142
+ def params
143
+ @_params ||= Parameters.new(@request, false)
101
144
  end
102
145
  end
103
146
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveFunction
4
- VERSION = "0.3.5"
4
+ VERSION = "0.4.1"
5
5
  end