activefunction 0.3.5 → 0.4.1

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