lua-users home
lua-l archive

Noob's attempt at Lua

[Date Prev][Date Next][Thread Prev][Thread Next] [Date Index] [Thread Index]


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'),
}

AltStyle によって変換されたページ (->オリジナル) /