├── .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 |
--------------------------------------------------------------------------------