libs/json: Completed JSON library

modules/rpc: Added experimental JSON-RPC API
This commit is contained in:
Steven Barth 2008-08-26 17:50:32 +00:00
parent 05b30bba2a
commit df40e4df5e
5 changed files with 297 additions and 65 deletions

View file

@ -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))

View file

@ -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
}

View file

@ -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

View file

@ -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

View file

@ -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