├── .dockerignore ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .ocamlformat ├── CHANGES.md ├── LICENSE ├── Makefile ├── README.md ├── dune-project ├── examples ├── api-gateway │ ├── Dockerfile │ ├── basic.ml │ ├── build.sh │ └── dune ├── asynchronous-handler │ ├── Dockerfile │ ├── basic.ml │ ├── build.sh │ └── dune ├── basic │ ├── Dockerfile │ ├── basic.ml │ ├── build.sh │ ├── dune │ └── opam.Dockerfile ├── error │ ├── Dockerfile │ ├── build.sh │ ├── dune │ └── error.ml ├── now-custom-runtime │ ├── Dockerfile │ ├── basic.ml │ ├── build.sh │ ├── dune │ └── now.json ├── vercel-lambda-reason │ ├── Dockerfile │ ├── basic.re │ ├── build.sh │ ├── dune │ └── now.json └── vercel-lambda │ ├── Dockerfile │ ├── basic.ml │ ├── build.sh │ ├── dune │ └── now.json ├── flake.lock ├── flake.nix ├── lambda-runtime.opam ├── lib ├── client.ml ├── config.ml ├── context.ml ├── dune ├── errors.ml ├── http.ml ├── httpv2.ml ├── json.ml ├── lambda_runtime.ml ├── lambda_runtime.mli ├── runtime.ml └── util.ml ├── nix ├── ci │ └── test.nix └── default.nix ├── scripts └── esy-docker.mk ├── shell.nix ├── test ├── api_gateway_test.ml ├── config_test.ml ├── dune ├── fixtures │ ├── apigw.json │ ├── apigw_real.json │ ├── apigw_real_trimmed.json │ ├── now_no_body.json │ ├── now_with_body.json │ ├── vercel-request-2.json │ └── vercel-request.json ├── runtime_test.ml ├── test.ml ├── test_common.ml └── vercel_test.ml ├── vercel.opam └── vercel ├── dune ├── message.ml ├── request.ml ├── response.ml ├── vercel.ml └── vercel.mli /.dockerignore: -------------------------------------------------------------------------------- 1 | _esy 2 | bootstrap 3 | build.sh 4 | ocaml.zip -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: "Build" 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | jobs: 8 | tests: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | ocamlVersion: [5_00] 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: cachix/install-nix-action@v18 16 | with: 17 | extra_nix_config: | 18 | extra-substituters = https://anmonteiro.nix-cache.workers.dev 19 | extra-trusted-public-keys = ocaml.nix-cache.com-1:/xI2h2+56rwFfKyyFVbkJSeGqSIYMC/Je+7XXqGKDIY= 20 | - name: "Run nix-build" 21 | run: nix-build ./nix/ci/test.nix --argstr ocamlVersion ${{ matrix.ocamlVersion }} 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | _esy 3 | node_modules 4 | *.install 5 | .merlin 6 | .docker 7 | bootstrap -------------------------------------------------------------------------------- /.ocamlformat: -------------------------------------------------------------------------------- 1 | break-infix = fit-or-vertical 2 | break-infix-before-func = false 3 | break-fun-decl = fit-or-vertical 4 | break-separators = before 5 | break-sequences = true 6 | cases-exp-indent = 2 7 | dock-collection-brackets = false 8 | field-space = loose 9 | if-then-else = keyword-first 10 | indicate-multiline-delimiters = no 11 | infix-precedence = parens 12 | leading-nested-match-parens = true 13 | let-and = sparse 14 | let-module = sparse 15 | ocp-indent-compat = true 16 | parens-tuple = multi-line-only 17 | parse-docstrings = true 18 | sequence-blank-line = preserve-one 19 | sequence-style = terminator 20 | single-case = sparse 21 | space-around-arrays= true 22 | space-around-lists= true 23 | space-around-records= true 24 | space-around-variants= true 25 | type-decl = sparse 26 | wrap-comments = true 27 | wrap-fun-args = false 28 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | Unreleased 2 | -------------- 3 | 4 | - Tolerate unknown fields when parsing JSON payloads 5 | (`@@deriving yojson {strict = false; }`) 6 | ([#62](https://github.com/anmonteiro/aws-lambda-ocaml-runtime/pull/62)) 7 | - Add API Gateway v2 definitions ([#68](https://github.com/anmonteiro/aws-lambda-ocaml-runtime/pull/68)) 8 | 9 | 0.1.0 2021-04-17 10 | -------------- 11 | 12 | - Initial public release 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 António Nuno Monteiro 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its contributors 16 | may be used to endorse or promote products derived from this software without 17 | specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include scripts/esy-docker.mk 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OCaml Runtime for AWS Lambda 2 | 3 | This package provides a [custom 4 | runtime](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html) for 5 | AWS Lambda. 6 | 7 | ## Installation 8 | 9 | This repository provides two libraries: 10 | 11 | - `lambda-runtime` provides a runtime and API for 12 | [AWS Lambda](https://aws.amazon.com/lambda/) and 13 | [API Gateway](https://aws.amazon.com/api-gateway/). 14 | - the `vercel` library depends on `lambda-runtime` and provides an interface to 15 | the [Vercel](https://vercel.com/) service that resembles a request / response 16 | exchange. 17 | 18 | 19 | The libraries in this repo are released to the OPAM package registry. 20 | 21 | You can depend on them via: 22 | 23 | 1. [__esy__](esy): `esy add @opam/lambda-runtime` and / or `esy add @opam/vercel` 24 | 2. [__OPAM__](opam): `opam install lambda-runtime vercel`. 25 | 26 | [esy]: https://esy.sh 27 | [opam]: https://opam.ocaml.org 28 | 29 | ## Example function 30 | 31 | See the [`examples`](./examples) folder. 32 | 33 | ## Deploying 34 | 35 | **Note**: Based on the instructions in this [blog 36 | post](https://aws.amazon.com/blogs/opensource/rust-runtime-for-aws-lambda/) and 37 | the Rust custom runtime 38 | [repository](https://github.com/awslabs/aws-lambda-rust-runtime) 39 | 40 | For a custom runtime, AWS Lambda looks for an executable called `bootstrap` in 41 | the deployment package zip. Rename the generated `basic` executable to 42 | `bootstrap` and add it to a zip archive. 43 | 44 | The Dockerfile (in conjunction with the [`build.sh`](./build.sh) script) in this 45 | repo does just that. It builds a static binary called `bootstrap` and drops it 46 | in the target directory. 47 | 48 | ```shell 49 | $ ./build.sh && zip -j ocaml.zip bootstrap 50 | ``` 51 | 52 | Now that we have a deployment package (`ocaml.zip`), we can use the [AWS 53 | CLI](https://aws.amazon.com/cli/) to create a new Lambda function. Make sure to 54 | replace the execution role with an existing role in your account! 55 | 56 | ```shell 57 | $ aws lambda create-function --function-name OCamlTest \ 58 | --handler doesnt.matter \ 59 | --zip-file file://./ocaml.zip \ 60 | --runtime provided \ 61 | --role arn:aws:iam::XXXXXXXXXXXXX:role/your_lambda_execution_role \ 62 | --tracing-config Mode=Active 63 | ``` 64 | 65 | You can now test the function using the AWS CLI or the AWS Lambda console 66 | 67 | ```shell 68 | $ aws lambda invoke --function-name OCamlTest \ 69 | --payload '{"firstName": "world"}' \ 70 | output.json 71 | $ cat output.json # Prints: {"message":"Hello, world!"} 72 | ``` 73 | 74 | ## Copyright & License 75 | 76 | Copyright © 2018 António Nuno Monteiro 77 | 78 | Distributed under the 3-clause BSD License (see [LICENSE](./LICENSE)). 79 | -------------------------------------------------------------------------------- /dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 3.0) 2 | 3 | (name lambda-runtime) 4 | -------------------------------------------------------------------------------- /examples/api-gateway/Dockerfile: -------------------------------------------------------------------------------- 1 | # start from node image so we can install esy from npm 2 | 3 | FROM node:alpine as build 4 | 5 | ENV TERM=dumb LD_LIBRARY_PATH=/usr/local/lib:/usr/lib:/lib 6 | 7 | RUN mkdir /esy 8 | WORKDIR /esy 9 | 10 | ENV NPM_CONFIG_PREFIX=/esy 11 | RUN npm install -g --unsafe-perm esy 12 | 13 | # now that we have esy installed we need a proper runtime 14 | 15 | FROM alpine as esy 16 | 17 | ENV TERM=dumb LD_LIBRARY_PATH=/usr/local/lib:/usr/lib:/lib 18 | 19 | WORKDIR / 20 | 21 | COPY --from=build /esy /esy 22 | 23 | RUN apk add --no-cache ca-certificates wget bash curl perl-utils git patch gcc g++ musl-dev make m4 coreutils 24 | 25 | RUN wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub 26 | RUN wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.30-r0/glibc-2.30-r0.apk 27 | RUN apk add --no-cache glibc-2.30-r0.apk 28 | 29 | ENV PATH=/esy/bin:$PATH 30 | 31 | RUN mkdir /app 32 | WORKDIR /app 33 | 34 | RUN echo ' \ 35 | {\ 36 | "name": "package-base", \ 37 | "dependencies": { \ 38 | "ocaml": "4.9.0", \ 39 | "@opam/dune": "*" \ 40 | } \ 41 | } \ 42 | ' > esy.json 43 | 44 | RUN esy 45 | 46 | COPY esy.json esy.json 47 | COPY esy.lock esy.lock 48 | 49 | RUN esy fetch 50 | RUN esy true 51 | 52 | COPY . . 53 | 54 | RUN esy b dune build examples/api-gateway/basic.exe --profile=static 55 | 56 | RUN mv $(esy bash -c 'echo $cur__target_dir/default/examples/api-gateway/basic.exe') bootstrap 57 | -------------------------------------------------------------------------------- /examples/api-gateway/basic.ml: -------------------------------------------------------------------------------- 1 | open Lambda_runtime 2 | module StringMap = Map.Make (String) 3 | 4 | let my_handler (evt : Http.api_gateway_proxy_request) _context = 5 | let body = match evt.Http.body with None -> "" | Some body -> body in 6 | Ok 7 | Http. 8 | { status_code = 200 9 | ; headers = StringMap.empty 10 | ; body 11 | ; is_base64_encoded = false 12 | } 13 | 14 | let setup_log ?style_renderer level = 15 | Fmt_tty.setup_std_outputs ?style_renderer (); 16 | Logs.set_level level; 17 | Logs.set_reporter (Logs_fmt.reporter ()); 18 | () 19 | 20 | let () = 21 | setup_log (Some Logs.Debug); 22 | Http.lambda my_handler 23 | -------------------------------------------------------------------------------- /examples/api-gateway/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -eo pipefail 4 | 5 | root_path=$PWD 6 | 7 | # Start in examples/api-gateway/ even if run from root directory 8 | cd "$(dirname "$0")" 9 | 10 | rm -rf bootstrap 11 | docker build ../.. --tag lambda -f ./Dockerfile 12 | docker rm example || true 13 | docker create --name example lambda 14 | docker cp example:/app/bootstrap bootstrap 15 | 16 | cd $root_path -------------------------------------------------------------------------------- /examples/api-gateway/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (name basic) 3 | (libraries lambda-runtime logs.fmt fmt.tty)) 4 | 5 | (env 6 | (static 7 | (flags 8 | (:standard -ccopt -static)))) 9 | -------------------------------------------------------------------------------- /examples/asynchronous-handler/Dockerfile: -------------------------------------------------------------------------------- 1 | # start from node image so we can install esy from npm 2 | 3 | FROM node:alpine as build 4 | 5 | ENV TERM=dumb LD_LIBRARY_PATH=/usr/local/lib:/usr/lib:/lib 6 | 7 | RUN mkdir /esy 8 | WORKDIR /esy 9 | 10 | ENV NPM_CONFIG_PREFIX=/esy 11 | RUN npm install -g --unsafe-perm esy 12 | 13 | # now that we have esy installed we need a proper runtime 14 | 15 | FROM alpine as esy 16 | 17 | ENV TERM=dumb LD_LIBRARY_PATH=/usr/local/lib:/usr/lib:/lib 18 | 19 | WORKDIR / 20 | 21 | COPY --from=build /esy /esy 22 | 23 | RUN apk add --no-cache ca-certificates wget bash curl perl-utils git patch gcc g++ musl-dev make m4 coreutils 24 | 25 | RUN wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub 26 | RUN wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.30-r0/glibc-2.30-r0.apk 27 | RUN apk add --no-cache glibc-2.30-r0.apk 28 | 29 | ENV PATH=/esy/bin:$PATH 30 | 31 | RUN mkdir /app 32 | WORKDIR /app 33 | 34 | RUN echo ' \ 35 | {\ 36 | "name": "package-base", \ 37 | "dependencies": { \ 38 | "ocaml": "4.9.0", \ 39 | "@opam/dune": "*" \ 40 | } \ 41 | } \ 42 | ' > esy.json 43 | 44 | RUN esy 45 | 46 | COPY esy.json esy.json 47 | COPY esy.lock esy.lock 48 | 49 | RUN esy fetch 50 | RUN esy true 51 | 52 | COPY . . 53 | 54 | RUN esy b dune build examples/asynchronous-handler/basic.exe --profile=static 55 | 56 | RUN mv $(esy bash -c 'echo $cur__target_dir/default/examples/asynchronous-handler/basic.exe') bootstrap 57 | -------------------------------------------------------------------------------- /examples/asynchronous-handler/basic.ml: -------------------------------------------------------------------------------- 1 | let my_handler evt _context = 2 | Logs.app (fun m -> m "Hello from the async handler"); 3 | Ok evt 4 | 5 | let setup_log ?style_renderer level = 6 | Fmt_tty.setup_std_outputs ?style_renderer (); 7 | Logs.set_level level; 8 | Logs.set_reporter (Logs_fmt.reporter ()); 9 | () 10 | 11 | let () = 12 | setup_log (Some Logs.Debug); 13 | Lambda_runtime.Json.lambda my_handler 14 | -------------------------------------------------------------------------------- /examples/asynchronous-handler/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -eo pipefail 4 | 5 | root_path=$PWD 6 | 7 | # Start in examples/asynchronous-handler/ even if run from root directory 8 | cd "$(dirname "$0")" 9 | 10 | rm -rf bootstrap 11 | docker build ../.. --tag async -f ./Dockerfile 12 | docker rm example || true 13 | docker create --name example async 14 | docker cp example:/app/bootstrap bootstrap 15 | 16 | cd $root_path -------------------------------------------------------------------------------- /examples/asynchronous-handler/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (name basic) 3 | (libraries lambda-runtime logs.fmt fmt.tty result)) 4 | 5 | (env 6 | (static 7 | (flags 8 | (:standard -ccopt -static)))) 9 | -------------------------------------------------------------------------------- /examples/basic/Dockerfile: -------------------------------------------------------------------------------- 1 | # start from node image so we can install esy from npm 2 | 3 | FROM node:alpine as build 4 | 5 | ENV TERM=dumb LD_LIBRARY_PATH=/usr/local/lib:/usr/lib:/lib 6 | 7 | RUN mkdir /esy 8 | WORKDIR /esy 9 | 10 | ENV NPM_CONFIG_PREFIX=/esy 11 | RUN npm install -g --unsafe-perm esy 12 | 13 | # now that we have esy installed we need a proper runtime 14 | 15 | FROM alpine as esy 16 | 17 | ENV TERM=dumb LD_LIBRARY_PATH=/usr/local/lib:/usr/lib:/lib 18 | 19 | WORKDIR / 20 | 21 | COPY --from=build /esy /esy 22 | 23 | RUN apk add --no-cache ca-certificates wget bash curl perl-utils git patch gcc g++ musl-dev make m4 gmp-dev linux-headers coreutils 24 | 25 | RUN wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub 26 | RUN wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.30-r0/glibc-2.30-r0.apk 27 | RUN apk add --no-cache glibc-2.30-r0.apk 28 | 29 | ENV PATH=/esy/bin:$PATH 30 | 31 | RUN mkdir /app 32 | WORKDIR /app 33 | 34 | RUN echo ' \ 35 | {\ 36 | "name": "package-base", \ 37 | "dependencies": { \ 38 | "ocaml": "4.9.0", \ 39 | "@opam/dune": "*" \ 40 | } \ 41 | } \ 42 | ' > esy.json 43 | 44 | RUN esy 45 | 46 | COPY esy.json esy.json 47 | COPY esy.lock esy.lock 48 | 49 | RUN esy fetch 50 | RUN esy true 51 | 52 | COPY . . 53 | 54 | RUN esy b dune build examples/basic/basic.exe --profile=static 55 | 56 | RUN mv $(esy bash -c 'echo $cur__target_dir/default/examples/basic/basic.exe') bootstrap 57 | -------------------------------------------------------------------------------- /examples/basic/basic.ml: -------------------------------------------------------------------------------- 1 | let my_handler evt _context = Ok evt 2 | 3 | let setup_log ?style_renderer level = 4 | Fmt_tty.setup_std_outputs ?style_renderer (); 5 | Logs.set_level level; 6 | Logs.set_reporter (Logs_fmt.reporter ()); 7 | () 8 | 9 | let () = 10 | setup_log (Some Logs.Debug); 11 | Lambda_runtime.Json.lambda my_handler 12 | -------------------------------------------------------------------------------- /examples/basic/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -eo pipefail 4 | 5 | root_path=$PWD 6 | 7 | # Start in examples/basic/ even if run from root directory 8 | cd "$(dirname "$0")" 9 | 10 | rm -rf bootstrap 11 | docker build ../.. --tag lambda -f ./Dockerfile 12 | docker rm example || true 13 | docker create --name example lambda 14 | docker cp example:/app/bootstrap bootstrap 15 | 16 | cd $root_path 17 | -------------------------------------------------------------------------------- /examples/basic/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (name basic) 3 | (libraries lambda-runtime logs.fmt fmt.tty)) 4 | 5 | (env 6 | (static 7 | (flags 8 | (:standard -ccopt -static)))) 9 | -------------------------------------------------------------------------------- /examples/basic/opam.Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ocaml/opam2:alpine-3.7-ocaml-4.07 as base 2 | 3 | RUN sudo apk add --no-cache libev yarn m4 libev-dev python util-linux 4 | 5 | USER opam 6 | 7 | RUN opam switch create 4.07.1+flambda+no-flat-float-array 8 | 9 | WORKDIR /app 10 | 11 | COPY --chown=opam:nogroup *.opam /app/ 12 | 13 | RUN opam update 14 | 15 | RUN opam pin add httpaf --dev-repo --yes && \ 16 | opam pin add httpaf-lwt https://github.com/inhabitedtype/httpaf --kind=git --yes 17 | 18 | RUN opam install . --deps-only --yes 19 | 20 | RUN opam install fmt 21 | 22 | RUN sudo chown -R opam:nogroup . 23 | 24 | COPY --chown=opam:nogroup . /app 25 | 26 | RUN opam config exec -- dune build examples/basic/basic.exe --profile=static 27 | 28 | RUN mv _build/default/examples/basic/basic.exe bootstrap 29 | -------------------------------------------------------------------------------- /examples/error/Dockerfile: -------------------------------------------------------------------------------- 1 | # start from node image so we can install esy from npm 2 | 3 | FROM node:alpine as build 4 | 5 | ENV TERM=dumb LD_LIBRARY_PATH=/usr/local/lib:/usr/lib:/lib 6 | 7 | RUN mkdir /esy 8 | WORKDIR /esy 9 | 10 | ENV NPM_CONFIG_PREFIX=/esy 11 | RUN npm install -g --unsafe-perm esy 12 | 13 | # now that we have esy installed we need a proper runtime 14 | 15 | FROM alpine as esy 16 | 17 | ENV TERM=dumb LD_LIBRARY_PATH=/usr/local/lib:/usr/lib:/lib 18 | 19 | WORKDIR / 20 | 21 | COPY --from=build /esy /esy 22 | 23 | RUN apk add --no-cache ca-certificates wget bash curl perl-utils git patch gcc g++ musl-dev make m4 coreutils 24 | 25 | RUN wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub 26 | RUN wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.30-r0/glibc-2.30-r0.apk 27 | RUN apk add --no-cache glibc-2.30-r0.apk 28 | 29 | ENV PATH=/esy/bin:$PATH 30 | 31 | RUN mkdir /app 32 | WORKDIR /app 33 | 34 | RUN echo ' \ 35 | {\ 36 | "name": "package-base", \ 37 | "dependencies": { \ 38 | "ocaml": "4.9.0", \ 39 | "@opam/dune": "*" \ 40 | } \ 41 | } \ 42 | ' > esy.json 43 | 44 | RUN esy 45 | 46 | COPY esy.json esy.json 47 | COPY esy.lock esy.lock 48 | 49 | RUN esy fetch 50 | RUN esy true 51 | 52 | COPY . . 53 | 54 | RUN esy b dune build examples/error/error.exe --profile=static 55 | 56 | RUN mv $(esy bash -c 'echo $cur__target_dir/default/examples/error/error.exe') bootstrap 57 | -------------------------------------------------------------------------------- /examples/error/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -eo pipefail 4 | 5 | root_path=$PWD 6 | 7 | # Start in examples/basic/ even if run from root directory 8 | cd "$(dirname "$0")" 9 | 10 | rm -rf bootstrap 11 | docker build ../.. --tag lambda-error -f ./Dockerfile 12 | docker rm example-error || true 13 | docker create --name example-error lambda-error 14 | docker cp example-error:/app/bootstrap bootstrap 15 | 16 | cd $root_path -------------------------------------------------------------------------------- /examples/error/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (name error) 3 | (libraries lambda-runtime logs.fmt fmt.tty)) 4 | 5 | (env 6 | (static 7 | (flags 8 | (:standard -ccopt -static)))) 9 | -------------------------------------------------------------------------------- /examples/error/error.ml: -------------------------------------------------------------------------------- 1 | let my_handler _evt _context = Error "Failed for some reason" 2 | 3 | let setup_log ?style_renderer level = 4 | Fmt_tty.setup_std_outputs ?style_renderer (); 5 | Logs.set_level level; 6 | Logs.set_reporter (Logs_fmt.reporter ()); 7 | () 8 | 9 | let () = 10 | setup_log (Some Logs.Debug); 11 | Lambda_runtime.Json.lambda my_handler 12 | -------------------------------------------------------------------------------- /examples/now-custom-runtime/Dockerfile: -------------------------------------------------------------------------------- 1 | # start from node image so we can install esy from npm 2 | 3 | FROM node:alpine as build 4 | 5 | ENV TERM=dumb LD_LIBRARY_PATH=/usr/local/lib:/usr/lib:/lib 6 | 7 | RUN mkdir /esy 8 | WORKDIR /esy 9 | 10 | ENV NPM_CONFIG_PREFIX=/esy 11 | RUN npm install -g --unsafe-perm esy 12 | 13 | # now that we have esy installed we need a proper runtime 14 | 15 | FROM alpine as esy 16 | 17 | ENV TERM=dumb LD_LIBRARY_PATH=/usr/local/lib:/usr/lib:/lib 18 | 19 | WORKDIR / 20 | 21 | COPY --from=build /esy /esy 22 | 23 | RUN apk add --no-cache ca-certificates wget bash curl perl-utils git patch gcc g++ musl-dev make m4 coreutils 24 | 25 | RUN wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub 26 | RUN wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.30-r0/glibc-2.30-r0.apk 27 | RUN apk add --no-cache glibc-2.30-r0.apk 28 | 29 | ENV PATH=/esy/bin:$PATH 30 | 31 | RUN mkdir /app 32 | WORKDIR /app 33 | 34 | RUN echo ' \ 35 | {\ 36 | "name": "package-base", \ 37 | "dependencies": { \ 38 | "ocaml": "4.9.0", \ 39 | "@opam/dune": "*" \ 40 | } \ 41 | } \ 42 | ' > esy.json 43 | 44 | RUN esy 45 | 46 | COPY esy.json esy.json 47 | COPY esy.lock esy.lock 48 | 49 | RUN esy fetch 50 | RUN esy true 51 | 52 | COPY . . 53 | 54 | RUN esy b dune build examples/now-custom-runtime/basic.exe --profile=static 55 | 56 | RUN mv $(esy bash -c 'echo $cur__target_dir/default/examples/now-custom-runtime/basic.exe') bootstrap 57 | -------------------------------------------------------------------------------- /examples/now-custom-runtime/basic.ml: -------------------------------------------------------------------------------- 1 | let my_handler evt _context = 2 | match Yojson.Safe.Util.member "body" evt with 3 | | `String body -> 4 | Ok 5 | (`Assoc 6 | [ "statusCode", `Int 200 7 | ; "body", `String (Yojson.Safe.prettify body) 8 | ; "headers", `Assoc [] 9 | ]) 10 | | _ -> Error "Body wasn't string" 11 | 12 | let setup_log ?style_renderer level = 13 | Fmt_tty.setup_std_outputs ?style_renderer (); 14 | Logs.set_level level; 15 | Logs.set_reporter (Logs_fmt.reporter ()); 16 | () 17 | 18 | let () = 19 | setup_log (Some Logs.Debug); 20 | Lambda_runtime.Json.lambda my_handler 21 | -------------------------------------------------------------------------------- /examples/now-custom-runtime/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -eo pipefail 4 | 5 | root_path=$PWD 6 | 7 | # Start in examples/now-custom-runtime/ even if run from root directory 8 | cd "$(dirname "$0")" 9 | 10 | rm -rf bootstrap 11 | docker build ../.. --tag lambda -f ./Dockerfile 12 | docker rm example || true 13 | docker create --name example lambda 14 | docker cp example:/app/bootstrap bootstrap 15 | 16 | cd $root_path -------------------------------------------------------------------------------- /examples/now-custom-runtime/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (name basic) 3 | (libraries lambda-runtime logs.fmt fmt.tty yojson)) 4 | 5 | (env 6 | (static 7 | (flags 8 | (:standard -ccopt -static)))) 9 | -------------------------------------------------------------------------------- /examples/now-custom-runtime/now.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic-now-ocaml", 3 | "public": true, 4 | "version": 2, 5 | "builds": [ 6 | { 7 | "src": "bootstrap", 8 | "use": "now-custom-runtime" 9 | } 10 | ], 11 | "routes": [ 12 | { 13 | "src": "^/.*", 14 | "dest": "/bootstrap" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /examples/vercel-lambda-reason/Dockerfile: -------------------------------------------------------------------------------- 1 | # start from node image so we can install esy from npm 2 | 3 | FROM node:alpine as build 4 | 5 | ENV TERM=dumb LD_LIBRARY_PATH=/usr/local/lib:/usr/lib:/lib 6 | 7 | RUN mkdir /esy 8 | WORKDIR /esy 9 | 10 | ENV NPM_CONFIG_PREFIX=/esy 11 | RUN npm install -g --unsafe-perm esy 12 | 13 | # now that we have esy installed we need a proper runtime 14 | 15 | FROM alpine as esy 16 | 17 | ENV TERM=dumb LD_LIBRARY_PATH=/usr/local/lib:/usr/lib:/lib 18 | 19 | WORKDIR / 20 | 21 | COPY --from=build /esy /esy 22 | 23 | RUN apk add --no-cache ca-certificates wget bash curl perl-utils git patch gcc g++ musl-dev make m4 coreutils 24 | 25 | RUN wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub 26 | RUN wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.30-r0/glibc-2.30-r0.apk 27 | RUN apk add --no-cache glibc-2.30-r0.apk 28 | 29 | ENV PATH=/esy/bin:$PATH 30 | 31 | RUN mkdir /app 32 | WORKDIR /app 33 | 34 | RUN echo ' \ 35 | {\ 36 | "name": "package-base", \ 37 | "dependencies": { \ 38 | "ocaml": "4.9.0", \ 39 | "@opam/dune": "*" \ 40 | } \ 41 | } \ 42 | ' > esy.json 43 | 44 | RUN esy 45 | 46 | COPY esy.json esy.json 47 | COPY esy.lock esy.lock 48 | 49 | RUN esy fetch 50 | RUN esy true 51 | 52 | COPY . . 53 | 54 | RUN esy dune build examples/now-lambda-reason/basic.exe --profile=static 55 | 56 | RUN mv $(esy bash -c 'echo $cur__target_dir/default/examples/now-lambda-reason/basic.exe') bootstrap 57 | -------------------------------------------------------------------------------- /examples/vercel-lambda-reason/basic.re: -------------------------------------------------------------------------------- 1 | module Client = Piaf.Client.Oneshot; 2 | 3 | let my_handler = (_request, {Lambda_runtime.Context.sw, env, _}) => { 4 | let uri = 5 | Uri.of_string( 6 | "http://api.giphy.com/v1/gifs/random?tag=cat&api_key=hamBGlVDz0XI5tYtxTuPgudCVhHSNX8q&limit=1", 7 | ); 8 | switch (Client.get(~sw, env, uri)) { 9 | | Ok(response) => 10 | switch (Piaf.Body.to_string(response.body)) { 11 | | Ok(body_str) => 12 | open Yojson.Safe; 13 | let body_json = Yojson.Safe.from_string(body_str); 14 | let img_url = 15 | body_json 16 | |> Util.member("data") 17 | |> Util.member("images") 18 | |> Util.member("original") 19 | |> Util.member("url") 20 | |> Util.to_string; 21 | let body = Printf.sprintf("", img_url); 22 | let response = 23 | Piaf.Response.of_string( 24 | ~body, 25 | ~headers=Piaf.Headers.of_list([("content-type", "text/html")]), 26 | `OK, 27 | ); 28 | Ok(response); 29 | | Error(_) => Error("Failed for some reason") 30 | } 31 | | Error(_) => Error("Failed for some reason") 32 | }; 33 | }; 34 | 35 | let setup_log = (~style_renderer=?, level) => { 36 | Fmt_tty.setup_std_outputs(~style_renderer?, ()); 37 | Logs.set_level(level); 38 | Logs.set_reporter(Logs_fmt.reporter()); 39 | (); 40 | }; 41 | 42 | let () = { 43 | setup_log(Some(Logs.Debug)); 44 | Vercel.lambda(my_handler); 45 | }; 46 | -------------------------------------------------------------------------------- /examples/vercel-lambda-reason/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -eo pipefail 4 | 5 | root_path=$PWD 6 | 7 | # Start in examples/now-custom-runtime/ even if run from root directory 8 | cd "$(dirname "$0")" 9 | 10 | rm -rf bootstrap 11 | docker build ../.. --tag lambda -f ./Dockerfile 12 | docker rm example || true 13 | docker create --name example lambda 14 | docker cp example:/app/bootstrap bootstrap 15 | 16 | cd $root_path -------------------------------------------------------------------------------- /examples/vercel-lambda-reason/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (name basic) 3 | (libraries vercel logs.fmt fmt.tty piaf uri bigstringaf yojson)) 4 | 5 | (env 6 | (static 7 | (flags 8 | (:standard -ccopt -static)))) 9 | -------------------------------------------------------------------------------- /examples/vercel-lambda-reason/now.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reasonml-lisbon-gifs", 3 | "public": true, 4 | "version": 2, 5 | "builds": [ 6 | { 7 | "src": "bootstrap", 8 | "use": "now-custom-runtime" 9 | } 10 | ], 11 | "routes": [ 12 | { 13 | "src": "^/.*", 14 | "dest": "/bootstrap" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /examples/vercel-lambda/Dockerfile: -------------------------------------------------------------------------------- 1 | # start from node image so we can install esy from npm 2 | 3 | FROM node:13-alpine as build 4 | 5 | ENV TERM=dumb LD_LIBRARY_PATH=/usr/local/lib:/usr/lib:/lib 6 | 7 | RUN mkdir /esy 8 | WORKDIR /esy 9 | 10 | ENV NPM_CONFIG_PREFIX=/esy 11 | RUN npm install -g --unsafe-perm esy 12 | 13 | # now that we have esy installed we need a proper runtime 14 | 15 | FROM alpine:3.10 as esy 16 | 17 | ENV TERM=dumb LD_LIBRARY_PATH=/usr/local/lib:/usr/lib:/lib 18 | 19 | WORKDIR / 20 | 21 | COPY --from=build /esy /esy 22 | 23 | RUN apk add --no-cache ca-certificates wget bash curl perl-utils git patch \ 24 | gcc g++ musl-dev make m4 linux-headers coreutils 25 | 26 | RUN wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub 27 | RUN wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.30-r0/glibc-2.30-r0.apk 28 | RUN apk add --no-cache glibc-2.30-r0.apk 29 | 30 | ENV PATH=/esy/bin:$PATH 31 | 32 | RUN mkdir /app 33 | WORKDIR /app 34 | 35 | RUN echo ' \ 36 | {\ 37 | "name": "package-base", \ 38 | "dependencies": { \ 39 | "ocaml": "~4.9.0", \ 40 | "@opam/dune": "*", \ 41 | "@opam/conf-libssl": "*" \ 42 | }, \ 43 | "resolutions": { \ 44 | "@opam/conf-libssl": "esy-packages/esy-openssl#860ad7f", \ 45 | "@opam/conf-pkg-config": "esy-packages/pkg-config#71c143c" \ 46 | } \ 47 | } \ 48 | ' > esy.json 49 | 50 | RUN esy 51 | 52 | COPY esy.json esy.json 53 | COPY esy.lock esy.lock 54 | 55 | RUN esy fetch 56 | RUN esy true 57 | 58 | COPY . . 59 | 60 | RUN esy b dune build examples/now-lambda/basic.exe --profile=static 61 | 62 | RUN mv $(esy bash -c 'echo $cur__target_dir/default/examples/now-lambda/basic.exe') bootstrap 63 | -------------------------------------------------------------------------------- /examples/vercel-lambda/basic.ml: -------------------------------------------------------------------------------- 1 | open Vercel 2 | 3 | let my_handler request _context = 4 | let { Request.headers; body; _ } = request in 5 | let host = Headers.get_exn headers "host" in 6 | let body = Result.get_ok (Piaf.Body.to_string body) in 7 | let body = 8 | if String.length body > 0 9 | then body 10 | else Format.asprintf "Didn't get an HTTP body from %s" host 11 | in 12 | let response = 13 | Response.of_string 14 | ~body 15 | ~headers:(Headers.of_list [ "Content-Type", "application/json" ]) 16 | `OK 17 | in 18 | Ok response 19 | 20 | let setup_log ?style_renderer level = 21 | Fmt_tty.setup_std_outputs ?style_renderer (); 22 | Logs.set_level level; 23 | Logs.set_reporter (Logs_fmt.reporter ()); 24 | () 25 | 26 | let () = 27 | setup_log (Some Logs.Debug); 28 | Vercel.lambda my_handler 29 | -------------------------------------------------------------------------------- /examples/vercel-lambda/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -eo pipefail 4 | 5 | root_path=$PWD 6 | 7 | # Start in examples/now-custom-runtime/ even if run from root directory 8 | cd "$(dirname "$0")" 9 | 10 | rm -rf bootstrap 11 | docker build ../.. --tag lambda -f ./Dockerfile 12 | docker rm example || true 13 | docker create --name example lambda 14 | docker cp example:/app/bootstrap bootstrap 15 | 16 | cd $root_path -------------------------------------------------------------------------------- /examples/vercel-lambda/dune: -------------------------------------------------------------------------------- 1 | (executable 2 | (name basic) 3 | (libraries vercel logs.fmt fmt.tty piaf result)) 4 | 5 | (env 6 | (static 7 | (flags 8 | (:standard -ccopt -static)))) 9 | -------------------------------------------------------------------------------- /examples/vercel-lambda/now.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic-now-ocaml", 3 | "public": true, 4 | "version": 2, 5 | "builds": [ 6 | { 7 | "src": "bootstrap", 8 | "use": "now-custom-runtime" 9 | } 10 | ], 11 | "routes": [ 12 | { 13 | "src": "^/.*", 14 | "dest": "/bootstrap" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "locked": { 5 | "lastModified": 1678901627, 6 | "narHash": "sha256-U02riOqrKKzwjsxc/400XnElV+UtPUQWpANPlyazjH0=", 7 | "owner": "numtide", 8 | "repo": "flake-utils", 9 | "rev": "93a2b84fc4b70d9e089d029deacc3583435c2ed6", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "numtide", 14 | "repo": "flake-utils", 15 | "type": "github" 16 | } 17 | }, 18 | "nix-filter": { 19 | "locked": { 20 | "lastModified": 1678109515, 21 | "narHash": "sha256-C2X+qC80K2C1TOYZT8nabgo05Dw2HST/pSn6s+n6BO8=", 22 | "owner": "numtide", 23 | "repo": "nix-filter", 24 | "rev": "aa9ff6ce4a7f19af6415fb3721eaa513ea6c763c", 25 | "type": "github" 26 | }, 27 | "original": { 28 | "owner": "numtide", 29 | "repo": "nix-filter", 30 | "type": "github" 31 | } 32 | }, 33 | "nixpkgs": { 34 | "inputs": { 35 | "flake-utils": [ 36 | "flake-utils" 37 | ], 38 | "nixpkgs": "nixpkgs_2" 39 | }, 40 | "locked": { 41 | "lastModified": 1679530726, 42 | "narHash": "sha256-PQCqTr4vgAPREHZ2/ZuFHUOuXMMRUmN5jHQnQIjk/aw=", 43 | "owner": "anmonteiro", 44 | "repo": "nix-overlays", 45 | "rev": "488ded93194797c0b84ef51e551d35b45388b307", 46 | "type": "github" 47 | }, 48 | "original": { 49 | "owner": "anmonteiro", 50 | "repo": "nix-overlays", 51 | "type": "github" 52 | } 53 | }, 54 | "nixpkgs_2": { 55 | "locked": { 56 | "lastModified": 1679457459, 57 | "narHash": "sha256-2CbdQtEHH6G010dj9Y1C9sxDD9Rjs/Rfpg4WL4hb3TI=", 58 | "owner": "NixOS", 59 | "repo": "nixpkgs", 60 | "rev": "c2111b6f27d057ab227e7a1341ea750c0cc76b7f", 61 | "type": "github" 62 | }, 63 | "original": { 64 | "owner": "NixOS", 65 | "repo": "nixpkgs", 66 | "rev": "c2111b6f27d057ab227e7a1341ea750c0cc76b7f", 67 | "type": "github" 68 | } 69 | }, 70 | "root": { 71 | "inputs": { 72 | "flake-utils": "flake-utils", 73 | "nix-filter": "nix-filter", 74 | "nixpkgs": "nixpkgs" 75 | } 76 | } 77 | }, 78 | "root": "root", 79 | "version": 7 80 | } 81 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Lambda Runtime Nix Flake"; 3 | 4 | inputs.nix-filter.url = "github:numtide/nix-filter"; 5 | inputs.flake-utils.url = "github:numtide/flake-utils"; 6 | inputs.nixpkgs.inputs.flake-utils.follows = "flake-utils"; 7 | inputs.nixpkgs.url = "github:anmonteiro/nix-overlays"; 8 | 9 | outputs = { self, nixpkgs, flake-utils, nix-filter }: 10 | flake-utils.lib.eachDefaultSystem (system: 11 | let 12 | pkgs = nixpkgs.legacyPackages."${system}".extend (self: super: { 13 | ocamlPackages = super.ocaml-ng.ocamlPackages_5_0; 14 | }); 15 | in 16 | rec { 17 | packages = (pkgs.callPackage ./nix { nix-filter = nix-filter.lib; }); 18 | defaultPackage = packages.lambda-runtime; 19 | devShell = pkgs.callPackage ./shell.nix { inherit packages; }; 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /lambda-runtime.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | maintainer: "Antonio Nuno Monteiro " 3 | authors: [ "Antonio Nuno Monteiro " ] 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 | "eio_main" 19 | "uri" 20 | "logs" 21 | "alcotest" {with-test} 22 | ] 23 | synopsis: 24 | "A custom runtime for AWS Lambda written in OCaml" 25 | description: """ 26 | lambda-runtime is a runtime for AWS Lambda that makes it easy to run AWS Lambda 27 | Functions written in OCaml. 28 | """ 29 | -------------------------------------------------------------------------------- /lib/client.ml: -------------------------------------------------------------------------------- 1 | (*---------------------------------------------------------------------------- 2 | * Copyright (c) 2018 António Nuno Monteiro 3 | * 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions are met: 8 | * 9 | * 1. Redistributions of source code must retain the above copyright notice, 10 | * this list of conditions and the following disclaimer. 11 | * 12 | * 2. Redistributions in binary form must reproduce the above copyright 13 | * notice, this list of conditions and the following disclaimer in the 14 | * documentation and/or other materials provided with the distribution. 15 | * 16 | * 3. Neither the name of the copyright holder nor the names of its 17 | * contributors may be used to endorse or promote products derived from this 18 | * software without specific prior written permission. 19 | * 20 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 24 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 27 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 28 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 | * POSSIBILITY OF SUCH DAMAGE. 31 | *---------------------------------------------------------------------------*) 32 | 33 | module Constants = struct 34 | let runtime_api_version = "2018-06-01" 35 | let api_content_type = "application/json" 36 | let api_error_content_type = "application/vnd.aws.lambda.error+json" 37 | let runtime_error_header = "Lambda-Runtime-Function-Error-Type" 38 | 39 | module RequestHeaders = struct 40 | let request_id = "Lambda-Runtime-Aws-Request-Id" 41 | let function_arn = "Lambda-Runtime-Invoked-Function-Arn" 42 | let trace_id = "Lambda-Runtime-Trace-Id" 43 | let deadline = "Lambda-Runtime-Deadline-Ms" 44 | let client_context = "Lambda-Runtime-Client-Context" 45 | let cognito_identity = "Lambda-Runtime-Cognito-Identity" 46 | end 47 | end 48 | 49 | type client_application = 50 | { (* The mobile app installation id *) 51 | installation_id : string 52 | ; (* The app title for the mobile app as registered with AWS' mobile 53 | services. *) 54 | app_title : string option [@default None] 55 | ; (* The version name of the application as registered with AWS' mobile 56 | services. *) 57 | app_version_name : string 58 | ; (* The app version code. *) 59 | app_version_code : string 60 | ; (* The package name for the mobile application invoking the function *) 61 | app_package_name : string 62 | } 63 | [@@deriving of_yojson { strict = false }] 64 | 65 | type client_context = 66 | { (* Information about the mobile application invoking the function. *) 67 | client : client_application 68 | ; (* Custom properties attached to the mobile event context. *) 69 | custom : Yojson.Safe.t 70 | ; (* Environment settings from the mobile client. *) 71 | env : Yojson.Safe.t 72 | } 73 | [@@deriving of_yojson { strict = false }] 74 | 75 | (* Cognito identity information sent with the event *) 76 | type cognito_identity = 77 | { (* The unique identity id for the Cognito credentials invoking the 78 | function. *) 79 | identity_id : string 80 | ; (* The identity pool id the caller is "registered" with. *) 81 | identity_pool_id : string 82 | } 83 | [@@deriving of_yojson { strict = false }] 84 | 85 | type event_context = 86 | { (* The ARN of the Lambda function being invoked. *) 87 | invoked_function_arn : string 88 | ; (* The AWS request ID generated by the Lambda service. *) 89 | aws_request_id : string 90 | ; (* The X-Ray trace ID for the current invocation. *) 91 | xray_trace_id : string option [@default None] 92 | ; (* The execution deadline for the current invocation in milliseconds. *) 93 | deadline : int64 94 | ; (* The client context object sent by the AWS mobile SDK. This field is empty 95 | unless the function is invoked using an AWS mobile SDK. *) 96 | client_context : client_context option 97 | ; (* The Cognito identity that invoked the function. This field is empty 98 | unless the invocation request to the Lambda APIs was made using AWS 99 | credentials issues by Amazon Cognito Identity Pools. *) 100 | identity : cognito_identity option 101 | } 102 | 103 | type t = Piaf.Client.t 104 | 105 | let make ~sw env endpoint = 106 | let uri = Uri.of_string (Format.asprintf "http://%s" endpoint) in 107 | Piaf.Client.create ~sw env uri 108 | 109 | let make_runtime_post_request client path output = 110 | let body = Yojson.Safe.to_string output in 111 | Piaf.Client.post 112 | client 113 | ~headers:[ "Content-Type", Constants.api_content_type ] 114 | ~body:(Piaf.Body.of_string body) 115 | path 116 | 117 | let event_response client request_id output = 118 | let open Piaf in 119 | let path = 120 | Format.asprintf 121 | "/%s/runtime/invocation/%s/response" 122 | Constants.runtime_api_version 123 | request_id 124 | in 125 | match make_runtime_post_request client path output with 126 | | Ok { Response.status; _ } -> 127 | if not (Status.is_successful status) 128 | then 129 | let error = 130 | Errors.make_api_error 131 | ~recoverable:false 132 | (Printf.sprintf 133 | "Error %d while sending response" 134 | (Status.to_code status)) 135 | in 136 | Error error 137 | else Ok () 138 | | Error _ -> 139 | let err = 140 | Errors.make_api_error 141 | ~recoverable:false 142 | (Printf.sprintf 143 | "Error when calling runtime API for request %s" 144 | request_id) 145 | in 146 | Error err 147 | 148 | let make_runtime_error_request connection path error = 149 | let open Piaf in 150 | let body = Errors.to_lambda_error error |> Yojson.Safe.to_string in 151 | Client.post 152 | connection 153 | ~headers: 154 | [ "Content-Type", Constants.api_error_content_type 155 | ; Constants.runtime_error_header, "RuntimeError" 156 | ] 157 | ~body:(Body.of_string body) 158 | path 159 | 160 | let event_error client request_id err = 161 | let open Piaf in 162 | let path = 163 | Format.asprintf 164 | "/%s/runtime/invocation/%s/error" 165 | Constants.runtime_api_version 166 | request_id 167 | in 168 | match make_runtime_error_request client path err with 169 | | Ok { Response.status; _ } -> 170 | if not (Status.is_successful status) 171 | then 172 | let error = 173 | Errors.make_api_error 174 | ~recoverable:true 175 | (Printf.sprintf 176 | "Error %d while sending response" 177 | (Status.to_code status)) 178 | in 179 | Error error 180 | else Ok () 181 | | Error _ -> 182 | let err = 183 | Errors.make_api_error 184 | ~recoverable:true 185 | (Printf.sprintf 186 | "Error when calling runtime API for request %s" 187 | request_id) 188 | in 189 | Error err 190 | 191 | let fail_init client err = 192 | let path = 193 | Format.asprintf "/%s/runtime/init/error" Constants.runtime_api_version 194 | in 195 | match make_runtime_error_request client path err with 196 | | Ok _ -> Ok () 197 | (* TODO: do we wanna "failwith" or just raise and then have a generic 198 | `Lwt.catch` that will `failwith`? *) 199 | | Error _ -> failwith "Error while sending init failed message" 200 | 201 | let get_event_context headers = 202 | let open Piaf in 203 | let report_error header = 204 | let err = 205 | Errors.make_api_error 206 | ~recoverable:true 207 | (Printf.sprintf "Missing %s header" header) 208 | in 209 | Error err 210 | in 211 | let open Constants in 212 | match Headers.get headers RequestHeaders.request_id with 213 | | None -> report_error RequestHeaders.request_id 214 | | Some aws_request_id -> 215 | (match Headers.get headers RequestHeaders.function_arn with 216 | | None -> report_error RequestHeaders.function_arn 217 | | Some invoked_function_arn -> 218 | (match Headers.get headers RequestHeaders.deadline with 219 | | None -> report_error RequestHeaders.deadline 220 | | Some deadline_str -> 221 | let deadline = Int64.of_string deadline_str in 222 | let client_context = 223 | match Headers.get headers RequestHeaders.client_context with 224 | | None -> None 225 | | Some ctx_json_str -> 226 | let ctx_json = Yojson.Safe.from_string ctx_json_str in 227 | (match client_context_of_yojson ctx_json with 228 | | Error _ -> None 229 | | Ok client_ctx -> Some client_ctx) 230 | in 231 | let identity = 232 | match Headers.get headers RequestHeaders.cognito_identity with 233 | | None -> None 234 | | Some cognito_json_str -> 235 | let cognito_json = Yojson.Safe.from_string cognito_json_str in 236 | (match cognito_identity_of_yojson cognito_json with 237 | | Error _ -> None 238 | | Ok cognito_identity -> Some cognito_identity) 239 | in 240 | let ctx = 241 | { aws_request_id 242 | ; invoked_function_arn 243 | ; xray_trace_id = Headers.get headers RequestHeaders.trace_id 244 | ; deadline 245 | ; client_context 246 | ; identity 247 | } 248 | in 249 | Ok ctx)) 250 | 251 | let next_event client = 252 | let open Piaf in 253 | let path = 254 | Format.asprintf "/%s/runtime/invocation/next" Constants.runtime_api_version 255 | in 256 | Logs.info (fun m -> m "Polling for next event. Path: %s\n" path); 257 | match Client.get client path with 258 | | Ok { Response.status; headers; body; _ } -> 259 | let code = Status.to_code status in 260 | if Status.is_client_error status 261 | then ( 262 | Logs.err (fun m -> 263 | m 264 | "Runtime API returned client error when polling for new events %d\n" 265 | code); 266 | let err = 267 | Errors.make_api_error 268 | ~recoverable:true 269 | (Printf.sprintf "Error %d when polling for events" code) 270 | in 271 | Error err) 272 | else if Status.is_server_error status 273 | then ( 274 | Logs.err (fun m -> 275 | m 276 | "Runtime API returned server error when polling for new events %d\n" 277 | code); 278 | let err = 279 | Errors.make_api_error 280 | ~recoverable:false 281 | "Server error when polling for new events" 282 | in 283 | Error err) 284 | else ( 285 | match get_event_context headers with 286 | | Error err -> 287 | Logs.err (fun m -> 288 | m "Failed to get event context: %s\n" (Errors.message err)); 289 | Error err 290 | | Ok ctx -> 291 | (match Body.to_string body with 292 | | Ok body_str -> Ok (body_str, ctx) 293 | | Error e -> 294 | let err = 295 | Errors.make_api_error 296 | ~recoverable:false 297 | (Format.asprintf 298 | "Server error when polling for new events: %a" 299 | Piaf.Error.pp_hum 300 | e) 301 | in 302 | Error err)) 303 | | Error _ -> 304 | let err = 305 | Errors.make_api_error 306 | ~recoverable:false 307 | "Server error when polling for new events" 308 | in 309 | Error err 310 | -------------------------------------------------------------------------------- /lib/config.ml: -------------------------------------------------------------------------------- 1 | (*---------------------------------------------------------------------------- 2 | * Copyright (c) 2018 António Nuno Monteiro 3 | * 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions are met: 8 | * 9 | * 1. Redistributions of source code must retain the above copyright notice, 10 | * this list of conditions and the following disclaimer. 11 | * 12 | * 2. Redistributions in binary form must reproduce the above copyright 13 | * notice, this list of conditions and the following disclaimer in the 14 | * documentation and/or other materials provided with the distribution. 15 | * 16 | * 3. Neither the name of the copyright holder nor the names of its 17 | * contributors may be used to endorse or promote products derived from this 18 | * software without specific prior written permission. 19 | * 20 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 24 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 27 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 28 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 | * POSSIBILITY OF SUCH DAMAGE. 31 | *---------------------------------------------------------------------------*) 32 | 33 | open Util 34 | 35 | type function_settings = 36 | { function_name : string 37 | ; memory_size : int 38 | ; version : string 39 | ; log_stream : string 40 | ; log_group : string 41 | } 42 | 43 | module Env_vars = struct 44 | let runtime_endpoint_var = "AWS_LAMBDA_RUNTIME_API" 45 | let lambda_function_name = "AWS_LAMBDA_FUNCTION_NAME" 46 | let lambda_function_version = "AWS_LAMBDA_FUNCTION_VERSION" 47 | let lambda_function_memory_size = "AWS_LAMBDA_FUNCTION_MEMORY_SIZE" 48 | let lambda_log_stream_name = "AWS_LAMBDA_LOG_STREAM_NAME" 49 | let lambda_log_group_name = "AWS_LAMBDA_LOG_GROUP_NAME" 50 | end 51 | 52 | let env = 53 | let env_lst = Array.to_list (Unix.environment ()) in 54 | List.fold_left 55 | (fun m var -> 56 | match String.split_on_char '=' var with 57 | | k :: v :: _ -> StringMap.add k v m 58 | | _ -> m) 59 | StringMap.empty 60 | env_lst 61 | 62 | let get_runtime_api_endpoint () = 63 | let var = Env_vars.runtime_endpoint_var in 64 | match StringMap.find_opt var env with 65 | | Some v -> Ok v 66 | | None -> Error (Printf.sprintf "Could not find runtime API env var: %s" var) 67 | 68 | let get_function_settings ?(env = env) () = 69 | let get_env_vars () = 70 | let function_name = StringMap.find_opt Env_vars.lambda_function_name env in 71 | let version = StringMap.find_opt Env_vars.lambda_function_version env in 72 | let memory_str = 73 | StringMap.find_opt Env_vars.lambda_function_memory_size env 74 | in 75 | let log_stream = StringMap.find_opt Env_vars.lambda_log_stream_name env in 76 | let log_group = StringMap.find_opt Env_vars.lambda_log_group_name env in 77 | function_name, version, memory_str, log_stream, log_group 78 | in 79 | match get_env_vars () with 80 | | ( Some function_name 81 | , Some version 82 | , Some memory_str 83 | , Some log_stream 84 | , Some log_group ) -> 85 | (match int_of_string memory_str with 86 | | memory_size -> 87 | Ok { function_name; memory_size; version; log_stream; log_group } 88 | | exception Failure _ -> 89 | Error 90 | (Printf.sprintf 91 | "Memory value from environment is not an int: %s" 92 | memory_str)) 93 | | _ -> 94 | Error 95 | "Could not find runtime API environment variables to determine function \ 96 | settings." 97 | -------------------------------------------------------------------------------- /lib/context.ml: -------------------------------------------------------------------------------- 1 | (*---------------------------------------------------------------------------- 2 | * Copyright (c) 2018 António Nuno Monteiro 3 | * 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions are met: 8 | * 9 | * 1. Redistributions of source code must retain the above copyright notice, 10 | * this list of conditions and the following disclaimer. 11 | * 12 | * 2. Redistributions in binary form must reproduce the above copyright 13 | * notice, this list of conditions and the following disclaimer in the 14 | * documentation and/or other materials provided with the distribution. 15 | * 16 | * 3. Neither the name of the copyright holder nor the names of its 17 | * contributors may be used to endorse or promote products derived from this 18 | * software without specific prior written permission. 19 | * 20 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 24 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 27 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 28 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 | * POSSIBILITY OF SUCH DAMAGE. 31 | *---------------------------------------------------------------------------*) 32 | 33 | type invocation_context = 34 | { memory_limit_in_mb : int 35 | ; function_name : string 36 | ; function_version : string 37 | ; invoked_function_arn : string 38 | ; aws_request_id : string 39 | ; xray_trace_id : string option 40 | ; log_stream_name : string 41 | ; log_group_name : string 42 | ; client_context : Client.client_context option 43 | ; identity : Client.cognito_identity option 44 | ; deadline : int64 45 | } 46 | 47 | type t = 48 | { invocation_context : invocation_context 49 | ; sw : Eio.Switch.t 50 | ; env : Eio.Stdenv.t 51 | } 52 | 53 | let make 54 | ~invoked_function_arn 55 | ~aws_request_id 56 | ~xray_trace_id 57 | ~client_context 58 | ~identity 59 | ~deadline 60 | settings 61 | = 62 | { memory_limit_in_mb = settings.Config.memory_size 63 | ; function_name = settings.function_name 64 | ; function_version = settings.version 65 | ; log_stream_name = settings.log_stream 66 | ; log_group_name = settings.log_group 67 | ; invoked_function_arn 68 | ; aws_request_id 69 | ; xray_trace_id 70 | ; client_context 71 | ; identity 72 | ; deadline 73 | } 74 | -------------------------------------------------------------------------------- /lib/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (name lambda_runtime) 3 | (public_name lambda-runtime) 4 | (preprocess 5 | (pps ppx_deriving_yojson)) 6 | (libraries 7 | yojson 8 | ppx_deriving_yojson.runtime 9 | bigstringaf 10 | uri 11 | logs 12 | eio 13 | eio_main 14 | piaf)) 15 | -------------------------------------------------------------------------------- /lib/errors.ml: -------------------------------------------------------------------------------- 1 | (*---------------------------------------------------------------------------- 2 | * Copyright (c) 2018 António Nuno Monteiro 3 | * 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions are met: 8 | * 9 | * 1. Redistributions of source code must retain the above copyright notice, 10 | * this list of conditions and the following disclaimer. 11 | * 12 | * 2. Redistributions in binary form must reproduce the above copyright 13 | * notice, this list of conditions and the following disclaimer in the 14 | * documentation and/or other materials provided with the distribution. 15 | * 16 | * 3. Neither the name of the copyright holder nor the names of its 17 | * contributors may be used to endorse or promote products derived from this 18 | * software without specific prior written permission. 19 | * 20 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 24 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 27 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 28 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 | * POSSIBILITY OF SUCH DAMAGE. 31 | *---------------------------------------------------------------------------*) 32 | 33 | type error_record = 34 | { msg : string 35 | ; (* stack_trace: string option; (* Option, *) *) 36 | (* The request id that generated this error *) 37 | request_id : string option 38 | ; (* Whether the error is recoverable or not. *) 39 | recoverable : bool 40 | } 41 | 42 | type lambda_error = 43 | { error_message : string [@key "errorMessage"] 44 | ; error_type : string [@key "errorType"] 45 | } 46 | [@@deriving to_yojson] 47 | 48 | type _ t = 49 | | RuntimeError : error_record -> [ `unhandled ] t 50 | | ApiError : error_record -> [ `unhandled ] t 51 | | HandlerError : lambda_error -> [ `handled ] t 52 | 53 | module Constants = struct 54 | let error_type_handled = "Handled" 55 | let error_type_unhandled = "Unhandled" 56 | end 57 | 58 | (* TODO: consider making `make_recoverable` / `make_unrecoverable` helpers *) 59 | let make_runtime_error ?request_id ~recoverable msg = 60 | RuntimeError { msg; request_id; recoverable } 61 | 62 | let make_api_error ?request_id ~recoverable msg = 63 | ApiError { msg; request_id; recoverable } 64 | 65 | let make_handler_error msg = 66 | HandlerError 67 | { error_message = msg; error_type = Constants.error_type_handled } 68 | 69 | let is_recoverable = function 70 | | ApiError { recoverable; _ } | RuntimeError { recoverable; _ } -> recoverable 71 | 72 | let message : type a. a t -> string = function 73 | | HandlerError { error_message; _ } -> error_message 74 | | ApiError { msg; _ } -> msg 75 | | RuntimeError { msg; _ } -> msg 76 | 77 | let request_id = function 78 | | ApiError { request_id; _ } -> request_id 79 | | RuntimeError { request_id; _ } -> request_id 80 | 81 | let to_lambda_error : type a. ?handled:bool -> a t -> Yojson.Safe.t = 82 | fun ?(handled = true) error -> 83 | let make_lambda_error e = 84 | { error_message = e.msg 85 | ; error_type = 86 | (if handled 87 | then Constants.error_type_handled 88 | else Constants.error_type_unhandled) 89 | } 90 | in 91 | let lambda_error = 92 | match error with 93 | | HandlerError e -> e 94 | | ApiError e -> make_lambda_error e 95 | | RuntimeError e -> make_lambda_error e 96 | in 97 | lambda_error_to_yojson lambda_error 98 | -------------------------------------------------------------------------------- /lib/http.ml: -------------------------------------------------------------------------------- 1 | (*---------------------------------------------------------------------------- 2 | * Copyright (c) 2018 António Nuno Monteiro 3 | * 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions are met: 8 | * 9 | * 1. Redistributions of source code must retain the above copyright notice, 10 | * this list of conditions and the following disclaimer. 11 | * 12 | * 2. Redistributions in binary form must reproduce the above copyright 13 | * notice, this list of conditions and the following disclaimer in the 14 | * documentation and/or other materials provided with the distribution. 15 | * 16 | * 3. Neither the name of the copyright holder nor the names of its 17 | * contributors may be used to endorse or promote products derived from this 18 | * software without specific prior written permission. 19 | * 20 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 24 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 27 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 28 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 | * POSSIBILITY OF SUCH DAMAGE. 31 | *---------------------------------------------------------------------------*) 32 | 33 | open Util 34 | 35 | (* APIGatewayRequestIdentity contains identity information for the request 36 | * caller. *) 37 | type api_gateway_request_identity = 38 | { cognito_identity_pool_id : string option [@key "cognitoIdentityPoolId"] 39 | ; account_id : string option [@key "accountId"] 40 | ; cognito_identity_id : string option [@key "cognitoIdentityId"] 41 | ; caller : string option 42 | ; access_key : string option [@key "accessKey"] 43 | ; api_key : string option [@key "apiKey"] [@default None] 44 | ; source_ip : string [@key "sourceIp"] 45 | ; cognito_authentication_type : string option 46 | [@key "cognitoAuthenticationType"] 47 | ; cognito_authentication_provider : string option 48 | [@key "cognitoAuthenticationProvider"] 49 | ; user_arn : string option [@key "userArn"] 50 | ; user_agent : string option [@key "userAgent"] 51 | ; user : string option 52 | } 53 | [@@deriving of_yojson { strict = false }] 54 | 55 | (* APIGatewayProxyRequestContext contains the information to identify the AWS 56 | * account and resources invoking the Lambda function. It also includes Cognito 57 | * identity information for the caller. *) 58 | type api_gateway_proxy_request_context = 59 | { account_id : string [@key "accountId"] 60 | ; resource_id : string [@key "resourceId"] 61 | ; stage : string 62 | ; request_id : string [@key "requestId"] 63 | ; identity : api_gateway_request_identity 64 | ; resource_path : string [@key "resourcePath"] 65 | ; (* authorizer: Yojson.Safe.json StringMap.t; *) 66 | authorizer : string StringMap.t option [@default None] 67 | ; http_method : string [@key "httpMethod"] 68 | ; protocol : string option [@default None] 69 | ; path : string option [@default None] 70 | ; api_id : string [@key "apiId"] (* The API Gateway REST API ID *) 71 | } 72 | [@@deriving of_yojson { strict = false }] 73 | 74 | type api_gateway_proxy_request = 75 | { resource : string 76 | ; path : string 77 | ; http_method : string [@key "httpMethod"] 78 | ; headers : string StringMap.t 79 | ; query_string_parameters : string StringMap.t 80 | [@key "queryStringParameters"] [@default StringMap.empty] 81 | ; path_parameters : string StringMap.t 82 | [@key "pathParameters"] [@default StringMap.empty] 83 | ; stage_variables : string StringMap.t 84 | [@key "stageVariables"] [@default StringMap.empty] 85 | ; request_context : api_gateway_proxy_request_context [@key "requestContext"] 86 | ; body : string option 87 | ; is_base64_encoded : bool [@key "isBase64Encoded"] 88 | } 89 | [@@deriving of_yojson { strict = false }] 90 | 91 | type api_gateway_proxy_response = 92 | { status_code : int [@key "statusCode"] 93 | ; headers : string StringMap.t 94 | ; body : string 95 | ; is_base64_encoded : bool [@key "isBase64Encoded"] 96 | } 97 | [@@deriving to_yojson] 98 | 99 | module API_gateway_request = struct 100 | type t = api_gateway_proxy_request [@@deriving of_yojson { strict = false }] 101 | end 102 | 103 | module API_gateway_response = struct 104 | type t = api_gateway_proxy_response [@@deriving to_yojson] 105 | end 106 | 107 | include Runtime.Make (API_gateway_request) (API_gateway_response) 108 | -------------------------------------------------------------------------------- /lib/httpv2.ml: -------------------------------------------------------------------------------- 1 | (*---------------------------------------------------------------------------- 2 | * Copyright (c) 2018 António Nuno Monteiro 3 | * Copyright (c) 2023 Christopher Armstrong 4 | * 5 | * All rights reserved. 6 | * 7 | * Redistribution and use in source and binary forms, with or without 8 | * modification, are permitted provided that the following conditions are met: 9 | * 10 | * 1. Redistributions of source code must retain the above copyright notice, 11 | * this list of conditions and the following disclaimer. 12 | * 13 | * 2. Redistributions in binary form must reproduce the above copyright 14 | * notice, this list of conditions and the following disclaimer in the 15 | * documentation and/or other materials provided with the distribution. 16 | * 17 | * 3. Neither the name of the copyright holder nor the names of its 18 | * contributors may be used to endorse or promote products derived from this 19 | * software without specific prior written permission. 20 | * 21 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 24 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 25 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 26 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 27 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 28 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 29 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 30 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 31 | * POSSIBILITY OF SUCH DAMAGE. 32 | *---------------------------------------------------------------------------*) 33 | 34 | open Util 35 | 36 | type api_gateway_proxy_request_context_http = 37 | { method_ : string [@key "method"] 38 | ; path : string 39 | ; protocol : string 40 | ; source_ip : string [@key "sourceIp"] 41 | ; user_agent : string [@key "userAgent"] 42 | } 43 | [@@deriving of_yojson { strict = false }] 44 | 45 | type api_gateway_request_context_jwt = 46 | { claims : string StringMap.t [@default StringMap.empty] 47 | ; scopes : string StringMap.t [@default StringMap.empty] 48 | } 49 | [@@deriving of_yojson { strict = false }] 50 | 51 | type api_gateway_proxy_request_context_authorizer = 52 | { jwt : api_gateway_request_context_jwt } 53 | [@@deriving of_yojson { strict = false }] 54 | 55 | (* APIGatewayProxyRequestContext contains the information to identify the AWS 56 | * account and resources invoking the Lambda function. It also includes Cognito 57 | * identity information for the caller. *) 58 | type api_gateway_proxy_request_context = 59 | { account_id : string [@key "accountId"] 60 | ; api_id : string [@key "apiId"] (* The API Gateway REST API ID *) 61 | ; domain_name : string [@key "domainName"] 62 | ; domain_prefix : string [@key "domainPrefix"] 63 | ; http : api_gateway_proxy_request_context_http 64 | ; resource_id : string option [@key "resourceId"] [@default None] 65 | ; stage : string 66 | ; request_id : string [@key "requestId"] 67 | ; route_key : string [@key "routeKey"] 68 | ; time : string 69 | ; time_epoch : int64 [@key "timeEpoch"] 70 | ; authorizer : api_gateway_proxy_request_context_authorizer option 71 | [@default None] 72 | } 73 | [@@deriving of_yojson { strict = false }] 74 | 75 | type api_gateway_proxy_request = 76 | { version : string 77 | ; route_key : string [@key "routeKey"] 78 | ; raw_query_string : string [@key "rawQueryString"] 79 | ; cookies : string list option [@default None] 80 | ; headers : string StringMap.t 81 | ; query_string_parameters : string StringMap.t 82 | [@key "queryStringParameters"] [@default StringMap.empty] 83 | ; request_context : api_gateway_proxy_request_context [@key "requestContext"] 84 | ; body : string option [@default None] 85 | ; path_parameters : string StringMap.t 86 | [@key "pathParameters"] [@default StringMap.empty] 87 | ; is_base64_encoded : bool [@key "isBase64Encoded"] 88 | ; stage_variables : string StringMap.t 89 | [@key "stageVariables"] [@default StringMap.empty] 90 | } 91 | [@@deriving of_yojson { strict = false }] 92 | 93 | type api_gateway_proxy_response = 94 | { status_code : int [@key "statusCode"] 95 | ; headers : string StringMap.t 96 | ; body : string 97 | ; is_base64_encoded : bool [@key "isBase64Encoded"] 98 | } 99 | [@@deriving to_yojson] 100 | 101 | module API_gateway_request = struct 102 | type t = api_gateway_proxy_request [@@deriving of_yojson { strict = false }] 103 | end 104 | 105 | module API_gateway_response = struct 106 | type t = api_gateway_proxy_response [@@deriving to_yojson] 107 | end 108 | 109 | include Runtime.Make (API_gateway_request) (API_gateway_response) 110 | -------------------------------------------------------------------------------- /lib/json.ml: -------------------------------------------------------------------------------- 1 | (*---------------------------------------------------------------------------- 2 | * Copyright (c) 2018 António Nuno Monteiro 3 | * 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions are met: 8 | * 9 | * 1. Redistributions of source code must retain the above copyright notice, 10 | * this list of conditions and the following disclaimer. 11 | * 12 | * 2. Redistributions in binary form must reproduce the above copyright 13 | * notice, this list of conditions and the following disclaimer in the 14 | * documentation and/or other materials provided with the distribution. 15 | * 16 | * 3. Neither the name of the copyright holder nor the names of its 17 | * contributors may be used to endorse or promote products derived from this 18 | * software without specific prior written permission. 19 | * 20 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 24 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 27 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 28 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 | * POSSIBILITY OF SUCH DAMAGE. 31 | *---------------------------------------------------------------------------*) 32 | 33 | module Id = struct 34 | type t = Yojson.Safe.t [@@deriving yojson] 35 | end 36 | 37 | include Runtime.Make (Id) (Id) 38 | -------------------------------------------------------------------------------- /lib/lambda_runtime.ml: -------------------------------------------------------------------------------- 1 | (*---------------------------------------------------------------------------- 2 | * Copyright (c) 2018 António Nuno Monteiro 3 | * 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions are met: 8 | * 9 | * 1. Redistributions of source code must retain the above copyright notice, 10 | * this list of conditions and the following disclaimer. 11 | * 12 | * 2. Redistributions in binary form must reproduce the above copyright 13 | * notice, this list of conditions and the following disclaimer in the 14 | * documentation and/or other materials provided with the distribution. 15 | * 16 | * 3. Neither the name of the copyright holder nor the names of its 17 | * contributors may be used to endorse or promote products derived from this 18 | * software without specific prior written permission. 19 | * 20 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 24 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 27 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 28 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 | * POSSIBILITY OF SUCH DAMAGE. 31 | *---------------------------------------------------------------------------*) 32 | 33 | module Client = Client 34 | module Context = Context 35 | module StringMap = Util.StringMap 36 | module Make = Runtime.Make 37 | module Json = Json 38 | module Http = Http 39 | module Httpv2 = Httpv2 40 | 41 | module type LambdaEvent = Runtime.LambdaEvent 42 | module type LambdaResponse = Runtime.LambdaResponse 43 | module type LambdaRuntime = Runtime.LambdaRuntime 44 | -------------------------------------------------------------------------------- /lib/lambda_runtime.mli: -------------------------------------------------------------------------------- 1 | (*---------------------------------------------------------------------------- 2 | * Copyright (c) 2018 António Nuno Monteiro 3 | * 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions are met: 8 | * 9 | * 1. Redistributions of source code must retain the above copyright notice, 10 | * this list of conditions and the following disclaimer. 11 | * 12 | * 2. Redistributions in binary form must reproduce the above copyright 13 | * notice, this list of conditions and the following disclaimer in the 14 | * documentation and/or other materials provided with the distribution. 15 | * 16 | * 3. Neither the name of the copyright holder nor the names of its 17 | * contributors may be used to endorse or promote products derived from this 18 | * software without specific prior written permission. 19 | * 20 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 24 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 27 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 28 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 | * POSSIBILITY OF SUCH DAMAGE. 31 | *---------------------------------------------------------------------------*) 32 | 33 | module Context : sig 34 | type invocation_context = 35 | { memory_limit_in_mb : int 36 | (** The amount of memory allocated to the lambda function in MB. This 37 | value is extracted from the `AWS_LAMBDA_FUNCTION_MEMORY_SIZE` 38 | environment variable set by the lambda service. *) 39 | ; function_name : string 40 | (** The name of the lambda function as registered with the lambda 41 | service. The value is extracted from the 42 | `AWS_LAMBDA_FUNCTION_NAME` environment variable set by the lambda 43 | service. *) 44 | ; function_version : string 45 | (** The version of the function being invoked. This value is extracted 46 | from the `AWS_LAMBDA_FUNCTION_VERSION` environment variable set by 47 | the lambda service. *) 48 | ; invoked_function_arn : string 49 | (** The fully qualified ARN (Amazon Resource Name) for the function 50 | invocation event. This value is returned by the lambda runtime 51 | APIs as a header. *) 52 | ; aws_request_id : string 53 | (** The AWS Request ID for the current invocation event. This value is 54 | returned by the lambda runtime APIs as a header. *) 55 | ; xray_trace_id : string option 56 | (** The x-ray trace id for the current invocation. this value is 57 | returned by the lambda runtime apis as a header. developers can 58 | use this value with the aws sdk to create new, custom sub-segments 59 | to the current invocation. *) 60 | ; log_stream_name : string 61 | (** The name of the cloudwatch log stream for the current execution 62 | environment. this value is extracted from the 63 | `aws_lambda_log_stream_name` environment variable set by the 64 | lambda service. *) 65 | ; log_group_name : string 66 | (** The name of the CloudWatch log group for the current execution 67 | environment. This value is extracted from the 68 | `AWS_LAMBDA_LOG_GROUP_NAME` environment variable set by the lambda 69 | service. *) 70 | ; client_context : Client.client_context option 71 | (** The client context sent by the AWS Mobile SDK with the invocation 72 | request. This value is returned by the lambda runtime APIs as a 73 | header. This value is populated only if the invocation request 74 | originated from an AWS Mobile SDK or an SDK that attached the 75 | client context information to the request. *) 76 | ; identity : Client.cognito_identity option 77 | (** The information of the Cognito Identity that sent the invocation 78 | request to the lambda service. This value is returned by the 79 | lambda runtime APIs in a header and it's only populated if the 80 | invocation request was performed with AWS credentials federated 81 | through the Cognito Identity service. *) 82 | ; deadline : int64 83 | (** The deadline for the current handler execution in nanoseconds. *) 84 | } 85 | 86 | type t = 87 | { invocation_context : invocation_context 88 | ; sw : Eio.Switch.t 89 | ; env : Eio.Stdenv.t 90 | } 91 | end 92 | 93 | module type LambdaEvent = sig 94 | type t 95 | 96 | val of_yojson : Yojson.Safe.t -> (t, string) result 97 | end 98 | 99 | module type LambdaResponse = sig 100 | type t 101 | 102 | val to_yojson : t -> Yojson.Safe.t 103 | end 104 | 105 | module type LambdaRuntime = sig 106 | type event 107 | type response 108 | 109 | val lambda : (event -> Context.t -> (response, string) result) -> unit 110 | end 111 | 112 | module Make (Event : LambdaEvent) (Response : LambdaResponse) : 113 | LambdaRuntime with type event := Event.t and type response := Response.t 114 | 115 | module Json : sig 116 | include 117 | LambdaRuntime 118 | with type event := Yojson.Safe.t 119 | and type response := Yojson.Safe.t 120 | end 121 | 122 | (** API Gateway v1 event and response definitions *) 123 | module Http : sig 124 | open Util 125 | 126 | (* APIGatewayRequestIdentity contains identity information for the request 127 | * caller. *) 128 | type api_gateway_request_identity = 129 | { cognito_identity_pool_id : string option 130 | ; account_id : string option 131 | ; cognito_identity_id : string option 132 | ; caller : string option 133 | ; access_key : string option 134 | ; api_key : string option 135 | ; source_ip : string 136 | ; cognito_authentication_type : string option 137 | ; cognito_authentication_provider : string option 138 | ; user_arn : string option 139 | ; user_agent : string option 140 | ; user : string option 141 | } 142 | 143 | (* APIGatewayProxyRequestContext contains the information to identify the AWS 144 | * account and resources invoking the Lambda function. It also includes 145 | * Cognito identity information for the caller. *) 146 | type api_gateway_proxy_request_context = 147 | { account_id : string 148 | ; resource_id : string 149 | ; stage : string 150 | ; request_id : string 151 | ; identity : api_gateway_request_identity 152 | ; resource_path : string 153 | ; authorizer : string StringMap.t option 154 | ; http_method : string 155 | ; protocol : string option 156 | ; path : string option 157 | ; api_id : string (** The API Gateway REST API ID *) 158 | } 159 | 160 | (* A request to API Gateway using the proxy integration *) 161 | type api_gateway_proxy_request = 162 | { resource : string 163 | ; path : string 164 | ; http_method : string 165 | ; headers : string StringMap.t 166 | ; query_string_parameters : string StringMap.t 167 | ; path_parameters : string StringMap.t 168 | ; stage_variables : string StringMap.t 169 | ; request_context : api_gateway_proxy_request_context 170 | ; body : string option 171 | ; is_base64_encoded : bool 172 | } 173 | 174 | type api_gateway_proxy_response = 175 | { status_code : int 176 | ; headers : string StringMap.t 177 | ; body : string 178 | ; is_base64_encoded : bool 179 | } 180 | 181 | include 182 | LambdaRuntime 183 | with type event := api_gateway_proxy_request 184 | and type response := api_gateway_proxy_response 185 | end 186 | 187 | (** API Gateway v2 event and response definitions *) 188 | module Httpv2 : sig 189 | open Util 190 | 191 | type api_gateway_proxy_request_context_http = 192 | { method_ : string 193 | ; path : string 194 | ; protocol : string 195 | ; source_ip : string 196 | ; user_agent : string 197 | } 198 | 199 | type api_gateway_request_context_jwt = 200 | { claims : string StringMap.t [@default StringMap.empty] 201 | ; scopes : string StringMap.t [@default StringMap.empty] 202 | } 203 | 204 | type api_gateway_proxy_request_context_authorizer = 205 | { jwt : api_gateway_request_context_jwt } 206 | 207 | (* APIGatewayProxyRequestContext contains the information to identify the AWS 208 | * account and resources invoking the Lambda function. It also includes Cognito 209 | * identity information for the caller. *) 210 | type api_gateway_proxy_request_context = 211 | { account_id : string 212 | ; api_id : string 213 | ; domain_name : string 214 | ; domain_prefix : string 215 | ; http : api_gateway_proxy_request_context_http 216 | ; resource_id : string option 217 | ; stage : string 218 | ; request_id : string 219 | ; route_key : string 220 | ; time : string 221 | ; time_epoch : int64 222 | ; authorizer : api_gateway_proxy_request_context_authorizer option 223 | } 224 | 225 | (* A request to API Gateway using the proxy integration (v2). 226 | 227 | This differs from Http.api_gateway_proxy_request in that it does away with 228 | multi_value_headers *) 229 | type api_gateway_proxy_request = 230 | { version : string 231 | ; route_key : string 232 | ; raw_query_string : string 233 | ; cookies : string list option 234 | ; headers : string StringMap.t 235 | ; query_string_parameters : string StringMap.t 236 | ; request_context : api_gateway_proxy_request_context 237 | ; body : string option 238 | ; path_parameters : string StringMap.t 239 | ; is_base64_encoded : bool 240 | ; stage_variables : string StringMap.t 241 | } 242 | 243 | type api_gateway_proxy_response = 244 | { status_code : int 245 | ; headers : string StringMap.t 246 | ; body : string 247 | ; is_base64_encoded : bool 248 | } 249 | 250 | include 251 | LambdaRuntime 252 | with type event := api_gateway_proxy_request 253 | and type response := api_gateway_proxy_response 254 | end 255 | -------------------------------------------------------------------------------- /lib/runtime.ml: -------------------------------------------------------------------------------- 1 | (*---------------------------------------------------------------------------- 2 | * Copyright (c) 2018 António Nuno Monteiro 3 | * 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions are met: 8 | * 9 | * 1. Redistributions of source code must retain the above copyright notice, 10 | * this list of conditions and the following disclaimer. 11 | * 12 | * 2. Redistributions in binary form must reproduce the above copyright 13 | * notice, this list of conditions and the following disclaimer in the 14 | * documentation and/or other materials provided with the distribution. 15 | * 16 | * 3. Neither the name of the copyright holder nor the names of its 17 | * contributors may be used to endorse or promote products derived from this 18 | * software without specific prior written permission. 19 | * 20 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 24 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 27 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 28 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 | * POSSIBILITY OF SUCH DAMAGE. 31 | *---------------------------------------------------------------------------*) 32 | 33 | module type LambdaEvent = sig 34 | type t 35 | 36 | val of_yojson : Yojson.Safe.t -> (t, string) result 37 | end 38 | 39 | module type LambdaResponse = sig 40 | type t 41 | 42 | val to_yojson : t -> Yojson.Safe.t 43 | end 44 | 45 | module type LambdaRuntime = sig 46 | type event 47 | type response 48 | 49 | val lambda : (event -> Context.t -> (response, string) result) -> unit 50 | end 51 | 52 | module Make (Event : LambdaEvent) (Response : LambdaResponse) = struct 53 | type 'a runtime = 54 | { client : Client.t 55 | ; settings : Config.function_settings 56 | ; handler : Event.t -> Context.t -> ('a, string) result 57 | ; max_retries : int 58 | } 59 | 60 | let make ~handler ~max_retries ~settings client = 61 | { client; settings; max_retries; handler } 62 | 63 | let rec get_next_event ?error runtime retries = 64 | match error with 65 | | Some err when retries > runtime.max_retries -> 66 | (match Errors.request_id err with 67 | | Some req_id -> Client.event_error runtime.client req_id err 68 | | None -> Client.fail_init runtime.client err) 69 | |> ignore; 70 | (* These errors are not recoverable. Either we can't communicate with the 71 | * runtime APIs or we cannot parse the event. panic to restart the 72 | * environment. *) 73 | failwith "Could not retrieve next event" 74 | | _ -> 75 | (match Client.next_event runtime.client with 76 | | Ok (ev_data, invocation_ctx) -> 77 | (match ev_data |> Yojson.Safe.from_string |> Event.of_yojson with 78 | | Ok ev -> 79 | let handler_ctx = 80 | Context.make 81 | ~invoked_function_arn:invocation_ctx.invoked_function_arn 82 | ~aws_request_id:invocation_ctx.aws_request_id 83 | ~xray_trace_id:invocation_ctx.xray_trace_id 84 | ~client_context:invocation_ctx.client_context 85 | ~identity:invocation_ctx.identity 86 | ~deadline:invocation_ctx.deadline 87 | runtime.settings 88 | in 89 | ev, handler_ctx 90 | | Error err -> 91 | Logs.err (fun m -> m "Could not parse event to type: %s" err); 92 | let error = 93 | Errors.make_runtime_error 94 | ~recoverable:true 95 | ~request_id:invocation_ctx.aws_request_id 96 | (Printf.sprintf "Could not unserialize from JSON: %s" err) 97 | in 98 | get_next_event ~error runtime (retries + 1) 99 | | exception _ -> 100 | let error = 101 | Errors.make_runtime_error 102 | ~recoverable:false 103 | ~request_id:invocation_ctx.aws_request_id 104 | (Printf.sprintf "Could not parse event to type: %s" ev_data) 105 | in 106 | get_next_event ~error runtime (retries + 1)) 107 | | Error e -> get_next_event ~error:e runtime (retries + 1)) 108 | 109 | let invoke { handler; _ } event ctx = 110 | try handler event ctx with 111 | | exn -> 112 | let backtrace = Printexc.get_backtrace () in 113 | let exn_str = Printexc.to_string exn in 114 | Error (Printf.sprintf "Handler raised: %s\n%s" exn_str backtrace) 115 | 116 | let rec start ~sw env runtime = 117 | let event, ctx = get_next_event runtime 0 in 118 | let request_id = ctx.aws_request_id in 119 | match invoke runtime event { invocation_context = ctx; sw; env } with 120 | | Ok response -> 121 | let response_json = Response.to_yojson response in 122 | (match Client.event_response runtime.client request_id response_json with 123 | | Ok _ -> start ~sw env runtime 124 | | Error e -> 125 | if not (Errors.is_recoverable e) 126 | then ( 127 | let (_ : _ result) = Client.fail_init runtime.client e in 128 | Logs.err (fun m -> 129 | m "Could not send error response %s" (Errors.message e)); 130 | failwith "Could not send error response") 131 | else start ~sw env runtime) 132 | | Error msg -> 133 | let handler_error = Errors.make_handler_error msg in 134 | (match Client.event_error runtime.client request_id handler_error with 135 | | Ok _ -> start ~sw env runtime 136 | | Error e -> 137 | if not (Errors.is_recoverable e) 138 | then ( 139 | Logs.err (fun m -> 140 | m "Could not send error response %s" (Errors.message e)); 141 | let (_ : _ result) = Client.fail_init runtime.client e in 142 | failwith "Could not send error response") 143 | else start ~sw env runtime) 144 | 145 | let start_with_runtime_endpoint ~sw env handler function_config endpoint = 146 | match Client.make ~sw env endpoint with 147 | | Ok client -> 148 | let runtime = 149 | make ~max_retries:3 ~settings:function_config ~handler client 150 | in 151 | start ~sw env runtime 152 | | Error e -> 153 | failwith 154 | (Format.asprintf "Could not start HTTP client: %a" Piaf.Error.pp_hum e) 155 | 156 | let lambda handler = 157 | match Config.get_runtime_api_endpoint () with 158 | | Ok endpoint -> 159 | (match Config.get_function_settings () with 160 | | Ok function_config -> 161 | Eio_main.run (fun env -> 162 | Eio.Switch.run (fun sw -> 163 | start_with_runtime_endpoint 164 | ~sw 165 | env 166 | handler 167 | function_config 168 | endpoint)) 169 | | Error msg -> failwith msg) 170 | | Error msg -> failwith msg 171 | end 172 | -------------------------------------------------------------------------------- /lib/util.ml: -------------------------------------------------------------------------------- 1 | (*---------------------------------------------------------------------------- 2 | * Copyright (c) 2018 António Nuno Monteiro 3 | * 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions are met: 8 | * 9 | * 1. Redistributions of source code must retain the above copyright notice, 10 | * this list of conditions and the following disclaimer. 11 | * 12 | * 2. Redistributions in binary form must reproduce the above copyright 13 | * notice, this list of conditions and the following disclaimer in the 14 | * documentation and/or other materials provided with the distribution. 15 | * 16 | * 3. Neither the name of the copyright holder nor the names of its 17 | * contributors may be used to endorse or promote products derived from this 18 | * software without specific prior written permission. 19 | * 20 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 24 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 27 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 28 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 | * POSSIBILITY OF SUCH DAMAGE. 31 | *---------------------------------------------------------------------------*) 32 | 33 | module StringMap = struct 34 | include Map.Make (String) 35 | 36 | let find_opt k t = 37 | match find k t with v -> Some v | exception Not_found -> None 38 | 39 | let to_yojson a_to_yojson t = 40 | let items = List.map (fun (key, v) -> key, a_to_yojson v) (bindings t) in 41 | `Assoc items 42 | 43 | let of_yojson a_of_yojson = function 44 | | `Assoc items -> 45 | let rec f map = function 46 | | [] -> Ok map 47 | | (name, json) :: xs -> 48 | (match a_of_yojson json with 49 | | Ok value -> f (add name value map) xs 50 | | Error _ as err -> err) 51 | in 52 | f empty items 53 | | `Null -> Ok empty 54 | | _ -> Error "expected an object" 55 | end 56 | 57 | let id : 'a. 'a -> 'a = fun x -> x 58 | -------------------------------------------------------------------------------- /nix/ci/test.nix: -------------------------------------------------------------------------------- 1 | { ocamlVersion }: 2 | 3 | let 4 | lock = builtins.fromJSON (builtins.readFile ./../../flake.lock); 5 | src = fetchGit { 6 | url = with lock.nodes.nixpkgs.locked;"https://github.com/${owner}/${repo}"; 7 | inherit (lock.nodes.nixpkgs.locked) rev; 8 | }; 9 | 10 | nix-filter-src = fetchGit { 11 | url = with lock.nodes.nix-filter.locked; "https://github.com/${owner}/${repo}"; 12 | inherit (lock.nodes.nix-filter.locked) rev; 13 | # inherit (lock.nodes.nixpkgs.original) ref; 14 | allRefs = true; 15 | }; 16 | nix-filter = import "${nix-filter-src}"; 17 | 18 | pkgs = import src { 19 | extraOverlays = [ 20 | (self: super: { 21 | ocamlPackages = super.ocaml-ng."ocamlPackages_${ocamlVersion}"; 22 | 23 | pkgsCross.musl64 = super.pkgsCross.musl64 // { 24 | ocamlPackages = super.pkgsCross.musl64.ocaml-ng."ocamlPackages_${ocamlVersion}"; 25 | }; 26 | }) 27 | ]; 28 | }; 29 | 30 | inherit (pkgs) lib stdenv fetchTarball ocamlPackages; 31 | 32 | lambda-pkgs = import ./.. { inherit pkgs nix-filter; }; 33 | lambda-drvs = lib.filterAttrs (_: value: lib.isDerivation value) lambda-pkgs; 34 | in 35 | stdenv.mkDerivation { 36 | name = "lambda-runtime-tests"; 37 | src = ./../..; 38 | dontBuild = true; 39 | installPhase = '' 40 | touch $out 41 | ''; 42 | buildInputs = (lib.attrValues lambda-drvs) ++ (with ocamlPackages; [ ocaml dune findlib ocamlformat reason ]); 43 | doCheck = true; 44 | checkPhase = '' 45 | # Check code is formatted with OCamlformat 46 | dune build @fmt 47 | dune build @examples/all 48 | ''; 49 | } 50 | -------------------------------------------------------------------------------- /nix/default.nix: -------------------------------------------------------------------------------- 1 | { nix-filter, pkgs }: 2 | 3 | let 4 | inherit (pkgs) lib stdenv ocamlPackages; 5 | in 6 | 7 | with ocamlPackages; 8 | 9 | let 10 | genSrc = { dirs, files }: 11 | with nix-filter; filter { 12 | root = ./..; 13 | include = [ "dune-project" ] ++ files ++ (builtins.map inDirectory dirs); 14 | }; 15 | 16 | build-lambda-runtime = args: buildDunePackage ({ 17 | useDune2 = true; 18 | version = "0.1.0-dev"; 19 | } // args); 20 | 21 | in 22 | rec { 23 | lambda-runtime = build-lambda-runtime { 24 | pname = "lambda-runtime"; 25 | src = genSrc { 26 | dirs = [ "lib" ]; 27 | files = [ "lambda-runtime.opam" ]; 28 | }; 29 | buildInputs = [ alcotest ]; 30 | doCheck = false; 31 | propagatedBuildInputs = [ yojson ppx_deriving_yojson piaf uri logs eio_main ]; 32 | }; 33 | 34 | vercel = build-lambda-runtime { 35 | pname = "vercel"; 36 | src = genSrc { 37 | dirs = [ "vercel" "test" ]; 38 | files = [ "vercel.opam" ]; 39 | }; 40 | buildInputs = [ alcotest ]; 41 | # tests lambda-runtime too 42 | doCheck = true; 43 | propagatedBuildInputs = [ 44 | lambda-runtime 45 | piaf 46 | yojson 47 | ppx_deriving_yojson 48 | base64 49 | ]; 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /scripts/esy-docker.mk: -------------------------------------------------------------------------------- 1 | define DOCKERFILE_ESY 2 | # start from node image so we can install esy from npm 3 | 4 | FROM node:10.13-alpine as build 5 | 6 | ENV TERM=dumb \ 7 | LD_LIBRARY_PATH=/usr/local/lib:/usr/lib:/lib 8 | 9 | RUN mkdir /esy 10 | WORKDIR /esy 11 | 12 | ENV NPM_CONFIG_PREFIX=/esy 13 | RUN npm install -g --unsafe-perm esy@0.5.6 14 | 15 | # now that we have esy installed we need a proper runtime 16 | 17 | FROM alpine:3.8 as esy 18 | 19 | ENV TERM=dumb \ 20 | LD_LIBRARY_PATH=/usr/local/lib:/usr/lib:/lib 21 | 22 | WORKDIR / 23 | 24 | COPY --from=build /esy /esy 25 | 26 | RUN apk add --no-cache \ 27 | ca-certificates wget \ 28 | bash curl perl-utils \ 29 | git patch gcc g++ musl-dev make m4 30 | 31 | RUN wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub 32 | RUN wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.28-r0/glibc-2.28-r0.apk 33 | RUN apk add --no-cache glibc-2.28-r0.apk 34 | 35 | ENV PATH=/esy/bin:$$PATH 36 | endef 37 | export DOCKERFILE_ESY 38 | 39 | define DOCKERIGNORE 40 | .git 41 | node_modules 42 | _esy 43 | endef 44 | export DOCKERIGNORE 45 | 46 | define GEN_DOCKERFILE_APP 47 | let fs = require('fs'); 48 | let childProcess = require('child_process'); 49 | 50 | let lock = JSON.parse(fs.readFileSync('./esy.lock/index.json').toString('utf8')); 51 | 52 | function findAllPackageIds(lock) { 53 | let ids = []; 54 | 55 | function traveseDependencies(id) { 56 | let node = lock.node[id]; 57 | let dependencies = node.dependencies || []; 58 | let devDependencies = node.devDependencies || []; 59 | 60 | let allDependencies = dependencies.concat(devDependencies); 61 | allDependencies.sort(); 62 | 63 | for (let dep of allDependencies) { 64 | traverse(dep, lock.node[dep]) 65 | } 66 | } 67 | 68 | function traverse(id) { 69 | let [name, version, _hash] = id.split('@'); 70 | let pkgid = `$${name}@$${version}`; 71 | if (ids.indexOf(pkgid) !== -1) { 72 | return; 73 | } 74 | traveseDependencies(id); 75 | ids.push(pkgid); 76 | } 77 | 78 | traveseDependencies(lock.root, lock.node[lock.root]); 79 | 80 | return ids; 81 | } 82 | 83 | let pkgs = findAllPackageIds(lock); 84 | 85 | const build = pkgs.map(pkg => `RUN esy build-package $${pkg}`); 86 | 87 | const esyImageId = process.argv[1]; 88 | 89 | const lines = [ 90 | `FROM $${esyImageId}`, 91 | 'RUN mkdir /app', 92 | 'WORKDIR /app', 93 | 'COPY package.json package.json', 94 | 'COPY esy.lock esy.lock', 95 | 'RUN esy fetch', 96 | ...build, 97 | 'COPY . .', 98 | ] 99 | 100 | console.log(lines.join('\n')); 101 | endef 102 | export GEN_DOCKERFILE_APP 103 | 104 | define USAGE 105 | Welcome to esy-docker! 106 | 107 | This is a set of make rules to produce docker images for esy projects. 108 | 109 | You can execute the following targets: 110 | 111 | esy-docker-build Builds an application 112 | esy-docker-shell Builds an application and executes bash in a container 113 | 114 | endef 115 | export USAGE 116 | 117 | .DEFAULT: print-usage 118 | 119 | print-usage: 120 | @echo "$$DOCKERFILE_ESY" > $(@) 121 | 122 | .docker: 123 | @mkdir -p $(@) 124 | 125 | .PHONY: .docker/Dockerfile.esy 126 | .docker/Dockerfile.esy: .docker 127 | @echo "$$DOCKERFILE_ESY" > $(@) 128 | 129 | .PHONY: Dockerfile.app 130 | .docker/Dockerfile.app: .docker .docker/image.esy 131 | @node -e "$$GEN_DOCKERFILE_APP" $$(cat .docker/image.esy) > $(@) 132 | 133 | .dockerignore: 134 | @echo "$$DOCKERIGNORE" > $(@) 135 | 136 | .docker/image.esy: .docker .dockerignore .docker/Dockerfile.esy 137 | @docker build . -f .docker/Dockerfile.esy --iidfile $(@) 138 | 139 | .docker/image.app: .docker .dockerignore .docker/Dockerfile.app 140 | @docker build . -f .docker/Dockerfile.app --iidfile $(@) 141 | 142 | esy-docker-shell-esy: .docker/image.esy 143 | @docker run -it $$(cat .docker/image.esy) /bin/bash 144 | 145 | esy-docker-build: .docker/image.app 146 | @docker run -it $$(cat .docker/image.app) esy build 147 | 148 | esy-docker-shell: .docker/image.app 149 | @docker run -it $$(cat .docker/image.app) /bin/bash 150 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { packages, pkgs, release-mode ? false }: 2 | 3 | let 4 | inherit (pkgs) stdenv lib; 5 | lambdaDrvs = lib.filterAttrs (_: value: lib.isDerivation value) packages; 6 | 7 | in 8 | (pkgs.mkShell { 9 | inputsFrom = lib.attrValues lambdaDrvs; 10 | buildInputs = (if release-mode then 11 | (with pkgs; [ 12 | cacert 13 | curl 14 | ocamlPackages.dune-release 15 | git 16 | opam 17 | ]) else [ ]) ++ (with pkgs.ocamlPackages; [ merlin ocamlformat ]); 18 | }).overrideAttrs (o: { 19 | propagatedBuildInputs = lib.filter 20 | (drv: 21 | # we wanna filter our own packages so we don't build them when entering 22 | # the shell. They always have `pname` 23 | !(lib.hasAttr "pname" drv) || 24 | drv.pname == null || 25 | !(lib.any (name: name == drv.pname) (lib.attrNames lambdaDrvs))) 26 | o.propagatedBuildInputs; 27 | }) 28 | -------------------------------------------------------------------------------- /test/api_gateway_test.ml: -------------------------------------------------------------------------------- 1 | open Test_common 2 | module Http = Lambda_runtime__.Http 3 | module StringMap = Map.Make (String) 4 | 5 | let apigw_response = 6 | (module struct 7 | type t = Http.api_gateway_proxy_response 8 | 9 | let pp formatter t = 10 | Format.pp_print_text 11 | formatter 12 | (t 13 | |> Http.API_gateway_response.to_yojson 14 | |> Yojson.Safe.pretty_to_string) 15 | 16 | let equal = ( = ) 17 | end : Alcotest.TESTABLE 18 | with type t = Http.api_gateway_proxy_response) 19 | 20 | module Http_runtime = struct 21 | include Http 22 | 23 | type event = Http.API_gateway_request.t 24 | type response = Http.API_gateway_response.t 25 | end 26 | 27 | let request = 28 | Test_common.make_test_request (module Http.API_gateway_request) "apigw_real" 29 | 30 | let test_fixture = Test_common.test_fixture (module Http.API_gateway_request) 31 | let test_runtime = test_runtime_generic (module Http_runtime) request 32 | 33 | let response = 34 | Http. 35 | { status_code = 200 36 | ; headers = StringMap.empty 37 | ; body = "Hello" 38 | ; is_base64_encoded = false 39 | } 40 | 41 | let suite = 42 | [ "deserialize (mock) API Gateway Proxy Request", `Quick, test_fixture "apigw" 43 | ; ( "deserialize (real world) API Gateway Proxy Request" 44 | , `Quick 45 | , test_fixture "apigw_real_trimmed" ) 46 | ; ( "successful handler invocation" 47 | , `Quick 48 | , test_runtime 49 | (fun _event _ctx -> Ok response) 50 | (fun output -> 51 | match output with 52 | | Ok result -> 53 | Alcotest.check 54 | apigw_response 55 | "runtime invoke output" 56 | response 57 | result 58 | | Error e -> Alcotest.fail e) ) 59 | ; ( "failed handler invocation" 60 | , `Quick 61 | , test_runtime 62 | (fun _event _ctx -> Error "I failed") 63 | (fun output -> 64 | match output with 65 | | Ok response -> 66 | let result_str = 67 | response 68 | |> Http.API_gateway_response.to_yojson 69 | |> Yojson.Safe.pretty_to_string 70 | in 71 | Alcotest.fail 72 | (Printf.sprintf 73 | "Expected to get an error but the call succeeded with: %s" 74 | result_str) 75 | | Error e -> 76 | Alcotest.(check string "Runtime invoke error" "I failed" e)) ) 77 | ; ( "simple asynchronous handler invocation" 78 | , `Quick 79 | , test_runtime 80 | (fun _event _ctx -> Ok response) 81 | (fun output -> 82 | match output with 83 | | Ok result -> 84 | Alcotest.check 85 | apigw_response 86 | "runtime invoke output" 87 | response 88 | result 89 | | Error e -> Alcotest.fail e) ) 90 | ] 91 | -------------------------------------------------------------------------------- /test/config_test.ml: -------------------------------------------------------------------------------- 1 | module Config = Lambda_runtime__.Config 2 | module StringMap = Map.Make (String) 3 | 4 | let set_endpoint_env_var () = 5 | let open Config in 6 | Unix.putenv Env_vars.runtime_endpoint_var "localhost:8080"; 7 | () 8 | 9 | let set_lambda_env_vars () = 10 | let open Config in 11 | Unix.putenv Env_vars.lambda_function_name "test_func"; 12 | Unix.putenv Env_vars.lambda_function_version "$LATEST"; 13 | Unix.putenv Env_vars.lambda_function_memory_size "128"; 14 | Unix.putenv Env_vars.lambda_log_stream_name "LogStreamName"; 15 | Unix.putenv Env_vars.lambda_log_group_name "LogGroup2"; 16 | () 17 | 18 | let unset_env_vars () = 19 | let open Config in 20 | Unix.putenv Env_vars.runtime_endpoint_var ""; 21 | Unix.putenv Env_vars.lambda_function_name ""; 22 | Unix.putenv Env_vars.lambda_function_version ""; 23 | Unix.putenv Env_vars.lambda_function_memory_size ""; 24 | Unix.putenv Env_vars.lambda_log_stream_name ""; 25 | Unix.putenv Env_vars.lambda_log_group_name ""; 26 | () 27 | 28 | let get_env () = 29 | let env_lst = Array.to_list (Unix.environment ()) in 30 | List.fold_left 31 | (fun m var -> 32 | match String.split_on_char '=' var with 33 | | k :: v :: _ -> StringMap.add k v m 34 | | _ -> m) 35 | StringMap.empty 36 | env_lst 37 | 38 | let setup_and_run f () = 39 | unset_env_vars (); 40 | set_endpoint_env_var (); 41 | set_lambda_env_vars (); 42 | f () 43 | 44 | let suite = 45 | [ ( "config from env vars" 46 | , `Quick 47 | , setup_and_run @@ fun () -> 48 | match Config.get_function_settings ~env:(get_env ()) () with 49 | | Ok env_settings -> 50 | Alcotest.( 51 | check 52 | int 53 | "memory size read from env" 54 | 128 55 | env_settings.Config.memory_size) 56 | | Error e -> Alcotest.fail e ) 57 | ; ( "errors when vars are not set up" 58 | , `Quick 59 | , fun () -> 60 | unset_env_vars (); 61 | match Config.get_function_settings () with 62 | | Ok _env_settings -> Alcotest.fail "Expected env to not be setup" 63 | | Error _e -> Alcotest.(check pass "" 1 1) ) 64 | ; ( "errors when runtime API endpoint is not set up" 65 | , `Quick 66 | , fun () -> 67 | unset_env_vars (); 68 | match Config.get_runtime_api_endpoint () with 69 | | Ok _endpoint -> Alcotest.fail "Expected env to not be setup" 70 | | Error _e -> Alcotest.(check pass "" 1 1) ) 71 | ] 72 | -------------------------------------------------------------------------------- /test/dune: -------------------------------------------------------------------------------- 1 | (test 2 | (name test) 3 | (deps 4 | (:test_exe test.exe) 5 | (source_tree "fixtures")) 6 | (libraries alcotest lambda-runtime vercel fmt piaf uri)) 7 | -------------------------------------------------------------------------------- /test/fixtures/apigw.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": "eyJ0ZXN0IjoiYm9keSJ9", 3 | "resource": "/{proxy+}", 4 | "path": "/path/to/resource", 5 | "httpMethod": "POST", 6 | "isBase64Encoded": true, 7 | "queryStringParameters": { 8 | "foo": "bar" 9 | }, 10 | "pathParameters": { 11 | "proxy": "/path/to/resource" 12 | }, 13 | "stageVariables": { 14 | "baz": "qux" 15 | }, 16 | "headers": { 17 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", 18 | "Accept-Encoding": "gzip, deflate, sdch", 19 | "Accept-Language": "en-US,en;q=0.8", 20 | "Cache-Control": "max-age=0", 21 | "CloudFront-Forwarded-Proto": "https", 22 | "CloudFront-Is-Desktop-Viewer": "true", 23 | "CloudFront-Is-Mobile-Viewer": "false", 24 | "CloudFront-Is-SmartTV-Viewer": "false", 25 | "CloudFront-Is-Tablet-Viewer": "false", 26 | "CloudFront-Viewer-Country": "US", 27 | "Host": "1234567890.execute-api.eu-west-2.amazonaws.com", 28 | "Upgrade-Insecure-Requests": "1", 29 | "User-Agent": "Custom User Agent String", 30 | "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", 31 | "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", 32 | "X-Forwarded-For": "127.0.0.1, 127.0.0.2", 33 | "X-Forwarded-Port": "443", 34 | "X-Forwarded-Proto": "https" 35 | }, 36 | "requestContext": { 37 | "accountId": "123456789012", 38 | "resourceId": "123456", 39 | "stage": "prod", 40 | "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", 41 | "identity": { 42 | "cognitoIdentityPoolId": null, 43 | "cognitoAmr": null, 44 | "accountId": null, 45 | "cognitoIdentityId": null, 46 | "caller": null, 47 | "accessKey": null, 48 | "sourceIp": "127.0.0.1", 49 | "cognitoAuthenticationType": null, 50 | "cognitoAuthenticationProvider": null, 51 | "userArn": null, 52 | "userAgent": "Custom User Agent String", 53 | "user": null 54 | }, 55 | "path": "/prod/path/to/resource", 56 | "resourcePath": "/{proxy+}", 57 | "httpMethod": "POST", 58 | "apiId": "1234567890", 59 | "protocol": "HTTP/1.1" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test/fixtures/apigw_real.json: -------------------------------------------------------------------------------- 1 | { 2 | "resource": "/test-ocaml", 3 | "path": "/test-ocaml", 4 | "httpMethod": "GET", 5 | "headers": { 6 | "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", 7 | "accept-encoding": "gzip, deflate, br", 8 | "accept-language": "en-US,en;q=0.9", 9 | "cache-control": "max-age=0", 10 | "Host": "2av2ywwnof.execute-api.eu-west-2.amazonaws.com", 11 | "upgrade-insecure-requests": "1", 12 | "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36", 13 | "X-Amzn-Trace-Id": "Root=1-5c0d9301-9a990aad2bfdf4b8bb15fb8d", 14 | "X-Forwarded-For": "81.193.239.136", 15 | "X-Forwarded-Port": "443", 16 | "X-Forwarded-Proto": "https" 17 | }, 18 | "multiValueHeaders": { 19 | "accept": [ 20 | "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8" 21 | ], 22 | "accept-encoding": [ 23 | "gzip, deflate, br" 24 | ], 25 | "accept-language": [ 26 | "en-US,en;q=0.9" 27 | ], 28 | "cache-control": [ 29 | "max-age=0" 30 | ], 31 | "Host": [ 32 | "2av2ywwnof.execute-api.eu-west-2.amazonaws.com" 33 | ], 34 | "upgrade-insecure-requests": [ 35 | "1" 36 | ], 37 | "User-Agent": [ 38 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36" 39 | ], 40 | "X-Amzn-Trace-Id": [ 41 | "Root=1-5c0d9301-9a990aad2bfdf4b8bb15fb8d" 42 | ], 43 | "X-Forwarded-For": [ 44 | "81.193.239.136" 45 | ], 46 | "X-Forwarded-Port": [ 47 | "443" 48 | ], 49 | "X-Forwarded-Proto": [ 50 | "https" 51 | ] 52 | }, 53 | "queryStringParameters": null, 54 | "multiValueQueryStringParameters": null, 55 | "pathParameters": null, 56 | "stageVariables": null, 57 | "requestContext": { 58 | "resourceId": "bd8bup", 59 | "resourcePath": "/test-ocaml", 60 | "httpMethod": "GET", 61 | "extendedRequestId": "RqPoOEuBLPEFi3g=", 62 | "requestTime": "09/Dec/2018:22:11:13 +0000", 63 | "path": "/anm/test-ocaml", 64 | "accountId": "286419478444", 65 | "protocol": "HTTP/1.1", 66 | "stage": "anm", 67 | "domainPrefix": "2av2ywwnof", 68 | "requestTimeEpoch": 1544393473487, 69 | "requestId": "574fa6bc-fbff-11e8-a287-b5957c8a6e8a", 70 | "identity": { 71 | "cognitoIdentityPoolId": null, 72 | "accountId": null, 73 | "cognitoIdentityId": null, 74 | "caller": null, 75 | "sourceIp": "81.193.239.136", 76 | "accessKey": null, 77 | "cognitoAuthenticationType": null, 78 | "cognitoAuthenticationProvider": null, 79 | "userArn": null, 80 | "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36", 81 | "user": null 82 | }, 83 | "domainName": "2av2ywwnof.execute-api.eu-west-2.amazonaws.com", 84 | "apiId": "2av2ywwnof" 85 | }, 86 | "body": null, 87 | "isBase64Encoded": false 88 | } -------------------------------------------------------------------------------- /test/fixtures/apigw_real_trimmed.json: -------------------------------------------------------------------------------- 1 | { 2 | "resource": "/test-ocaml", 3 | "path": "/test-ocaml", 4 | "httpMethod": "GET", 5 | "headers": { 6 | "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8", 7 | "accept-encoding": "gzip, deflate, br", 8 | "accept-language": "en-US,en;q=0.9", 9 | "cache-control": "max-age=0", 10 | "Host": "2av2ywwnof.execute-api.eu-west-2.amazonaws.com", 11 | "upgrade-insecure-requests": "1", 12 | "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36", 13 | "X-Amzn-Trace-Id": "Root=1-5c0d9301-9a990aad2bfdf4b8bb15fb8d", 14 | "X-Forwarded-For": "81.193.239.136", 15 | "X-Forwarded-Port": "443", 16 | "X-Forwarded-Proto": "https" 17 | }, 18 | "requestContext": { 19 | "resourceId": "bd8bup", 20 | "resourcePath": "/test-ocaml", 21 | "httpMethod": "GET", 22 | "path": "/anm/test-ocaml", 23 | "accountId": "286419478444", 24 | "protocol": "HTTP/1.1", 25 | "stage": "anm", 26 | "requestId": "574fa6bc-fbff-11e8-a287-b5957c8a6e8a", 27 | "identity": { 28 | "cognitoIdentityPoolId": null, 29 | "accountId": null, 30 | "cognitoIdentityId": null, 31 | "caller": null, 32 | "sourceIp": "81.193.239.136", 33 | "accessKey": null, 34 | "cognitoAuthenticationType": null, 35 | "cognitoAuthenticationProvider": null, 36 | "userArn": null, 37 | "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36", 38 | "user": null 39 | }, 40 | "apiId": "2av2ywwnof" 41 | }, 42 | "body": null, 43 | "isBase64Encoded": false 44 | } -------------------------------------------------------------------------------- /test/fixtures/now_no_body.json: -------------------------------------------------------------------------------- 1 | { 2 | "Action": "Invoke", 3 | "body": 4 | "{\"path\":\"\\/\",\"method\":\"GET\",\"host\":\"now-custom-runtime-echo-nwksbhnl0.now.sh\",\"headers\":{\"x-now-trace\":\",bru1\",\"accept\":\"text\\/html,application\\/xhtml+xml,application\\/xml;q=0.9,image\\/webp,image\\/apng,*\\/*;q=0.8\",\"host\":\"now-custom-runtime-echo-nwksbhnl0.now.sh\",\"x-forwarded-proto\":\"https\",\"cf-ray\":\"4879392c2aab6260-MAD\",\"upgrade-insecure-requests\":\"1\",\"x-forwarded-for\":\"81.193.239.136\",\"cf-ipcountry\":\"PT\",\"connection\":\"Keep-Alive\",\"x-real-ip\":\"81.193.239.136\",\"accept-encoding\":\"gzip\",\"true-client-ip\":\"81.193.239.136\",\"cf-connecting-ip\":\"81.193.239.136\",\"cf-visitor\":\"{\\\"scheme\\\":\\\"https\\\"}\",\"cookie\":\"__cfduid=d0f3e8a925d3e2e996ade5b440d23c5ff1544484607\",\"accept-language\":\"en-US,en;q=0.9\",\"cache-control\":\"max-age=0\",\"user-agent\":\"Mozilla\\/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit\\/537.36 (KHTML, like Gecko) Chrome\\/71.0.3578.80 Safari\\/537.36\"}}" 5 | } -------------------------------------------------------------------------------- /test/fixtures/now_with_body.json: -------------------------------------------------------------------------------- 1 | { 2 | "Action": "Invoke", 3 | "body": 4 | "{\"path\":\"\\/\",\"method\":\"POST\",\"encoding\":\"base64\",\"body\":\"e30=\",\"host\":\"now-custom-runtime-echo-nwksbhnl0.now.sh\",\"headers\":{\"true-client-ip\":\"81.193.239.136\",\"x-forwarded-for\":\"81.193.239.136\",\"cf-ipcountry\":\"PT\",\"connection\":\"Keep-Alive\",\"content-length\":\"2\",\"x-now-trace\":\",bru1\",\"x-real-ip\":\"81.193.239.136\",\"accept-encoding\":\"gzip\",\"host\":\"now-custom-runtime-echo-nwksbhnl0.now.sh\",\"cf-connecting-ip\":\"81.193.239.136\",\"x-forwarded-proto\":\"https\",\"cf-visitor\":\"{\\\"scheme\\\":\\\"https\\\"}\",\"cf-ray\":\"487be1fdabfb620c-LIS\",\"content-type\":\"application\\/x-www-form-urlencoded\",\"accept\":\"*\\/*\",\"user-agent\":\"curl\\/7.62.0\"}}" 5 | } -------------------------------------------------------------------------------- /test/fixtures/vercel-request-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": "{\"host\":\"basic-now-ocaml-fctq50jb5-anmonteiro.vercel.app\",\"path\":\"\\/\",\"method\":\"GET\",\"headers\":{\"host\":\"basic-now-ocaml-fctq50jb5-anmonteiro.vercel.app\",\"cf-visitor\":\"{\\\"scheme\\\":\\\"https\\\"}\",\"x-vercel-edge-region\":\"dev\",\"x-real-ip\":\"107.3.165.86\",\"sec-fetch-user\":\"?1\",\"sec-ch-ua-mobile\":\"?0\",\"x-vercel-deployment-url\":\"basic-now-ocaml-fctq50jb5-anmonteiro.vercel.app\",\"cdn-loop\":\"cloudflare; subreqs=1\",\"cache-control\":\"max-age=0\",\"cf-ray\":\"7a50ec97a646faa6-SJC\",\"cf-ew-via\":\"15\",\"x-vercel-forwarded-for\":\"107.3.165.86\",\"cf-worker\":\"vercel-workers.com\",\"sec-ch-ua\":\"\\\"Chromium\\\";v=\\\"110\\\", \\\"Not A(Brand\\\";v=\\\"24\\\", \\\"Brave\\\";v=\\\"110\\\"\",\"sec-ch-ua-platform\":\"\\\"macOS\\\"\",\"x-forwarded-host\":\"basic-now-ocaml-fctq50jb5-anmonteiro.vercel.app\",\"accept\":\"text\\/html,application\\/xhtml+xml,application\\/xml;q=0.9,image\\/avif,image\\/webp,image\\/apng,*\\/*;q=0.8\",\"x-vercel-ip-timezone\":\"America\\/Los_Angeles\",\"x-forwarded-for\":\"107.3.165.86\",\"user-agent\":\"Mozilla\\/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit\\/537.36 (KHTML, like Gecko) Chrome\\/110.0.0.0 Safari\\/537.36\",\"x-vercel-proxied-for\":[\"107.3.165.86\",\"172.71.158.21\"],\"x-middleware-subrequest\":\"b29cab85d64b06f809fc1715969771c9fe15249e\",\"x-vercel-ip-latitude\":\"37.3859\",\"forwarded\":\"for=107.3.165.86;host=basic-now-ocaml-fctq50jb5-anmonteiro.vercel.app;proto=https;sig=0QmVhcmVyIDI0NmVmNjQ4ZGU0ZjM3YTU5YmQ3MmI5NDRiMTIxODEwOGExNDZmNjhjZDFiOTkyODJiYzliNGI4ODg3ZWEwMmE=;exp=1678340107\",\"cf-connecting-ip\":\"107.3.165.86\",\"upgrade-insecure-requests\":\"1\",\"x-vercel-id\":\"sfo1:sfo1::2sg6q-1678339807959-b06468598121\",\"referer\":\"https:\\/\\/vercel.com\\/\",\"x-vercel-proxy-signature\":\"Bearer 246ef648de4f37a59bd72b944b1218108a146f68cd1b99282bc9b4b8887ea02a\",\"x-vercel-ip-longitude\":\"-122.0882\",\"accept-language\":\"en-US,en;q=0.8\",\"sec-gpc\":\"1\",\"x-forwarded-proto\":\"https\",\"x-vercel-ip-country\":\"US\",\"x-vercel-ip-country-region\":\"CA\",\"sec-fetch-mode\":\"navigate\",\"accept-encoding\":\"gzip\",\"x-vercel-proxy-signature-ts\":\"1678340107\",\"x-vercel-skip-toolbar\":\"1\",\"x-vercel-ip-city\":\"Mountain%20View\",\"sec-fetch-dest\":\"document\",\"sec-fetch-site\":\"cross-site\"},\"features\":{\"nodeBridgeFetchO11y\":true}}", 3 | "Action": "Invoke" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixtures/vercel-request.json: -------------------------------------------------------------------------------- 1 | { 2 | "body": "{\"host\":\"basic-now-ocaml-i0mmkmnq1-anmonteiro.vercel.app\",\"path\":\"\\/\",\"method\":\"GET\",\"headers\":{\"host\":\"basic-now-ocaml-i0mmkmnq1-anmonteiro.vercel.app\",\"cf-visitor\":\"{\\\"scheme\\\":\\\"https\\\"}\",\"x-real-ip\":\"172.69.134.56\",\"x-vercel-proxy-signature-ts\":\"1678337211\",\"x-middleware-subrequest\":\"19f465c729262ad28ca529dada8c08940238ee29\",\"cf-connecting-ip\":\"2a06:98c0:3600::103\",\"cdn-loop\":\"cloudflare; subreqs=1\",\"cf-ray\":\"7a50a5e1a5a096a8-SJC\",\"x-vercel-ip-latitude\":\"37.1835\",\"x-vercel-forwarded-for\":\"172.69.134.56\",\"cf-worker\":\"cloudflare-workers.vercel-infra-production.com\",\"forwarded\":\"for=172.69.134.56;host=basic-now-ocaml-i0mmkmnq1-anmonteiro.vercel.app;proto=https;sig=0QmVhcmVyIDg3MzdhNjFmNjUwMmE3ZGU3ZTliNGU5MWE1YjhkY2RlNmYxOGZhZTM0MzAwMDIyMTIxZDlkMjY2NTM2NjE5ZTc=;exp=1678337211\",\"x-vercel-id\":\"sfo1:sfo1::tsjrn-1678336911646-e52b1423b8d2\",\"x-vercel-deployment-url\":\"basic-now-ocaml-i0mmkmnq1-anmonteiro.vercel.app\",\"x-forwarded-host\":\"basic-now-ocaml-i0mmkmnq1-anmonteiro.vercel.app\",\"x-vercel-ip-longitude\":\"-121.7714\",\"x-vercel-proxied-for\":\"172.69.134.56\",\"accept-encoding\":\"gzip\",\"user-agent\":\"Vercelbot\\/0.1 (+https:\\/\\/vercel.com)\",\"x-vercel-ip-country\":\"US\",\"x-forwarded-proto\":\"https\",\"x-vercel-ip-timezone\":\"America\\/Los_Angeles\",\"x-forwarded-for\":\"172.69.134.56\",\"x-api-header-cookie\":\"_vercel_jwt=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJwcm90ZWN0aW9uLXN5c3RlbSIsImF1ZCI6ImJhc2ljLW5vdy1vY2FtbC1pMG1ta21ucTEtYW5tb250ZWlyby52ZXJjZWwuYXBwIiwiaWF0IjoxNjc4MzM2OTExLCJleHAiOjE2NzgzMzY5NDF9.zCAgO7DLPxBXdxrVzplVDpiLzNoREgPpJfh1lDIR_n8\",\"x-vercel-ip-country-region\":\"CA\",\"x-vercel-ip-city\":\"San%20Jose\",\"x-vercel-proxy-signature\":\"Bearer 8737a61f6502a7de7e9b4e91a5b8dcde6f18fae34300022121d9d266536619e7\",\"cf-ew-via\":\"15\"},\"features\":{\"nodeBridgeFetchO11y\":true}}", 3 | "Action": "Invoke" 4 | } 5 | -------------------------------------------------------------------------------- /test/runtime_test.ml: -------------------------------------------------------------------------------- 1 | open Test_common 2 | 3 | module Runtime = struct 4 | include 5 | Lambda_runtime__.Runtime.Make 6 | (Lambda_runtime__.Json.Id) 7 | (Lambda_runtime__.Json.Id) 8 | 9 | type event = Lambda_runtime__.Json.Id.t 10 | type response = Lambda_runtime__.Json.Id.t 11 | end 12 | 13 | let request = `String "test" 14 | let test_runtime = test_runtime_generic (module Runtime) request 15 | 16 | exception User_code_exception 17 | 18 | let suite = 19 | [ ( "successful handler invocation" 20 | , `Quick 21 | , test_runtime 22 | (fun _event _ctx -> Ok (`String "Hello")) 23 | (fun output -> 24 | match output with 25 | | Ok result -> 26 | Alcotest.check 27 | yojson 28 | "runtime invoke output" 29 | (`String "Hello") 30 | result 31 | | Error e -> Alcotest.fail e) ) 32 | ; ( "failed handler invocation" 33 | , `Quick 34 | , test_runtime 35 | (fun _event _ctx -> Error "I failed") 36 | (fun output -> 37 | match output with 38 | | Ok result -> 39 | let result_str = Yojson.Safe.to_string result in 40 | Alcotest.fail 41 | (Printf.sprintf 42 | "Expected to get an error but the call succeeded with: %s" 43 | result_str) 44 | | Error e -> 45 | Alcotest.(check string "Runtime invoke error" "I failed" e)) ) 46 | ; ( "simple asynchronous handler invocation" 47 | , `Quick 48 | , test_runtime 49 | (fun _event _ctx -> Ok (`String "Hello")) 50 | (fun output -> 51 | match output with 52 | | Ok result -> 53 | Alcotest.check 54 | yojson 55 | "runtime invoke output" 56 | (`String "Hello") 57 | result 58 | | Error e -> Alcotest.fail e) ) 59 | ; ( "failed handler invocation" 60 | , `Quick 61 | , test_runtime 62 | (fun _event _ctx -> raise User_code_exception) 63 | (fun output -> 64 | match output with 65 | | Ok result -> 66 | let result_str = Yojson.Safe.to_string result in 67 | Alcotest.fail 68 | (Printf.sprintf 69 | "Expected to get an error but the call succeeded with: %s" 70 | result_str) 71 | | Error e -> 72 | let expected = 73 | "Handler raised: Dune__exe__Runtime_test.User_code_exception\n" 74 | in 75 | Alcotest.( 76 | check 77 | string 78 | "Runtime invoke error" 79 | expected 80 | (String.sub e 0 (String.length expected)))) ) 81 | ] 82 | -------------------------------------------------------------------------------- /test/test.ml: -------------------------------------------------------------------------------- 1 | let () = 2 | Alcotest.run 3 | "lambda-runtime" 4 | [ "config", Config_test.suite 5 | ; "runtime", Runtime_test.suite 6 | ; "API Gateway", Api_gateway_test.suite 7 | ; "Vercel Lambda", Vercel_test.suite 8 | ] 9 | -------------------------------------------------------------------------------- /test/test_common.ml: -------------------------------------------------------------------------------- 1 | module Errors = Lambda_runtime__.Errors 2 | module Config = Lambda_runtime__.Config 3 | module Context = Lambda_runtime__.Context 4 | module Client = Lambda_runtime__.Client 5 | 6 | let id : 'a. 'a -> 'a = fun x -> x 7 | 8 | let yojson = 9 | (module struct 10 | type t = Yojson.Safe.t 11 | 12 | let pp formatter t = 13 | Format.pp_print_text formatter (Yojson.Safe.pretty_to_string t) 14 | 15 | let equal = ( = ) 16 | end : Alcotest.TESTABLE 17 | with type t = Yojson.Safe.t) 18 | 19 | let error = ref false 20 | 21 | module MockConfigProvider = struct 22 | let get_function_settings () = 23 | if !error 24 | then Error (Errors.make_runtime_error ~recoverable:false "Mock error") 25 | else 26 | Ok 27 | { Config.function_name = "MockFunction" 28 | ; memory_size = 128 29 | ; version = "$LATEST" 30 | ; log_stream = "LogStream" 31 | ; log_group = "LogGroup" 32 | } 33 | 34 | let get_runtime_api_endpoint () = 35 | if !error 36 | then Error (Errors.make_runtime_error ~recoverable:false "Mock error") 37 | else Ok "localhost:8080" 38 | 39 | let get_deadline secs = 40 | let now_ms = Unix.gettimeofday () *. 1000. in 41 | let deadline_f = now_ms +. (float_of_int secs *. 1000.) in 42 | Int64.of_float deadline_f 43 | 44 | let test_context deadline = 45 | { Context.memory_limit_in_mb = 128 46 | ; function_name = "test_func" 47 | ; function_version = "$LATEST" 48 | ; invoked_function_arn = "arn:aws:lambda" 49 | ; aws_request_id = "123" 50 | ; xray_trace_id = Some "123" 51 | ; log_stream_name = "logStream" 52 | ; log_group_name = "logGroup" 53 | ; client_context = None 54 | ; identity = None 55 | ; deadline = get_deadline deadline 56 | } 57 | end 58 | 59 | module type Runtime = sig 60 | type event 61 | type response 62 | 63 | type 'a runtime = 64 | { client : Client.t 65 | ; settings : Config.function_settings 66 | ; handler : event -> Context.t -> ('a, string) result 67 | ; max_retries : int 68 | } 69 | 70 | val make : 71 | handler:(event -> Context.t -> ('a, string) result) 72 | -> max_retries:int 73 | -> settings:Config.function_settings 74 | -> Client.t 75 | -> 'a runtime 76 | 77 | val invoke : 'a runtime -> event -> Context.t -> ('a, string) result 78 | end 79 | 80 | let test_runtime_generic 81 | (type event response) 82 | (module Runtime : Runtime 83 | with type event = event 84 | and type response = response) 85 | event 86 | handler 87 | test_fn 88 | () 89 | = 90 | Eio_main.run (fun env -> 91 | Eio.Switch.run (fun sw -> 92 | match MockConfigProvider.get_runtime_api_endpoint () with 93 | | Error _ -> Alcotest.fail "Could not get runtime endpoint" 94 | | Ok _runtime_api_endpoint -> 95 | (* Avoid creating a TCP connection for every test *) 96 | let client = Obj.magic () in 97 | (match MockConfigProvider.get_function_settings () with 98 | | Error _ -> Alcotest.fail "Could not load environment config" 99 | | Ok settings -> 100 | let runtime = 101 | Runtime.make ~handler ~max_retries:3 ~settings client 102 | in 103 | let output = 104 | Runtime.invoke 105 | runtime 106 | event 107 | { invocation_context = MockConfigProvider.test_context 10 108 | ; sw 109 | ; env 110 | } 111 | in 112 | test_fn output))) 113 | 114 | let read_all path = 115 | let file = open_in path in 116 | try really_input_string file (in_channel_length file) with 117 | | exn -> 118 | close_in file; 119 | raise exn 120 | 121 | let test_fixture (module Request : Lambda_runtime.LambdaEvent) fixture () = 122 | let fixture = 123 | read_all (Printf.sprintf "fixtures/%s.json" fixture) 124 | |> Yojson.Safe.from_string 125 | in 126 | match Request.of_yojson fixture with 127 | | Ok _req -> Alcotest.(check pass) "Parses correctly" true true 128 | | Error err -> Alcotest.fail err 129 | 130 | let make_test_request 131 | (type a) 132 | (module Request : Lambda_runtime.LambdaEvent with type t = a) 133 | fixture 134 | = 135 | let fixture = 136 | read_all (Printf.sprintf "fixtures/%s.json" fixture) 137 | |> Yojson.Safe.from_string 138 | in 139 | match Request.of_yojson fixture with 140 | | Ok req -> req 141 | | Error err -> 142 | failwith 143 | (Printf.sprintf 144 | "Failed to parse API Gateway fixture into a mock request: %s\n" 145 | err) 146 | -------------------------------------------------------------------------------- /test/vercel_test.ml: -------------------------------------------------------------------------------- 1 | open Test_common 2 | module Vercel = Vercel__ 3 | 4 | let vercel_lambda_response = 5 | (module struct 6 | open Vercel 7 | 8 | type t = Response.t 9 | 10 | let pp formatter t = 11 | Format.pp_print_text 12 | formatter 13 | (t |> Response.to_yojson |> Yojson.Safe.pretty_to_string) 14 | 15 | let equal = ( = ) 16 | end : Alcotest.TESTABLE 17 | with type t = Vercel.Response.t) 18 | 19 | module Runtime = struct 20 | include Lambda_runtime__.Runtime.Make (Vercel.Request) (Vercel.Response) 21 | 22 | type event = Vercel.Request.t 23 | type response = Vercel.Response.t 24 | end 25 | 26 | let request = 27 | Test_common.make_test_request (module Vercel.Request) "now_with_body" 28 | 29 | let test_fixture = Test_common.test_fixture (module Vercel.Request) 30 | let test_runtime = test_runtime_generic (module Runtime) request 31 | let response = Piaf.Response.of_string `OK ~body:"" 32 | 33 | let suite = 34 | [ ( "deserialize Vercel Proxy Request without HTTP Body" 35 | , `Quick 36 | , test_fixture "now_no_body" ) 37 | ; ( "deserialize Vercel Proxy Request with HTTP Body" 38 | , `Quick 39 | , test_fixture "now_with_body" ) 40 | ; ( "deserialize Vercel Proxy Request 2/2023" 41 | , `Quick 42 | , test_fixture "vercel-request" ) 43 | ; ( "deserialize Vercel Proxy Request 2/2023 / 2" 44 | , `Quick 45 | , test_fixture "vercel-request-2" ) 46 | ; ( "successful handler invocation" 47 | , `Quick 48 | , test_runtime 49 | (fun _event _ctx -> Ok response) 50 | (fun output -> 51 | match output with 52 | | Ok result -> 53 | Alcotest.check 54 | vercel_lambda_response 55 | "runtime invoke output" 56 | response 57 | result 58 | | Error e -> Alcotest.fail e) ) 59 | ; ( "failed handler invocation" 60 | , `Quick 61 | , test_runtime 62 | (fun _event _ctx -> Error "I failed") 63 | (fun output -> 64 | match output with 65 | | Ok response -> 66 | let result_str = 67 | response 68 | |> Vercel.Response.to_yojson 69 | |> Yojson.Safe.pretty_to_string 70 | in 71 | Alcotest.fail 72 | (Printf.sprintf 73 | "Expected to get an error but the call succeeded with: %s" 74 | result_str) 75 | | Error e -> 76 | Alcotest.(check string "Runtime invoke error" "I failed" e)) ) 77 | ; ( "simple asynchronous handler invocation" 78 | , `Quick 79 | , test_runtime 80 | (fun _event _ctx -> Ok response) 81 | (fun output -> 82 | match output with 83 | | Ok result -> 84 | Alcotest.check 85 | vercel_lambda_response 86 | "runtime invoke output" 87 | response 88 | result 89 | | Error e -> Alcotest.fail e) ) 90 | ] 91 | -------------------------------------------------------------------------------- /vercel.opam: -------------------------------------------------------------------------------- 1 | opam-version: "2.0" 2 | maintainer: "Antonio Nuno Monteiro " 3 | authors: [ "Antonio Nuno Monteiro " ] 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" 16 | "lwt" 17 | "base64" 18 | "piaf" 19 | "ppx_deriving_yojson" 20 | "lambda-runtime" {=version} 21 | "alcotest" {with-test} 22 | ] 23 | synopsis: 24 | "A custom runtime for Vercel.com (Now v2) written in OCaml" 25 | description: 26 | """ 27 | lambda-runtime is an OCaml custom runtime for AWS Lambda. The vercel 28 | package provides an adapter and API for lambda-runtime that works with 29 | Vercel's (Now v2) service. 30 | """ 31 | -------------------------------------------------------------------------------- /vercel/dune: -------------------------------------------------------------------------------- 1 | (library 2 | (public_name vercel) 3 | (libraries lambda-runtime yojson ppx_deriving_yojson.runtime base64 piaf) 4 | (preprocess 5 | (pps ppx_deriving_yojson))) 6 | -------------------------------------------------------------------------------- /vercel/message.ml: -------------------------------------------------------------------------------- 1 | (*---------------------------------------------------------------------------- 2 | * Copyright (c) 2019 António Nuno Monteiro 3 | * 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions are met: 8 | * 9 | * 1. Redistributions of source code must retain the above copyright notice, 10 | * this list of conditions and the following disclaimer. 11 | * 12 | * 2. Redistributions in binary form must reproduce the above copyright 13 | * notice, this list of conditions and the following disclaimer in the 14 | * documentation and/or other materials provided with the distribution. 15 | * 16 | * 3. Neither the name of the copyright holder nor the names of its 17 | * contributors may be used to endorse or promote products derived from this 18 | * software without specific prior written permission. 19 | * 20 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 24 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 27 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 28 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 | * POSSIBILITY OF SUCH DAMAGE. 31 | *---------------------------------------------------------------------------*) 32 | 33 | let decode_body ~encoding body = 34 | match body, encoding with 35 | | None, _ -> None 36 | | Some body, Some "base64" -> 37 | (match Base64.decode body with Ok body -> Some body | Error _ -> None) 38 | | Some body, _ -> 39 | (* base64 is the only supported encoding *) 40 | Some body 41 | -------------------------------------------------------------------------------- /vercel/request.ml: -------------------------------------------------------------------------------- 1 | (*---------------------------------------------------------------------------- 2 | * Copyright (c) 2019 António Nuno Monteiro 3 | * 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions are met: 8 | * 9 | * 1. Redistributions of source code must retain the above copyright notice, 10 | * this list of conditions and the following disclaimer. 11 | * 12 | * 2. Redistributions in binary form must reproduce the above copyright 13 | * notice, this list of conditions and the following disclaimer in the 14 | * documentation and/or other materials provided with the distribution. 15 | * 16 | * 3. Neither the name of the copyright holder nor the names of its 17 | * contributors may be used to endorse or promote products derived from this 18 | * software without specific prior written permission. 19 | * 20 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 24 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 27 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 28 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 | * POSSIBILITY OF SUCH DAMAGE. 31 | *---------------------------------------------------------------------------*) 32 | 33 | module Request = Piaf.Request 34 | module Body = Piaf.Body 35 | 36 | module Headers = struct 37 | include Piaf.Headers 38 | 39 | let of_yojson json = 40 | match json with 41 | | `Assoc xs -> 42 | let exception Local in 43 | (try 44 | Ok 45 | (List.fold_left 46 | (fun hs (name, json) -> 47 | match json with 48 | | `String value -> add hs name value 49 | | `List values -> 50 | let values = List.map Yojson.Safe.Util.to_string values in 51 | add_multi hs [ name, values ] 52 | | _ -> raise Local) 53 | empty 54 | xs) 55 | with 56 | | Local -> 57 | Error 58 | (Format.asprintf 59 | "Failed to parse event to Vercel request type: %a" 60 | (Yojson.Safe.pretty_print ?std:None) 61 | json)) 62 | | _ -> Ok empty 63 | end 64 | 65 | type vercel_proxy_request = 66 | { path : string 67 | ; http_method : string [@key "method"] 68 | ; host : string 69 | ; headers : Headers.t 70 | ; body : string option [@default None] 71 | ; encoding : string option [@default None] 72 | } 73 | [@@deriving of_yojson { strict = false }] 74 | 75 | type vercel_event = 76 | { action : string [@key "Action"] 77 | ; body : string 78 | } 79 | [@@deriving of_yojson { strict = false }] 80 | 81 | type t = Request.t 82 | 83 | let of_yojson json = 84 | match vercel_event_of_yojson json with 85 | | Ok { body = event_body; _ } -> 86 | (match 87 | Yojson.Safe.from_string event_body |> vercel_proxy_request_of_yojson 88 | with 89 | | Ok { body; encoding; path; http_method; host; headers } -> 90 | let meth = Piaf.Method.of_string http_method in 91 | let headers = 92 | match Headers.mem headers "host" with 93 | | true -> headers 94 | | false -> Headers.add headers "host" host 95 | in 96 | let body = 97 | match Message.decode_body ~encoding body with 98 | | None -> Body.empty 99 | | Some s -> Body.of_string s 100 | in 101 | let request = 102 | Request.create 103 | ~scheme:`HTTP 104 | ~version:Piaf.Versions.HTTP.HTTP_1_1 105 | ~headers 106 | ~meth 107 | ~body 108 | path 109 | in 110 | Ok request 111 | | Error _ -> 112 | Error 113 | (Format.asprintf 114 | "Failed to parse event to Vercel request type: %a" 115 | (Yojson.Safe.pretty_print ?std:None) 116 | json) 117 | | exception Yojson.Json_error error -> 118 | Error 119 | (Printf.sprintf 120 | "Failed to parse event to Vercel request type: %s" 121 | error)) 122 | | Error _ -> 123 | Error 124 | (Format.asprintf 125 | "Failed to parse event to Vercel request type: %a" 126 | (Yojson.Safe.pretty_print ?std:None) 127 | json) 128 | -------------------------------------------------------------------------------- /vercel/response.ml: -------------------------------------------------------------------------------- 1 | (*---------------------------------------------------------------------------- 2 | * Copyright (c) 2019 António Nuno Monteiro 3 | * 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions are met: 8 | * 9 | * 1. Redistributions of source code must retain the above copyright notice, 10 | * this list of conditions and the following disclaimer. 11 | * 12 | * 2. Redistributions in binary form must reproduce the above copyright 13 | * notice, this list of conditions and the following disclaimer in the 14 | * documentation and/or other materials provided with the distribution. 15 | * 16 | * 3. Neither the name of the copyright holder nor the names of its 17 | * contributors may be used to endorse or promote products derived from this 18 | * software without specific prior written permission. 19 | * 20 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 24 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 27 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 28 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 | * POSSIBILITY OF SUCH DAMAGE. 31 | *---------------------------------------------------------------------------*) 32 | 33 | module Response = Piaf.Response 34 | module StringSet = Set.Make (String) 35 | 36 | module Headers = struct 37 | include Piaf.Headers 38 | 39 | let to_yojson headers = 40 | let keys = StringSet.of_list (List.map fst (to_list headers)) in 41 | `Assoc 42 | (StringSet.fold 43 | (fun name acc -> 44 | let element = 45 | match get_multi headers name with 46 | | [ x ] -> name, `String x 47 | | xs -> name, `List (List.map (fun v -> `String v) xs) 48 | in 49 | element :: acc) 50 | keys 51 | []) 52 | end 53 | 54 | type vercel_proxy_response = 55 | { status_code : int [@key "statusCode"] 56 | ; headers : Headers.t 57 | ; body : string 58 | ; encoding : string option 59 | } 60 | [@@deriving to_yojson] 61 | 62 | type t = Response.t 63 | 64 | let to_yojson { Response.status; headers; body; _ } = 65 | let body = Result.get_ok (Piaf.Body.to_string body) in 66 | let vercel_proxy_response = 67 | { status_code = Piaf.Status.to_code status; headers; body; encoding = None } 68 | in 69 | vercel_proxy_response_to_yojson vercel_proxy_response 70 | -------------------------------------------------------------------------------- /vercel/vercel.ml: -------------------------------------------------------------------------------- 1 | (*---------------------------------------------------------------------------- 2 | * Copyright (c) 2019 António Nuno Monteiro 3 | * 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions are met: 8 | * 9 | * 1. Redistributions of source code must retain the above copyright notice, 10 | * this list of conditions and the following disclaimer. 11 | * 12 | * 2. Redistributions in binary form must reproduce the above copyright 13 | * notice, this list of conditions and the following disclaimer in the 14 | * documentation and/or other materials provided with the distribution. 15 | * 16 | * 3. Neither the name of the copyright holder nor the names of its 17 | * contributors may be used to endorse or promote products derived from this 18 | * software without specific prior written permission. 19 | * 20 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 24 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 27 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 28 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 | * POSSIBILITY OF SUCH DAMAGE. 31 | *---------------------------------------------------------------------------*) 32 | 33 | include Lambda_runtime.Make (Request) (Response) 34 | 35 | (* Proxy to Piaf for Headers, Request and Response for convenience *) 36 | module Headers = Piaf.Headers 37 | module Request = Piaf.Request 38 | module Response = Piaf.Response 39 | -------------------------------------------------------------------------------- /vercel/vercel.mli: -------------------------------------------------------------------------------- 1 | (*---------------------------------------------------------------------------- 2 | * Copyright (c) 2019 António Nuno Monteiro 3 | * 4 | * All rights reserved. 5 | * 6 | * Redistribution and use in source and binary forms, with or without 7 | * modification, are permitted provided that the following conditions are met: 8 | * 9 | * 1. Redistributions of source code must retain the above copyright notice, 10 | * this list of conditions and the following disclaimer. 11 | * 12 | * 2. Redistributions in binary form must reproduce the above copyright 13 | * notice, this list of conditions and the following disclaimer in the 14 | * documentation and/or other materials provided with the distribution. 15 | * 16 | * 3. Neither the name of the copyright holder nor the names of its 17 | * contributors may be used to endorse or promote products derived from this 18 | * software without specific prior written permission. 19 | * 20 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 23 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 24 | * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 25 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 26 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 27 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 28 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 29 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 30 | * POSSIBILITY OF SUCH DAMAGE. 31 | *---------------------------------------------------------------------------*) 32 | 33 | open Lambda_runtime 34 | 35 | (* To keep module equality. See e.g.: 36 | * https://stackoverflow.com/a/37307124/3417023 *) 37 | module Headers : module type of struct 38 | include Piaf.Headers 39 | end 40 | 41 | module Request : module type of struct 42 | include Piaf.Request 43 | end 44 | 45 | module Response : module type of struct 46 | include Piaf.Response 47 | end 48 | 49 | include 50 | LambdaRuntime with type event := Request.t and type response := Response.t 51 | --------------------------------------------------------------------------------