1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
|
# 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
See an [example site](https://git.sr.ht/~unwox/luna/tree/master/item/example).
## 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
|