libs/json: Completed JSON library
authorSteven Barth <steven@midlink.org>
Tue, 26 Aug 2008 17:50:32 +0000 (17:50 +0000)
committerSteven Barth <steven@midlink.org>
Tue, 26 Aug 2008 17:50:32 +0000 (17:50 +0000)
modules/rpc: Added experimental JSON-RPC API

contrib/package/luci/Makefile
libs/json/luasrc/json.lua
libs/web/luasrc/http.lua
modules/rpc/luasrc/controller/rpc.lua
modules/rpc/luasrc/jsonrpc.lua

index 1cb3a79..e8a1d36 100644 (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))
index 0d38ed4..3d6a576 100644 (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
\ 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
index 17b2748..b1ffac2 100644 (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
index aa77a8f..98fafe3 100644 (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
\ No newline at end of file
index c4fed2a..1c0db8b 100644 (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