activefunction 0.3.5 → 0.4.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: 291381221a57e279bfad4dddf88cbeb3695a83e2b4882960b1255304e01a78db
4
- data.tar.gz: 8a2a8d432700fff63717b76198bc032986bc2c6656e7eb9dbf034f78552bcb29
3
+ metadata.gz: 4f2bbae1e08a02af0dbabf4b235ece3823d17aec26e76b24ad7f4b514ea8526c
4
+ data.tar.gz: 6097f177bb4b5dff0583c21c40d45c67aeb4bd81eb6551b52e59c440b3e5e027
5
5
  SHA512:
6
- metadata.gz: 70f3d994a4703b373f91a4df21e0b02a424f0a049a391caf04558ffbcebd80546a3f6683fce5c5a66111336d0827aaa2049ce4b5fbafa64fa7720e13278cf2b7
7
- data.tar.gz: fc0bd5b0c69e64147e7d1729c179c5ef44a931733b9b71b3adec4aa623a7f01f50d70553c7bd58ade6080c2f4e8dab52d2716ebea0b042556296a17d61276e0c
6
+ metadata.gz: 51cfd314a65879813c1e8c8c23ea80b32621a1479a5c4ea09c789557ddcc80b05b29af2d84181fec90984c4137f8cc4735bd24e76a6eb2e38d81cfa543605ea7
7
+ data.tar.gz: 2ea0d89e66aa2a3c89f3b011a84bed5628a9e2e1de78c506f24d93393ab389922581ba1429509c3f8e30883a6788216069078aec3d38cd692c6eda1314b6a69f
data/CHANGELOG.md CHANGED
@@ -46,4 +46,11 @@
46
46
 
47
47
  - Start gem restructuring for modularizaition
48
48
  - Introduce `activefunction-core` gem with RubyNext integration
49
-
49
+
50
+ # [0.4.0] - 2024-01-11
51
+
52
+ - Replace `ActiveFunction::Functions::Callbacks` with `ActiveFunctionCore::Plugins::Hooks`
53
+ - Introduce Plugin system
54
+ - Global refactoring
55
+
56
+
data/README.md CHANGED
@@ -1,144 +1,213 @@
1
1
  # ActiveFunction
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
+ Playground gem for Ruby 3.2+ features and more.
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
+ Collection of gems designed to be used with FaaS (Function as a Service) computing instances. Inspired by aws-sdk v3 gem structure & rails/activesupport.
6
6
 
7
+ ## Features
7
8
 
