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 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 define Package/luci-sys
$(call Package/luci/libtemplate) $(call Package/luci/libtemplate)
TITLE:=LuCI Linux/POSIX system library TITLE:=LuCI Linux/POSIX system library
@ -354,6 +364,17 @@ define Package/luci-admin-full/install
endef 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 define Package/luci-mod-freifunk
$(call Package/luci/fftemplate) $(call Package/luci/fftemplate)
DEPENDS:=+luci-admin-full DEPENDS:=+luci-admin-full
@ -607,6 +628,9 @@ endif
ifneq ($(CONFIG_PACKAGE_luci-ipkg),) ifneq ($(CONFIG_PACKAGE_luci-ipkg),)
PKG_SELECTED_MODULES+=libs/ipkg PKG_SELECTED_MODULES+=libs/ipkg
endif endif
ifneq ($(CONFIG_PACKAGE_luci-json),)
PKG_SELECTED_MODULES+=libs/json
endif
ifneq ($(CONFIG_PACKAGE_luci-uci),) ifneq ($(CONFIG_PACKAGE_luci-uci),)
PKG_SELECTED_MODULES+=libs/uci PKG_SELECTED_MODULES+=libs/uci
endif endif
@ -649,6 +673,9 @@ endif
ifneq ($(CONFIG_PACKAGE_luci-admin-full),) ifneq ($(CONFIG_PACKAGE_luci-admin-full),)
PKG_SELECTED_MODULES+=modules/admin-full PKG_SELECTED_MODULES+=modules/admin-full
endif endif
ifneq ($(CONFIG_PACKAGE_luci-admin-rpc),)
PKG_SELECTED_MODULES+=modules/rpc
endif
ifneq ($(CONFIG_PACKAGE_luci-mod-freifunk),) ifneq ($(CONFIG_PACKAGE_luci-mod-freifunk),)
PKG_SELECTED_MODULES+=modules/freifunk PKG_SELECTED_MODULES+=modules/freifunk
endif endif
@ -733,6 +760,7 @@ $(eval $(call BuildPackage,luci-cbi))
$(eval $(call BuildPackage,luci-fastindex)) $(eval $(call BuildPackage,luci-fastindex))
$(eval $(call BuildPackage,luci-http)) $(eval $(call BuildPackage,luci-http))
$(eval $(call BuildPackage,luci-ipkg)) $(eval $(call BuildPackage,luci-ipkg))
$(eval $(call BuildPackage,luci-json))
$(eval $(call BuildPackage,luci-uci)) $(eval $(call BuildPackage,luci-uci))
$(eval $(call BuildPackage,luci-sys)) $(eval $(call BuildPackage,luci-sys))
$(eval $(call BuildPackage,luci-web)) $(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-core))
$(eval $(call BuildPackage,luci-admin-mini)) $(eval $(call BuildPackage,luci-admin-mini))
$(eval $(call BuildPackage,luci-admin-full)) $(eval $(call BuildPackage,luci-admin-full))
$(eval $(call BuildPackage,luci-admin-rpc))
$(eval $(call BuildPackage,luci-mod-freifunk)) $(eval $(call BuildPackage,luci-mod-freifunk))
$(eval $(call BuildPackage,luci-app-ffwizard-leipzig)) $(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 http://www.apache.org/licenses/LICENSE-2.0
$Id$ $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 util = require "luci.util"
local ltn12 = require "luci.ltn12"
local table = require "table" local table = require "table"
local string = require "string"
local coroutine = require "coroutine" local coroutine = require "coroutine"
local assert = assert local assert = assert
local tonumber = tonumber local tonumber = tonumber
local tostring = tostring
local error = error local error = error
local type = type
local pairs = pairs
local ipairs = ipairs
local next = next
local getmetatable = getmetatable
module "luci.json" module "luci.json"
@ -30,9 +63,161 @@ function null()
return null return null
end 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() 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 -- @return LTN12 sink
function Decoder.sink(self) function Decoder.sink(self)
local sink = coroutine.create(self.dispatch) local sink = coroutine.create(self.dispatch)
@ -48,62 +233,41 @@ function Decoder.get(self)
return self.data return self.data
end end
function Decoder.dispatch(self, chunk, src_err, strict) function Decoder.dispatch(self, chunk, src_err, strict)
local robject, object local robject, object
local oset = false
while chunk do while chunk do
if #chunk < 1 then while chunk and #chunk < 1 do
chunk = self:fetch() chunk = self:fetch()
end end
assert(not strict or chunk, "Unexpected EOS") assert(not strict or chunk, "Unexpected EOS")
if not chunk then if not chunk then break end
break
end
local parser = nil
local char = chunk:sub(1, 1) 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 chunk, robject = parser(self, chunk)
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
if parser then if parser ~= self.parse_space then
chunk, robject = parser(self, chunk) assert(not oset, "Scope violation: Too many objects")
object = robject
oset = true
if robject ~= nil then if strict then
assert(object == nil, "Scope violation: Too many objects")
object = robject
end
if strict and object ~= nil then
return chunk, object return chunk, object
end end
else
error("Unexpected char '%s'" % char)
end end
end end
assert(not src_err, src_err) assert(not src_err, src_err)
assert(object ~= nil, "Unexpected EOS") assert(oset, "Unexpected EOS")
self.data = object self.data = object
return chunk, object
end end
@ -162,7 +326,7 @@ end
function Decoder.parse_null(self, chunk) function Decoder.parse_null(self, chunk)
return self:parse_literal(chunk, "null", null) return self:parse_literal(chunk, "null", self.cnull and null)
end end
@ -224,6 +388,14 @@ function Decoder.parse_escape(self, chunk)
return chunk, '"' return chunk, '"'
elseif char == "\\" then elseif char == "\\" then
return chunk, "\\" 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 elseif char == "/" then
return chunk, "/" return chunk, "/"
elseif char == "b" then elseif char == "b" then
@ -236,14 +408,6 @@ function Decoder.parse_escape(self, chunk)
return chunk, "\r" return chunk, "\r"
elseif char == "t" then elseif char == "t" then
return chunk, "\t" 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 else
error("Unexpected escaping sequence '\\%s'" % char) error("Unexpected escaping sequence '\\%s'" % char)
end end
@ -253,6 +417,7 @@ end
function Decoder.parse_array(self, chunk) function Decoder.parse_array(self, chunk)
chunk = chunk:sub(2) chunk = chunk:sub(2)
local array = {} local array = {}
local nextp = 1
local chunk, object = self:parse_delimiter(chunk, "%]") local chunk, object = self:parse_delimiter(chunk, "%]")
@ -262,7 +427,8 @@ function Decoder.parse_array(self, chunk)
repeat repeat
chunk, object = self:dispatch(chunk, nil, true) 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, ",%]") chunk, object = self:parse_delimiter(chunk, ",%]")
assert(object, "Delimiter expected") assert(object, "Delimiter expected")
@ -317,3 +483,13 @@ function Decoder.parse_delimiter(self, chunk, delimiter)
end end
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) header("Content-Type", mime)
end 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. --- Set the HTTP status code and status message.
-- @param code Status code -- @param code Status code
-- @param message Status message -- @param message Status message

