Skip to content

Commit 5d089a9

Browse files
committed
PoC: Lambda dev server
1 parent 4e1f3ad commit 5d089a9

File tree

6 files changed

+236
-18
lines changed

6 files changed

+236
-18
lines changed

dune-project

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,4 @@
22

33
(using fmt 1.1)
44

5-
(implicit_transitive_deps false)
6-
75
(name lambda-runtime)

lib/dune

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
(preprocess
55
(pps ppx_deriving_yojson))
66
(libraries yojson ppx_deriving_yojson.runtime bigstringaf bigarray-compat
7-
uri logs.lwt lwt lwt.unix piaf result))
7+
uri logs.lwt lwt lwt.unix piaf result uuidm))

lib/http.ml

Lines changed: 94 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ type api_gateway_request_identity =
5050
; user_agent : string option [@key "userAgent"]
5151
; user : string option
5252
}
53-
[@@deriving of_yojson]
53+
[@@deriving yojson]
5454

5555
(* APIGatewayProxyRequestContext contains the information to identify the AWS
5656
* account and resources invoking the Lambda function. It also includes Cognito
@@ -69,7 +69,7 @@ type api_gateway_proxy_request_context =
6969
; path : string option [@default None]
7070
; api_id : string [@key "apiId"] (* The API Gateway REST API ID *)
7171
}
72-
[@@deriving of_yojson { strict = false }]
72+
[@@deriving yojson { strict = false }]
7373

7474
type api_gateway_proxy_request =
7575
{ resource : string
@@ -86,7 +86,7 @@ type api_gateway_proxy_request =
8686
; body : string option
8787
; is_base64_encoded : bool [@key "isBase64Encoded"]
8888
}
89-
[@@deriving of_yojson { strict = false }]
89+
[@@deriving yojson { strict = false }]
9090

