[Date Prev][Date Next][Thread Prev][Thread Next]
[Date Index]
[Thread Index]
- Subject: Noob's attempt at Lua
- From: siiky <github-siiky@...>
- Date: Sat, 23 Apr 2022 13:04:26 +0100
Hi there, list o/
Lua noob here and long time lurker. I've dabbled in Lua a few times here
and there through the years, but I've never used it for anything "serious".
Yesterday I decided to "port" my CHICKEN Scheme implementation of an
IPFS HTTP API client library to Lua because I want to try to make
something with MPV+IPFS. I saw there is one IPFS library on LuaRocks but
it's incomplete, looks too complicated to maintain, and after the Scheme
implementation I know I can do better.
The heavy lifting isn't far from done, I only have to define convenience
writer procedures. Then, defining each of the endpoints with the right
arguments/parameters, &c, will be the tedious part (but maybe I can
automate it since I already have a similar description in the Scheme
library).
Thus, I have a few RFCs for you, the Lua ML netizens:
1. The IPFS HTTP API is "REST-like" with a ton endpoints (I counted
134, excluding some of the deprecated endpoints) of the form
"/path/to/thing", and many have common prefixes. E.g.: /bootstrap,
/bootstrap/add, /bootstrap/add/default, /bootstrap/list, /bootstrap/rm,
/bootstrap/rm/all.
I would like to know what would be the most idiomatic Lua interface to
export. I was thinking of using something similar to what I did in the
Scheme implementation, and export each of the endpoints as a function
such that you could use them as `ipfs:bootstrap(...)`,
`ipfs:bootstrap.add(...)`, `ipfs:bootstrap.add.default(...)`, &c.
But there's the problem (for me at least): can a table be callable?
Would that be possible to do?
If this isn't very idiomatic or there's a more idiomatic way to define
similar APIs I'm all ears :)
2. The server's replies are almost all in JSON. Is it worth it to make
the parser parameterized such that a user of the library may use any one
they like? For whatever reason -- it could be performance, less deps,
&c. It should be really easy to implement, it's just one extra instance
parameter.
3. If you'd like to spend some of your precious time to educate a
noob, could you take a quick look at the code and see if there are any
glaring points to improve? :)
Thanks,
siiky
local IPFS = {}
local http = require('./http') -- https://luarocks.org/modules/daurnimator/http
local json = require('lunajson') -- https://luarocks.org/modules/grafi/lunajson
local util = require('./util')
function array_to_query(array)
if #array == 0 then return "" end
local r, i = {}, 0
for j, kv in ipairs(array) do
i = i + 1
r[i] = http.util.encodeURIComponent(kv[1]).."="..http.util.encodeURIComponent(kv[2])
end
return table.concat(r, "&", 1, i)
end
-- The Type functions should return a string to be used as a value in the query
-- string of the request.
function Bool(v)
if v then
return 'true'
else
return 'false'
end
end
function Int(v)
if type(v) == 'number' then
return tostring(v)
else
return nil
end
end
function UInt(v)
if type(v) == 'number' and v >= 0 then
return tostring(v)
else
return nil
end
end
function String(v)
if type(v) == 'string' then
-- TODO: Escape character?
return v
else
return nil
end
end
function Array(t)
return function(v)
if type(v) ~= 'table' then return nil end
v = util.map(t, v)
if util.all(function(x) return x ~= nil end, v) then
return v
else
return nil
end
end
end
-- Check that a required argument is present
function Yes(v)
return not not v
end
-- No need to check optional arguments
function No(v)
return true
end
-- @brief Make the actual request.
-- @param ipfs An IPFS instance.
-- @param endpoint The API endpoint, excluding the "/api/v0/" prefix.
-- @param arguments Array of positional arguments.
-- @param parameters Table of optional parameters.
-- @param options Per-request options: `reader`, `writer`, `timeout`.
-- @returns body, status, headers, stream
--
-- `body` is the reply's body. If there is no reader, it's returned as a
-- string; if there is a reader, it's the result of applying the reader to
-- the body as a string.
-- `status` is the reply's HTTP status code.
-- `headers` are the reply's HTTP headers.
-- `stream` is the reply's body, as return by the `http` library.
--
-- @see For terminology details: http://docs.ipfs.io.ipns.localhost:8081/reference/http/api
function call_api_endpoint(ipfs, endpoint, arguments, parameters, options)
local query = array_to_query(
util.append(
util.map(function(v) return {"arg", v} end, arguments),
util.table_to_entries(parameters)
)
)
local headers = http.headers.new()
headers:append(':method', "POST")
local request = http.request.new_from_uri({
scheme = ipfs.scheme,
host = ipfs.host,
port = ipfs.port,
path = "/api/v0/" .. endpoint,
query = query,
}, headers)
if options.writer then
request:set_body(options.writer)
end
local headers, stream = assert(request:go(options.timeout or ipfs.timeout))
local status = headers:get(':status')
local body = assert(stream:get_body_as_string())
if status == '200' and type(options.reader) == 'function' then
body = options.reader(body)
end
return body, status, headers, stream
end
function failed(msg)
return msg, nil, nil, nil
end
--
function make_ipfs_endpoint(
endpoint,
default_reader,
default_writer,
arguments_spec,
parameters_types
)
if type(endpoint) ~= 'string' then
error "`endpoint` must be a string"
end
default_reader = default_reader or json.decode
arguments_spec = arguments_spec or {}
parameters_types = parameters_types or {}
local is_required = {}
local arguments_types = {}
for i, tr in pairs(arguments_spec) do
table.insert(arguments_types, tr[1])
table.insert(is_required, tr[2])
end
-- @param ipfs An IPFS instance.
-- @param arguments Array of positional arguments.
-- @param parameters Table of optional parameters.
-- @param options Per-request options: `reader`, `writer`, `timeout`.
--
-- The default reader and writer may be disabled if given the value false.
return function(ipfs, arguments, parameters, options)
arguments = arguments or {}
parameters = parameters or {}
options = options or {}
arguments = util.map_table(arguments_types, arguments)
if not util.all(function(has_req, i) return has_req(arguments[i]) end, is_required) then
return failed("Missing arguments")
end
parameters = util.map_table(parameters_types, parameters)
options.reader = options.reader == false or default_reader
options.writer = options.writer == false or default_writer
return call_api_endpoint(ipfs, endpoint, arguments, parameters, options)
end
end
function IPFS:new(o)
o = o or {}
o = {
scheme = o.scheme or 'http',
host = o.host or "localhost",
port = o.port or 5001,
timeout = o.timeout,
}
self.__index = self
return setmetatable(o, self)
end
IPFS.add = make_ipfs_endpoint(
"add",
json.decode,
nil,
{},
{
["chunker"]=String,
["fscache"]=Bool,
["hash"]=String,
["inline"]=Bool,
["inline-limit"]=Int,
["nocopy"]=Bool,
["only-hash"]=Bool,
["pin"]=Bool,
["progress"]=Bool,
["raw-leaves"]=Bool,
["silent"]=Bool,
["trickle"]=Bool,
["wrap-with-directory"]=Bool,
}
)
IPFS.ls = make_ipfs_endpoint(
"ls",
json.decode,
nil,
{{String, Yes}},
{
["resolve-type"]=Bool,
["size"]=Bool,
["stream"]=Bool,
}
)
IPFS.bootstrap = make_ipfs_endpoint("bootstrap")
-- Would like: IPFS.bootstrap.add
IPFS.bootstrap_add = make_ipfs_endpoint("bootstrap/add", json.decode, nil, {{String, No}})
-- Would like: IPFS.bootstrap.add.default
IPFS.bootstrap_add_default = make_ipfs_endpoint("bootstrap/add/default")
-- Would like: IPFS.bootstrap.list
IPFS.bootstrap_list = make_ipfs_endpoint("bootstrap/list")
-- Would like: IPFS.bootstrap.rm
IPFS.bootstrap_rm = make_ipfs_endpoint("bootstrap/rm", json.decode, nil, {{String, No}})
-- Would like: IPFS.bootstrap.rm.all
IPFS.bootstrap_rm_all = make_ipfs_endpoint("bootstrap/rm/all")
return function (o)
return IPFS:new(o)
end
local IPFS = require('./ipfs')
local ipfs = IPFS({
scheme = 'http',
host = "localhost",
port = 5001,
})
-- or
--local ipfs = require('./ipfs')({...})
print(ipfs:ls(
{"/ipfs/bafybeiho5ltbrfregvososmshzmr6kgzsbd7ufphsgfbgsiauxvvlcmrbi"},
{
size=true,
["resolve-type"]=false,
},
{ reader = false, }
))
local module = {}
function module.assign(t1, t2)
for k, v in pairs(t2) do
t1[k] = v
end
return t1
end
function module.map(func, array)
for i, v in ipairs(array) do
array[i] = func(v)
end
return array
end
function module.all(pred, array)
for i, v in pairs(array) do
if not pred(v, i) then
return false
end
end
return true
end
function module.map_table(maps, table)
local ret = {}
for k, map in pairs(maps) do
ret[k] = map(table[k])
end
return ret
end
function module.table_to_entries(table)
local ret = {}
for k, v in pairs(table) do
ret[#ret + 1] = {k, v}
end
return ret
end
function module.append(t1, t2)
for i, v in ipairs(t2) do
table.insert(t1, v)
end
return t1
end
function module.each(proc, table)
for k, v in pairs(table) do
proc(v, k)
end
end
return module
return {
bit = require('http.bit'),
client = require('http.client'),
cookie = require('http.cookie'),
h1 = {
connection = require('http.h1_connection'),
reason_phrases = require('http.h1_reason_phrases'),
stream = require('http.h1_stream'),
},
h2 = {
connection = require('http.h2_connection'),
error = require('http.h2_error'),
stream = require('http.h2_stream'),
},
headers = require('http.headers'),
hpack = require('http.hpack'),
hsts = require('http.hsts'),
proxies = require('http.proxies'),
request = require('http.request'),
server = require('http.server'),
socks = require('http.socks'),
tls = require('http.tls'),
util = require('http.util'),
version = require('http.version'),
websocket = require('http.websocket'),
--zlib = require('http.zlib'),
}