From: Steven Barth Date: Tue, 26 Aug 2008 17:50:32 +0000 (+0000) Subject: libs/json: Completed JSON library X-Git-Tag: 0.8.0~298 X-Git-Url: https://git.archive.openwrt.org/?p=project%2Fluci.git;a=commitdiff_plain;h=df40e4df5e3485d1d70548b420e02b81aa61e60b libs/json: Completed JSON library modules/rpc: Added experimental JSON-RPC API --- diff --git a/contrib/package/luci/Makefile b/contrib/package/luci/Makefile index 1cb3a79a6..e8a1d3698 100644 --- a/contrib/package/luci/Makefile +++ b/contrib/package/luci/Makefile @@ -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)) diff --git a/libs/json/luasrc/json.lua b/libs/json/luasrc/json.lua index 0d38ed479..3d6a576a8 100644 --- a/libs/json/luasrc/json.lua +++ b/libs/json/luasrc/json.lua @@ -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 \ No newline at end of file +end + + +Decoder.parsers = { + ['"'] = Decoder.parse_string, + ['t'] = Decoder.parse_true, + ['f'] = Decoder.parse_false, + ['n'] = Decoder.parse_null, + ['['] = Decoder.parse_array, + ['{'] = Decoder.parse_object +} \ No newline at end of file diff --git a/libs/web/luasrc/http.lua b/libs/web/luasrc/http.lua index 17b27482e..b1ffac2be 100644 --- a/libs/web/luasrc/http.lua +++ b/libs/web/luasrc/http.lua @@ -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 diff --git a/modules/rpc/luasrc/controller/rpc.lua b/modules/rpc/luasrc/controller/rpc.lua index aa77a8f24..98fafe3f1 100644 --- a/modules/rpc/luasrc/controller/rpc.lua +++ b/modules/rpc/luasrc/controller/rpc.lua @@ -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 \ No newline at end of file diff --git a/modules/rpc/luasrc/jsonrpc.lua b/modules/rpc/luasrc/jsonrpc.lua index c4fed2acc..1c0db8bce 100644 --- a/modules/rpc/luasrc/jsonrpc.lua +++ b/modules/rpc/luasrc/jsonrpc.lua @@ -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