9
+ - **Ruby Version Compatibility:** Implemented with most of ruby 3.2+ features, BUT supports Ruby versions >= 2.6 through the use of the [RubyNext](https://github.com/ruby-next/ruby-next) transpiler. (CI'ed)
10
+ - **Type Safety:** Achieves type safety through the use of RBS and [Steep](https://github.com/soutaro/steep). (CI'ed)
11
+ - Note: disabled due to the presence of Ruby::UnsupportedSyntax errors.
12
+ - **Plugins System:** Provides a simple Plugin system (inspired by [Polishing Ruby Programming by Jeremy Evans](https://github.com/PacktPublishing/Polished-Ruby-Programming)) to load gem plugins as well as self-defined plugins.
13
+ - **Gem Collection** Provides a collection of gems designed to be used within ActiveFunction or standalone.
8
14
 
9
- ## A Short Example
15
+ # Gems
16
+
17
+ - [activefunction](/) - Main gem, provides rails/action-controller like API with callbacks, strong parameters and rendering under plugins.
18
+ - [activefunction-core](/gems/activefunction-core/README.md) - Provides RubyNext integration and External Standalone Plugins
19
+ - activefunction-orm - WIP (ORM around AWS PartiQL)
20
+
21
+ ## Quick Start
10
22
 
11
23
  Here's a simple example of a function that uses ActiveFunction:
12
24
 
13
25
  ```ruby
14
- require 'active_function'
26
+ require "active_function"
15
27
 
16
28
  class AppFunction < ActiveFunction::Base
17
- def index
18
- render json: SomeTable.all
19
- end
29
+ def index
30
+ response_with_error if @request[:data].nil?
31
+
32
+ return if performed?
33
+
34
+ @response.status = 200
35
+ @response.headers = {"Content-Type" => "application/json"}
36
+ @response.body = @request[:data]
37
+ end
38
+
39
+ private def response_with_error
40
+ @response.status = 400
41
+ @response.commit!
42
+ end
20
43
  end
44
+
45
+ AppFunction.process(:index, {data: {id: 1}}) # => { :statusCode=>200, :headers=> {"Content-Type"=>"application/json"}, :body=>{id: 1}"}
21
46
  ```
22
47
 
23
- Use `#process` method to proceed the request:
48
+ ## Plugins
24
49
 
25
- ```ruby
26
- AppFunction.process(:index) # processes index action of AppFunction instance
27
- ```
28
- Also check extended [example](https://github.com/DanilMaximov/activefunction/tree/master/active_function_example)
29
- ## Callbacks
30
- ActiveFunction supports simple callbacks `:before` and `:after` which runs around provided action in `#process`.
50
+ ActiveFunction supports plugins which can be loaded by `ActiveFunction.config` method. Currently, there are 3 plugins available:
51
+ - [:callbacks](#callbacks) - provides `:before_action` and `:after_action` callbacks with `:if`, `:unless` & `:only` options.
52
+ - [:strong_parameters](#strong-parameters) - provides strong parameters support via `#params` instance method.
53
+ - [:rendering](#rendering) - provides rendering support via `#render` instance method.
54
+
55
+ ## Configuration
56
+
57
+ To configure ActiveFunction with plugins, use the `ActiveFunction.config` method.
31
58
 
32
59
  ```ruby
33
- class AppFunction < ActiveFunction::Base
34
- before_action :set_user
35
- after_action :log_response
36
-
37
- # some action ...
60
+ # config/initializers/active_function.rb
61
+ require "active_function"
38
62
 
39
- private
63
+ ActiveFunction.config do
64
+ plugin :callbacks
65
+ plugin :strong_parameters
66
+ plugin :rendering
67
+ end
68
+ ```
40
69
 
41
- def set_user
42
- @user = User.first
43
- end
70
+ ```ruby
71
+ # app/functions/app_function.rb
72
+ class AppFunction < ActiveFunction::Base
73
+ before_action :parse_user_data
44
74
 
45
- def log_response
46
- Logger.info @response
75
+ def index
76
+ render json: @user_data
47
77
  end
78
+
79
+ private def parse_user_data = @user_data = params.require(:data).permit(:id, :name).to_h
48
80
  end
49
81
  ```
50
82
 
51
- Callbacks also can be user with `only: Array[Symbol]` and `if: Symbol` options.
83
+ Use `#process` method to proceed the request:
52
84
 
53
85
  ```ruby
54
- class AppFunction < ActiveFunction::Base
55
- before_action :set_user, only: %i[show update destroy], if: :request_valid?
56
-
57
- # some actions ...
58
-
59
- private def request_valid? = true
60
- end
86
+ # handler.rb
87
+ AppFunction.process(:index, {data: { id: 1, name: 2}}) # => { :statusCode=>200, :headers=> {"Content-Type"=>"application/json"}, :body=>"{\"id\":1,\"name\":2}"}
61
88
  ```
62
89
 
63
- Callbacks are inheritable so all callbacks calls will be inherited from base class
90
+
91
+ ## Callbacks
92
+
93
+ Simple callbacks engined by [ActiveFunctionCore::Plugins::Hooks](https://github.com/DanilMaximov/activefunction/tree/master/gems/activefunction-core#hooks) external plugin and provides `:before_action` and `:after_action` which runs around provided action in `#process`.
94
+
64
95
  ```ruby
65
- class BaseFunction < ActiveFunction::Base
66
- before_action :set_current_user
96
+ require "active_function"
67
97
 
68
- def set_current_user
69
- @current_user = User.first
70
- end
98
+ ActiveFunction.config do
99
+ plugin :callbacks
71
100
  end
72
101
 
73
- class PostsFunction < BaseFunction
102
+ class AppFunction < ActiveFunction::Base
103
+ before_action :set_user
104
+ after_action :log_response
105
+
74
106
  def index
75
- render json: @current_user
107
+ # some actions ...
76
108
  end
109
+
110
+ private
111
+
112
+ def set_user = @_user ||= User.find(@request[:id])
113
+ def log_response = Logger.info(@response)
77
114
  end
78
115
  ```
79
- ## Strong Parameters
80
- ActiveFunction supports strong parameters which can be accessed by `#params` instance method. Strong parameters hash can be passed in `#process` as second argument.
116
+
117
+ ### Callbacks options
118
+
119
+ Supports default [ActiveFunctionCore::Plugins::Hooks::Hook::Callback options](https://github.com/DanilMaximov/activefunction/tree/master/gems/activefunction-core#options) `:if => Symbol` & `:unless => Symbol` options.
120
+
121
+ Support custom defined in ActiveFunction::Function::Callbacks `only: Array[Symbol]` option.
81
122
 
82
123
  ```ruby
83
- PostFunction.process(:index, data: { id: 1, name: "Pupa" })
124
+ class AppFunction < ActiveFunction::Base
125
+ before_action :set_user, only: %i[show update destroy], if: :request_valid?
126
+
127
+ # some actions ...
128
+
129
+ private def request_valid? = true
130
+ end
84
131
  ```
132
+ More details in [ActiveFunctionCore::Plugins::Hooks readme](https://github.com/DanilMaximov/activefunction/tree/master/gems/activefunction-core#hooks)
133
+
134
+ ## Strong Parameters
135
+
136
+ ActiveFunction supports strong parameters which can be accessed by `#params` instance method.
137
+
138
+ The `#params` method represents a Ruby 3.2 Data class that allows the manipulation of request parameters. It supports the following methods:
139
+
140
+ - `[]`: Access parameters by key.
141
+ - `permit`: Specify the allowed parameters.
142
+ - `require`: Ensure the presence of a specific parameter.
143
+ - `to_h`: Convert the parameters to a hash.
144
+
145
+ Usage Example:
85
146
 
86
- Simple usage:
87
147
  ```ruby
148
+ require "active_function"
149
+
150
+ ActiveFunction.config do
151
+ plugin :strong_parameters
152
+ end
153
+
88
154
  class PostsFunction < ActiveFunction::Base
89
- def index
90
- render json: permitted_params
91
- end
155
+ def index
156
+ @response.body = permitted_params
157
+ end
92
158
 
93
159
  def permitted_params = params
94
160
  .require(:data)
95
161
  .permit(:id, :name)
96
162
  .to_h
97
- end
163
+ end
164
+
165
+ PostFunction.process(:index, data: {id: 1, name: "Pupa"})
98
166
  ```
167
+
99
168
  Strong params supports nested attributes
100
- ```ruby
169
+ ```ruby
101
170
  params.permit(:id, :name, :address => [:city, :street])
102
171
  ```
103
172
 
104
173
  ## Rendering
105
- ActiveFunction supports rendering of JSON. Rendering is obligatory for any function naction and can be done by `#render` method.
106
- ```ruby
107
- class PostsFunction < ActiveFunction::Base
108
- def index
109
- render json: { id: 1, name: "Pupa" }
110
- end
111
- end
112
- ```
113
- default status code is 200, but it can be changed by `:status` option
174
+
175
+ ActiveFunction supports rendering of JSON. The #render method is used for rendering responses. It accepts the following options:
176
+
177
+ - `head`: Set response headers. Defaults to `{ "Content-Type" => "application/json" }`.
178
+ - `json`: Set the response body with a JSON-like object. Defaults to `{}`.
179
+ - `status`: Set the HTTP status code. Defaults to `200`.
180
+
181
+ Additionally, the method automatically commits the response and JSONifies the body.
182
+
183
+ Usage Example:
114
184
  ```ruby
115
- class PostsFunction < ActiveFunction::Base
116
- def index
117
- render json: { id: 1, name: "Pupa" }, status: 201
118
- end
185
+ require "active_function"
186
+
187
+ ActiveFunction.config do
188
+ plugin :rendering
119
189
  end
120
- ```
121
- Headers can be passed by `:headers` option. Default headers are `{"Content-Type" => "application/json"}`.
122
- ```ruby
190
+
123
191
  class PostsFunction < ActiveFunction::Base
124
- def index
125
- render json: { id: 1, name: "Pupa" }, headers: { "X-Request-Id" => "123" }
126
- end
192
+ def index
193
+ render json: {id: 1, name: "Pupa"}, status: 200, head: {"Some-Header" => "Some-Value"}
194
+ end
127
195
  end
128
- ```
129
196
 
197
+ PostFunction.process(:index) # => { :statusCode=>200, :headers=> {"Content-Type"=>"application/json", "Some-Header" => "Some-Value"}, :body=>"{\"id\":1,\"name\":\"Pupa\"}"}
198
+ ```
130
199
 
131
200
  ## Installation
132
201
 
133
202
  Add this line to your application's Gemfile:
134
203
 
135
204
  ```ruby
136
- gem 'activefunction', git: "https://github.com/DanilMaximov/activefunction.git"
205
+ gem "activefunction"
137
206
  ```
138
207
 
139
208
  ## Development
140
209
 
141
- 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.
210
+ 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.
142
211
 
143
212
  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).
144
213
 
@@ -3,52 +3,45 @@
3
3
  require "forwardable"
4
4
 
5
5
  module ActiveFunction
6
- class ParameterMissingError < Error
7
- MESSAGE_TEMPLATE = "Missing parameter: %s"
6
+ module Functions
7
+ module StrongParameters
8
+ ActiveFunction.register_plugin :strong_parameters, self
8
9
 
9
- attr_reader :message
10
+ Error = Class.new(StandardError)
10
11
 
11
- def initialize(param)
12
- MESSAGE_TEMPLATE % param
13
- end
14
- end
12
+ class Parameters < Data.define(:params, :permitted)
13
+ class ParameterMissingError < Error
14
+ MESSAGE_TEMPLATE = "Missing parameter: %s"
15
15
 
16
- class UnpermittedParameterError < Error
17
- MESSAGE_TEMPLATE = "Unpermitted parameter: %s"
16
+ attr_reader :message
18
17
 
19
- attr_reader :message
20
-
21
- def initialize(param)
22
- MESSAGE_TEMPLATE % param
23
- end
24
- end
18
+ def initialize(param)
19
+ MESSAGE_TEMPLATE % param
20
+ end
21
+ end
25
22
 
26
- module Functions
27
- module StrongParameters
28
- def params
29
- @_params ||= Parameters.new(@request)
30
- end
23
+ class UnpermittedParameterError < Error
24
+ MESSAGE_TEMPLATE = "Unpermitted parameter: %s"
31
25
 
32
- class Parameters
33
- extend Forwardable
34
- def_delegators :@parameters, :each, :map
35
- include Enumerable
26
+ attr_reader :message
36
27
 
37
- def initialize(parameters, permitted: false)
38
- @parameters = parameters
39
- @permitted = permitted
28
+ def initialize(param)
29
+ MESSAGE_TEMPLATE % param
30
+ end
40
31
  end
41
32
 
33
+ protected :params
34
+
42
35
  def [](attribute)
43
- nested_attribute(parameters[attribute])
36
+ nested_attribute(params[attribute])
44
37
  end
45
38
 
46
39
  def require(attribute)
47
- value = self[attribute]
48
-
49
- raise ParameterMissingError, attribute if value.nil?
50
-
51
- value
40
+ if (value = self[attribute])
41
+ value
42
+ else
43
+ raise ParameterMissingError, attribute
44
+ end
52
45
  end
53
46
 
54
47
  def permit(*attributes)
@@ -60,28 +53,37 @@ module ActiveFunction
60
53
  pparams[k] = process_nested(self[k], :permit, v)
61
54
  end
62
55
  else
63
- next unless parameters.key?(attribute)
56
+ next unless params.key?(attribute)
64
57
 
65
58
  pparams[attribute] = self[attribute]
66
59
  end
67
60
  end
68
61
 
69
- Parameters.new(pparams, permitted: true)
62
+ with(params: pparams, permitted: true)
70
63
  end
71
64
 
65
+ # Redefines RubyNext::Core::Data instance methods
72
66
  def to_h
73
- raise UnpermittedParameterError, parameters.keys unless @permitted
67
+ raise UnpermittedParameterError, params.keys unless permitted
74
68
 
75
- parameters.transform_values { |_1| process_nested(_1, :to_h) }
69
+ params.transform_values { |_1| process_nested(_1, :to_h) }
70
+ end
71
+
72
+ def hash
73
+ @attributes.to_h.hash
74
+ end
75
+
76
+ def with(params:, permitted: false)
77
+ self.class.new(params, permitted)
76
78
  end
77
79
 
78
80
  private
79
81
 
80
82
  def nested_attribute(attribute)
81
83
  if attribute.is_a? Hash
82
- Parameters.new(attribute)
84
+ with(params: attribute)
83
85
  elsif attribute.is_a?(Array) && attribute[0].is_a?(Hash)
84
- attribute.map { |_1| Parameters.new(_1) }
86
+ attribute.map { |it| with(params: it) }
85
87
  else
86
88
  attribute
87
89
  end
@@ -96,8 +98,10 @@ module ActiveFunction
96
98
  attribute
97
99
  end
98
100
  end
101
+ end
99
102
 
100
- attr_reader :parameters
103
+ def params
104
+ @_params ||= Parameters.new(@request, false)
101
105
  end
102
106
  end
103
107
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveFunction
4
+ class SuperBase
5
+ attr_reader :action_name, :request, :response
6
+
7
+ def initialize(action_name, request, response)
8
+ @action_name = action_name
9
+ @request = request
10
+ @response = response
11
+ end
12
+
13
+ def dispatch
14
+ process(action_name)
15
+
16
+ @response.commit! unless performed?
17
+
18
+ @response.to_h
19
+ end
20
+
21
+ def process(action) ; public_send(action); end
22
+
23
+ private def performed? ; @response.committed?; end
24
+ end
25
+
26
+ class Base < SuperBase
27
+ Error = Class.new(StandardError)
28
+
29
+ # @param [String, Symbol] action_name - name of method to call
30
+ # @param [Hash] request - request params, accessible through `#params` method
31
+ # @param [Response] response - response object
32
+ def self.process(action_name, request = {}, response = Response.new)
33
+ raise ArgumentError, "Action method #{action_name} is not defined" unless method_defined?(action_name)
34
+
35
+ new(action_name, request, response).dispatch
36
+ end
37
+ end
38
+ end
@@ -2,29 +2,26 @@
2
2
 
3
3
  module ActiveFunction
4
4
  module Functions
5
- class Response
6
- attr_accessor :status, :headers, :body
5
+ module Response
6
+ ActiveFunction.register_plugin :response, self
7
7
 
8
- def initialize(status: 200, headers: {}, body: nil)
9
- @status = status
10
- @headers = headers
11
- @body = body
12
- @committed = false
13
- end
8
+ class Response < Struct.new(:status, :headers, :body, :committed)
9
+ def initialize(status: 200, headers: {}, body: nil, committed: false) ; super(status, headers, body, committed); end
14
10
 
15
- def to_h
16
- {
17
- statusCode: status,
18
- headers: headers,
19
- body: body
20
- }
21
- end
11
+ def to_h
12
+ {
13
+ statusCode: status,
14
+ headers: headers,
15
+ body: body
16
+ }
17
+ end
22
18
 
23
- def commit!
24
- @committed = true
25
- end
19
+ def commit!
20
+ self.committed = true
21
+ end
26
22
 
27
- def committed? ; @committed; end
23
+ alias_method :committed?, :committed
24
+ end
28
25
  end
29
26
  end
30
27
  end
@@ -0,0 +1,50 @@
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.
12
+ #
13
+ # @param block [Proc]
14
+ # @return [void]
15
+ def config(&block)
16
+ class_eval(&block)
17
+ @_plugins.freeze
18
+ self::Base.freeze
19
+ end
20
+
21
+ def plugins ; @_plugins ||= {}; end
22
+
23
+ # Register plugin.
24
+ #
25
+ # @param symbol [Symbol]
26
+ # @param mod [Module]
27
+ def register_plugin(symbol, mod)
28
+ plugins[symbol] = mod
29
+ end
30
+
31
+ # Monkey patch ActiveFunction::Base with provided plugin.
32
+ #
33
+ # @param mod [Symbol, Module]
34
+ # @return [void]
35
+ def plugin(mod)
36
+ if mod.is_a? Symbol
37
+ begin
38
+ require "active_function/functions/#{mod}"
39
+ mod = plugins.fetch(mod)
40
+ rescue LoadError
41
+ raise ArgumentError, "Unknown plugin #{mod}"
42
+ end
43
+ end
44
+
45
+ self::Base.include(mod)
46
+ end
47
+ end
48
+
49
+ plugin :response
50
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveFunction
4
+ module Functions
5
+ module Response
6
+ ActiveFunction.register_plugin :response, self
7
+
8
+ class Response < Struct.new(:status, :headers, :body, :committed)
9
+ def initialize(status: 200, headers: {}, body: nil, committed: false) = super(status, headers, body, committed)
10
+
11
+ def to_h
12
+ {
13
+ statusCode: status,
14
+ headers: headers,
15
+ body: body
16
+ }
17
+ end
18
+
19
+ def commit!
20
+ self.committed = true
21
+ end
22
+
23
+ alias_method :committed?, :committed
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,20 +1,38 @@
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
+ class SuperBase
5
+ attr_reader :action_name, :request, :response
6
+
7
+ def initialize(action_name, request, response)
8
+ @action_name = action_name
9
+ @request = request
10
+ @response = response
11
+ end
12
+
13
+ def dispatch
14
+ process(action_name)
15
+
16
+ @response.commit! unless performed?
17
+
18
+ @response.to_h
19
+ end
20
+
21
+ def process(action) = public_send(action)
22
+
23
+ private def performed? = @response.committed?
24
+ end
25
+
26
+ class Base < SuperBase
27
+ Error = Class.new(StandardError)
28
+
29
+ # @param [String, Symbol] action_name - name of method to call
30
+ # @param [Hash] request - request params, accessible through `#params` method
31
+ # @param [Response] response - response object
32
+ def self.process(action_name, request = {}, response = Response.new)
33
+ raise ArgumentError, "Action method #{action_name} is not defined" unless method_defined?(action_name)
34
+
35
+ new(action_name, request, response).dispatch
18
36
  end
19
37
  end
20
38
  end
@@ -1,68 +1,14 @@
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
15
5
  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
6
+ ActiveFunction.register_plugin :callbacks, self
29
7
 
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
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
42
- end
43
-
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
8
+ def self.included(base)
9
+ base.include ActiveFunctionCore::Plugins::Hooks
10
+ base.define_hooks_for :process, name: :action
11
+ base.set_callback_options only: ->(args, context:) { args.to_set === context.action_name }
66
12
  end
67
13
  end
68
14
  end
@@ -3,18 +3,22 @@
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
+ module Rendering
8
+ ActiveFunction.register_plugin :rendering, self
8
9
 
9
- attr_reader :message
10
+ Error = Class.new(StandardError)
10
11
 
11
- def initialize(context)
12
- @message = MESSAGE_TEMPLATE % context
13
- end
14
- end
12
+ class DoubleRenderError < Error
13
+ MESSAGE_TEMPLATE = "#render was called multiple times in action: %s"
14
+
15
+ attr_reader :message
16
+
17
+ def initialize(context)
18
+ @message = MESSAGE_TEMPLATE % context
19
+ end
20
+ end
15
21
 
16
- module Functions
17
- module Rendering
18
22
  DEFAULT_HEADER = {"Content-Type" => "application/json"}.freeze
19
23
 
20
24
  def render(status: 200, json: {}, head: {})
@@ -2,29 +2,26 @@
2
2
 
3
3
  module ActiveFunction
4
4
  module Functions
5
- class Response
6
- attr_accessor :status, :headers, :body
5
+ module Response
6
+ ActiveFunction.register_plugin :response, self
7
7
 
8
- def initialize(status: 200, headers: {}, body: nil)
9
- @status = status
10
- @headers = headers
11
- @body = body
12
- @committed = false
13
- end
8
+ class Response < Struct.new(:status, :headers, :body, :committed)
9
+ def initialize(status: 200, headers: {}, body: nil, committed: false) = super(status, headers, body, committed)
14
10
 
15
- def to_h
16
- {
17
- statusCode: status,
18
- headers: headers,
19
- body: body
20
- }
21
- end
11
+ def to_h
12
+ {
13
+ statusCode: status,
14
+ headers:,
15
+ body:
16
+ }
17
+ end
22
18
 
23
- def commit!
24
- @committed = true
25
- end
19
+ def commit!
20
+ self.committed = true
21
+ end
26
22
 
27
- def committed? = @committed
23
+ alias_method :committed?, :committed
24
+ end
28
25
  end
29
26
  end
30
27
  end
@@ -3,52 +3,45 @@
3
3
  require "forwardable"
4
4
 
5
5
  module ActiveFunction
6
- class ParameterMissingError < Error
7
- MESSAGE_TEMPLATE = "Missing parameter: %s"
6
+ module Functions
7
+ module StrongParameters
8
+ ActiveFunction.register_plugin :strong_parameters, self
8
9
 
9
- attr_reader :message
10
+ Error = Class.new(StandardError)
10
11
 
11
- def initialize(param)
12
- MESSAGE_TEMPLATE % param
13
- end
14
- end
12
+ class Parameters < Data.define(:params, :permitted)
13
+ class ParameterMissingError < Error
14
+ MESSAGE_TEMPLATE = "Missing parameter: %s"
15
15
 
16
- class UnpermittedParameterError < Error
17
- MESSAGE_TEMPLATE = "Unpermitted parameter: %s"
16
+ attr_reader :message
18
17
 
19
- attr_reader :message
20
-
21
- def initialize(param)
22
- MESSAGE_TEMPLATE % param
23
- end
24
- end
18
+ def initialize(param)
19
+ MESSAGE_TEMPLATE % param
20
+ end
21
+ end
25
22
 
26
- module Functions
27
- module StrongParameters
28
- def params
29
- @_params ||= Parameters.new(@request)
30
- end
23
+ class UnpermittedParameterError < Error
24
+ MESSAGE_TEMPLATE = "Unpermitted parameter: %s"
31
25
 
32
- class Parameters
33
- extend Forwardable
34
- def_delegators :@parameters, :each, :map
35
- include Enumerable
26
+ attr_reader :message
36
27
 
37
- def initialize(parameters, permitted: false)
38
- @parameters = parameters
39
- @permitted = permitted
28
+ def initialize(param)
29
+ MESSAGE_TEMPLATE % param
30
+ end
40
31
  end
41
32
 
33
+ protected :params
34
+
42
35
  def [](attribute)
43
- nested_attribute(parameters[attribute])
36
+ nested_attribute(params[attribute])
44
37
  end
45
38
 
46
39
  def require(attribute)
47
- value = self[attribute]
48
-
49
- raise ParameterMissingError, attribute if value.nil?
50
-
51
- value
40
+ if (value = self[attribute])
41
+ value
42
+ else
43
+ raise ParameterMissingError, attribute
44
+ end
52
45
  end
53
46
 
54
47
  def permit(*attributes)
@@ -60,28 +53,37 @@ module ActiveFunction
60
53
  pparams[k] = process_nested(self[k], :permit, v)
61
54
  end
62
55
  else
63
- next unless parameters.key?(attribute)
56
+ next unless params.key?(attribute)
64
57
 
65
58
  pparams[attribute] = self[attribute]
66
59
  end
67
60
  end
68
61
 
69
- Parameters.new(pparams, permitted: true)
62
+ with(params: pparams, permitted: true)
70
63
  end
71
64
 
65
+ # Redefines RubyNext::Core::Data instance methods
72
66
  def to_h
73
- raise UnpermittedParameterError, parameters.keys unless @permitted
67
+ raise UnpermittedParameterError, params.keys unless permitted
74
68
 
75
- parameters.transform_values { process_nested(_1, :to_h) }
69
+ params.transform_values { process_nested(_1, :to_h) }
70
+ end
71
+
72
+ def hash
73
+ @attributes.to_h.hash
74
+ end
75
+
76
+ def with(params:, permitted: false)
77
+ self.class.new(params, permitted)
76
78
  end
77
79
 
78
80
  private
79
81
 
80
82
  def nested_attribute(attribute)
81
83
  if attribute.is_a? Hash
82
- Parameters.new(attribute)
84
+ with(params: attribute)
83
85
  elsif attribute.is_a?(Array) && attribute[0].is_a?(Hash)
84
- attribute.map { Parameters.new(_1) }
86
+ attribute.map { |it| with(params: it) }
85
87
  else
86
88
  attribute
87
89
  end
@@ -96,8 +98,10 @@ module ActiveFunction
96
98
  attribute
97
99
  end
98
100
  end
101
+ end
99
102
 
100
- attr_reader :parameters
103
+ def params
104
+ @_params ||= Parameters.new(@request, false)
101
105
  end
102
106
  end
103
107
  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.0"
5
5
  end
@@ -1,12 +1,50 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_function_core"
4
+ require "active_function/version"
5
+ require "active_function/base"
4
6
 
5
7
  RubyNext::Language.setup_gem_load_path(transpile: true)
6
8
 
7
9
  module ActiveFunction
8
- class Error < StandardError; end
10
+ class << self
11
+ # Configure ActiveFunction.
12
+ #
13
+ # @param block [Proc]
14
+ # @return [void]
15
+ def config(&block)
16
+ class_eval(&block)
17
+ @_plugins.freeze
18
+ self::Base.freeze
19
+ end
9
20
 
10
- require "active_function/version"
11
- require "active_function/base"
21
+ def plugins = @_plugins ||= {}
22
+
23
+ # Register plugin.
24
+ #
25
+ # @param symbol [Symbol]
26
+ # @param mod [Module]
27
+ def register_plugin(symbol, mod)
28
+ plugins[symbol] = mod
29
+ end
30
+
31
+ # Monkey patch ActiveFunction::Base with provided plugin.
32
+ #
33
+ # @param mod [Symbol, Module]
34
+ # @return [void]
35
+ def plugin(mod)
36
+ if mod.is_a? Symbol
37
+ begin
38
+ require "active_function/functions/#{mod}"
39
+ mod = plugins.fetch(mod)
40
+ rescue LoadError
41
+ raise ArgumentError, "Unknown plugin #{mod}"
42
+ end
43
+ end
44
+
45
+ self::Base.include(mod)
46
+ end
47
+ end
48
+
49
+ plugin :response
12
50
  end
metadata CHANGED
@@ -1,76 +1,39 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activefunction
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.5
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nerbyk
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-08-20 00:00:00.000000000 Z
11
+ date: 2024-01-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activefunction-core
15
15
  requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: 0.0.1
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - ">="
25
- - !ruby/object:Gem::Version
26
- version: 0.0.1
27
- - !ruby/object:Gem::Dependency
28
- name: rake
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '13.0'
34
- type: :development
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '13.0'
41
- - !ruby/object:Gem::Dependency
42
- name: minitest
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: 5.15.0
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
16
  requirements:
52
17
  - - "~>"
53
18
  - !ruby/object:Gem::Version
54
- version: 5.15.0
55
- - !ruby/object:Gem::Dependency
56
- name: minitest-reporters
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - "~>"
60
- - !ruby/object:Gem::Version
61
- version: 1.4.3
62
- type: :development
19
+ version: 0.2.2
20
+ type: :runtime
63
21
  prerelease: false
64
22
  version_requirements: !ruby/object:Gem::Requirement
65
23
  requirements:
66
24
  - - "~>"
67
25
  - !ruby/object:Gem::Version
68
- version: 1.4.3
69
- description: "\n rails/action_controller like gem which provides lightweight callbacks,\n
70
- \ strong parameters & rendering features. It's designed to be used with\n AWS
71
- Lambda functions, but can be also used with any Ruby application.\n\n Implemented
72
- with some of ruby 3.x features, but also supports\n ruby 2.6.x thanks to RubyNext
73
- transpiler. Type safety achieved\n by RBS and Steep.\n "
26
+ version: 0.2.2
27
+ description: "\n ActiveFunction is a collection of gems designed to be used with
28
+ Function as a Service (FaaS) computing instances. Inspired by aws-sdk v3 gem structure
29
+ and rails/activesupport.\n\n Features:\n - Ruby Version Compatibility: Implemented
30
+ with most of Ruby 3.2+ features, with support for Ruby versions >= 2.6 through the
31
+ RubyNext transpiler (CI'ed).\n - Type Safety: Achieves type safety through the
32
+ use of RBS and Steep (CI'ed) [Note: disabled due to the presence of Ruby::UnsupportedSyntax
33
+ errors].\n - Plugins System: Provides a simple Plugin system inspired by Polishing
34
+ Ruby Programming by Jeremy Evans to load gem plugins and self-defined plugins.\n
35
+ \ - Gem Collection: Offers a collection of gems designed for use within ActiveFunction
36
+ or as standalone components.\n "
74
37
  email:
75
38
  - danil.maximov2000@gmail.com
76
39
  executables: []
@@ -86,12 +49,13 @@ files:
86
49
  - bin/ruby-next
87
50
  - bin/setup
88
51
  - lib/.rbnext/2.7/active_function/functions/strong_parameters.rb
89
- - lib/.rbnext/3.0/active_function/functions/core.rb
52
+ - lib/.rbnext/3.0/active_function.rb
53
+ - lib/.rbnext/3.0/active_function/base.rb
90
54
  - lib/.rbnext/3.0/active_function/functions/response.rb
55
+ - lib/.rbnext/3.1/active_function/functions/response.rb
91
56
  - lib/active_function.rb
92
57
  - lib/active_function/base.rb
93
58
  - lib/active_function/functions/callbacks.rb
94
- - lib/active_function/functions/core.rb
95
59
  - lib/active_function/functions/rendering.rb
96
60
  - lib/active_function/functions/response.rb
97
61
  - lib/active_function/functions/strong_parameters.rb
@@ -105,7 +69,7 @@ metadata:
105
69
  homepage_uri: https://github.com/DanilMaximov/activefunction
106
70
  source_code_uri: https://github.com/DanilMaximov/activefunction
107
71
  changelog_uri: https://github.com/DanilMaximov/activefunction/CHANGELOG.md
108
- post_install_message:
72
+ post_install_message:
109
73
  rdoc_options: []
110
74
  require_paths:
111
75
  - lib
@@ -120,9 +84,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
120
84
  - !ruby/object:Gem::Version
121
85
  version: '0'
122
86
  requirements: []
123
- rubygems_version: 3.4.10
124
- signing_key:
87
+ rubygems_version: 3.5.4
88
+ signing_key:
125
89
  specification_version: 4
126
- summary: rails/action_controller like gem which provides callbacks, strong parameters
127
- & rendering features.
90
+ summary: Playground gem for Ruby 3.2+ features and more, designed for FaaS computing
91
+ instances, but mostly used for experiments.
128
92
  test_files: []
@@ -1,49 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActiveFunction
4
- class MissingRouteMethod < Error
5
- MESSAGE_TEMPLATE = "Missing function route: %s"
6
-
7
- attr_reader :message
8
-
9
- def initialize(context)
10
- @message = MESSAGE_TEMPLATE % context
11
- end
12
- end
13
-
14
- class NotRenderedError < Error
15
- MESSAGE_TEMPLATE = "render was not called: %s"
16
-
17
- attr_reader :message
18
-
19
- def initialize(context)
20
- @message = MESSAGE_TEMPLATE % context
21
- end
22
- end
23
-
24
- module Functions
25
- module Core
26
- attr_reader :action_name, :request, :response
27
-
28
- def dispatch(action_name, request, response)
29
- @action_name = action_name
30
- @request = request
31
- @response = response
32
-
33
- raise MissingRouteMethod, @action_name unless respond_to?(action_name)
34
-
35
- process(@action_name)
36
-
37
- raise NotRenderedError, @action_name unless performed?
38
-
39
- @response.to_h
40
- end
41
-
42
- private
43
-
44
- def process(action) ; public_send(action); end
45
-
46
- def performed? ; @response.committed?; end
47
- end
48
- end
49
- end
@@ -1,49 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActiveFunction
4
- class MissingRouteMethod < Error
5
- MESSAGE_TEMPLATE = "Missing function route: %s"
6
-
7
- attr_reader :message
8
-
9
- def initialize(context)
10
- @message = MESSAGE_TEMPLATE % context
11
- end
12
- end
13
-
14
- class NotRenderedError < Error
15
- MESSAGE_TEMPLATE = "render was not called: %s"
16
-
17
- attr_reader :message
18
-
19
- def initialize(context)
20
- @message = MESSAGE_TEMPLATE % context
21
- end
22
- end
23
-
24
- module Functions
25
- module Core
26
- attr_reader :action_name, :request, :response
27
-
28
- def dispatch(action_name, request, response)
29
- @action_name = action_name
30
- @request = request
31
- @response = response
32
-
33
- raise MissingRouteMethod, @action_name unless respond_to?(action_name)
34
-
35
- process(@action_name)
36
-
37
- raise NotRenderedError, @action_name unless performed?
38
-
39
- @response.to_h
40
- end
41
-
42
- private
43
-
44
- def process(action) = public_send(action)
45
-
46
- def performed? = @response.committed?
47
- end
48
- end
49
- end