diff --git a/dev/dune b/dev/dune new file mode 100644 index 0000000..adc96a6 --- /dev/null +++ b/dev/dune @@ -0,0 +1,5 @@ +(library + (name lambda_runtime_dev) + (public_name lambda-runtime-dev) + (libraries lambda-runtime lwt lwt.unix yojson ppx_deriving_yojson.runtime piaf uuidm) + (preprocess (pps ppx_deriving_yojson))) \ No newline at end of file diff --git a/dev/http.ml b/dev/http.ml new file mode 100644 index 0000000..c7e039f --- /dev/null +++ b/dev/http.ml @@ -0,0 +1,99 @@ + +module DevEvent = struct + type t = Lambda_runtime.Http.api_gateway_proxy_request + + let of_yojson _ = + Error "Not implemented" + + let of_piaf (ctx: Unix.sockaddr Piaf.Server.ctx) = + let open Lambda_runtime in + let headers = + ctx.request.headers + |> Piaf.Headers.to_list + |> List.to_seq + |> StringMap.of_seq + in + let identity = Http.{ cognito_identity_pool_id = None + ; account_id = None + ; cognito_identity_id = None + ; caller = None + ; access_key = None + ; api_key = None + ; source_ip = "127.0.0.1" (* TODO: get the real value *) + ; cognito_authentication_type = None + ; cognito_authentication_provider = None + ; user_arn = None + ; user_agent = Piaf.Headers.get ctx.request.headers "user-agent" + ; user = None } + in + let uri = Piaf.Request.uri ctx.request in + let request_id = + Random.self_init (); + Uuidm.to_string @@ Uuidm.v4_gen (Random.get_state ()) () + in + let request_context = Http.{ account_id = "123456789012" + ; resource_id = "123456" + ; stage = "dev" + ; request_id + ; identity + ; resource_path = "/{proxy+}" + ; authorizer = None + ; http_method = Piaf.Method.to_string ctx.request.meth + ; protocol = Some (Piaf.Versions.HTTP.to_string ctx.request.version) + ; path = Some (Uri.path uri) + ; api_id = "1234567890" } + in + let body = + Lwt_result.map_err Piaf.Error.to_string @@ Piaf.Body.to_string ctx.request.body + in + let query_string_parameters = + Uri.query uri + |> List.map (fun (key, values) -> + (* TODO: Handle this properly, we need multivalue query strings *) + match values with + | [] -> (key, "") + | [value] -> (key, value) + | _ -> failwith "Multiple values not supported for query strings now") + |> List.to_seq + |> StringMap.of_seq + in + Lwt_result.map (fun body -> + Http.{ resource = (Uri.path uri) + ; path = request_context.resource_path + ; http_method = request_context.http_method + ; headers + ; query_string_parameters + ; path_parameters = StringMap.empty + ; stage_variables = StringMap.empty + ; request_context + ; body = if body = String.empty then None else Some body + ; is_base64_encoded = false }) + body + +end + +module DevResponse = struct + type t = Lambda_runtime.Http.api_gateway_proxy_response + + let to_yojson _ = `Null + + let to_piaf response = + let open Lambda_runtime in + let headers = + response.Http.headers + |> StringMap.to_seq + |> List.of_seq + |> Piaf.Headers.of_list + in + (* TODO: Handle base64 *) + assert (not response.Http.is_base64_encoded); + + let body = response.Http.body in + Piaf.Response.of_string + ~headers + ~body + (Piaf.Status.of_code response.Http.status_code) + +end + +include Runtime.Make (DevEvent) (DevResponse) diff --git a/dev/json.ml b/dev/json.ml new file mode 100644 index 0000000..3923016 --- /dev/null +++ b/dev/json.ml @@ -0,0 +1,32 @@ +module DevEvent = struct + type t = Yojson.Safe.t [@@deriving of_yojson] + + let of_piaf (ctx: 'a Piaf.Server.ctx) = + let event = + let open Lwt_result.Infix in + Piaf.Body.to_string ctx.request.body + >|= (fun body -> if body = String.empty then None else Some body) + (* TODO: failing to parse body should respond with 400 error and not 500 *) + >|= Option.map Yojson.Safe.from_string + >|= Option.value ~default:`Null + in + Lwt_result.map_err Piaf.Error.to_string event +end + +module DevResponse = struct + type t = Yojson.Safe.t [@@deriving to_yojson] + + let to_yojson t = to_yojson t + + let content_json headers = + Piaf.Headers.add headers "content-type" "application/json" + + let to_piaf response = + let body = Yojson.Safe.to_string response in + Piaf.Response.of_string + ~headers:(content_json Piaf.Headers.empty) + ~body + `OK +end + +include Runtime.Make (DevEvent) (DevResponse) diff --git a/dev/lambda_runtime_dev.ml b/dev/lambda_runtime_dev.ml new file mode 100644 index 0000000..09b6e4f --- /dev/null +++ b/dev/lambda_runtime_dev.ml @@ -0,0 +1,3 @@ + +module Http = Http +module Json = Json diff --git a/dev/runtime.ml b/dev/runtime.ml new file mode 100644 index 0000000..8838e3c --- /dev/null +++ b/dev/runtime.ml @@ -0,0 +1,111 @@ +open Lwt.Infix + +module type DevEvent = sig + type t + + val of_yojson : Yojson.Safe.t -> (t, string) result + + val of_piaf : Unix.sockaddr Piaf.Server.ctx -> (t, string) Lwt_result.t +end + +module type DevResponse = sig + type t + + val to_yojson : t -> Yojson.Safe.t + + val to_piaf : t -> Piaf.Response.t +end + +module Make + (Event : DevEvent) + (Response : DevResponse) = struct + let make_mocked_context () = + (* TODO: add proper values for this *) + Lambda_runtime.Context.{ memory_limit_in_mb = 128 + ; function_name = "hello" + ; function_version = "$LATEST" + ; invoked_function_arn = "" + ; aws_request_id = "" + ; xray_trace_id = None + ; log_stream_name = "" + ; log_group_name = "" + ; client_context = None + ; identity = None + ; deadline = 0L } + + let invoke_locally ~lift handler event = + let context = make_mocked_context () in + lift (handler event context) + + let run_locally ~lift handler = + let event = + if Array.length Sys.argv > 1 then + Lwt_io.(open_file ~mode:Input Sys.argv.(2)) + >>= Lwt_io.read + >|= Yojson.Safe.from_string + else + Lwt.return `Null + in + event + >|= Event.of_yojson + >|= (function + | Ok event -> event + (* TODO: This shouldnt fail, it should show a better error message instead. (stack trace?) *) + | Error msg -> failwith msg) + >>= invoke_locally ~lift handler + >|= (function + | Ok response -> + response + |> Response.to_yojson + |> Yojson.Safe.to_string + | Error msg -> + "Error: " ^ msg) + >|= print_endline + + let start_locally ~lift handler = + let server_handler ctx = + let event = + Event.of_piaf ctx + (* TODO: Shouldn't fail, return 400 message instead. *) + >|= (function Ok event -> event | Error msg -> failwith msg) + in + let response = + event + >>= invoke_locally ~lift handler + in + response + >|= (function + | Ok response -> + Response.to_piaf response + | Error body -> + Piaf.Response.of_string ~body `Internal_server_error) + in + Lwt.async (fun () -> + let address = Unix.(ADDR_INET (inet_addr_loopback, 5000)) in + Lwt_io.establish_server_with_client_socket + address + (Piaf.Server.create server_handler) + >|= fun _server -> + print_endline "Server started at: http://127.0.0.1:5000" + ); + let forever, _ = Lwt.wait () in + forever + + let start_lambda ~lift handler = + let p = + match Array.length Sys.argv with + | 1 -> start_locally ~lift handler + | 2 when Sys.argv.(1) = "invoke" -> + run_locally ~lift handler + | 2 when Sys.argv.(1) = "start-api" -> + start_locally ~lift handler + | 3 when Sys.argv.(1) = "invoke" -> + run_locally ~lift handler + | _ -> + failwith "Invalid command line arguments!" + in + Lwt_main.run p + + let lambda handler = + start_lambda ~lift:Lwt.return handler +end diff --git a/dune-project b/dune-project index cd4d183..c8e7bd0 100644 --- a/dune-project +++ b/dune-project @@ -2,6 +2,4 @@ (using fmt 1.1) -(implicit_transitive_deps false) - (name lambda-runtime) diff --git a/examples/dev-runtime/basic.ml b/examples/dev-runtime/basic.ml new file mode 100644 index 0000000..96270e9 --- /dev/null +++ b/examples/dev-runtime/basic.ml @@ -0,0 +1,10 @@ +open Lambda_runtime + +let handler _event _ctx = + Ok Http.{ status_code = 200 + ; headers = StringMap.empty + ; body = "Hello world" + ; is_base64_encoded = false } + +let () = + Lambda_runtime_dev.Http.lambda handler diff --git a/examples/dev-runtime/dune b/examples/dev-runtime/dune new file mode 100644 index 0000000..207276c --- /dev/null +++ b/examples/dev-runtime/dune @@ -0,0 +1,3 @@ +(executable + (name basic) + (libraries lambda-runtime lambda-runtime-dev)) \ No newline at end of file diff --git a/lambda-runtime-dev.opam b/lambda-runtime-dev.opam new file mode 100644 index 0000000..e1dc091 --- /dev/null +++ b/lambda-runtime-dev.opam @@ -0,0 +1,28 @@ +opam-version: "2.0" +maintainer: "Antonio Nuno Monteiro " +authors: [ "Antonio Nuno Monteiro " ] +license: "BSD-3-clause" +homepage: "https://github.com/anmonteiro/aws-lambda-ocaml-runtime" +bug-reports: "https://github.com/anmonteiro/aws-lambda-ocaml-runtime/issues" +dev-repo: "git+https://github.com/anmonteiro/aws-lambda-ocaml-runtime.git" +build: [ + ["dune" "build" "-p" name "-j" jobs] +] +depends: [ + "ocaml" {>= "4.08"} + "dune" {>= "1.7"} + "result" + "yojson" {>= "1.6.0" & < "2.0.0"} + "ppx_deriving_yojson" + "piaf" + "uri" + "logs" + "lwt" + "alcotest" {with-test} +] +synopsis: + "A development environment for lambda-runtime" +description: """ +lambda-runtime-dev is a development environment for running lambdas +locally with AWS lambda-runtime. +"""