Skip to content

Commit 9c7e013

Browse files
committed
Build a dev environment for lambda runtime
1 parent 4e1f3ad commit 9c7e013

9 files changed

Lines changed: 291 additions & 2 deletions

File tree

dev/dune

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
(library
2+
(name lambda_runtime_dev)
3+
(public_name lambda-runtime-dev)
4+
(libraries lambda-runtime lwt lwt.unix yojson ppx_deriving_yojson.runtime piaf uuidm)
5+
(preprocess (pps ppx_deriving_yojson)))

dev/http.ml

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
2+
module DevEvent = struct
3+
type t = Lambda_runtime.Http.api_gateway_proxy_request
4+
5+
let of_yojson _ =
6+
Error "Not implemented"
7+
8+
let of_piaf (ctx: Unix.sockaddr Piaf.Server.ctx) =
9+
let open Lambda_runtime in
10+
let headers =
11+
ctx.request.headers
12+
|> Piaf.Headers.to_list
13+
|> List.to_seq
14+
|> StringMap.of_seq
15+
in
16+
let identity = Http.{ cognito_identity_pool_id = None
17+
; account_id = None
18+
; cognito_identity_id = None
19+
; caller = None
20+
; access_key = None
21+
; api_key = None
22+
; source_ip = "127.0.0.1" (* TODO: get the real value *)
23+
; cognito_authentication_type = None
24+
; cognito_authentication_provider = None
25+
; user_arn = None
26+
; user_agent = Piaf.Headers.get ctx.request.headers "user-agent"
27+
; user = None }
28+
in
29+
let uri = Piaf.Request.uri ctx.request in
30+
let request_id =
31+
Random.self_init ();
32+
Uuidm.to_string @@ Uuidm.v4_gen (Random.get_state ()) ()
33+
in
34+
let request_context = Http.{ account_id = "123456789012"
35+
; resource_id = "123456"
36+
; stage = "dev"
37+
; request_id
38+
; identity
39+
; resource_path = "/{proxy+}"
40+
; authorizer = None
41+
; http_method = Piaf.Method.to_string ctx.request.meth
42+
; protocol = Some (Piaf.Versions.HTTP.to_string ctx.request.version)
43+
; path = Some (Uri.path uri)
44+
; api_id = "1234567890" }
45+
in
46+
let body =
47+
Lwt_result.map_err Piaf.Error.to_string @@ Piaf.Body.to_string ctx.request.body
48+
in
49+
let query_string_parameters =
50+
Uri.query uri
51+
|> List.map (fun (key, values) ->
52+
(* TODO: Handle this properly, we need multivalue query strings *)
53+
match values with
54+
| [] -> (key, "")
55+
| [value] -> (key, value)
56+
| _ -> failwith "Multiple values not supported for query strings now")
57+
|> List.to_seq
58+
|> StringMap.of_seq
59+
in
60+
Lwt_result.map (fun body ->
61+
Http.{ resource = (Uri.path uri)
62+
; path = request_context.resource_path
63+
; http_method = request_context.http_method
64+
; headers
65+
; query_string_parameters
66+
; path_parameters = StringMap.empty
67+
; stage_variables = StringMap.empty
68+
; request_context
69+
; body = if body = String.empty then None else Some body
70+
; is_base64_encoded = false })
71+
body
72+
73+
end
74+
75+
module DevResponse = struct
76+
type t = Lambda_runtime.Http.api_gateway_proxy_response
77+
78+
let to_yojson _ = `Null
79+
80+
let to_piaf response =
81+
let open Lambda_runtime in
82+
let headers =
83+
response.Http.headers
84+
|> StringMap.to_seq
85+
|> List.of_seq
86+
|> Piaf.Headers.of_list
87+
in
88+
(* TODO: Handle base64 *)
89+
assert (not response.Http.is_base64_encoded);
90+
91+
let body = response.Http.body in
92+
Piaf.Response.of_string
93+
~headers
94+
~body
95+
(Piaf.Status.of_code response.Http.status_code)
96+
97+
end
98+
99+
include Runtime.Make (DevEvent) (DevResponse)

dev/json.ml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
module DevEvent = struct
2+
type t = Yojson.Safe.t [@@deriving of_yojson]
3+
4+
let of_piaf (ctx: 'a Piaf.Server.ctx) =
5+
let event =
6+
let open Lwt_result.Infix in
7+
Piaf.Body.to_string ctx.request.body
8+
>|= (fun body -> if body = String.empty then None else Some body)
9+
(* TODO: failing to parse body should respond with 400 error and not 500 *)
10+
>|= Option.map Yojson.Safe.from_string
11+
>|= Option.value ~default:`Null
12+
in
13+
Lwt_result.map_err Piaf.Error.to_string event
14+
end
15+
16+
module DevResponse = struct
17+
type t = Yojson.Safe.t [@@deriving to_yojson]
18+
19+
let to_yojson t = to_yojson t
20+
21+
let content_json headers =
22+
Piaf.Headers.add headers "content-type" "application/json"
23+
24+
let to_piaf response =
25+
let body = Yojson.Safe.to_string response in
26+
Piaf.Response.of_string
27+
~headers:(content_json Piaf.Headers.empty)
28+
~body
29+
`OK
30+
end
31+
32+
include Runtime.Make (DevEvent) (DevResponse)

dev/lambda_runtime_dev.ml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
2+
module Http = Http
3+
module Json = Json

dev/runtime.ml

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
open Lwt.Infix
2+
3+
module type DevEvent = sig
4+
type t
5+
6+
val of_yojson : Yojson.Safe.t -> (t, string) result
7+
8+
val of_piaf : Unix.sockaddr Piaf.Server.ctx -> (t, string) Lwt_result.t
9+
end
10+
11+
module type DevResponse = sig
12+
type t
13+
14+
val to_yojson : t -> Yojson.Safe.t
15+
16+
val to_piaf : t -> Piaf.Response.t
17+
end
18+
19+
module Make
20+
(Event : DevEvent)
21+
(Response : DevResponse) = struct
22+
let make_mocked_context () =
23+
(* TODO: add proper values for this *)
24+
Lambda_runtime.Context.{ memory_limit_in_mb = 128
25+
; function_name = "hello"
26+
; function_version = "$LATEST"
27+
; invoked_function_arn = ""
28+
; aws_request_id = ""
29+
; xray_trace_id = None
30+
; log_stream_name = ""
31+
; log_group_name = ""
32+
; client_context = None
33+
; identity = None
34+
; deadline = 0L }
35+
36+
let invoke_locally ~lift handler event =
37+
let context = make_mocked_context () in
38+
lift (handler event context)
39+
40+
let run_locally ~lift handler =
41+
let event =
42+
if Array.length Sys.argv > 1 then
43+
Lwt_io.(open_file ~mode:Input Sys.argv.(2))
44+
>>= Lwt_io.read
45+
>|= Yojson.Safe.from_string
46+
else
47+
Lwt.return `Null
48+
in
49+
event
50+
>|= Event.of_yojson
51+
>|= (function
52+
| Ok event -> event
53+
(* TODO: This shouldnt fail, it should show a better error message instead. (stack trace?) *)
54+
| Error msg -> failwith msg)
55+
>>= invoke_locally ~lift handler
56+
>|= (function
57+
| Ok response ->
58+
response
59+
|> Response.to_yojson
60+
|> Yojson.Safe.to_string
61+
| Error msg ->
62+
"Error: " ^ msg)
63+
>|= print_endline
64+
65+
let start_locally ~lift handler =
66+
let server_handler ctx =
67+
let event =
68+
Event.of_piaf ctx
69+
(* TODO: Shouldn't fail, return 400 message instead. *)
70+
>|= (function Ok event -> event | Error msg -> failwith msg)
71+
in
72+
let response =
73+
event
74+
>>= invoke_locally ~lift handler
75+
in
76+
response
77+
>|= (function
78+
| Ok response ->
79+
Response.to_piaf response
80+
| Error body ->
81+
Piaf.Response.of_string ~body `Internal_server_error)
82+
in
83+
Lwt.async (fun () ->
84+
let address = Unix.(ADDR_INET (inet_addr_loopback, 5000)) in
85+
Lwt_io.establish_server_with_client_socket
86+
address
87+
(Piaf.Server.create server_handler)
88+
>|= fun _server ->
89+
print_endline "Server started at: http://127.0.0.1:5000"
90+
);
91+
let forever, _ = Lwt.wait () in
92+
forever
93+
94+
let start_lambda ~lift handler =
95+
let p =
96+
match Array.length Sys.argv with
97+
| 1 -> start_locally ~lift handler
98+
| 2 when Sys.argv.(1) = "invoke" ->
99+
run_locally ~lift handler
100+
| 2 when Sys.argv.(1) = "start-api" ->
101+
start_locally ~lift handler
102+
| 3 when Sys.argv.(1) = "invoke" ->
103+
run_locally ~lift handler
104+
| _ ->
105+
failwith "Invalid command line arguments!"
106+
in
107+
Lwt_main.run p
108+
109+
let lambda handler =
110+
start_lambda ~lift:Lwt.return handler
111+
end

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)

examples/dev-runtime/basic.ml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
open Lambda_runtime
2+
3+
let handler _event _ctx =
4+
Ok Http.{ status_code = 200
5+
; headers = StringMap.empty
6+
; body = "Hello world"
7+
; is_base64_encoded = false }
8+
9+
let () =
10+
Lambda_runtime_dev.Http.lambda handler

examples/dev-runtime/dune

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
(executable
2+
(name basic)
3+
(libraries lambda-runtime lambda-runtime-dev))

lambda-runtime-dev.opam

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
opam-version: "2.0"
2+
maintainer: "Antonio Nuno Monteiro <anmonteiro@gmail.com>"
3+
authors: [ "Antonio Nuno Monteiro <anmonteiro@gmail.com>" ]
4+
license: "BSD-3-clause"
5+
homepage: "https://github.com/anmonteiro/aws-lambda-ocaml-runtime"
6+
bug-reports: "https://github.com/anmonteiro/aws-lambda-ocaml-runtime/issues"
7+
dev-repo: "git+https://github.com/anmonteiro/aws-lambda-ocaml-runtime.git"
8+
build: [
9+
["dune" "build" "-p" name "-j" jobs]
10+
]
11+
depends: [
12+
"ocaml" {>= "4.08"}
13+
"dune" {>= "1.7"}
14+
"result"
15+
"yojson" {>= "1.6.0" & < "2.0.0"}
16+
"ppx_deriving_yojson"
17+
"piaf"
18+
"uri"
19+
"logs"
20+
"lwt"
21+
"alcotest" {with-test}
22+
]
23+
synopsis:
24+
"A development environment for lambda-runtime"
25+
description: """
26+
lambda-runtime-dev is a development environment for running lambdas
27+
locally with AWS lambda-runtime.
28+
"""

0 commit comments

Comments
 (0)