lua-users home
lua-l archive

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