JSON: Add encode / decode shortcut
[project/luci.git] / libs / json / luasrc / json.lua
1 --[[
2 LuCI - Lua Configuration Interface
3
4 Copyright 2008 Steven Barth <steven@midlink.org>
5 Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
6
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
10
11 http://www.apache.org/licenses/LICENSE-2.0
12
13 $Id$
14
15 Decoder:
16 Info:
17 null will be decoded to luci.json.null if first parameter of Decoder() is true
18
19 Example:
20 decoder = luci.json.Decoder()
21 luci.ltn12.pump.all(luci.ltn12.source.string("decodableJSON"), decoder:sink())
22 luci.util.dumptable(decoder:get())
23
24 Known issues:
25 does not support unicode conversion \uXXYY with XX != 00 will be ignored
26
27
28 Encoder:
29 Info:
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
36
37 Example:
38 encoder = luci.json.Encoder(encodableData)
39 luci.ltn12.pump.all(encoder:source(), luci.ltn12.sink.file(io.open("someFile", w)))
40 ]]--
41
42 local util = require "luci.util"
43 local table = require "table"
44 local string = require "string"
45 local coroutine = require "coroutine"
46
47 local assert = assert
48 local tonumber = tonumber
49 local tostring = tostring
50 local error = error
51 local type = type
52 local pairs = pairs
53 local ipairs = ipairs
54 local next = next
55 local pcall = pcall
56
57 local getmetatable = getmetatable
58
59 --- LuCI JSON-Library
60 -- @cstyle instance
61 module "luci.json"
62
63
64 --- Directly decode a JSON string
65 -- @param json JSON-String
66 -- @return Lua object
67 function decode(json, ...)
68 local a = ActiveDecoder(function() return nil end, ...)
69 a.chunk = json
70 local s, obj = pcall(a.get, a)
71 return s and obj or nil
72 end
73
74
75 --- Direcly encode a Lua object into a JSON string.
76 -- @param obj Lua Object
77 -- @return JSON string
78 function encode(obj, ...)
79 local out = {}
80 local e = Encoder(obj, 1, ...):source()
81 local chnk, err
82 repeat
83 chnk, err = e()
84 out[#out+1] = chnk
85 until chnk
86 return not err and table.concat(out) or nil
87 end
88
89
90 --- Null replacement function
91 -- @return null
92 function null()
93 return null
94 end
95
96 --- Create a new JSON-Encoder.
97 -- @class function
98 -- @name 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()
104
105 function Encoder.__init__(self, data, buffersize, fastescape)
106 self.data = data
107 self.buffersize = buffersize or 512
108 self.buffer = ""
109 self.fastescape = fastescape
110
111 getmetatable(self).__call = Encoder.source
112 end
113
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)
118 return function()
119 local res, data = coroutine.resume(source, self, self.data, true)
120 if res then
121 return data
122 else
123 return nil, data
124 end
125 end
126 end
127
128 function Encoder.dispatch(self, data, start)
129 local parser = self.parsers[type(data)]
130
131 parser(self, data)
132
133 if start then
134 if #self.buffer > 0 then
135 coroutine.yield(self.buffer)
136 end
137
138 coroutine.yield()
139 end
140 end
141
142 function Encoder.put(self, chunk)
143 if self.buffersize < 2 then
144 corountine.yield(chunk)
145 else
146 if #self.buffer + #chunk > self.buffersize then
147 local written = 0
148 local fbuffer = self.buffersize - #self.buffer
149
150 coroutine.yield(self.buffer .. chunk:sub(written + 1, fbuffer))
151 written = fbuffer
152
153 while #chunk - written > self.buffersize do
154 fbuffer = written + self.buffersize
155 coroutine.yield(chunk:sub(written + 1, fbuffer))
156 written = fbuffer
157 end
158
159 self.buffer = chunk:sub(written + 1)
160 else
161 self.buffer = self.buffer .. chunk
162 end
163 end
164 end
165
166 function Encoder.parse_nil(self)
167 self:put("null")
168 end
169
170 function Encoder.parse_bool(self, obj)
171 self:put(obj and "true" or "false")
172 end
173
174 function Encoder.parse_number(self, obj)
175 self:put(tostring(obj))
176 end
177
178 function Encoder.parse_string(self, obj)
179 if self.fastescape then
180 self:put('"' .. obj:gsub('\\', '\\\\'):gsub('"', '\\"') .. '"')
181 else
182 self:put('"' ..
183 obj:gsub('[%c\\"]',
184 function(char)
185 return '\\u00%02x' % char:byte()
186 end
187 )
188 .. '"')
189 end
190 end
191
192 function Encoder.parse_iter(self, obj)
193 if obj == null then
194 return self:put("null")
195 end
196
197 if type(obj) == "table" and (#obj == 0 and next(obj)) then
198 self:put("{")
199 local first = true
200
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))
205 self:put(":")
206 self:dispatch(entry)
207 end
208
209 self:put("}")
210 else
211 self:put("[")
212 local first = true
213
214 if type(obj) == "table" then
215 for i=1, #obj do
216 first = first or self:put(",")
217 first = first and nil
218 self:dispatch(obj[i])
219 end
220 else
221 for entry in obj do
222 first = first or self:put(",")
223 first = first and nil
224 self:dispatch(entry)
225 end
226 end
227
228 self:put("]")
229 end
230 end
231
232 Encoder.parsers = {
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
239 }
240
241
242 --- Create a new JSON-Decoder.
243 -- @class function
244 -- @name Decoder
245 -- @param customnull Use luci.json.null instead of nil for decoding null
246 -- @return JSON-Decoder
247 Decoder = util.class()
248
249 function Decoder.__init__(self, customnull)
250 self.cnull = customnull
251 getmetatable(self).__call = Decoder.sink
252 end
253
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)
258 return function(...)
259 return coroutine.resume(sink, self, ...)
260 end
261 end
262
263
264 --- Get the decoded data packets after the rawdata has been sent to the sink.
265 -- @return Decoded data
266 function Decoder.get(self)
267 return self.data
268 end
269
270 function Decoder.dispatch(self, chunk, src_err, strict)
271 local robject, object
272 local oset = false
273
274 while chunk do
275 while chunk and #chunk < 1 do
276 chunk = self:fetch()
277 end
278
279 assert(not strict or chunk, "Unexpected EOS")
280 if not chunk then break end
281
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)
287
288 chunk, robject = parser(self, chunk)
289
290 if parser ~= self.parse_space then
291 assert(not oset, "Scope violation: Too many objects")
292 object = robject
293 oset = true
294
295 if strict then
296 return chunk, object
297 end
298 end
299 end
300
301 assert(not src_err, src_err)
302 assert(oset, "Unexpected EOS")
303
304 self.data = object
305 end
306
307
308 function Decoder.fetch(self)
309 local tself, chunk, src_err = coroutine.yield()
310 assert(chunk or not src_err, src_err)
311 return chunk
312 end
313
314
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
320 end
321
322 return chunk
323 end
324
325
326 function Decoder.fetch_until(self, chunk, pattern)
327 local start = chunk:find(pattern)
328
329 while not start do
330 local nchunk = self:fetch()
331 assert(nchunk, "Unexpected EOS")
332 chunk = chunk .. nchunk
333 start = chunk:find(pattern)
334 end
335
336 return chunk, start
337 end
338
339
340 function Decoder.parse_space(self, chunk)
341 local start = chunk:find("[^%s]")
342
343 while not start do
344 chunk = self:fetch()
345 if not chunk then
346 return nil
347 end
348 start = chunk:find("[^%s]")
349 end
350
351 return chunk:sub(start)
352 end
353
354
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
359 end
360
361
362 function Decoder.parse_null(self, chunk)
363 return self:parse_literal(chunk, "null", self.cnull and null)
364 end
365
366
367 function Decoder.parse_true(self, chunk)
368 return self:parse_literal(chunk, "true", true)
369 end
370
371
372 function Decoder.parse_false(self, chunk)
373 return self:parse_literal(chunk, "false", false)
374 end
375
376
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
382 end
383
384
385 function Decoder.parse_string(self, chunk)
386 local str = ""
387 local object = nil
388 assert(chunk:sub(1, 1) == '"', 'Expected "')
389 chunk = chunk:sub(2)
390
391 while true do
392 local spos = chunk:find('[\\"]')
393 if spos then
394 str = str .. chunk:sub(1, spos - 1)
395
396 local char = chunk:sub(spos, spos)
397 if char == '"' then -- String end
398 chunk = chunk:sub(spos + 1)
399 break
400 elseif char == "\\" then -- Escape sequence
401 chunk, object = self:parse_escape(chunk:sub(spos))
402 str = str .. object
403 end
404 else
405 str = str .. chunk
406 chunk = self:fetch()
407 assert(chunk, "Unexpected EOS while parsing a string")
408 end
409 end
410
411 return chunk, str
412 end
413
414
415 function Decoder.parse_escape(self, chunk)
416 local str = ""
417 chunk = self:fetch_atleast(chunk:sub(2), 1)
418 local char = chunk:sub(1, 1)
419 chunk = chunk:sub(2)
420
421 if char == '"' then
422 return chunk, '"'
423 elseif char == "\\" then
424 return chunk, "\\"
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")
430
431 -- ToDo: Unicode support
432 return chunk:sub(5), s1 == 0 and string.char(s2) or ""
433 elseif char == "/" then
434 return chunk, "/"
435 elseif char == "b" then
436 return chunk, "\b"
437 elseif char == "f" then
438 return chunk, "\f"
439 elseif char == "n" then
440 return chunk, "\n"
441 elseif char == "r" then
442 return chunk, "\r"
443 elseif char == "t" then
444 return chunk, "\t"
445 else
446 error("Unexpected escaping sequence '\\%s'" % char)
447 end
448 end
449
450
451 function Decoder.parse_array(self, chunk)
452 chunk = chunk:sub(2)
453 local array = {}
454 local nextp = 1
455
456 local chunk, object = self:parse_delimiter(chunk, "%]")
457
458 if object then
459 return chunk, array
460 end
461
462 repeat
463 chunk, object = self:dispatch(chunk, nil, true)
464 table.insert(array, nextp, object)
465 nextp = nextp + 1
466
467 chunk, object = self:parse_delimiter(chunk, ",%]")
468 assert(object, "Delimiter expected")
469 until object == "]"
470
471 return chunk, array
472 end
473
474
475 function Decoder.parse_object(self, chunk)
476 chunk = chunk:sub(2)
477 local array = {}
478 local name
479
480 local chunk, object = self:parse_delimiter(chunk, "}")
481
482 if object then
483 return chunk, array
484 end
485
486 repeat
487 chunk = self:parse_space(chunk)
488 assert(chunk, "Unexpected EOS")
489
490 chunk, name = self:parse_string(chunk)
491
492 chunk, object = self:parse_delimiter(chunk, ":")
493 assert(object, "Separator expected")
494
495 chunk, object = self:dispatch(chunk, nil, true)
496 array[name] = object
497
498 chunk, object = self:parse_delimiter(chunk, ",}")
499 assert(object, "Delimiter expected")
500 until object == "}"
501
502 return chunk, array
503 end
504
505
506 function Decoder.parse_delimiter(self, chunk, delimiter)
507 while true do
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
515 else
516 return chunk, nil
517 end
518 end
519 end
520
521
522 Decoder.parsers = {
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
529 }
530
531
532 --- Create a new Active JSON-Decoder.
533 -- @class function
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)
538
539 function ActiveDecoder.__init__(self, source, customnull)
540 Decoder.__init__(self, customnull)
541 self.source = source
542 self.chunk = nil
543 getmetatable(self).__call = self.get
544 end
545
546
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()
553 else
554 chunk = self.chunk
555 end
556
557 self.chunk, object = self:dispatch(chunk, src_err, true)
558 return object
559 end
560
561
562 function ActiveDecoder.fetch(self)
563 local chunk, src_err = self.source()
564 assert(chunk or not src_err, src_err)
565 return chunk
566 end