LFA 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,236 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require 'cgi'
5
+ require 'stringio'
6
+
7
+ module LFA
8
+ class Adapter
9
+ module LambdaRackBridge
10
+ def request_path(env:)
11
+ env["REQUEST_PATH"]
12
+ end
13
+
14
+ def http_method(env:)
15
+ env["REQUEST_METHOD"]
16
+ end
17
+
18
+ def lambda_event(env:, path_parameters:)
19
+ headers, mv_headers = build_headers(env)
20
+ query_params, mv_query_params = build_query_params(env)
21
+ {
22
+ "resource" => env["REQUEST_PATH"],
23
+ "path" => env["REQUEST_PATH"],
24
+ "httpMethod" => env["REQUEST_METHOD"],
25
+ "headers" => headers,
26
+ "multiValueHeaders" => mv_headers,
27
+ "queryStringParameters" => query_params,
28
+ "multiValueQueryStringParameters" => mv_query_params,
29
+ "pathParameters" => path_parameters, # set when path parameters are supported
30
+ "stageVariables" => nil, # LFA will not support deployments on stages
31
+ "requestContext" => {}, # skipped for now because it seems not useful...
32
+ "body" => env["rack.input"],
33
+ "isBase64Encoded" => false, # always false about request? true if the request was binary?
34
+ }
35
+ end
36
+
37
+ def lambda_context(function_name:)
38
+ LambdaContext.new(function_name)
39
+ end
40
+
41
+ def convert_to_rack_response(resp)
42
+ ### Lambda output
43
+ # {
44
+ # statusCode: 200,
45
+ # body: records.to_json,
46
+ # headers: {"content-type" => "application/json"},
47
+ # }
48
+ ### Rack response
49
+ # [200, {"content-type" => "application/json"}, body]
50
+ body = StringIO.new(resp[:body])
51
+ [resp[:statusCode], resp[:headers], body]
52
+ end
53
+
54
+ HEADER_ENV_KEYS = ['CONTENT_TYPE', 'CONTENT_LENGTH']
55
+
56
+ def build_headers(env)
57
+ headers = Headers.new
58
+ multi_value_headers = Headers.new
59
+ env.keys.each do |env_key_name|
60
+ next unless env_key_name =~ /^HTTP_/ || HEADER_ENV_KEYS.include?(env_key_name)
61
+ next if env_key_name == 'HTTP_VERSION'
62
+ header_name = header_name_from_env_name(env_key_name)
63
+ value = env[env_key_name]
64
+ if value.include?(", ")
65
+ multi_value_headers[header_name] = value.split(", ").select{|s| !s.empty? }.compact
66
+ headers[header_name] = value.split(", ").select{|s| !s.empty? }.compact.last
67
+ else
68
+ multi_value_headers[header_name] = [value]
69
+ headers[header_name] = value
70
+ end
71
+ end
72
+ return headers, multi_value_headers
73
+ end
74
+
75
+ def build_query_params(env)
76
+ query = env["QUERY_STRING"]
77
+ return nil, nil if !query || query.empty?
78
+
79
+ multi_value_query_params = CGI.parse(query)
80
+ query_params = {}
81
+ multi_value_query_params.each do |key, value|
82
+ query_params[key] = value.last
83
+ end
84
+ return query_params, multi_value_query_params
85
+ end
86
+
87
+ def header_name_from_env_name(name)
88
+ name.sub('HTTP_', '').gsub('_', '-').downcase
89
+ end
90
+
91
+ class Headers < Hash
92
+ def [](key)
93
+ super(key.downcase)
94
+ end
95
+
96
+ def []=(key, value)
97
+ super(key.downcase, value)
98
+ end
99
+
100
+ def has_key?(key)
101
+ super(key.downcase)
102
+ end
103
+ end
104
+
105
+ class LambdaContext
106
+ # Lambda Context https://docs.aws.amazon.com/lambda/latest/dg/ruby-context.html
107
+ # Context methods
108
+ # # get_remaining_time_in_millis – Returns the number of milliseconds left before the execution times out.
109
+ # Context properties
110
+ # # function_name – The name of the Lambda function.
111
+ # # function_version – The version of the function.
112
+ # # invoked_function_arn – The Amazon Resource Name (ARN) that's used to invoke the function.
113
+ # # Indicates if the invoker specified a version number or alias.
114
+ # # memory_limit_in_mb – The amount of memory that's allocated for the function.
115
+ # # aws_request_id – The identifier of the invocation request.
116
+ # # log_group_name – The log group for the function.
117
+ # # log_stream_name – The log stream for the function instance.
118
+ # # deadline_ms– The date that the execution times out, in Unix time milliseconds.
119
+ # # identity – (mobile apps) Information about the Amazon Cognito identity that authorized the request.
120
+ # # client_context– (mobile apps) Client context that's provided to Lambda by the client application.
121
+
122
+ attr_reader :function_name, :function_version, :invoked_function_arn, :memory_limit_in_mb
123
+ attr_reader :aws_request_id, :log_group_name, :log_stream_name, :deadline_ms, :identity, :client_context
124
+
125
+ def initialize(function_name)
126
+ @function_name = function_name
127
+ @function_version = 1
128
+ @invoked_function_arn = "arn:tagomoris:lambda:ap-northeast-999:0000000000000:function:#{function_name}"
129
+ @memory_limit_in_mb = 1024 # TODO: configurable?
130
+ @aws_request_id = SecureRandom.uuid
131
+ @log_group_name = "/tagomoris/lambda/#{function_name}"
132
+ @log_stream_name = "2022/12/15[$LATEST]0000000000000000000000"
133
+ @deadline_ms = 3600 * 1000
134
+ @identity = nil
135
+ @client_context = nil
136
+ end
137
+
138
+ def get_remaining_time_in_millis
139
+ 3600 * 1000
140
+ end
141
+ end
142
+
143
+ # Rack ENV
144
+ # {
145
+ # "rack.version"=>[1, 6],
146
+ # "rack.errors"=>obj,#<Rack::Lint::Wrapper::ErrorWrapper:0x0000000107036620 @error=#<IO:<STDERR>>>,
147
+ # "rack.multithread"=>true,
148
+ # "rack.multiprocess"=>false,
149
+ # "rack.run_once"=>false,
150
+ # "rack.url_scheme"=>"http",
151
+ # "SCRIPT_NAME"=>"",
152
+ # "QUERY_STRING"=>"key=value",
153
+ # "SERVER_SOFTWARE"=>"puma 6.0.0 Sunflower",
154
+ # "GATEWAY_INTERFACE"=>"CGI/1.2",
155
+ # "REQUEST_METHOD"=>"GET",
156
+ # "REQUEST_PATH"=>"/api/language",
157
+ # "REQUEST_URI"=>"/api/language?key=value",
158
+ # "SERVER_PROTOCOL"=>"HTTP/1.1",
159
+ # "HTTP_HOST"=>"127.0.0.1:9292",
160
+ # "HTTP_USER_AGENT"=>"curl/7.79.1",
161
+ # "HTTP_ACCEPT"=>"*/*",
162
+ # "HTTP_X_YAY"=>"one, two", # Multi value headers
163
+ # "puma.request_body_wait"=>0.010000228881835938,
164
+ # "SERVER_NAME"=>"127.0.0.1",
165
+ # "SERVER_PORT"=>"9292",
166
+ # "PATH_INFO"=>"/api/language",
167
+ # "REMOTE_ADDR"=>"127.0.0.1",
168
+ # "HTTP_VERSION"=>"HTTP/1.1",
169
+ # "puma.socket"=>obj,#<TCPSocket:fd 16, AF_INET, 127.0.0.1, 9292>,
170
+ # "rack.hijack?"=>true,
171
+ # "rack.hijack"=>obj,#<Proc:0x0000000107036a30 /versions/3.1.0/lib/ruby/gems/3.1.0/gems/rack-3.0.2/lib/rack/lint.rb:556>,
172
+ # "rack.input"=>obj,#<Rack::Lint::Wrapper::InputWrapper:0x0000000107036710 @input=#<Puma::NullIO:0x0000000106fb4b48>>,
173
+ # "rack.after_reply"=>[],
174
+ # "puma.config"=>obj,
175
+ # "rack.tempfiles"=>[],
176
+ # }
177
+
178
+ # Lambda Event
179
+ # {
180
+ # "resource"=>"/api/raw_data",
181
+ # "path"=>"/api/raw_data",
182
+ # "httpMethod"=>"GET",
183
+ # "headers"=>{
184
+ # "accept"=>"*/*", "Host"=>"1jlbwglsci.execute-api.ap-northeast-1.amazonaws.com",
185
+ # "User-Agent"=>"curl/7.77.0", "X-Amzn-Trace-Id"=>"Root=1-61d5681d-2f0156ce0aeee3896ab2cf1f",
186
+ # "X-Forwarded-For"=>"138.64.70.55", "X-Forwarded-Port"=>"443", "X-Forwarded-Proto"=>"https",
187
+ # "x-pathtraq-apikey"=>"711e31a7-5248-45bc-92b8-c39740842a5f"
188
+ # },
189
+ # "multiValueHeaders"=>{
190
+ # "accept"=>["*/*"], "Host"=>["1jlbwglsci.execute-api.ap-northeast-1.amazonaws.com"],
191
+ # "User-Agent"=>["curl/7.77.0"], "X-Amzn-Trace-Id"=>["Root=1-61d5681d-2f0156ce0aeee3896ab2cf1f"],
192
+ # "X-Forwarded-For"=>["138.64.70.55"], "X-Forwarded-Port"=>["443"], "X-Forwarded-Proto"=>["https"],
193
+ # "x-pathtraq-apikey"=>["711e31a7-5248-45bc-92b8-c39740842a5f"]
194
+ # },
195
+ # "queryStringParameters"=>nil,
196
+ # "multiValueQueryStringParameters"=>nil,
197
+ # "pathParameters"=>nil,
198
+ # "stageVariables"=>nil,
199
+ # "requestContext"=>{
200
+ # "resourceId"=>"541ciq", "resourcePath"=>"/api/raw_data",
201
+ # "httpMethod"=>"GET", "extendedRequestId"=>"Ld00sEvoNjMF8Zg=",
202
+ # "requestTime"=>"05/Jan/2022:09:42:53 +0000",
203
+ # "path"=>"/test/api/raw_data", "accountId"=>"752037627773",
204
+ # "protocol"=>"HTTP/1.1", "stage"=>"test", "domainPrefix"=>"1jlbwglsci",
205
+ # "requestTimeEpoch"=>1641375773896, "requestId"=>"32d4b1da-ba45-4758-a2db-467194ffca11",
206
+ # "identity"=>{
207
+ # "cognitoIdentityPoolId"=>nil, "accountId"=>nil, "cognitoIdentityId"=>nil, "caller"=>nil,
208
+ # "sourceIp"=>"138.64.70.55", "principalOrgId"=>nil, "accessKey"=>nil,
209
+ # "cognitoAuthenticationType"=>nil, "cognitoAuthenticationProvider"=>nil, "userArn"=>nil,
210
+ # "userAgent"=>"curl/7.77.0", "user"=>nil
211
+ # },
212
+ # "domainName"=>"1jlbwglsci.execute-api.ap-northeast-1.amazonaws.com",
213
+ # "apiId"=>"1jlbwglsci"
214
+ # },
215
+ # "body"=>nil,
216
+ # "isBase64Encoded"=>false
217
+ # }
218
+
219
+ # Lambda Context https://docs.aws.amazon.com/lambda/latest/dg/ruby-context.html
220
+ # Context methods
221
+ # # get_remaining_time_in_millis – Returns the number of milliseconds left before the execution times out.
222
+ # Context properties
223
+ # # function_name – The name of the Lambda function.
224
+ # # function_version – The version of the function.
225
+ # # invoked_function_arn – The Amazon Resource Name (ARN) that's used to invoke the function.
226
+ # # Indicates if the invoker specified a version number or alias.
227
+ # # memory_limit_in_mb – The amount of memory that's allocated for the function.
228
+ # # aws_request_id – The identifier of the invocation request.
229
+ # # log_group_name – The log group for the function.
230
+ # # log_stream_name – The log stream for the function instance.
231
+ # # deadline_ms– The date that the execution times out, in Unix time milliseconds.
232
+ # # identity – (mobile apps) Information about the Amazon Cognito identity that authorized the request.
233
+ # # client_context– (mobile apps) Client context that's provided to Lambda by the client application.
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'adapter/executor'
4
+ require_relative 'adapter/lambda_rack_bridge'
5
+ require_relative 'adapter/environment'
6
+
7
+ __original_warning_level = $VERBOSE
8
+ begin
9
+ $VERBOSE = nil
10
+ ::ENV = LFA::Adapter::EnvMimic.new
11
+ ensure
12
+ $VERBOSE = __original_warning_level
13
+ end
14
+
15
+ module LFA
16
+ class Adapter
17
+ def initialize(resolver)
18
+ @resolver = resolver
19
+ @executors = {}
20
+ end
21
+
22
+ include LambdaRackBridge
23
+
24
+ def call(env)
25
+ begin
26
+ path = request_path(env: env)
27
+ method = http_method(env: env)
28
+
29
+ matched = @resolver.resolve(path, method)
30
+ unless matched
31
+ return [404, {}, ["Resource not found"]]
32
+ end
33
+ function = matched.function
34
+ executor = @executors.fetch(function.name){ Executor.setup(function) }
35
+ unless @executors.has_key?(function.name)
36
+ @executors[function.name] = executor
37
+ end
38
+
39
+ event = lambda_event(env: env, path_parameters: matched.path_parameters.dup)
40
+ context = lambda_context(function_name: function.name)
41
+
42
+ result = executor.call(event: event, context: context)
43
+ return convert_to_rack_response(result) # => [200, {}, ["OK"]]
44
+ rescue => e
45
+ p(here: "unexpected error", error_class: e.class, error: e.message)
46
+ puts e.backtrace
47
+ return [500, {}, ["Internal Server Error"]]
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LFA
4
+ module Handler
5
+ module Parameterized
6
+ def stringify(value)
7
+ case value
8
+ when Array
9
+ value.map(&:to_s).join(', ')
10
+ when String
11
+ value
12
+ when nil
13
+ nil
14
+ else
15
+ value.to_s
16
+ end
17
+ end
18
+ end
19
+
20
+ class CORSPreflight
21
+ include Parameterized
22
+
23
+ def initialize(params)
24
+ @allow_origins = stringify(params[:allowOrigins])
25
+ @mirror_allow_origin = params[:mirrorAllowOrigin]
26
+
27
+ @allow_credentials = stringify(params[:allowCredentials])
28
+ @allow_headers = stringify(params[:allowHeaders])
29
+ @allow_methods = stringify(params[:allowMethods])
30
+ @expose_headers = stringify(params[:exposeHeaders])
31
+ @max_age = stringify(params[:maxAge])
32
+
33
+ if @allow_origins && @mirror_allow_origin
34
+ raise "Configuration error, allowOrigins and mirrorAllowOrigin are exclusive"
35
+ end
36
+ end
37
+
38
+ def call(event:, context:)
39
+ unless event.fetch("httpMethod") == 'OPTIONS'
40
+ raise "CORS handler can respond to OPTIONS only, but the request is '#{event.fetch("httpMethod")}'"
41
+ end
42
+
43
+ # This handler ignores the preflight request headers below:
44
+ # * Access-Control-Request-Method
45
+ # * Access-Control-Request-Headers
46
+ # System administrator should be able to configure this handler without use of those headers, probably.
47
+ origin = event.dig("headers", "origin")
48
+ {statusCode: 200, body: '', headers: cors_headers(origin)}
49
+ end
50
+
51
+ def cors_headers(origin)
52
+ headers = {}
53
+ if @allow_origins
54
+ headers['access-control-allow-origin'] = @allow_origins
55
+ end
56
+ if @mirror_allow_origin
57
+ headers['access-control-allow-origin'] = origin
58
+ headers['vary'] = 'Origin'
59
+ end
60
+ if @allow_credentials
61
+ headers['access-control-allow-credentials'] = @allow_credentials
62
+ end
63
+ if @allow_headers
64
+ headers['access-control-allow-headers'] = @allow_headers
65
+ end
66
+ if @allow_methods
67
+ headers['access-control-allow-methods'] = @allow_methods
68
+ end
69
+ if @expose_headers
70
+ headers['access-control-expose-headers'] = @expose_headers
71
+ end
72
+ if @max_age
73
+ headers['access-control-max-age'] = @max_age
74
+ end
75
+
76
+ headers
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module LFA
6
+ module Router
7
+ RESOURCE_METHODS = [
8
+ :GET, :POST, :PUT, :OPTIONS, :ANY,
9
+ ].freeze
10
+
11
+ class Config
12
+ attr_reader :resources, :functions
13
+
14
+ def self.parse(yaml_filename)
15
+ tree = File.open(yaml_filename) do |file|
16
+ YAML.load(file.read, symbolize_names: true, aliases: true)
17
+ end
18
+ dirname = File.dirname(File.absolute_path(yaml_filename))
19
+ Config.new(tree, dirname)
20
+ end
21
+
22
+ def initialize(tree, dirname)
23
+ # TODO: path-method-to-function cache
24
+ raise "functions is not specified" unless tree[:functions]
25
+ raise "resources is not specified" unless tree[:resources]
26
+ @functions = Hash[*(tree[:functions].map{|f| Function.new(f, dirname) }.map{|f| [f.name, f] }.flatten)]
27
+ @resources = tree[:resources].map{|r| Resource.new(r, @functions) }
28
+ end
29
+
30
+ def dig(path, method)
31
+ raise "invalid path" unless path.start_with?("/")
32
+ parts = if path == "/"
33
+ ["/"]
34
+ else
35
+ path.split("/").filter{|str| str.size > 0 }.map{|part| "/" + part}
36
+ end
37
+ resource = self
38
+ path_parameters = {}
39
+ parts.each_with_index do |part, index|
40
+ resource = resource.resources.find{|r| r.is_wildcard? || r.path == part }
41
+ if resource
42
+ if resource.is_wildcard? && resource.is_greedy_wildcard?
43
+ path_parameters[resource.parameter_name] = (parts[index..-1].join)[1..-1] # omit heading '/'
44
+ break
45
+ elsif resource.is_wildcard?
46
+ path_parameters[resource.parameter_name] = part[1..-1] # omit heading '/'
47
+ end
48
+ else # resource == nil
49
+ break
50
+ end
51
+ end
52
+ if resource
53
+ if resource.is_greedy_wildcard?
54
+ MatchedFunction.new(
55
+ function: resource.methods.fetch(:ANY),
56
+ path_parameters: path_parameters,
57
+ )
58
+ else
59
+ func = resource.methods[method.to_sym]
60
+ if func
61
+ MatchedFunction.new(
62
+ function: func,
63
+ path_parameters: path_parameters.size > 0 ? path_parameters : nil,
64
+ )
65
+ else
66
+ nil
67
+ end
68
+ end
69
+ else
70
+ nil # when the resource is not found
71
+ end
72
+ end
73
+ end
74
+
75
+ MatchedFunction = Data.define(:function, :path_parameters)
76
+
77
+ class Resource
78
+ attr_reader :path, :parameter_name, :methods, :resources
79
+
80
+ def initialize(obj, functions)
81
+ @path = obj[:path]
82
+ raise "path must start with '/'" unless @path.start_with?('/')
83
+ @methods_hash = obj[:methods] || []
84
+ @resources_array = obj[:resources] || []
85
+ @methods = {}
86
+ @methods_hash.each do |method_name, function_name|
87
+ raise "unsupported method '#{method_name}'" unless RESOURCE_METHODS.include?(method_name)
88
+ raise "function name missing '#{function_name}'" unless functions.has_key?(function_name)
89
+ raise "duplicated config on method '#{method_name}'" if @methods[method_name]
90
+ @methods[method_name] = functions[function_name]
91
+ end
92
+ @resources = @resources_array.map{|resource_obj| Resource.new(resource_obj, functions) }
93
+
94
+ @parameter_name = nil
95
+ @greedy_match = nil
96
+ if @path.start_with?('/{') && @path.end_with?('}')
97
+ if @path.end_with?('+}')
98
+ @parameter_name = @path[2..-3]
99
+ @greedy_match = true
100
+ else
101
+ @parameter_name = @path[2..-2]
102
+ @greedy_match = false
103
+ end
104
+ end
105
+ if @greedy_match.!.! && @methods.keys != [:ANY]
106
+ raise "resource with a greedy path parameter '{part+}' must respond to only ANY method"
107
+ end
108
+ end
109
+
110
+ def is_wildcard?
111
+ @parameter_name != nil
112
+ end
113
+
114
+ def is_greedy_wildcard?
115
+ @greedy_match.!.!
116
+ end
117
+ end
118
+
119
+ class Function
120
+ attr_reader :name, :handler, :env, :params, :dirname
121
+
122
+ def initialize(obj, dirname)
123
+ raise "function name, handler are mandatory" unless obj[:name] && obj[:handler]
124
+ @name = obj[:name]
125
+ @handler_name = obj[:handler]
126
+ @env = obj[:env] || {}
127
+ @params = obj[:params] || {}
128
+ @handler = if @handler_name =~ /\A[A-Z0-9_]+\z/
129
+ BuiltInHandler.new(@handler_name)
130
+ else
131
+ Handler.parse(@handler_name, dirname)
132
+ end
133
+ end
134
+
135
+ def inspect
136
+ "<Function name: #{@name}, handler: #{@handler}, env: #{@env}, params: #{@params}>"
137
+ end
138
+ end
139
+
140
+ class BuiltInHandler
141
+ attr_reader :name
142
+
143
+ def initialize(handler_name)
144
+ @name = handler_name
145
+ end
146
+
147
+ def builtin?
148
+ true
149
+ end
150
+
151
+ def inspect
152
+ "<BuiltInHandler #{@name}>"
153
+ end
154
+ end
155
+
156
+ class Handler
157
+ attr_reader :filename, :klass, :method
158
+
159
+ def initialize(filename, klass, method, dirname)
160
+ @filename = filename
161
+ @klass = klass
162
+ @method = method
163
+ @dirname = dirname
164
+ end
165
+
166
+ def self.parse(name, dirname)
167
+ filename, klass, method = name.split('.', 3)
168
+ raise "invalid handler format '#{name}'" unless filename && klass && method
169
+ Handler.new(filename, klass, method, dirname)
170
+ end
171
+
172
+ def builtin?
173
+ false
174
+ end
175
+
176
+ def inspect
177
+ "<Handler #{@filename}.#{@klass}.#{@method}>"
178
+ end
179
+
180
+ def path
181
+ File.join(@dirname, @filename + '.rb')
182
+ end
183
+ end
184
+ end
185
+ end
data/lib/LFA/router.rb ADDED
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './router/config'
4
+
5
+ module LFA
6
+ module Router
7
+ def self.resolver(config_filename)
8
+ config = Config.parse(config_filename)
9
+ return Resolver.new(config)
10
+ end
11
+
12
+ class Resolver
13
+ def initialize(config)
14
+ @config = config
15
+ @cache = {}
16
+ end
17
+
18
+ def resolve(path, method)
19
+ cache_key = "#{method}\t#{path}"
20
+ return @cache[cache_key] if @cache.has_key?(cache_key) # return stored nil when the cache key exists
21
+
22
+ matched = @config.dig(path, method)
23
+ @cache[cache_key] = matched # store negative cache to not pay too much cost for 404 even when nil (404)
24
+ matched
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LFA
4
+ VERSION = "0.2.0"
5
+ end
data/lib/LFA.rb ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "LFA/version"
4
+ require_relative "LFA/router"
5
+ require_relative "LFA/adapter"
6
+
7
+ module LFA
8
+ def self.ignition!(config_filename)
9
+ router = Router.resolver(config_filename)
10
+ return Adapter.new(router)
11
+ end
12
+ end
data/myfunc.rb ADDED
@@ -0,0 +1,41 @@
1
+ require 'json'
2
+
3
+ module Countries
4
+ MESSAGE = ENV.fetch("KEY2", "yay")
5
+
6
+ def self.process(event:, context:)
7
+ data = ENV.fetch("KEY1", "y")
8
+ {
9
+ statusCode: 200,
10
+ body: {"data" => data, "message" => MESSAGE}.to_json,
11
+ headers: {"content-type" => "application/json"},
12
+ }
13
+ end
14
+ end
15
+
16
+ require_relative 'data'
17
+
18
+ module City
19
+ # "resource"=>"/api/place/{place_id}", "path"=>"/api/place/tokyo", "pathParameters"=>{"place_id"=>"tokyo"}
20
+ def self.process(event:, context:)
21
+ city_name = event.dig("pathParameters", "place_id")
22
+ {
23
+ statusCode: 200,
24
+ body: {"city_name" => city_name, "message" => "good city!"}.to_json,
25
+ headers: {"content-type" => "application/json"},
26
+ }
27
+ end
28
+ end
29
+
30
+ module Town
31
+ # "resource"=>"/api/town/{names+}", "path"=>"/api/checkin/yay1", "pathParameters"=>{"names"=>"yay1"}
32
+ # "resource"=>"/api/town/{names+}", "path"=>"/api/checkin/yay1/foo2/bar3", "pathParameters"=>{"names"=>"yay1/foo2/bar3"},
33
+ def self.process(event:, context:)
34
+ towns = event.dig("pathParameters", "names").split("/")
35
+ {
36
+ statusCode: 200,
37
+ body: {"towns" => towns, "message" => "good towns!"}.to_json,
38
+ headers: {"content-type" => "application/json"},
39
+ }
40
+ end
41
+ end
data/sig/LFA.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module LFA
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
data/test.rb ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/LFA'
4
+
5
+ config = LFA::Router::Config.parse('config.yaml')
6
+ pp(here: "GET /api/country", function: config.dig("/api/country", "GET"))
7
+ pp(here: "GET /api/language", function: config.dig("/api/language", "GET"))
8
+ pp(here: "PUT /api/language", function: config.dig("/api/language", "PUT"))
9
+ pp(here: "GET /api/data/csv", function: config.dig("/api/data/csv", "GET"))
10
+ pp(here: "GET /api/data/json", function: config.dig("/api/data/json", "GET"))
11
+