libs/json: Completed JSON library
modules/rpc: Added experimental JSON-RPC API
This commit is contained in:
parent
05b30bba2a
commit
df40e4df5e
5 changed files with 297 additions and 65 deletions
|
@ -187,6 +187,16 @@ define Package/luci-ipkg/install
|
|||
endef
|
||||
|
||||
|
||||
define Package/luci-json
|
||||
$(call Package/luci/libtemplate)
|
||||
TITLE:=LuCI JSON Library
|
||||
endef
|
||||
|
||||
define Package/luci-json/install
|
||||
$(call Package/luci/install/template,$(1),libs/json)
|
||||
endef
|
||||
|
||||
|
||||
define Package/luci-sys
|
||||
$(call Package/luci/libtemplate)
|
||||
TITLE:=LuCI Linux/POSIX system library
|
||||
|
@ -354,6 +364,17 @@ define Package/luci-admin-full/install
|
|||
endef
|
||||
|
||||
|
||||
define Package/luci-admin-rpc
|
||||
$(call Package/luci/webtemplate)
|
||||
DEPENDS+=+luci-json
|
||||
TITLE:=LuCI RPC - JSON-RPC API
|
||||
endef
|
||||
|
||||
define Package/luci-admin-rpc/install
|
||||
$(call Package/luci/install/template,$(1),modules/rpc)
|
||||
endef
|
||||
|
||||
|
||||
define Package/luci-mod-freifunk
|
||||
$(call Package/luci/fftemplate)
|
||||
DEPENDS:=+luci-admin-full
|
||||
|
@ -607,6 +628,9 @@ endif
|
|||
ifneq ($(CONFIG_PACKAGE_luci-ipkg),)
|
||||
PKG_SELECTED_MODULES+=libs/ipkg
|
||||
endif
|
||||
ifneq ($(CONFIG_PACKAGE_luci-json),)
|
||||
PKG_SELECTED_MODULES+=libs/json
|
||||
endif
|
||||
ifneq ($(CONFIG_PACKAGE_luci-uci),)
|
||||
PKG_SELECTED_MODULES+=libs/uci
|
||||
endif
|
||||
|
@ -649,6 +673,9 @@ endif
|
|||
ifneq ($(CONFIG_PACKAGE_luci-admin-full),)
|
||||
PKG_SELECTED_MODULES+=modules/admin-full
|
||||
endif
|
||||
ifneq ($(CONFIG_PACKAGE_luci-admin-rpc),)
|
||||
PKG_SELECTED_MODULES+=modules/rpc
|
||||
endif
|
||||
ifneq ($(CONFIG_PACKAGE_luci-mod-freifunk),)
|
||||
PKG_SELECTED_MODULES+=modules/freifunk
|
||||
endif
|
||||
|
@ -733,6 +760,7 @@ $(eval $(call BuildPackage,luci-cbi))
|
|||
$(eval $(call BuildPackage,luci-fastindex))
|
||||
$(eval $(call BuildPackage,luci-http))
|
||||
$(eval $(call BuildPackage,luci-ipkg))
|
||||
$(eval $(call BuildPackage,luci-json))
|
||||
$(eval $(call BuildPackage,luci-uci))
|
||||
$(eval $(call BuildPackage,luci-sys))
|
||||
$(eval $(call BuildPackage,luci-web))
|
||||
|
@ -749,6 +777,7 @@ $(eval $(call BuildPackage,luci-ff-augsburg))
|
|||
$(eval $(call BuildPackage,luci-admin-core))
|
||||
$(eval $(call BuildPackage,luci-admin-mini))
|
||||
$(eval $(call BuildPackage,luci-admin-full))
|
||||
$(eval $(call BuildPackage,luci-admin-rpc))
|
||||
$(eval $(call BuildPackage,luci-mod-freifunk))
|
||||
|
||||
$(eval $(call BuildPackage,luci-app-ffwizard-leipzig))
|
||||
|
|
|
@ -11,16 +11,49 @@ You may obtain a copy of the License at
|
|||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
$Id$
|
||||
|
||||
Decoder:
|
||||
Info:
|
||||
null will be decoded to luci.json.null if first parameter of Decoder() is true
|
||||
|
||||
Example:
|
||||
decoder = luci.json.Decoder()
|
||||
luci.ltn12.pump.all(luci.ltn12.source.string("decodableJSON"), decoder:sink())
|
||||
luci.util.dumptable(decoder:get())
|
||||
|
||||
Known issues:
|
||||
does not support unicode conversion \uXXYY with XX != 00 will be ignored
|
||||
|
||||
|
||||
Encoder:
|
||||
Info:
|
||||
Accepts numbers, strings, nil, booleans as they are
|
||||
Accepts luci.json.null as replacement for nil
|
||||
Accepts full associative and full numerically indexed tables
|
||||
Mixed tables will loose their associative values during conversion
|
||||
Iterator functions will be encoded as an array of their return values
|
||||
Non-iterator functions will probably corrupt the encoder
|
||||
|
||||
Example:
|
||||
encoder = luci.json.Encoder(encodableData)
|
||||
luci.ltn12.pump.all(encoder:source(), luci.ltn12.sink.file(io.open("someFile", w)))
|
||||
]]--
|
||||
|
||||
local util = require "luci.util"
|
||||
local ltn12 = require "luci.ltn12"
|
||||
local table = require "table"
|
||||
local string = require "string"
|
||||
local coroutine = require "coroutine"
|
||||
|
||||
local assert = assert
|
||||
local tonumber = tonumber
|
||||
local tostring = tostring
|
||||
local error = error
|
||||
local type = type
|
||||
local pairs = pairs
|
||||
local ipairs = ipairs
|
||||
local next = next
|
||||
|
||||
local getmetatable = getmetatable
|
||||
|
||||
module "luci.json"
|
||||
|
||||
|
@ -30,9 +63,161 @@ function null()
|
|||
return null
|
||||
end
|
||||
|
||||
|
||||
Encoder = util.class()
|
||||
|
||||
--- Creates a new Encoder.
|
||||
-- @param data Data to be encoded.
|
||||
-- @param buffersize Buffersize of returned data.
|
||||
-- @param fastescape Use non-standard escaping (don't escape control chars)
|
||||
function Encoder.__init__(self, data, buffersize, fastescape)
|
||||
self.data = data
|
||||
self.buffersize = buffersize or 512
|
||||
self.buffer = ""
|
||||
self.fastescape = fastescape
|
||||
|
||||
getmetatable(self).__call = Encoder.source
|
||||
end
|
||||
|
||||
--- Create an LTN12 source from the encoder object
|
||||
-- @return LTN12 source
|
||||
function Encoder.source(self)
|
||||
local source = coroutine.create(self.dispatch)
|
||||
return function()
|
||||
local res, data = coroutine.resume(source, self, self.data, true)
|
||||
if res then
|
||||
return data
|
||||
else
|
||||
return nil, data
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Encoder.dispatch(self, data, start)
|
||||
local parser = self.parsers[type(data)]
|
||||
|
||||
parser(self, data)
|
||||
|
||||
if start then
|
||||
if #self.buffer > 0 then
|
||||
coroutine.yield(self.buffer)
|
||||
end
|
||||
|
||||
coroutine.yield()
|
||||
end
|
||||
end
|
||||
|
||||
function Encoder.put(self, chunk)
|
||||
if self.buffersize < 2 then
|
||||
corountine.yield(chunk)
|
||||
else
|
||||
if #self.buffer + #chunk > self.buffersize then
|
||||
local written = 0
|
||||
local fbuffer = self.buffersize - #self.buffer
|
||||
|
||||
coroutine.yield(self.buffer .. chunk:sub(written + 1, fbuffer))
|
||||
written = fbuffer
|
||||
|
||||
while #chunk - written > self.buffersize do
|
||||
fbuffer = written + self.buffersize
|
||||
coroutine.yield(chunk:sub(written + 1, fbuffer))
|
||||
written = fbuffer
|
||||
end
|
||||
|
||||
self.buffer = chunk:sub(written + 1)
|
||||
else
|
||||
self.buffer = self.buffer .. chunk
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Encoder.parse_nil(self)
|
||||
self:put("null")
|
||||
end
|
||||
|
||||
function Encoder.parse_bool(self, obj)
|
||||
self:put(obj and "true" or "false")
|
||||
end
|
||||
|
||||
function Encoder.parse_number(self, obj)
|
||||
self:put(tostring(obj))
|
||||
end
|
||||
|
||||
function Encoder.parse_string(self, obj)
|
||||
if self.fastescape then
|
||||
self:put('"' .. obj:gsub('\\', '\\\\'):gsub('"', '\\"') .. '"')
|
||||
else
|
||||
self:put('"' ..
|
||||
obj:gsub('[%c\\"]',
|
||||
function(char)
|
||||
return '\\u00%02x' % char:byte()
|
||||
end
|
||||
)
|
||||
.. '"')
|
||||
end
|
||||
end
|
||||
|
||||
function Encoder.parse_iter(self, obj)
|
||||
if obj == null then
|
||||
return self:put("null")
|
||||
end
|
||||
|
||||
if type(obj) == "table" and (#obj == 0 and next(obj)) then
|
||||
self:put("{")
|
||||
local first = true
|
||||
|
||||
for key, entry in pairs(obj) do
|
||||
first = first or self:put(",")
|
||||
first = first and false
|
||||
self:parse_string(tostring(key))
|
||||
self:put(":")
|
||||
self:dispatch(entry)
|
||||
end
|
||||
|
||||
self:put("}")
|
||||
else
|
||||
self:put("[")
|
||||
local first = true
|
||||
|
||||
if type(obj) == "table" then
|
||||
for i, entry in pairs(obj) do
|
||||
first = first or self:put(",")
|
||||
first = first and nil
|
||||
self:dispatch(entry)
|
||||
end
|
||||
else
|
||||
for entry in obj do
|
||||
first = first or self:put(",")
|
||||
first = first and nil
|
||||
self:dispatch(entry)
|
||||
end
|
||||
end
|
||||
|
||||
self:put("]")
|
||||
end
|
||||
end
|
||||
|
||||
Encoder.parsers = {
|
||||
['nil'] = Encoder.parse_nil,
|
||||
['table'] = Encoder.parse_iter,
|
||||
['number'] = Encoder.parse_number,
|
||||
['string'] = Encoder.parse_string,
|
||||
['boolean'] = Encoder.parse_bool,
|
||||
['function'] = Encoder.parse_iter
|
||||
}
|
||||
|
||||
|
||||
|
||||
Decoder = util.class()
|
||||
|
||||
--- Create an LTN12 sink from the decoder object
|
||||
--- Create a new Decoder object.
|
||||
-- @param customnull User luci.json.null instead of nil
|
||||
function Decoder.__init__(self, customnull)
|
||||
self.cnull = customnull
|
||||
getmetatable(self).__call = Decoder.sink
|
||||
end
|
||||
|
||||
--- Create an LTN12 sink from the decoder object.
|
||||
-- @return LTN12 sink
|
||||
function Decoder.sink(self)
|
||||
local sink = coroutine.create(self.dispatch)
|
||||
|
@ -48,62 +233,41 @@ function Decoder.get(self)
|
|||
return self.data
|
||||
end
|
||||
|
||||
|
||||
function Decoder.dispatch(self, chunk, src_err, strict)
|
||||
local robject, object
|
||||
local oset = false
|
||||
|
||||
while chunk do
|
||||
if #chunk < 1 then
|
||||
while chunk and #chunk < 1 do
|
||||
chunk = self:fetch()
|
||||
end
|
||||
|
||||
assert(not strict or chunk, "Unexpected EOS")
|
||||
if not chunk then
|
||||
break
|
||||
end
|
||||
if not chunk then break end
|
||||
|
||||
local parser = nil
|
||||
local char = chunk:sub(1, 1)
|
||||
local parser = self.parsers[char]
|
||||
or (char:match("%s") and self.parse_space)
|
||||
or (char:match("[0-9-]") and self.parse_number)
|
||||
or error("Unexpected char '%s'" % char)
|
||||
|
||||
if char == '"' then
|
||||
parser = self.parse_string
|
||||
elseif char == 't' then
|
||||
parser = self.parse_true
|
||||
elseif char == 'f' then
|
||||
parser = self.parse_false
|
||||
elseif char == 'n' then
|
||||
parser = self.parse_null
|
||||
elseif char == '[' then
|
||||
parser = self.parse_array
|
||||
elseif char == '{' then
|
||||
parser = self.parse_object
|
||||
elseif char:match("%s") then
|
||||
parser = self.parse_space
|
||||
elseif char:match("[0-9-]") then
|
||||
parser = self.parse_number
|
||||
end
|
||||
chunk, robject = parser(self, chunk)
|
||||
|
||||
if parser then
|
||||
chunk, robject = parser(self, chunk)
|
||||
|
||||
if robject ~= nil then
|
||||
assert(object == nil, "Scope violation: Too many objects")
|
||||
object = robject
|
||||
end
|
||||
|
||||
if strict and object ~= nil then
|
||||
if parser ~= self.parse_space then
|
||||
assert(not oset, "Scope violation: Too many objects")
|
||||
object = robject
|
||||
oset = true
|
||||
|
||||
if strict then
|
||||
return chunk, object
|
||||
end
|
||||
else
|
||||
error("Unexpected char '%s'" % char)
|
||||
end
|
||||
end
|
||||
|
||||
assert(not src_err, src_err)
|
||||
assert(object ~= nil, "Unexpected EOS")
|
||||
assert(oset, "Unexpected EOS")
|
||||
|
||||
self.data = object
|
||||
return chunk, object
|
||||
end
|
||||
|
||||
|
||||
|
@ -162,7 +326,7 @@ end
|
|||
|
||||
|
||||
function Decoder.parse_null(self, chunk)
|
||||
return self:parse_literal(chunk, "null", null)
|
||||
return self:parse_literal(chunk, "null", self.cnull and null)
|
||||
end
|
||||
|
||||
|
||||
|
@ -224,6 +388,14 @@ function Decoder.parse_escape(self, chunk)
|
|||
return chunk, '"'
|
||||
elseif char == "\\" then
|
||||
return chunk, "\\"
|
||||
elseif char == "u" then
|
||||
chunk = self:fetch_atleast(chunk, 4)
|
||||
local s1, s2 = chunk:sub(1, 2), chunk:sub(3, 4)
|
||||
s1, s2 = tonumber(s1, 16), tonumber(s2, 16)
|
||||
assert(s1 and s2, "Invalid Unicode character")
|
||||
|
||||
-- ToDo: Unicode support
|
||||
return chunk:sub(5), s1 == 0 and string.char(s2) or ""
|
||||
elseif char == "/" then
|
||||
return chunk, "/"
|
||||
elseif char == "b" then
|
||||
|
@ -236,14 +408,6 @@ function Decoder.parse_escape(self, chunk)
|
|||
return chunk, "\r"
|
||||
elseif char == "t" then
|
||||
return chunk, "\t"
|
||||
elseif char == "u" then
|
||||
chunk = self:fetch_atleast(chunk, 4)
|
||||
local s1, s2 = chunk:sub(1, 4):match("^([0-9a-fA-F][0-9a-fA-F])([0-9a-fA-F][0-9a-fA-F])$")
|
||||
assert(s1 and s2, "Invalid Unicode character 'U+%s%s'" % {s1, s2})
|
||||
s1, s2 = tonumber(s1, 16), tonumber(s2, 16)
|
||||
|
||||
-- ToDo: Unicode support
|
||||
return chunk:sub(5), s1 == 0 and s2 or ""
|
||||
else
|
||||
error("Unexpected escaping sequence '\\%s'" % char)
|
||||
end
|
||||
|
@ -253,6 +417,7 @@ end
|
|||
function Decoder.parse_array(self, chunk)
|
||||
chunk = chunk:sub(2)
|
||||
local array = {}
|
||||
local nextp = 1
|
||||
|
||||
local chunk, object = self:parse_delimiter(chunk, "%]")
|
||||
|
||||
|
@ -262,7 +427,8 @@ function Decoder.parse_array(self, chunk)
|
|||
|
||||
repeat
|
||||
chunk, object = self:dispatch(chunk, nil, true)
|
||||
table.insert(array, object)
|
||||
table.insert(array, nextp, object)
|
||||
nextp = nextp + 1
|
||||
|
||||
chunk, object = self:parse_delimiter(chunk, ",%]")
|
||||
assert(object, "Delimiter expected")
|
||||
|
@ -316,4 +482,14 @@ function Decoder.parse_delimiter(self, chunk, delimiter)
|
|||
return chunk, nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
Decoder.parsers = {
|
||||
['"'] = Decoder.parse_string,
|
||||
['t'] = Decoder.parse_true,
|
||||
['f'] = Decoder.parse_false,
|
||||
['n'] = Decoder.parse_null,
|
||||
['['] = Decoder.parse_array,
|
||||
['{'] = Decoder.parse_object
|
||||
}
|
|
@ -197,6 +197,12 @@ function prepare_content(mime)
|
|||
header("Content-Type", mime)
|
||||
end
|
||||
|
||||
--- Get the RAW HTTP input source
|
||||
-- @return HTTP LTN12 source
|
||||
function source()
|
||||
return context.request.input
|
||||
end
|
||||
|
||||
--- Set the HTTP status code and status message.
|
||||
-- @param code Status code
|
||||
-- @param message Status message
|
||||
|
|
|
@ -51,6 +51,7 @@ function rpc_auth()
|
|||
local sauth = require "luci.sauth"
|
||||
local http = require "luci.http"
|
||||
local sys = require "luci.sys"
|
||||
local ltn12 = require "luci.ltn12"
|
||||
|
||||
http.setfilehandler()
|
||||
|
||||
|
@ -70,35 +71,53 @@ function rpc_auth()
|
|||
end
|
||||
|
||||
http.prepare_content("application/json")
|
||||
http.write(jsonrpc.handle(server, http.content()))
|
||||
ltn12.pump.all(jsonrpc.handle(server, http.source()), http.write)
|
||||
end
|
||||
|
||||
function rpc_uci()
|
||||
local uci = require "luci.controller.rpc.uci"
|
||||
local jsonrpc = require "luci.jsonrpc"
|
||||
local http = require "luci.http"
|
||||
local ltn12 = require "luci.ltn12"
|
||||
|
||||
http.setfilehandler()
|
||||
http.prepare_content("application/json")
|
||||
http.write(jsonrpc.handle(uci, http.content()))
|
||||
ltn12.pump.all(jsonrpc.handle(uci, http.source()), http.write)
|
||||
end
|
||||
|
||||
function rpc_fs()
|
||||
local fs = require "luci.fs"
|
||||
local util = require "luci.util"
|
||||
local fs = util.clone(require "luci.fs")
|
||||
local jsonrpc = require "luci.jsonrpc"
|
||||
local http = require "luci.http"
|
||||
local ltn12 = require "luci.ltn12"
|
||||
|
||||
function fs.readfile(filename)
|
||||
if not pcall(require, "mime") then
|
||||
error("Base64 support not available. Please install LuaSocket.")
|
||||
end
|
||||
|
||||
return ltn12.source.chain(ltn12.source.file(filename), mime.encode("base64"))
|
||||
end
|
||||
|
||||
function fs.writefile(filename, data)
|
||||
if not pcall(require, "mime") then
|
||||
error("Base64 support not available. Please install LuaSocket.")
|
||||
end
|
||||
|
||||
local sink = ltn12.sink.chain(mime.decode("base64"), ltn12.sink.file(filename))
|
||||
return ltn12.pump.all(ltn12.source.string(data), sink)
|
||||
end
|
||||
|
||||
http.setfilehandler()
|
||||
http.prepare_content("application/json")
|
||||
http.write(jsonrpc.handle(fs, http.content()))
|
||||
ltn12.pump.all(jsonrpc.handle(fs, http.source()), http.write)
|
||||
end
|
||||
|
||||
function rpc_sys()
|
||||
local sys = require "luci.sys"
|
||||
local jsonrpc = require "luci.jsonrpc"
|
||||
local http = require "luci.http"
|
||||
local ltn12 = require "luci.ltn12"
|
||||
|
||||
http.setfilehandler()
|
||||
http.prepare_content("application/json")
|
||||
http.write(jsonrpc.handle(sys, http.content()))
|
||||
ltn12.pump.all(jsonrpc.handle(sys, http.source()), http.write)
|
||||
end
|
|
@ -34,8 +34,10 @@ function resolve(mod, method)
|
|||
end
|
||||
end
|
||||
|
||||
function handle(tbl, rawdata)
|
||||
local stat, json = luci.util.copcall(luci.json.Decode, rawdata)
|
||||
function handle(tbl, rawsource, ...)
|
||||
local decoder = luci.json.Decoder()
|
||||
local stat = luci.ltn12.pump.all(rawsource, decoder:sink())
|
||||
local json = decoder:get()
|
||||
local response
|
||||
local success = false
|
||||
|
||||
|
@ -54,22 +56,22 @@ function handle(tbl, rawdata)
|
|||
nil, {code=-32600, message="Invalid request."})
|
||||
end
|
||||
else
|
||||
response = reply(json.jsonrpc, nil,
|
||||
response = reply("2.0", nil,
|
||||
nil, {code=-32700, message="Parse error."})
|
||||
end
|
||||
|
||||
return luci.json.Encode(response)
|
||||
return luci.json.Encoder(response, ...):source()
|
||||
end
|
||||
|
||||
function reply(jsonrpc, id, res, err)
|
||||
require "luci.json"
|
||||
id = id or luci.json.Null
|
||||
id = id or luci.json.null
|
||||
|
||||
-- 1.0 compatibility
|
||||
if jsonrpc ~= "2.0" then
|
||||
jsonrpc = nil
|
||||
res = res or luci.json.Null
|
||||
err = err or luci.json.Null
|
||||
res = res or luci.json.null
|
||||
err = err or luci.json.null
|
||||
end
|
||||
|
||||
return {id=id, result=res, error=err, jsonrpc=jsonrpc}
|
||||
|
@ -83,7 +85,7 @@ function proxy(method, ...)
|
|||
return nil, {code=-32602, message="Invalid params.", data=table.remove(res, 1)}
|
||||
else
|
||||
if #res <= 1 then
|
||||
return res[1] or luci.json.Null
|
||||
return res[1] or luci.json.null
|
||||
else
|
||||
return res
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue