2 LuCI - Lua Configuration Interface
4 Copyright 2008 Steven Barth <steven@midlink.org>
5 Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
7 Licensed under the Apache License, Version 2.0 (the "License");
8 you may not use this file except in compliance with the License.
9 You may obtain a copy of the License at
11 http://www.apache.org/licenses/LICENSE-2.0
17 null will be decoded to luci.json.null if first parameter of Decoder() is true
20 decoder = luci.json.Decoder()
21 luci.ltn12.pump.all(luci.ltn12.source.string("decodableJSON"), decoder:sink())
22 luci.util.dumptable(decoder:get())
25 does not support unicode conversion \uXXYY with XX != 00 will be ignored
30 Accepts numbers, strings, nil, booleans as they are
31 Accepts luci.json.null as replacement for nil
32 Accepts full associative and full numerically indexed tables
33 Mixed tables will loose their associative values during conversion
34 Iterator functions will be encoded as an array of their return values
35 Non-iterator functions will probably corrupt the encoder
38 encoder = luci.json.Encoder(encodableData)
39 luci.ltn12.pump.all(encoder:source(), luci.ltn12.sink.file(io.open("someFile", w)))
42 local util = require "luci.util"
43 local table = require "table"
44 local string = require "string"
45 local coroutine = require "coroutine"
48 local tonumber = tonumber
49 local tostring = tostring
57 local getmetatable = getmetatable
64 --- Directly decode a JSON string
65 -- @param json JSON-String
67 function decode(json, ...)
68 local a = ActiveDecoder(function() return nil end, ...)
70 local s, obj = pcall(a.get, a)
71 return s and obj or nil
75 --- Direcly encode a Lua object into a JSON string.
76 -- @param obj Lua Object
77 -- @return JSON string
78 function encode(obj, ...)
80 local e = Encoder(obj, 1, ...):source()
86 return not err and table.concat(out) or nil
90 --- Null replacement function
96 --- Create a new JSON-Encoder.
99 -- @param data Lua-Object to be encoded.
100 -- @param buffersize Blocksize of returned data source.
101 -- @param fastescape Use non-standard escaping (don't escape control chars)
102 -- @return JSON-Encoder
103 Encoder = util.class()
105 function Encoder.__init__(self, data, buffersize, fastescape)
107 self.buffersize = buffersize or 512
109 self.fastescape = fastescape
111 getmetatable(self).__call = Encoder.source
114 --- Create an LTN12 source providing the encoded JSON-Data.
115 -- @return LTN12 source
116 function Encoder.source(self)
117 local source = coroutine.create(self.dispatch)
119 local res, data = coroutine.resume(source, self, self.data, true)
128 function Encoder.dispatch(self, data, start)
129 local parser = self.parsers[type(data)]
134 if #self.buffer > 0 then
135 coroutine.yield(self.buffer)
142 function Encoder.put(self, chunk)
143 if self.buffersize < 2 then
144 coroutine.yield(chunk)
146 if #self.buffer + #chunk > self.buffersize then
148 local fbuffer = self.buffersize - #self.buffer
150 coroutine.yield(self.buffer .. chunk:sub(written + 1, fbuffer))
153 while #chunk - written > self.buffersize do
154 fbuffer = written + self.buffersize
155 coroutine.yield(chunk:sub(written + 1, fbuffer))
159 self.buffer = chunk:sub(written + 1)
161 self.buffer = self.buffer .. chunk
166 function Encoder.parse_nil(self)
170 function Encoder.parse_bool(self, obj)
171 self:put(obj and "true" or "false")
174 function Encoder.parse_number(self, obj)
175 self:put(tostring(obj))
178 function Encoder.parse_string(self, obj)
179 if self.fastescape then
180 self:put('"' .. obj:gsub('\\', '\\\\'):gsub('"', '\\"') .. '"')
185 return '\\u00%02x' % char:byte()
192 function Encoder.parse_iter(self, obj)
194 return self:put("null")
197 if type(obj) == "table" and (#obj == 0 and next(obj)) then
201 for key, entry in pairs(obj) do
202 first = first or self:put(",")
203 first = first and false
204 self:parse_string(tostring(key))
214 if type(obj) == "table" then
216 first = first or self:put(",")
217 first = first and nil
218 self:dispatch(obj[i])
222 first = first or self:put(",")
223 first = first and nil
233 ['nil'] = Encoder.parse_nil,
234 ['table'] = Encoder.parse_iter,
235 ['number'] = Encoder.parse_number,
236 ['string'] = Encoder.parse_string,
237 ['boolean'] = Encoder.parse_bool,
238 ['function'] = Encoder.parse_iter
242 --- Create a new JSON-Decoder.
245 -- @param customnull Use luci.json.null instead of nil for decoding null
246 -- @return JSON-Decoder
247 Decoder = util.class()
249 function Decoder.__init__(self, customnull)
250 self.cnull = customnull
251 getmetatable(self).__call = Decoder.sink
254 --- Create an LTN12 sink from the decoder object which accepts the JSON-Data.
255 -- @return LTN12 sink
256 function Decoder.sink(self)
257 local sink = coroutine.create(self.dispatch)
259 return coroutine.resume(sink, self, ...)
264 --- Get the decoded data packets after the rawdata has been sent to the sink.
265 -- @return Decoded data
266 function Decoder.get(self)
270 function Decoder.dispatch(self, chunk, src_err, strict)
271 local robject, object
275 while chunk and #chunk < 1 do
279 assert(not strict or chunk, "Unexpected EOS")
280 if not chunk then break end
282 local char = chunk:sub(1, 1)
283 local parser = self.parsers[char]
284 or (char:match("%s") and self.parse_space)
285 or (char:match("[0-9-]") and self.parse_number)
286 or error("Unexpected char '%s'" % char)
288 chunk, robject = parser(self, chunk)
290 if parser ~= self.parse_space then
291 assert(not oset, "Scope violation: Too many objects")
301 assert(not src_err, src_err)
302 assert(oset, "Unexpected EOS")
308 function Decoder.fetch(self)
309 local tself, chunk, src_err = coroutine.yield()
310 assert(chunk or not src_err, src_err)
315 function Decoder.fetch_atleast(self, chunk, bytes)
316 while #chunk < bytes do
317 local nchunk = self:fetch()
318 assert(nchunk, "Unexpected EOS")
319 chunk = chunk .. nchunk
326 function Decoder.fetch_until(self, chunk, pattern)
327 local start = chunk:find(pattern)
330 local nchunk = self:fetch()
331 assert(nchunk, "Unexpected EOS")
332 chunk = chunk .. nchunk
333 start = chunk:find(pattern)
340 function Decoder.parse_space(self, chunk)
341 local start = chunk:find("[^%s]")
348 start = chunk:find("[^%s]")
351 return chunk:sub(start)
355 function Decoder.parse_literal(self, chunk, literal, value)
356 chunk = self:fetch_atleast(chunk, #literal)
357 assert(chunk:sub(1, #literal) == literal, "Invalid character sequence")
358 return chunk:sub(#literal + 1), value
362 function Decoder.parse_null(self, chunk)
363 return self:parse_literal(chunk, "null", self.cnull and null)
367 function Decoder.parse_true(self, chunk)
368 return self:parse_literal(chunk, "true", true)
372 function Decoder.parse_false(self, chunk)
373 return self:parse_literal(chunk, "false", false)
377 function Decoder.parse_number(self, chunk)
378 local chunk, start = self:fetch_until(chunk, "[^0-9eE.+-]")
379 local number = tonumber(chunk:sub(1, start - 1))
380 assert(number, "Invalid number specification")
381 return chunk:sub(start), number
385 function Decoder.parse_string(self, chunk)
388 assert(chunk:sub(1, 1) == '"', 'Expected "')
392 local spos = chunk:find('[\\"]')
394 str = str .. chunk:sub(1, spos - 1)
396 local char = chunk:sub(spos, spos)
397 if char == '"' then -- String end
398 chunk = chunk:sub(spos + 1)
400 elseif char == "\\" then -- Escape sequence
401 chunk, object = self:parse_escape(chunk:sub(spos))
407 assert(chunk, "Unexpected EOS while parsing a string")
415 function Decoder.parse_escape(self, chunk)
417 chunk = self:fetch_atleast(chunk:sub(2), 1)
418 local char = chunk:sub(1, 1)
423 elseif char == "\\" then
425 elseif char == "u" then
426 chunk = self:fetch_atleast(chunk, 4)
427 local s1, s2 = chunk:sub(1, 2), chunk:sub(3, 4)
428 s1, s2 = tonumber(s1, 16), tonumber(s2, 16)
429 assert(s1 and s2, "Invalid Unicode character")
431 -- ToDo: Unicode support
432 return chunk:sub(5), s1 == 0 and string.char(s2) or ""
433 elseif char == "/" then
435 elseif char == "b" then
437 elseif char == "f" then
439 elseif char == "n" then
441 elseif char == "r" then
443 elseif char == "t" then
446 error("Unexpected escaping sequence '\\%s'" % char)
451 function Decoder.parse_array(self, chunk)
456 local chunk, object = self:parse_delimiter(chunk, "%]")
463 chunk, object = self:dispatch(chunk, nil, true)
464 table.insert(array, nextp, object)
467 chunk, object = self:parse_delimiter(chunk, ",%]")
468 assert(object, "Delimiter expected")
475 function Decoder.parse_object(self, chunk)
480 local chunk, object = self:parse_delimiter(chunk, "}")
487 chunk = self:parse_space(chunk)
488 assert(chunk, "Unexpected EOS")
490 chunk, name = self:parse_string(chunk)
492 chunk, object = self:parse_delimiter(chunk, ":")
493 assert(object, "Separator expected")
495 chunk, object = self:dispatch(chunk, nil, true)
498 chunk, object = self:parse_delimiter(chunk, ",}")
499 assert(object, "Delimiter expected")
506 function Decoder.parse_delimiter(self, chunk, delimiter)
508 chunk = self:fetch_atleast(chunk, 1)
509 local char = chunk:sub(1, 1)
510 if char:match("%s") then
511 chunk = self:parse_space(chunk)
512 assert(chunk, "Unexpected EOS")
513 elseif char:match("[%s]" % delimiter) then
514 return chunk:sub(2), char
523 ['"'] = Decoder.parse_string,
524 ['t'] = Decoder.parse_true,
525 ['f'] = Decoder.parse_false,
526 ['n'] = Decoder.parse_null,
527 ['['] = Decoder.parse_array,
528 ['{'] = Decoder.parse_object
532 --- Create a new Active JSON-Decoder.
534 -- @name ActiveDecoder
535 -- @param customnull Use luci.json.null instead of nil for decoding null
536 -- @return Active JSON-Decoder
537 ActiveDecoder = util.class(Decoder)
539 function ActiveDecoder.__init__(self, source, customnull)
540 Decoder.__init__(self, customnull)
543 getmetatable(self).__call = self.get
547 --- Fetches one JSON-object from given source
548 -- @return Decoded object
549 function ActiveDecoder.get(self)
550 local chunk, src_err, object
551 if not self.chunk then
552 chunk, src_err = self.source()
557 self.chunk, object = self:dispatch(chunk, src_err, true)
562 function ActiveDecoder.fetch(self)
563 local chunk, src_err = self.source()
564 assert(chunk or not src_err, src_err)