Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,4 +7,5 @@ node_modules
.psci
.spago
output
.direnv/
.psc-ide-port
54 changes: 54 additions & 0 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,3 +4,57 @@ Purescript Lua bindings for the openresty/lua-nginx-module

* [PureScript Compiler Backend for Lua](https://github.com/purescript-lua/purescript-lua)
* [NGINX API for Lua](https://github.com/openresty/lua-nginx-module#nginx-api-for-lua)

## Building

```sh
nix develop -c ./scripts/build
```

Spago compiles the PureScript to CoreFn (a no-op `backend` keeps codegen on
corefn rather than JavaScript), then pslua links each module to a flat Lua file
under `dist/`, for example `dist/Lua.Ngx.lua`.

## Running

The bindings wrap the `ngx.*` API that the lua-nginx-module injects, so they
only work inside an OpenResty worker. Plain `lua` cannot run them (`ngx` is
nil) and neither can vanilla nginx (no Lua). The dev shell ships OpenResty, and
`scripts/run` serves the compiled modules so you can curl them:

```sh
nix develop -c ./scripts/build # produce dist/*.lua
nix develop -c ./scripts/run # serve on http://127.0.0.1:8099
```

Then, in another terminal:

```sh
curl http://127.0.0.1:8099/say
# hello from purescript-lua-ngx

curl -i http://127.0.0.1:8099/status
# HTTP/1.1 404 Not Found
# ...
# HTTP_OK constant = 200
# ngx.status now = 404
```

`PORT=9000 nix develop -c ./scripts/run` picks another port.

### Using the bindings from your own config

A linked module is a flat file whose name contains literal dots, such as
`Lua.Ngx.lua`, so load it with `dofile` by absolute path. `require("Lua.Ngx")`
would rewrite the dot to a directory separator and miss the file. Effectful
bindings are curried, so a `String -> Effect Unit` like `say` is called as
`say(msg)()`:

```nginx
location /hello {
content_by_lua_block {
local Ngx = dofile("/path/to/dist/Lua.Ngx.lua")
Ngx.say("hello")()
}
}
```
1 change: 1 addition & 0 deletions flake.nix
Original file line numberDiff line numberDiff line change
Expand Up@@ -42,6 +42,7 @@
lua51Packages.luacheck
luaformatter
nixfmt-rfc-style
openresty # nginx + lua-nginx-module, for ./scripts/run
pslua.packages.${system}.default
purs-bin.purs-0_15_16
spago-bin.spago-1_0_4
Expand Down
98 changes: 98 additions & 0 deletions scripts/run
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
#!/usr/bin/env bash
set -euo pipefail

# Demo runner: serves the compiled Lua.Ngx bindings under OpenResty so you can
# curl them. The bindings wrap the `ngx.*` API the lua-nginx-module injects, so
# they only run inside an OpenResty worker (plain `lua` or vanilla nginx will
# not work). Build first with ./scripts/build, then:
#
# nix develop -c ./scripts/run # serve on :8099
# PORT=9000 nix develop -c ./scripts/run # pick another port
#
# In another terminal:
# curl http://127.0.0.1:8099/say
# curl -i http://127.0.0.1:8099/status

here="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
dist="$here/dist"
port="${PORT:-8099}"

# PORT is interpolated into `listen $port;`, so reject anything that is not a
# plain TCP port number before it reaches the config.
if ! [[ $port =~ ^[0-9]+$ ]] || [ "$port" -lt 1 ] || [ "$port" -gt 65535 ]; then
echo "PORT must be an integer in 1..65535 (got: '$port')." >&2
exit 1
fi

if ! command -v openresty >/dev/null 2>&1; then
echo "openresty is not on PATH." >&2
echo "Run inside the dev shell: nix develop -c ./scripts/run" >&2
echo "(With direnv, reload it after pulling: direnv reload)" >&2
exit 1
fi

# Both modules are served below (/say and /status), and both come from the same
# ./scripts/build run, so fail fast if either is missing rather than only at the
# first request that needs it.
for f in Lua.Ngx.lua Lua.Ngx.Http.Status.lua; do
if [ ! -f "$dist/$f" ]; then
echo "dist/$f is missing. Run ./scripts/build first." >&2
exit 1
fi
done

work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
mkdir -p "$work/logs"
Comment thread
Unisay marked this conversation as resolved.

# A linked dist module is a flat file named e.g. `Lua.Ngx.lua` (a literal dot in
# the filename, not a path), so consumers load it with dofile by absolute path
# rather than require, which would rewrite the dot to a directory separator.
cat > "$work/nginx.conf" <<EOF
worker_processes 1;
daemon off;
error_log stderr info;
pid $work/logs/nginx.pid;
events { worker_connections 64; }
http {
access_log off;
lua_code_cache off; # reload the dist on every request while iterating
client_body_temp_path $work/logs/client_body;
proxy_temp_path $work/logs/proxy;
fastcgi_temp_path $work/logs/fastcgi;
uwsgi_temp_path $work/logs/uwsgi;
scgi_temp_path $work/logs/scgi;
server {
listen $port;

# Lua.Ngx.say is an Effect (String -> Effect Unit), so the generated value
# is curried: say(msg) returns a thunk that performs the write when called.
location /say {
content_by_lua_block {
local Ngx = dofile("$dist/Lua.Ngx.lua")
Ngx.say("hello from purescript-lua-ngx")()
}
}

# Lua.Ngx.Http.Status exposes the HTTP_* constants plus get/set for
# ngx.status (set is likewise an effectful thunk).
location /status {
content_by_lua_block {
local Ngx = dofile("$dist/Lua.Ngx.lua")
local Status = dofile("$dist/Lua.Ngx.Http.Status.lua")
Status.set(Status.notFound)()
Ngx.say("HTTP_OK constant = " .. tostring(Status.ok))()
Ngx.say("ngx.status now = " .. tostring(Status.get()))()
}
}
}
}
EOF

echo "Serving compiled Lua.Ngx on http://127.0.0.1:$port"
echo " curl http://127.0.0.1:$port/say"
echo " curl -i http://127.0.0.1:$port/status"
echo "Ctrl-C to stop."
# -e stderr sets the error log before the config is parsed, so nginx does not
# try (and noisily fail) to open its compiled-in default /var/log/nginx path.
openresty -p "$work" -e stderr -c "$work/nginx.conf"