View file

@ -51,6 +51,7 @@ function rpc_auth()
local sauth = require "luci.sauth" local sauth = require "luci.sauth"
local http = require "luci.http" local http = require "luci.http"
local sys = require "luci.sys" local sys = require "luci.sys"
local ltn12 = require "luci.ltn12"
http.setfilehandler() http.setfilehandler()
@ -70,35 +71,53 @@ function rpc_auth()
end end
http.prepare_content("application/json") http.prepare_content("application/json")
http.write(jsonrpc.handle(server, http.content())) ltn12.pump.all(jsonrpc.handle(server, http.source()), http.write)
end end
function rpc_uci() function rpc_uci()
local uci = require "luci.controller.rpc.uci" local uci = require "luci.controller.rpc.uci"
local jsonrpc = require "luci.jsonrpc" local jsonrpc = require "luci.jsonrpc"
local http = require "luci.http" local http = require "luci.http"
local ltn12 = require "luci.ltn12"
http.setfilehandler()
http.prepare_content("application/json") http.prepare_content("application/json")
http.write(jsonrpc.handle(uci, http.content())) ltn12.pump.all(jsonrpc.handle(uci, http.source()), http.write)
end end
function rpc_fs() 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 jsonrpc = require "luci.jsonrpc"
local http = require "luci.http" 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.prepare_content("application/json")
http.write(jsonrpc.handle(fs, http.content())) ltn12.pump.all(jsonrpc.handle(fs, http.source()), http.write)
end end
function rpc_sys() function rpc_sys()
local sys = require "luci.sys" local sys = require "luci.sys"
local jsonrpc = require "luci.jsonrpc" local jsonrpc = require "luci.jsonrpc"
local http = require "luci.http" local http = require "luci.http"
local ltn12 = require "luci.ltn12"
http.setfilehandler()
http.prepare_content("application/json") http.prepare_content("application/json")
http.write(jsonrpc.handle(sys, http.content())) ltn12.pump.all(jsonrpc.handle(sys, http.source()), http.write)
end end

View file

@ -34,8 +34,10 @@ function resolve(mod, method)
end end
end end
function handle(tbl, rawdata) function handle(tbl, rawsource, ...)
local stat, json = luci.util.copcall(luci.json.Decode, rawdata) local decoder = luci.json.Decoder()
local stat = luci.ltn12.pump.all(rawsource, decoder:sink())
local json = decoder:get()
local response local response
local success = false local success = false
@ -54,22 +56,22 @@ function handle(tbl, rawdata)
nil, {code=-32600, message="Invalid request."}) nil, {code=-32600, message="Invalid request."})
end end
else else
response = reply(json.jsonrpc, nil, response = reply("2.0", nil,
nil, {code=-32700, message="Parse error."}) nil, {code=-32700, message="Parse error."})
end end
return luci.json.Encode(response) return luci.json.Encoder(response, ...):source()
end end
function reply(jsonrpc, id, res, err) function reply(jsonrpc, id, res, err)
require "luci.json" require "luci.json"
id = id or luci.json.Null id = id or luci.json.null
-- 1.0 compatibility -- 1.0 compatibility
if jsonrpc ~= "2.0" then if jsonrpc ~= "2.0" then
jsonrpc = nil jsonrpc = nil
res = res or luci.json.Null res = res or luci.json.null
err = err or luci.json.Null err = err or luci.json.null
end end
return {id=id, result=res, error=err, jsonrpc=jsonrpc} 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)} return nil, {code=-32602, message="Invalid params.", data=table.remove(res, 1)}
else else
if #res <= 1 then if #res <= 1 then
return res[1] or luci.json.Null return res[1] or luci.json.null
else else
return res return res
end end