9191
type api_gateway_proxy_response =
9292
{ status_code : int [@key "statusCode"]
@@ -97,13 +97,103 @@ type api_gateway_proxy_response =
9797
[@@deriving to_yojson]
9898

9999
module API_gateway_request = struct
100-
type t = api_gateway_proxy_request [@@deriving of_yojson]
100+
type t = api_gateway_proxy_request [@@deriving yojson]
101+
102+
let mock (ctx: 'a Piaf.Server.ctx) =
103+
let piaf_headers_to_string_map headers =
104+
headers
105+
|> Piaf.Headers.to_list
106+
|> List.to_seq
107+
|> StringMap.of_seq
108+
in
109+
let identity = { cognito_identity_pool_id = None
110+
; account_id = None
111+
; cognito_identity_id = None
112+
; caller = None
113+
; access_key = None
114+
; api_key = None
115+
(* TODO: Use the actual IP address from Unix.sockaddr *)
116+
; source_ip = "127.0.0.1"
117+
; cognito_authentication_type = None
118+
; cognito_authentication_provider = None
119+
; user_arn = None
120+
; user_agent = Piaf.Headers.get ctx.request.headers "User-Agent"
121+
; user = None }
122+
in
123+
let uri = Piaf.Request.uri ctx.request in
124+
let request_id =
125+
(* TODO: Not sure if this is equivalent to aws_request_id, but probably. Need to confirm. *)
126+
Random.self_init ();
127+
Uuidm.to_string @@ Uuidm.v4_gen (Random.get_state ()) ()
128+
in
129+
let request_context = { account_id = "123456789012"
130+
; resource_id = "123456"
131+
; stage = "dev"
132+
; request_id
133+
; identity
134+
; resource_path = "/{proxy+}"
135+
; authorizer = None
136+
; http_method = Piaf.Method.to_string ctx.request.meth
137+
; protocol = Some (Piaf.Versions.HTTP.to_string ctx.request.version)
138+
; path = Some (Uri.path uri)
139+
; api_id = "1234567890" }
140+
in
141+
let body =
142+
Lwt_result.map_err Piaf.Error.to_string (Piaf.Body.to_string ctx.request.body)
143+
in
144+
let query_string_parameters =
145+
Uri.query uri
146+
|> List.map (fun (key, values) ->
147+
match List.length values with
148+
| 0 -> (key, "")
149+
| 1 -> (key, List.hd values)
150+
(* TODO: Property handle this one *)
151+
| _ -> failwith "Multiple values not supported for query strings for now")
152+
|> List.to_seq
153+
|> StringMap.of_seq
154+
in
155+
Lwt_result.map (fun body ->
156+
(to_yojson { resource = (Uri.path uri)
157+
; path = "/{proxy+}"
158+
; http_method = request_context.http_method
159+
; headers = piaf_headers_to_string_map ctx.request.headers
160+
; query_string_parameters
161+
; path_parameters = StringMap.empty
162+
; stage_variables = StringMap.empty
163+
; request_context
164+
; body = if body = String.empty then None else Some body
165+
; is_base64_encoded = false }))
166+
body
167+
168+
let response _ =
169+
Error "Not implemented"
101170
end
102171

103172
module API_gateway_response = struct
104173
type t = api_gateway_proxy_response [@@deriving to_yojson]
105174

106175
let to_yojson t = Lwt.return (to_yojson t)
176+
177+
let mock _ =
178+
Lwt_result.fail "Not implemented"
179+
180+
let response response =
181+
let string_map_to_piaf_headers headers =
182+
headers
183+
|> StringMap.to_seq
184+
|> List.of_seq
185+
|> Piaf.Headers.of_list
186+
in
187+
(* TODO: Support base64? *)
188+
assert (not response.is_base64_encoded);
189+
190+
let response =
191+
Piaf.Response.of_string
192+
~headers:(string_map_to_piaf_headers response.headers)
193+
~body:(response.body)
194+
(Piaf.Status.of_code response.status_code)
195+
in
196+
Ok response
107197
end
108198

109199
include Runtime.Make (API_gateway_request) (API_gateway_response)

lib/json.ml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,26 @@ module Id = struct
3434
type t = Yojson.Safe.t [@@deriving yojson]
3535

3636
let to_yojson t = Lwt.return (to_yojson t)
37+
38+
let mock (ctx: 'a Piaf.Server.ctx) =
39+
let event =
40+
let open Lwt_result.Infix in
41+
Piaf.Body.to_string ctx.request.body
42+
>|= (fun body -> if body = String.empty then None else Some body)
43+
>|= Option.map Yojson.Safe.from_string
44+
>|= Option.value ~default:`Null
45+
in
46+
Lwt_result.map_err Piaf.Error.to_string event
47+
48+
let response response =
49+
let body = Yojson.Safe.to_string response in
50+
let response = Piaf.Response.of_string
51+
~headers:(Piaf.Headers.of_list [ ("Content-Type", "application/json") ])
52+
~body:body
53+
`OK
54+
in
55+
Ok response
56+
3757
end
3858

3959
include Runtime.Make (Id) (Id)

lib/runtime.ml

Lines changed: 113 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232

3333
open Lwt.Infix
3434

35+
module StringMap = Map.Make(String)
36+
3537
module Make
3638
(Event : Runtime_intf.LambdaEvent)
3739
(Response : Runtime_intf.LambdaResponse) =
@@ -151,19 +153,119 @@ struct
151153
failwith
152154
(Format.asprintf "Could not start HTTP client: %a" Piaf.Error.pp_hum e)
153155

156+
let make_mocked_context () =
157+
let function_settings = Config.{ function_name = "unknown"
158+
; memory_size = 128 (* typical value for a lambda *)
159+
; version = "$LATEST"
160+
(* TODO: Find values for those *)
161+
; log_stream = ""
162+
; log_group = "" }
163+
in
164+
let request_id =
165+
Random.self_init ();
166+
Uuidm.to_string @@ Uuidm.v4_gen (Random.get_state ()) ()
167+
in
168+
Context.make
169+
~invoked_function_arn:("arn:aws:lambda:local:123456789012:function:" ^ function_settings.function_name)
170+
~aws_request_id:request_id
171+
~xray_trace_id:None
172+
~client_context:None
173+
~identity:None
174+
~deadline:0L
175+
function_settings
176+
177+
(* TODO: Maybe local invocation functions can be in another module. *)
178+
let invoke_locally ~lift handler event =
179+
let context = make_mocked_context () in
180+
let open Lwt_result.Infix in
181+
Lwt_result.lift @@ Event.of_yojson event
182+
>>= fun event -> lift (handler event context)
183+
184+
let start_locally ~lift handler =
185+
let make_response res =
186+
match Result.bind res Response.response with
187+
| Ok response ->
188+
response
189+
| Error msg ->
190+
let body = Printf.sprintf "Error: %s" msg in
191+
Piaf.Response.of_string ~body `Internal_server_error
192+
in
193+
let server_handler (_ctx: Unix.sockaddr Piaf.Server.ctx) =
194+
let event =
195+
Event.mock _ctx
196+
>|= (function
197+
| Ok event -> event
198+
| Error msg -> failwith msg)
199+
in
200+
let response =
201+
event
202+
>>= invoke_locally ~lift handler
203+
in
204+
response >|= make_response
205+
in
206+
Lwt.async (fun () ->
207+
let address = Unix.(ADDR_INET (inet_addr_loopback, 5000)) in
208+
Lwt_io.establish_server_with_client_socket
209+
address
210+
(Piaf.Server.create server_handler)
211+
>|= fun _server ->
212+
print_endline "Server started at: http://127.0.0.1:5000"
213+
);
214+
let forever, _ = Lwt.wait () in
215+
forever
216+
217+
let handle_local_invoke ~lift handler event_file =
218+
let event =
219+
match event_file with
220+
| Some filename ->
221+
Lwt_io.(open_file ~mode:Input filename)
222+
>>= Lwt_io.read
223+
>|= Yojson.Safe.from_string
224+
| None ->
225+
Lwt.return `Null
226+
in
227+
event
228+
>>= invoke_locally ~lift handler
229+
>>= (function
230+
| Ok response ->
231+
Response.to_yojson response
232+
>|= Yojson.Safe.to_string
233+
| Error msg ->
234+
Lwt.return ("Error: " ^ msg))
235+
>|= print_endline
236+
154237
let start_lambda ~lift handler =
155-
match Config.get_runtime_api_endpoint () with
156-
| Ok endpoint ->
157-
(match Config.get_function_settings () with
158-
| Ok function_config ->
159-
let p =
160-
start_with_runtime_endpoint ~lift handler function_config endpoint
238+
let p =
239+
if Array.length Sys.argv = 1 then
240+
let endpoint =
241+
match Config.get_runtime_api_endpoint () with
242+
| Ok endpoint -> endpoint
243+
| Error msg -> failwith msg
161244
in
162-
Lwt_main.run p
163-
| Error msg ->
164-
failwith msg)
165-
| Error msg ->
166-
failwith msg
245+
let function_config =
246+
match Config.get_function_settings () with
247+
| Ok function_config -> function_config
248+
| Error msg -> failwith msg
249+
in
250+
start_with_runtime_endpoint ~lift handler function_config endpoint
251+
else
252+
(* TODO: Handle commands and options with a more robust solution
253+
like Cmdliner or Arg. *)
254+
match Sys.argv.(1) with
255+
| "invoke" ->
256+
let event_file =
257+
if Array.length Sys.argv > 2 then
258+
Some Sys.argv.(2)
259+
else
260+
None
261+
in
262+
handle_local_invoke ~lift handler event_file
263+
| "start-api" ->
264+
start_locally ~lift handler
265+
| command ->
266+
failwith ("Invalid command: " ^ command)
267+
in
268+
Lwt_main.run p
167269

168270
let lambda handler = start_lambda ~lift:Lwt.return handler
169271

lib/runtime_intf.ml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,20 @@ module type LambdaEvent = sig
3434
type t
3535

3636
val of_yojson : Yojson.Safe.t -> (t, string) result
37+
38+
val mock : 'a Piaf.Server.ctx -> (Yojson.Safe.t, string) Lwt_result.t
39+
40+
val response : t -> (Piaf.Response.t, string) result
3741
end
3842

3943
module type LambdaResponse = sig
4044
type t
4145

4246
val to_yojson : t -> Yojson.Safe.t Lwt.t
47+
48+
val mock : 'a Piaf.Server.ctx -> (Yojson.Safe.t, string) Lwt_result.t
49+
50+
val response : t -> (Piaf.Response.t, string) result
4351
end
4452

4553
module type LambdaRuntime = sig

0 commit comments

Comments
 (0)