hanami-lambda 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e6434425a9bc1477663be14851eb304da6fd9fc5ebe9a9ff3b800afb72986f1d
4
- data.tar.gz: 94b184c7584379f57d7214c24e4963e3ca2b66d9a5cac02781e6612254f868a3
3
+ metadata.gz: c8a8ab1a1e0affa3af3a684eb7f96cf8b4d5f19a18f73cbc9f86a0661726a8cd
4
+ data.tar.gz: 281445c481e12c80c41e9a217e719eb3306aaa9e8214feedeb9999e7a7347276
5
5
  SHA512:
6
- metadata.gz: 296484573f0c74479a2f3ddf78f4eef69c8d41e7ae34eae2bc18055aaa17c7d9ad49f843f016555a08c72743272d237e289d64dba5aadc8a7dcb696f9e07d5a4
7
- data.tar.gz: d8b969b733db865b342cb55a9e800020ed416922db7061115ff36cb4d391bc76ff78d83f4e49e731e40c512a41ebda80b416d3bece37c559429eba5fed29b507
6
+ metadata.gz: 59c7fe7721eaf15a3481d418ded8ecf61f6ff5774024c6a6702d413f7792624565b394f99db736013961750e48c801febfb531962a77e39cd38532e619b1f470
7
+ data.tar.gz: c4abc0406ebb796740201d79e1e00986c08f3b8ea23ec3a7e056943d4ab1b641a3aec90c291edddb86477175fb3cab91c56b7712051ae545b32a181be3b7984a
data/README.md CHANGED
@@ -10,7 +10,7 @@ Hanami Lambda is a gem that provides a way to run hanami application on AWS Lamb
10
10
 
11
11
  ## Rubies
12
12
 
13
- **Hanami::Cucumber** supports Ruby (MRI) 3.0+
13
+ **Hanami::Lambda** supports Ruby (MRI) 3.0+
14
14
 
15
15
  ## Installation
16
16
 
@@ -26,14 +26,25 @@ And then execute:
26
26
  $ bundle install
27
27
  ```
28
28
 
29
- Create `config/lambda.rb` with below content
29
+ Update `config/app.rb` with below content
30
30
 
31
31
  ```ruby
32
+ require 'hanami'
32
33
  require 'hanami/lambda'
33
34
 
34
35
  module MyApp # Rename to your app name
35
- class Lambda < Hanami::Lambda::Application
36
- end
36
+ class Lambda < Hanami::App
37
+ extend Hanami::Lambda::Application
38
+ end
39
+ end
40
+ ```
41
+
42
+ Create `app/function.rb` as handler base class
43
+
44
+ ```ruby
45
+ module MyApp
46
+ class Function < Hanami::Lambda::Function
47
+ end
37
48
  end
38
49
  ```
39
50
 
@@ -44,17 +55,45 @@ end
44
55
  Use `config/lambda.Hanami::Lambda.call` as the function handler
45
56
 
46
57
  ```yaml
58
+ # AWS SAM
47
59
  Resources:
48
- ExampleHandler:
60
+ MyFunction:
49
61
  Type: AWS::Serverless::Function
50
- Name: "example-api"
51
62
  Properties:
52
63
  CodeUri: .
53
64
  Handler: config/lambda.Hanami::Lambda.call
54
65
  Runtime: ruby3.2
