LFA 0.2.0

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