]>
Commit | Line | Data |
---|---|---|
1 | ----------------------------------------------------------------------------- | |
2 | -- JSON4Lua: JSON encoding / decoding support for the Lua language. | |
3 | -- json Module. | |
4 | -- Author: Craig Mason-Jones | |
5 | -- Homepage: http://json.luaforge.net/ | |
6 | -- Version: 0.9.20 | |
7 | -- This module is released under the The GNU General Public License (GPL). | |
8 | -- Please see LICENCE.txt for details. | |
9 | -- | |
10 | -- USAGE: | |
11 | -- This module exposes two functions: | |
12 | -- encode(o) | |
13 | -- Returns the table / string / boolean / number / nil / json.null value as a JSON-encoded string. | |
14 | -- decode(json_string) | |
15 | -- Returns a Lua object populated with the data encoded in the JSON string json_string. | |
16 | -- | |
17 | -- REQUIREMENTS: | |
18 | -- compat-5.1 if using Lua 5.0 | |
19 | -- | |
20 | -- CHANGELOG | |
21 | -- 0.9.20 Introduction of local Lua functions for private functions (removed _ function prefix). | |
22 | -- Fixed Lua 5.1 compatibility issues. | |
23 | -- Introduced json.null to have null values in associative arrays. | |
24 | -- encode() performance improvement (more than 50%) through table.concat rather than .. | |
25 | -- Introduced decode ability to ignore /**/ comments in the JSON string. | |
26 | -- 0.9.10 Fix to array encoding / decoding to correctly manage nil/null values in arrays. | |
27 | ----------------------------------------------------------------------------- | |
28 | ||
29 | ----------------------------------------------------------------------------- | |
30 | -- Imports and dependencies | |
31 | ----------------------------------------------------------------------------- | |
32 | local math = require('math') | |
33 | local string = require("string") | |
34 | local table = require("table") | |
35 | ||
36 | local base = _G | |
37 | ||
38 | ----------------------------------------------------------------------------- | |
39 | -- Module declaration | |
40 | ----------------------------------------------------------------------------- | |
41 | module("json") | |
42 | ||
43 | -- Public functions | |
44 | ||
45 | -- Private functions | |
46 | local decode_scanArray | |
47 | local decode_scanComment | |
48 | local decode_scanConstant | |
49 | local decode_scanNumber | |
50 | local decode_scanObject | |
51 | local decode_scanString | |
52 | local decode_scanWhitespace | |
53 | local encodeString | |
54 | local isArray | |
55 | local isEncodable | |
56 | ||
57 | ----------------------------------------------------------------------------- | |
58 | -- PUBLIC FUNCTIONS | |
59 | ----------------------------------------------------------------------------- | |
60 | --- Encodes an arbitrary Lua object / variable. | |
61 | -- @param v The Lua object / variable to be JSON encoded. | |
62 | -- @return String containing the JSON encoding in internal Lua string format (i.e. not unicode) | |
63 | function encode (v) | |
64 | -- Handle nil values | |
65 | if v==nil then | |
66 | return "null" | |
67 | end | |
68 | ||
69 | local vtype = base.type(v) | |
70 | ||
71 | -- Handle strings | |
72 | if vtype=='string' then | |
73 | return '"' .. encodeString(v) .. '"' -- Need to handle encoding in string | |
74 | end | |
75 | ||
76 | -- Handle booleans | |
77 | if vtype=='number' or vtype=='boolean' then | |
78 | return base.tostring(v) | |
79 | end | |
80 | ||
81 | -- Handle tables | |
82 | if vtype=='table' then | |
83 | local rval = {} | |
84 | -- Consider arrays separately | |
85 | local bArray, maxCount = isArray(v) | |
86 | if bArray then | |
87 | for i = 1,maxCount do | |
88 | table.insert(rval, encode(v[i])) | |
89 | end | |
90 | else -- An object, not an array | |
91 | for i,j in base.pairs(v) do | |
92 | if isEncodable(i) and isEncodable(j) then | |
93 | table.insert(rval, '"' .. encodeString(i) .. '":' .. encode(j)) | |
94 | end | |
95 | end | |
96 | end | |
97 | if bArray then | |
98 | return '[' .. table.concat(rval,',') ..']' | |
99 | else | |
100 | return '{' .. table.concat(rval,',') .. '}' | |
101 | end | |
102 | end | |
103 | ||
104 | -- Handle null values | |
105 | if vtype=='function' and v==null then | |
106 | return 'null' | |
107 | end | |
108 | ||
109 | base.assert(false,'encode attempt to encode unsupported type ' .. vtype .. ':' .. base.tostring(v)) | |
110 | end | |
111 | ||
112 | ||
113 | --- Decodes a JSON string and returns the decoded value as a Lua data structure / value. | |
114 | -- @param s The string to scan. | |
115 | -- @param [startPos] Optional starting position where the JSON string is located. Defaults to 1. | |
116 | -- @param Lua object, number The object that was scanned, as a Lua table / string / number / boolean or nil, | |
117 | -- and the position of the first character after | |
118 | -- the scanned JSON object. | |
119 | function decode(s, startPos) | |
120 | startPos = startPos and startPos or 1 | |
121 | startPos = decode_scanWhitespace(s,startPos) | |
122 | base.assert(startPos<=string.len(s), 'Unterminated JSON encoded object found at position in [' .. s .. ']') | |
123 | local curChar = string.sub(s,startPos,startPos) | |
124 | -- Object | |
125 | if curChar=='{' then | |
126 | return decode_scanObject(s,startPos) | |
127 | end | |
128 | -- Array | |
129 | if curChar=='[' then | |
130 | return decode_scanArray(s,startPos) | |
131 | end | |
132 | -- Number | |
133 | if string.find("+-0123456789.e", curChar, 1, true) then | |
134 | return decode_scanNumber(s,startPos) | |
135 | end | |
136 | -- String | |
137 | if curChar==[["]] or curChar==[[']] then | |
138 | return decode_scanString(s,startPos) | |
139 | end | |
140 | if string.sub(s,startPos,startPos+1)=='/*' then | |
141 | return decode(s, decode_scanComment(s,startPos)) | |
142 | end | |
143 | -- Otherwise, it must be a constant | |
144 | return decode_scanConstant(s,startPos) | |
145 | end | |
146 | ||
147 | --- The null function allows one to specify a null value in an associative array (which is otherwise | |
148 | -- discarded if you set the value with 'nil' in Lua. Simply set t = { first=json.null } | |
149 | function null() | |
150 | return null -- so json.null() will also return null ;-) | |
151 | end | |
152 | ----------------------------------------------------------------------------- | |
153 | -- Internal, PRIVATE functions. | |
154 | -- Following a Python-like convention, I have prefixed all these 'PRIVATE' | |
155 | -- functions with an underscore. | |
156 | ----------------------------------------------------------------------------- | |
157 | ||
158 | --- Scans an array from JSON into a Lua object | |
159 | -- startPos begins at the start of the array. | |
160 | -- Returns the array and the next starting position | |
161 | -- @param s The string being scanned. | |
162 | -- @param startPos The starting position for the scan. | |
163 | -- @return table, int The scanned array as a table, and the position of the next character to scan. | |
164 | function decode_scanArray(s,startPos) | |
165 | local array = {} -- The return value | |
166 | local stringLen = string.len(s) | |
167 | base.assert(string.sub(s,startPos,startPos)=='[','decode_scanArray called but array does not start at position ' .. startPos .. ' in string:\n'..s ) | |
168 | startPos = startPos + 1 | |
169 | -- Infinite loop for array elements | |
170 | repeat | |
171 | startPos = decode_scanWhitespace(s,startPos) | |
172 | base.assert(startPos<=stringLen,'JSON String ended unexpectedly scanning array.') | |
173 | local curChar = string.sub(s,startPos,startPos) | |
174 | if (curChar==']') then | |
175 | return array, startPos+1 | |
176 | end | |
177 | if (curChar==',') then | |
178 | startPos = decode_scanWhitespace(s,startPos+1) | |
179 | end | |
180 | base.assert(startPos<=stringLen, 'JSON String ended unexpectedly scanning array.') | |
181 | object, startPos = decode(s,startPos) | |
182 | table.insert(array,object) | |
183 | until false | |
184 | end | |
185 | ||
186 | --- Scans a comment and discards the comment. | |
187 | -- Returns the position of the next character following the comment. | |
188 | -- @param string s The JSON string to scan. | |
189 | -- @param int startPos The starting position of the comment | |
190 | function decode_scanComment(s, startPos) | |
191 | base.assert( string.sub(s,startPos,startPos+1)=='/*', "decode_scanComment called but comment does not start at position " .. startPos) | |
192 | local endPos = string.find(s,'*/',startPos+2) | |
193 | base.assert(endPos~=nil, "Unterminated comment in string at " .. startPos) | |
194 | return endPos+2 | |
195 | end | |
196 | ||
197 | --- Scans for given constants: true, false or null | |
198 | -- Returns the appropriate Lua type, and the position of the next character to read. | |
199 | -- @param s The string being scanned. | |
200 | -- @param startPos The position in the string at which to start scanning. | |
201 | -- @return object, int The object (true, false or nil) and the position at which the next character should be | |
202 | -- scanned. | |
203 | function decode_scanConstant(s, startPos) | |
204 | local consts = { ["true"] = true, ["false"] = false, ["null"] = nil } | |
205 | local constNames = {"true","false","null"} | |
206 | ||
207 | for i,k in base.pairs(constNames) do | |
208 | --print ("[" .. string.sub(s,startPos, startPos + string.len(k) -1) .."]", k) | |
209 | if string.sub(s,startPos, startPos + string.len(k) -1 )==k then | |
210 | return consts[k], startPos + string.len(k) | |
211 | end | |
212 | end | |
213 | base.assert(nil, 'Failed to scan constant from string ' .. s .. ' at starting position ' .. startPos) | |
214 | end | |
215 | ||
216 | --- Scans a number from the JSON encoded string. | |
217 | -- (in fact, also is able to scan numeric +- eqns, which is not | |
218 | -- in the JSON spec.) | |
219 | -- Returns the number, and the position of the next character | |
220 | -- after the number. | |
221 | -- @param s The string being scanned. | |
222 | -- @param startPos The position at which to start scanning. | |
223 | -- @return number, int The extracted number and the position of the next character to scan. | |
224 | function decode_scanNumber(s,startPos) | |
225 | local endPos = startPos+1 | |
226 | local stringLen = string.len(s) | |
227 | local acceptableChars = "+-0123456789.e" | |
228 | while (string.find(acceptableChars, string.sub(s,endPos,endPos), 1, true) | |
229 | and endPos<=stringLen | |
230 | ) do | |
231 | endPos = endPos + 1 | |
232 | end | |
233 | local stringValue = 'return ' .. string.sub(s,startPos, endPos-1) | |
234 | local stringEval = base.loadstring(stringValue) | |
235 | base.assert(stringEval, 'Failed to scan number [ ' .. stringValue .. '] in JSON string at position ' .. startPos .. ' : ' .. endPos) | |
236 | return stringEval(), endPos | |
237 | end | |
238 | ||
239 | --- Scans a JSON object into a Lua object. | |
240 | -- startPos begins at the start of the object. | |
241 | -- Returns the object and the next starting position. | |
242 | -- @param s The string being scanned. | |
243 | -- @param startPos The starting position of the scan. | |
244 | -- @return table, int The scanned object as a table and the position of the next character to scan. | |
245 | function decode_scanObject(s,startPos) | |
246 | local object = {} | |
247 | local stringLen = string.len(s) | |
248 | local key, value | |
249 | base.assert(string.sub(s,startPos,startPos)=='{','decode_scanObject called but object does not start at position ' .. startPos .. ' in string:\n' .. s) | |
250 | startPos = startPos + 1 | |
251 | repeat | |
252 | startPos = decode_scanWhitespace(s,startPos) | |
253 | base.assert(startPos<=stringLen, 'JSON string ended unexpectedly while scanning object.') | |
254 | local curChar = string.sub(s,startPos,startPos) | |
255 | if (curChar=='}') then | |
256 | return object,startPos+1 | |
257 | end | |
258 | if (curChar==',') then | |
259 | startPos = decode_scanWhitespace(s,startPos+1) | |
260 | end | |
261 | base.assert(startPos<=stringLen, 'JSON string ended unexpectedly scanning object.') | |
262 | -- Scan the key | |
263 | key, startPos = decode(s,startPos) | |
264 | base.assert(startPos<=stringLen, 'JSON string ended unexpectedly searching for value of key ' .. key) | |
265 | startPos = decode_scanWhitespace(s,startPos) | |
266 | base.assert(startPos<=stringLen, 'JSON string ended unexpectedly searching for value of key ' .. key) | |
267 | base.assert(string.sub(s,startPos,startPos)==':','JSON object key-value assignment mal-formed at ' .. startPos) | |
268 | startPos = decode_scanWhitespace(s,startPos+1) | |
269 | base.assert(startPos<=stringLen, 'JSON string ended unexpectedly searching for value of key ' .. key) | |
270 | value, startPos = decode(s,startPos) | |
271 | object[key]=value | |
272 | until false -- infinite loop while key-value pairs are found | |
273 | end | |
274 | ||
275 | --- Scans a JSON string from the opening inverted comma or single quote to the | |
276 | -- end of the string. | |
277 | -- Returns the string extracted as a Lua string, | |
278 | -- and the position of the next non-string character | |
279 | -- (after the closing inverted comma or single quote). | |
280 | -- @param s The string being scanned. | |
281 | -- @param startPos The starting position of the scan. | |
282 | -- @return string, int The extracted string as a Lua string, and the next character to parse. | |
283 | function decode_scanString(s,startPos) | |
284 | base.assert(startPos, 'decode_scanString(..) called without start position') | |
285 | local startChar = string.sub(s,startPos,startPos) | |
286 | base.assert(startChar==[[']] or startChar==[["]],'decode_scanString called for a non-string') | |
287 | local escaped = false | |
288 | local endPos = startPos + 1 | |
289 | local bEnded = false | |
290 | local stringLen = string.len(s) | |
291 | repeat | |
292 | local curChar = string.sub(s,endPos,endPos) | |
293 | if not escaped then | |
294 | if curChar==[[\]] then | |
295 | escaped = true | |
296 | else | |
297 | bEnded = curChar==startChar | |
298 | end | |
299 | else | |
300 | -- If we're escaped, we accept the current character come what may | |
301 | escaped = false | |
302 | end | |
303 | endPos = endPos + 1 | |
304 | base.assert(endPos <= stringLen+1, "String decoding failed: unterminated string at position " .. endPos) | |
305 | until bEnded | |
306 | local stringValue = 'return ' .. string.sub(s, startPos, endPos-1) | |
307 | local stringEval = base.loadstring(stringValue) | |
308 | base.assert(stringEval, 'Failed to load string [ ' .. stringValue .. '] in JSON4Lua.decode_scanString at position ' .. startPos .. ' : ' .. endPos) | |
309 | return stringEval(), endPos | |
310 | end | |
311 | ||
312 | --- Scans a JSON string skipping all whitespace from the current start position. | |
313 | -- Returns the position of the first non-whitespace character, or nil if the whole end of string is reached. | |
314 | -- @param s The string being scanned | |
315 | -- @param startPos The starting position where we should begin removing whitespace. | |
316 | -- @return int The first position where non-whitespace was encountered, or string.len(s)+1 if the end of string | |
317 | -- was reached. | |
318 | function decode_scanWhitespace(s,startPos) | |
319 | local whitespace=" \n\r\t" | |
320 | local stringLen = string.len(s) | |
321 | while ( string.find(whitespace, string.sub(s,startPos,startPos), 1, true) and startPos <= stringLen) do | |
322 | startPos = startPos + 1 | |
323 | end | |
324 | return startPos | |
325 | end | |
326 | ||
327 | --- Encodes a string to be JSON-compatible. | |
328 | -- This just involves back-quoting inverted commas, back-quotes and newlines, I think ;-) | |
329 | -- @param s The string to return as a JSON encoded (i.e. backquoted string) | |
330 | -- @return The string appropriately escaped. | |
331 | function encodeString(s) | |
332 | s = string.gsub(s,'\\','\\\\') | |
333 | s = string.gsub(s,'"','\\"') | |
334 | -- slug | |
335 | -- s = string.gsub(s,"'","\\'") | |
336 | s = string.gsub(s,'\n','\\n') | |
337 | s = string.gsub(s,'\t','\\t') | |
338 | return s | |
339 | end | |
340 | ||
341 | -- Determines whether the given Lua type is an array or a table / dictionary. | |
342 | -- We consider any table an array if it has indexes 1..n for its n items, and no | |
343 | -- other data in the table. | |
344 | -- I think this method is currently a little 'flaky', but can't think of a good way around it yet... | |
345 | -- @param t The table to evaluate as an array | |
346 | -- @return boolean, number True if the table can be represented as an array, false otherwise. If true, | |
347 | -- the second returned value is the maximum | |
348 | -- number of indexed elements in the array. | |
349 | function isArray(t) | |
350 | -- Next we count all the elements, ensuring that any non-indexed elements are not-encodable | |
351 | -- (with the possible exception of 'n') | |
352 | local maxIndex = 0 | |
353 | for k,v in base.pairs(t) do | |
354 | if (base.type(k)=='number' and math.floor(k)==k and 1<=k) then -- k,v is an indexed pair | |
355 | if (not isEncodable(v)) then return false end -- All array elements must be encodable | |
356 | maxIndex = math.max(maxIndex,k) | |
357 | else | |
358 | if (k=='n') then | |
359 | if v ~= table.getn(t) then return false end -- False if n does not hold the number of elements | |
360 | else -- Else of (k=='n') | |
361 | if isEncodable(v) then return false end | |
362 | end -- End of (k~='n') | |
363 | end -- End of k,v not an indexed pair | |
364 | end -- End of loop across all pairs | |
365 | return true, maxIndex | |
366 | end | |
367 | ||
368 | --- Determines whether the given Lua object / table / variable can be JSON encoded. The only | |
369 | -- types that are JSON encodable are: string, boolean, number, nil, table and json.null. | |
370 | -- In this implementation, all other types are ignored. | |
371 | -- @param o The object to examine. | |
372 | -- @return boolean True if the object should be JSON encoded, false if it should be ignored. | |
373 | function isEncodable(o) | |
374 | local t = base.type(o) | |
375 | return (t=='string' or t=='boolean' or t=='number' or t=='nil' or t=='table') or (t=='function' and o==null) | |
376 | end | |
377 |