55
66
  ```
56
67
 
57
- > Currently, the only `APIGateWay` event is supported
68
+ ### Delegate
69
+
70
+ If the lambda function isn't trigger by APIGateway, we can use `delegate` method to define the handler function.
71
+
72
+ Create `config/lambda.rb` with below content
73
+
74
+ ```ruby
75
+ module MyApp
76
+ class Lambda < Hanami::Lambda::Dispatcher
77
+ delegate "MyFunction", to: "daily_task"
78
+ end
79
+ end
80
+ ```
81
+
82
+ > The IaC generated function will be `my-app-MyFunction-r8faNAo3iUqx` therefore the dispatcher will use include `MyFunction` to find targeted function
83
+
84
+ Add `app/functions/daily_task.rb` to define the handle action
85
+
86
+ ```ruby
87
+ module MyApp
88
+ module Functions
89
+ class DailyTask < MyApp::Function
90
+ def handle(event, context)
91
+ # ...
92
+ end
93
+ end
94
+ end
95
+ end
96
+ ```
58
97
 
59
98
  ## Development
60
99
 
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ module Lambda
5
+ # Base error for Hanami::Lambda
6
+ #
7
+ # @api public
8
+ # @since 0.2.0
9
+ Error = Class.new(StandardError)
10
+
11
+ # Raised when {Hanami::Lambda::Application} fails to load.
12
+ #
13
+ # @api public
14
+ # @since 0.2.0
15
+ AppLoadError = Class.new(Error)
16
+ end
17
+ end
@@ -2,34 +2,62 @@
2
2
 
3
3
  module Hanami
4
4
  module Lambda
5
+ HANDLER_KEY_NAMESPACE = "functions"
6
+
5
7
  # The application to configure for AWS Lambda.
6
8
  #
7
9
  # @since 0.1.0
8
- class Application
10
+ module Application
11
+ # @since 0.2.0
12
+ # @api private
13
+ def self.extended(base)
14
+ base.class_eval do
15
+ prepare_load_path if respond_to?(:prepare_load_path)
16
+ end
17
+ end
18
+
19
+ # Dispatch event to the handler
20
+ #
9
21
  # @api private
10
- def self.inherited(subclass)
11
- super
22
+ # @since 0.1.0
23
+ def handle_lambda(event:, context:)
24
+ lambda_dispatcher.call(event: event, context: context)
25
+ end
12
26
 
13
- Hanami::Lambda.app = subclass
14
- subclass.extend(ClassMethods)
27
+ # Get lambda dispatcher
28
+ #
29
+ # @return [Hanami::Lambda::Dispatcher] the dispatcher
30
+ #
31
+ # @since 0.1.0
32
+ def lambda_dispatcher
33
+ @lambda_dispatcher ||= load_lambda_dispatcher
15
34
  end
16
35
 
17
- module ClassMethods
18
- # Dispatch event to the handler
19
- #
20
- # @api private
21
- # @since 0.1.0
22
- def call(event:, context:)
23
- handler = lookup(event: event, context: context)
24
- handler.call
36
+ # Load lambda dispatcher
37
+ #
38
+ # @return [Hanami::Lambda::Dispatcher] the dispatcher
39
+ #
40
+ # @since 0.2.0
41
+ # @api private
42
+ def load_lambda_dispatcher
43
+ if root.directory?
44
+ dispatcher_path = File.join(root, LAMBDA_CONFIG_PATH)
45
+
46
+ begin
47
+ require dispatcher_path
48
+ rescue LoadError => exception
49
+ raise exception unless exception.path == dispatcher_path
50
+ end
25
51
  end
26
52
 
27
- # Lookup the handler for the given event
28
- #
29
- # @api private
30
- # @since 0.1.0
31
- def lookup(event:, context:)
32
- Rack.new(Hanami.app, event: event, context: context)
53
+ begin
54
+ dispatcher_class = namespace.const_get(LAMBDA_CLASS_NAME)
55
+ dispatcher_class.build(
56
+ rack_app: app.rack_app,
57
+ resolver: ->(to) { app.resolve("#{HANDLER_KEY_NAMESPACE}.#{to}") }
58
+ )
59
+ rescue NameError => exception
60
+ raise exception unless exception.name == LAMBDA_CLASS_NAME
33
61
  end
34
62
  end
35
63
  end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ module Lambda
5
+ # Dispatch Event to the Handler
6
+ #
7
+ # @api private
8
+ class Dispatcher
9
+ # @since 0.2.0
10
+ # @api private
11
+ DEFAULT_RESOLVER = ->(to) { to }
12
+
13
+ attr_reader :rack_app, :handlers, :default, :resolver
14
+
15
+ # @since 0.2.0
16
+ def initialize(rack_app:, resolver: DEFAULT_RESOLVER)
17
+ @handlers = {}
18
+ @resolver = resolver
19
+ @default = Rack.new(rack_app)
20
+ end
21
+
22
+ # Call the handler
23
+ #
24
+ # @param event [Hash] the event
25
+ # @param context [Hash] the context
26
+ #
27
+ # @since 0.2.0
28
+ def call(event:, context:)
29
+ handler = lookup(event: event, context: context)
30
+ return default.call(event: event, context: context) unless handler
31
+
32
+ handler.call(event: event, context: context)
33
+ end
34
+
35
+ # Lookup the handler
36
+ #
37
+ # @param event [Hash] the event
38
+ # @param context [Hash] the context
39
+ #
40
+ # @return [Handler] the handler
41
+ def lookup(event:, context:) # rubocop:disable Lint/UnusedMethodArgument
42
+ function_name = ENV.fetch("AWS_LAMBDA_FUNCTION_NAME", context.function_name)
43
+ handlers
44
+ .select { |name, _| function_name.include?(name) }
45
+ .max_by { |name, _| name.length }
46
+ &.last
47
+ end
48
+
49
+ # Register a handler
50
+ #
51
+ # @param name [String] the name of the handler
52
+ # @param args [Array] the arguments to pass to the handler
53
+ # @param kwargs [Hash] the keyword arguments to pass to the handler
54
+ # @param block [Proc] the block to pass to the handler
55
+ #
56
+ # @since 0.2.0
57
+ def register(name, to: nil)
58
+ handlers[name] =
59
+ if to.nil?
60
+ @default
61
+ else
62
+ resolver.call(to)
63
+ end
64
+ end
65
+
66
+ class << self
67
+ # Definitions of handlers
68
+ #
69
+ # @api private
70
+ def definitions
71
+ @definitions ||= []
72
+ end
73
+
74
+ # Define function delegate action
75
+ #
76
+ # @param name [String] the name of the handler
77
+ # @param args [Array] the arguments to pass to the handler
78
+ # @param kwargs [Hash] the keyword arguments to pass to the handler
79
+ # @param block [Proc] the block to pass to the handler
80
+ def delegate(name, *args, **kwargs, &block)
81
+ definitions << [name, args, kwargs, block]
82
+ end
83
+
84
+ # Build Dispatcher
85
+ #
86
+ # @api private
87
+ def build(rack_app:, resolver:)
88
+ new(rack_app: rack_app, resolver: resolver).tap do |dispatcher|
89
+ definitions.each do |(name, args, kwargs, block)|
90
+ if block
91
+ dispatcher.register(name, *args, **kwargs, &block)
92
+ else
93
+ dispatcher.register(name, *args, **kwargs)
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "hanami/utils/hash"
4
+
5
+ module Hanami
6
+ module Lambda
7
+ # Base event class
8
+ #
9
+ # @since 0.2.0
10
+ class Event
11
+ attr_reader :raw
12
+
13
+ # @param event [Hash] the raw event from AWS Lambda
14
+ # @return [Hanami::Lambda::Event]
15
+ #
16
+ # @since 0.2.0
17
+ def initialize(event)
18
+ @raw = event
19
+ @event = Hanami::Utils::Hash.deep_symbolize(@raw)
20
+ freeze
21
+ end
22
+
23
+ # Return the value of the given key
24
+ #
25
+ # @param key [Symbol] the key to fetch
26
+ #
27
+ # @return [Object,NilClass] the associated value if found
28
+ #
29
+ # @since 0.2.0
30
+ def [](key)
31
+ @event[key]
32
+ end
33
+
34
+ # Return an value associated with the given event key
35
+ #
36
+ # @param keys [Array<Symbol, Integer>] the keys to fetch
37
+ #
38
+ # @return [Object,NilClass] the associated value if found
39
+ #
40
+ # @since 0.2.0
41
+ def get(*keys)
42
+ @event.dig(*keys)
43
+ end
44
+ alias_method :dig, :get
45
+
46
+ # Return the hash of the event
47
+ #
48
+ # @return [Hash] the hash of the event
49
+ #
50
+ # @since 0.2.0
51
+ def to_h
52
+ @event
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ module Lambda
5
+ module Events
6
+ class Base < Dry::Struct
7
+ transform_keys do |key|
8
+ Hanami::Lambda.inflector.underscore(key).to_sym
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ module Lambda
5
+ module Events
6
+ class EventBridge < Base
7
+ attribute :version, Types::Integer
8
+ attribute :id, Types::String
9
+ attribute :detail_type, Types::String
10
+ attribute :source, Types::String
11
+ attribute :account, Types::String
12
+ attribute :time, Types::Time
13
+ attribute :region, Types::String
14
+ attribute :resources, Types::Array.of(Types::String)
15
+ attribute :detail, Types::String
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hanami
4
+ module Lambda
5
+ # The base class for handler
6
+ #
7
+ # @since 0.2.0
8
+ class Function
9
+ # Override the Ruby's hook for modules
10
+ #
11
+ # @param base [Class] the target class
12
+ #
13
+ # @since 0.2.0
14
+ # @api private
15
+ def self.inherited(subclass)
16
+ super
17
+
18
+ subclass.extend ClassMethods
19
+
20
+ if instance_variable_defined?(:@event_type)
21
+ subclass.instance_variable_set(:@event_type, @event_type)
22
+ end
23
+ end
24
+
25
+ module ClassMethods
26
+ # Return the class which define the event type
27
+ #
28
+ # @return [Class] the class which define the event type
29
+ #
30
+ # @since 0.2.0
31
+ # @api private
32
+ def event_type
33
+ @event_type || Event
34
+ end
35
+
36
+ # Define the event type
37
+ #
38
+ # @param klass [Class] the class which define the event type
39
+ #
40
+ # @since 0.2.0
41
+ # @api private
42
+ def type(klass)
43
+ @event_type = klass
44
+ end
45
+ end
46
+
47
+ # @since 0.2.0
48
+ def call(event:, context:)
49
+ event = self.class.event_type.new(event)
50
+ handle(event, context)
51
+ end
52
+
53
+ protected
54
+
55
+ def handle(_event, _context); end
56
+ end
57
+ end
58
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "rack"
4
+
3
5
  module Hanami
4
6
  module Lambda
5
7
  # Rack interface for AWS Lambda.
@@ -7,13 +9,13 @@ module Hanami
7
9
  # @api private
8
10
  # @since 0.1.0
9
11
  class Rack
10
- attr_reader :app, :event, :context
12
+ attr_reader :app
11
13
 
12
- # @api private
13
- def initialize(app, event:, context:)
14
+ # Initialize the Rack interface
15
+ #
16
+ # @since 0.1.0
17
+ def initialize(app)
14
18
  @app = app
15
- @event = event
16
- @context = context
17
19
  end
18
20
 
19
21
  # Handle the request
@@ -21,7 +23,8 @@ module Hanami
21
23
  # @return [Hash] the response
22
24
  #
23
25
  # @since 0.1.0
24
- def call
26
+ def call(event:, context:)
27
+ env = build_env(event, context)
25
28
  status_code, headers, body = app.call(env)
26
29
 
27
30
  {
@@ -36,12 +39,14 @@ module Hanami
36
39
  # @return [Hash] the Rack environment
37
40
  #
38
41
  # @since 0.1.0
39
- def env
42
+ def build_env(event, context)
40
43
  {
41
44
  ::Rack::REQUEST_METHOD => event["httpMethod"],
42
45
  ::Rack::PATH_INFO => event["path"] || "",
43
46
  ::Rack::VERSION => ::Rack::VERSION,
44
- ::Rack::RACK_INPUT => StringIO.new(event["body"] || "")
47
+ ::Rack::RACK_INPUT => StringIO.new(event["body"] || ""),
48
+ ::Hanami::Lambda::LAMBDA_EVENT => event,
49
+ ::Hanami::Lambda::LAMBDA_CONTEXT => context
45
50
  }
46
51
  end
47
52
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-struct"
4
+
5
+ module Hanami
6
+ module Lambda
7
+ module Types
8
+ include Dry.Types(default: :params)
9
+ end
10
+ end
11
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Hanami
4
4
  module Lambda
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.0"
6
6
  end
7
7
  end
data/lib/hanami/lambda.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "zeitwerk"
4
+ require "dry-struct"
5
+ require "hanami"
4
6
 
5
7
  # @see Hanami::Lambda
6
8
  # @since 0.1.0
@@ -10,41 +12,52 @@ module Hanami
10
12
  # @since 0.1.0
11
13
  # @api private
12
14
  module Lambda
15
+ # @since 0.1.0
16
+ LAMBDA_EVENT = "lambda.event"
17
+
18
+ # @since 0.1.0
19
+ LAMBDA_CONTEXT = "lambda.context"
20
+
21
+ # @since 0.2.0
22
+ LAMBDA_CONFIG_PATH = File.join("config", "lambda")
23
+
24
+ # @since 0.2.0
25
+ LAMBDA_CLASS_NAME = "Lambda"
26
+
13
27
  @_mutex = Mutex.new
14
28
 
15
- # Returns the Hanami::Lambda application.
16
- #
17
- # @return [Hanami::Lambda::Application] the application
18
- # @raise [Hanami::AppLoadError] if the application isn't configured
29
+ # Return the application
19
30
  #
20
31
  # @api public
21
32
  # @since 0.1.0
33
+ #
34
+ # @return [Hanami::Lambda::Application] the application
22
35
  def self.app
23
- @_mutex.synchronize do
24
- unless defined?(@_app)
25
- raise Hanami::AppLoadError,
26
- "Hanami::Lambda.app is not yet configured. "
27
- end
36
+ Hanami.app
37
+ end
28
38
 
29
- @_app
30
- end
39
+ # Run the application
40
+ #
41
+ # @api public
42
+ def self.call(event:, context:)
43
+ app.boot
44
+ app.handle_lambda(event: event, context: context)
31
45
  end
32
46
 
47
+ # Inflector to convert event key
48
+ #
49
+ # @return [Dry::Inflector]
50
+ #
51
+ # @since 0.2.0
33
52
  # @api private
34
- # @since 0.1.0
35
- def self.app=(klass)
53
+ def self.inflector
36
54
  @_mutex.synchronize do
37
- raise AppLoadError, "Hanami::Lambda.app is already configured." if instance_variable_defined?(:@_app)
55
+ return @inflector if defined?(@inflector)
38
56
 
39
- @_app = klass unless klass.name.nil?
57
+ @inflector ||= Dry::Inflector.new
40
58
  end
41
- end
42
-
43
- def self.call(event:, context:)
44
- require "hanami/setup"
45
59
 
46
- Hanami.boot
47
- app.call(event: event, context: context)
60
+ @inflector
48
61
  end
49
62
 
50
63
  # @since 0.1.0
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hanami-lambda
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aotokitsuruya
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-01-01 00:00:00.000000000 Z
11
+ date: 2024-01-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: zeitwerk
@@ -24,6 +24,62 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '2.6'
27
+ - !ruby/object:Gem::Dependency
28
+ name: hanami
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: hanami-utils
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: dry-struct
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: dry-inflector
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.0'
27
83
  - !ruby/object:Gem::Dependency
28
84
  name: rubocop
29
85
  requirement: !ruby/object:Gem::Requirement
@@ -52,9 +108,16 @@ files:
52
108
  - README.md
53
109
  - Rakefile
54
110
  - lib/hanami-lambda.rb
111
+ - lib/hanami/errors.rb
55
112
  - lib/hanami/lambda.rb
56
113
  - lib/hanami/lambda/application.rb
114
+ - lib/hanami/lambda/dispatcher.rb
115
+ - lib/hanami/lambda/event.rb
116
+ - lib/hanami/lambda/events/base.rb
117
+ - lib/hanami/lambda/events/event_bridge.rb
118
+ - lib/hanami/lambda/function.rb
57
119
  - lib/hanami/lambda/rack.rb
120
+ - lib/hanami/lambda/types.rb
58
121
  - lib/hanami/lambda/version.rb
59
122
  - sig/hanami/lambda.rbs
60
123
  homepage: https://github.com/elct9620/hanami-lambda