# Luna Luna is a simple web server programmable via opinionated Lua API. It has the following key features: * support for both PUC Lua and luajit * sqlite3 for data storage * asynchronous I/O operations * small and maintainable codebase * handles relatively high number of requests per second ## Build You'll need `lua` or `luajit` libraries installed on your system: ```sh git clone https://git.sr.ht/~unwox/luna cd luna go build -tags=puc . # for lua go build -tags=jit . # for luajit ``` To install the server to the $GOBIN ($GOPATH/bin by default) directory: ```sh go install -tags=puc . # for lua go install -tags=jit . # for luajit ``` ## Usage ``` luna [options] LUAFILE -D Debug/development mode -l string Address HTTP-server will listen to (default "127.0.0.1:3000") -n int Number of HTTP-workers to start (default $(nproc)) ``` LUAFILE must specify a Lua file that may or may not register routes to handle (via calling `luna.route.static` or `luna.route.route`). Whatever the LUAFILE returns is ignored. If LUAFILE defines no routes luna will execute the file and then exit. This behavior is useful when you want to have a script file with an access to the luna API. If luna is started with -D flag it will accept user input into its stdio. The input is executed in Lua state as is or (if `luna.evalfn` was called in LUAFILE) is passed to a custom eval handler. ## Lua API All API functions return 2 values: the first one is a boolean indicating if the call was successful (true for successful and false if there was an error) and the second one is the value (or an error text if the call was unsuccessful). API names are hyphenated since I am mostly calling the functions from [fennel language](https://fennel-lang.org/) and for lisps hyphenation is the norm. Every argument in every API function is required: luna does not support optinal arguments yet. Pass an empty value for corresponding type instead if you do not want to specify an argument: {} for tables, "" for strings, 0 for numbers. __luna.router.route(pattern, handler)__: registers a new route for Go to proxy to Lua. For pattern specification see [Go documentation](https://pkg.go.dev/net/http#hdr-Patterns-ServeMux). Handler is a Lua function that accepts a table representing request and returns 3 values: response status code, headers and body. ```lua luna.router.route("GET /test", function (request) return 200, {["Content-Type"] = "application/json"}, "{\"foo\": \"world\"}" end) ``` __luna.router.static(pattern, directory)__: registers a new route for Go to serve static files. ```lua luna.router.static("GET /static/", "./whatever/path/static") ``` __luna.http.request(method, url, headers, body)__: sends an HTTP request returning a response object: `{status = ..., headers = ..., body = ...}`. ```lua luna.http.request("GET", "https://git.sr.ht", {Accept = "text/html"}, "") ``` __luna.http\["encode-url"\](string)__: encodes a STRING for a safe usage in URLs. __luna.db.open(file)__: opens a connection to an sqlite database. For FILE specification see [mattn/go-sqlite3 library documentation](https://github.com/mattn/go-sqlite3?tab=readme-ov-file#connection-string). ```lua local ok, db = luna.db.open("file:var/db.sqlite?_journal=WAL&_sync=NORMAL") ``` __luna.db.begin(db)__: starts a transaction for a DB. ```lua local ok, tx = luna.db.begin(db) ``` __luna.db.commit(tx)__: commits a transaction TX. ```lua local ok, tx = luna.db.begin(db) local ok, err = luna.db["exec-tx"](tx, "DELETE FROM foobar;") local ok, err = luna.db.commit(tx) ``` __luna.db.rollback(tx)__: rolls back a transaction TX. ```lua local ok, tx = luna.db.begin(db) local ok, err = luna.db["exec-tx"](tx, "DELETE FROM foobar;", {}) local ok, err = luna.db.rollback(tx) -- the tx was rolled back so "DELETE FROM foobar;" isn't executed ``` __luna.db\["exec-tx"\](tx, query, args)__: executes a QUERY with ARGS in the context of a given transaction TX. ```lua local ok, tx = luna.db.begin(db) local ok, err = luna.db["exec-tx"]( tx, "INSERT INTO foobar VALUES (?, ?, ?);" {1, "hello!", "2024-12-26 12:00:00"} ) ``` __luna.db.exec(db, query, args)__: executes a QUERY with ARGS in a given DB: ```lua local ok, db = luna.db.open("file:var/db.sqlite?_journal=WAL&_sync=NORMAL") local ok, err = luna.db.exec( db, "INSERT INTO foobar VALUES (?, ?, ?);" {1, "hello!", "2024-12-26 12:00:00"} ) ``` __luna.db.query(db, query, args)__: executes a QUERY with ARGS in a given DB and returns result as an array of arrays: ```lua local ok, db = luna.db.open("file:var/db.sqlite?_journal=WAL&_sync=NORMAL") local ok, res = luna.db.query( db, "SELECT * FROM foobar WHERE name = '?'", {"hello!"} ) -- if the table foobar has 3 columns (id, name, creation_time) the RES variable -- would contain something like this: -- {{1, "hello!", "2024-12-26 12:00:00"}} ``` __luna.db.\["query\*"\](db, query, args)__: executes a QUERY with ARGS in a given DB and returns result as an array of tables where keys are column names and values are associated value: ```lua local ok, db = luna.db.open("file:var/db.sqlite?_journal=WAL&_sync=NORMAL") local ok, res = luna.db["query*"]( db, "SELECT * FROM foobar WHERE name = '?'", {"hello!"} ) -- if the table foobar has 3 columns (id, name, creation_time) the RES variable -- would contain something like this: -- {{id = 1, name = "hello!", creation_time = "2024-12-26 12:00:00"}} ``` __luna.db.close(db)__: closes a connection to DB. ```lua local ok, db = luna.db.open("file:var/db.sqlite?_journal=WAL&_sync=NORMAL") local ok, err = luna.db.close(db) ``` __luna.utf8.len(string)__: returns a number of UTF-8 symbols in a STRING. __luna.utf8.lower(string)__: returns a copy of a STRING with all letters mapped to their lower case. __luna.utf8.upper(string)__: returns a copy of a STRING with all letters mapped to their upper case. __luna.utf8.sub(string, start, length)__: returns a substring of a STRING starting at index START (1-indexed position) with a LENGTH. __luna.crypto.sha1(string)__: returns hex-encoded SHA1 hash of a STRING. __luna.evalfn(handler)__: sets an eval handler for server commands. Handler accepts one argument TEXT. If the server is started with -D flag it starts listening for input from stdio. In this case if eval handler is set the handler will receive the user input and should handle it appropriately. If eval handler is not set REPL input is executed in Lua state as is. ``` local fennel = require("fennel") luna.evalfn(function (text) fennel.eval(text, {env = _G}) end) ``` __luna.debug__: a variable that indicates whether the server was started with -D (debug) flag. ## Contribution Send patches to me@unwox.com. Possible ideas for patches are * adding more *useful* API functions. It's easy to do, see `main.go` for that. * replacing sqlite with something else since its sqlite3_step function is blocking and it's the major perfomance bottleneck right now. * supporting redis for synchronization, maybe? * supporting websocket connections, maybe? * compiling Lua code and luna into one binary, maybe? I will not accept patches that are * relying on too many Go (or external) dependencies. * adding too much complexity into the existing codebase. * adding a new major functionality that should be implemented outside (for example service management, use systemd, shepher or whatever). ## Useful links * https://lucasklassmann.com/blog/2019-02-02-embedding-lua-in-c/ * https://pgl.yoyo.org/luai/i/3.7+Functions+and+Types