diff --git a/dist.info b/dist.info index c0edd7b..9945679 100644 --- a/dist.info +++ b/dist.info @@ -1,7 +1,7 @@ --- This file is part of LuaDist project name = "dkjson" -version = "2.4" +version = "2.5" desc = "dkjson is a module for encoding and decoding JSON data. It supports UTF-8." author = "David Kolf" diff --git a/dkjson-2.5-2.rockspec b/dkjson-2.5-2.rockspec new file mode 100644 index 0000000..9a56ca1 --- /dev/null +++ b/dkjson-2.5-2.rockspec @@ -0,0 +1,30 @@ +package = "dkjson" +version = "2.5-2" +source = { + url = "http://dkolf.de/src/dkjson-lua.fsl/tarball/dkjson-2.5.tar.gz?uuid=release_2_5", + file = "dkjson-2.5.tar.gz" +} +description = { + summary = "David Kolf's JSON module for Lua", + detailed = [[ +dkjson is a module for encoding and decoding JSON data. It supports UTF-8. + +JSON (JavaScript Object Notation) is a format for serializing data based +on the syntax for JavaScript data structures. + +dkjson is written in Lua without any dependencies, but +when LPeg is available dkjson uses it to speed up decoding. +]], + homepage = "http://dkolf.de/src/dkjson-lua.fsl/", + license = "MIT/X11" +} +dependencies = { + "lua >= 5.1, < 5.4" +} +build = { + type = "builtin", + modules = { + dkjson = "dkjson.lua" + } +} + diff --git a/dkjson.lua b/dkjson.lua index b7475ba..b63d5ac 100644 --- a/dkjson.lua +++ b/dkjson.lua @@ -1,178 +1,23 @@ - -- Module options: - local always_try_using_lpeg = true - local register_global_module_table = false - local global_module_name = 'json' +-- Module options: +local always_try_using_lpeg = true +local register_global_module_table = false +local global_module_name = 'json' - --[==[ +--[==[ David Kolf's JSON module for Lua 5.1/5.2 -======================================== -*Version 2.4* +Version 2.5 -In the default configuration this module writes no global values, not even -the module table. Import it using - json = require ("dkjson") - -In environments where `require` or a similiar function are not available -and you cannot receive the return value of the module, you can set the -option `register_global_module_table` to `true`. The module table will -then be saved in the global variable with the name given by the option -`global_module_name`. - -Exported functions and values: - -`json.encode (object [, state])` --------------------------------- - -Create a string representing the object. `Object` can be a table, -a string, a number, a boolean, `nil`, `json.null` or any object with -a function `__tojson` in its metatable. A table can only use strings -and numbers as keys and its values have to be valid objects as -well. It raises an error for any invalid data types or reference -cycles. - -`state` is an optional table with the following fields: - - - `indent` - When `indent` (a boolean) is set, the created string will contain - newlines and indentations. Otherwise it will be one long line. - - `keyorder` - `keyorder` is an array to specify the ordering of keys in the - encoded output. If an object has keys which are not in this array - they are written after the sorted keys. - - `level` - This is the initial level of indentation used when `indent` is - set. For each level two spaces are added. When absent it is set - to 0. - - `buffer` - `buffer` is an array to store the strings for the result so they - can be concatenated at once. When it isn't given, the encode - function will create it temporary and will return the - concatenated result. - - `bufferlen` - When `bufferlen` is set, it has to be the index of the last - element of `buffer`. - - `tables` - `tables` is a set to detect reference cycles. It is created - temporary when absent. Every table that is currently processed - is used as key, the value is `true`. - -When `state.buffer` was set, the return value will be `true` on -success. Without `state.buffer` the return value will be a string. - -`json.decode (string [, position [, null]])` --------------------------------------------- - -Decode `string` starting at `position` or at 1 if `position` was -omitted. - -`null` is an optional value to be returned for null values. The -default is `nil`, but you could set it to `json.null` or any other -value. - -The return values are the object or `nil`, the position of the next -character that doesn't belong to the object, and in case of errors -an error message. - -Two metatables are created. Every array or object that is decoded gets -a metatable with the `__jsontype` field set to either `array` or -`object`. If you want to provide your own metatables use the syntax - - json.decode (string, position, null, objectmeta, arraymeta) - -To prevent the assigning of metatables pass `nil`: - - json.decode (string, position, null, nil) - -`.__jsonorder` -------------------------- - -`__jsonorder` can overwrite the `keyorder` for a specific table. - -`.__jsontype` ------------------------- - -`__jsontype` can be either `"array"` or `"object"`. This value is only -checked for empty tables. (The default for empty tables is `"array"`). - -`.__tojson (self, state)` ------------------------------------- - -You can provide your own `__tojson` function in a metatable. In this -function you can either add directly to the buffer and return true, -or you can return a string. On errors nil and a message should be -returned. - -`json.null` ------------ - -You can use this value for setting explicit `null` values. - -`json.version` --------------- - -Set to `"dkjson 2.4"`. - -`json.quotestring (string)` ---------------------------- - -Quote a UTF-8 string and escape critical characters using JSON -escape sequences. This function is only necessary when you build -your own `__tojson` functions. - -`json.addnewline (state)` -------------------------- - -When `state.indent` is set, add a newline to `state.buffer` and spaces -according to `state.level`. - -LPeg support ------------- - -When the local configuration variable `always_try_using_lpeg` is set, -this module tries to load LPeg to replace the `decode` function. The -speed increase is significant. You can get the LPeg module at - . -When LPeg couldn't be loaded, the pure Lua functions stay active. - -In case you don't want this module to require LPeg on its own, -disable the option `always_try_using_lpeg` in the options section at -the top of the module. - -In this case you can later load LPeg support using - -### `json.use_lpeg ()` - -Require the LPeg module and replace the functions `quotestring` and -and `decode` with functions that use LPeg patterns. -This function returns the module table, so you can load the module -using: - - json = require "dkjson".use_lpeg() - -Alternatively you can use `pcall` so the JSON module still works when -LPeg isn't found. - - json = require "dkjson" - pcall (json.use_lpeg) - -### `json.using_lpeg` - -This variable is set to `true` when LPeg was loaded successfully. - ---------------------------------------------------------------------- - -Contact -------- +For the documentation see the corresponding readme.txt or visit +. You can contact the author by sending an e-mail to 'david' at the domain 'dkolf.de'. ---------------------------------------------------------------------- -*Copyright (C) 2010-2013 David Heiko Kolf* +Copyright (C) 2010-2014 David Heiko Kolf Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -194,13 +39,7 @@ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - diff --git a/jsontest.lua b/jsontest.lua index 62b9f80..2884d84 100644 --- a/jsontest.lua +++ b/jsontest.lua @@ -12,6 +12,7 @@ local test_module, opt = ... -- command line argument --local test_module = 'nm-json' --local test_module = 'sb-json' --local test_module = 'th-json' +test_module = test_module or 'dkjson' --local opt = "esc" -- Test which characters in the BMP get escaped and whether this is correct --local opt = "esc_full" -- Full range from 0 to 0x10ffff @@ -209,6 +210,122 @@ else print("In locale 0.5 isn't converted into valid JSON:", x) end end) + + -- special tests for dkjson: + if test_module == 'dkjson' then + do -- encode a function + local why, value, exstate + local state = { + exception = function (w, v, s) + why, value, exstate = w, v, s + return "\"demo\"" + end + } + local encfunction = function () end + r, x = pcall(dkencode, { encfunction }, state ) + if not r then + print("encoding a function with exception handler raises an error:", x) + else + if x ~= "[\"demo\"]" then + print("expected to see output of exception handler for type exception, but got", x) + end + if why ~= "unsupported type" then + print("expected exception reason to be 'unsupported type' for type exception") + end + if value ~= encfunction then + print("expected to recieve value for type exception") + end + if exstate ~= state then + print("expected to recieve state for type exception") + end + end + + r, x = pcall(dkencode, { function () end }, { + exception = function (w, v, s) + return nil, "demo" + end + }) + if r or x ~= "demo" then + print("expected custom error for type exception, but got:", r, x) + end + + r, x = pcall(dkencode, { function () end }, { + exception = function (w, v, s) + return nil + end + }) + if r or x ~= "type 'function' is not supported by JSON." then + print("expected default error for type exception, but got:", r, x) + end + end + + do -- encode a reference cycle + local why, value, exstate + local state = { + exception = function (w, v, s) + why, value, exstate = w, v, s + return "\"demo\"" + end + } + local a = {} + a[1] = a + r, x = pcall(dkencode, a, state ) + if not r then + print("encoding a reference cycle with exception handler raises an error:", x) + else + if x ~= "[\"demo\"]" then + print("expected to see output of exception handler for reference cycle exception, but got", x) + end + if why ~= "reference cycle" then + print("expected exception reason to be 'reference cycle' for reference cycle exception") + end + if value ~= a then + print("expected to recieve value for reference cycle exception") + end + if exstate ~= state then + print("expected to recieve state for reference cycle exception") + end + end + end + + do -- example exception handler + r = dkencode(function () end, { exception = require "dkjson".encodeexception }) + if r ~= [[""]] then + print("expected the exception encoder to encode default error message, but got", r) + end + end + + do -- test state buffer for custom __tojson function + local origstate = {} + local usedstate, usedbuffer, usedbufferlen + dkencode({ setmetatable({}, { + __tojson = function(self, state) + usedstate = state + usedbuffer = state.buffer + usedbufferlen = state.bufferlen + return true + end + }) }, origstate) + if usedstate ~= origstate then print("expected tojson-function to recieve the original state") end + if type(usedbuffer) ~= 'table' or #usedbuffer < 1 then print("expected buffer in tojson-function to be an array") end + if usedbufferlen ~= 1 then print("expected bufferlen in tojson-function to be 1, but got "..tostring(usedbufferlen)) end + end + + do -- do not keep buffer and bufferlen when they were not present initially + local origstate = {} + dkencode(setmetatable({}, {__tojson = function() return true end}), origstate) + if origstate.buffer ~= nil then print("expected buffer to be reset to nil") end + if origstate.bufferlen ~= nil then print("expected bufferlen to be reset to nil") end + end + + do -- keep buffer and update bufferlen when they were present initially + local origbuffer = {} + local origstate = { buffer = origbuffer } + dkencode(true, origstate) + if origstate.buffer ~= origbuffer then print("expected original buffer to remain") end + if origstate.bufferlen ~= 1 then print("expected bufferlen to be updated") end + end + end end if not decode then @@ -318,6 +435,30 @@ else if p ~= 4 or type(m) ~= 'string' or not m:find("at line 2, column 2$") then print (("Invalid location: position=%d, message=%q"):format(p,m)) end + + do -- single line comments + local x, p, m = dkdecode [[ +{"test://" // comment // --? + : [ // continues + 0] // +} +]] + if type(x) ~= 'table' or type(x["test://"]) ~= 'table' or x["test://"][1] ~= 0 then + print("could not decode a string with single line comments: "..tostring(m)) + end + end + + do -- multi line comments + local x, p, m = dkdecode [[ +{"test:/*"/**//* + hi! this is a comment +*/ : [/** / **/ 0] +} +]] + if type(x) ~= 'table' or type(x["test:/*"]) ~= 'table' or x["test:/*"][1] ~= 0 then + print("could not decode a string with multi line comments: "..tostring(m)) + end + end end end diff --git a/readme.txt b/readme.txt new file mode 100644 index 0000000..86ada92 --- /dev/null +++ b/readme.txt @@ -0,0 +1,211 @@ +David Kolf's JSON module for Lua 5.1/5.2 +======================================== + +*Version 2.5* + +In the default configuration this module writes no global values, not even +the module table. Import it using + + json = require ("dkjson") + +In environments where `require` or a similiar function are not available +and you cannot receive the return value of the module, you can set the +option `register_global_module_table` to `true`. The module table will +then be saved in the global variable with the name given by the option +`global_module_name`. + +Exported functions and values: + +`json.encode (object [, state])` +-------------------------------- + +Create a string representing the object. `Object` can be a table, +a string, a number, a boolean, `nil`, `json.null` or any object with +a function `__tojson` in its metatable. A table can only use strings +and numbers as keys and its values have to be valid objects as +well. It raises an error for any invalid data types or reference +cycles. + +`state` is an optional table with the following fields: + + - `indent` + When `indent` (a boolean) is set, the created string will contain + newlines and indentations. Otherwise it will be one long line. + - `keyorder` + `keyorder` is an array to specify the ordering of keys in the + encoded output. If an object has keys which are not in this array + they are written after the sorted keys. + - `level` + This is the initial level of indentation used when `indent` is + set. For each level two spaces are added. When absent it is set + to 0. + - `buffer` + `buffer` is an array to store the strings for the result so they + can be concatenated at once. When it isn't given, the encode + function will create it temporary and will return the + concatenated result. + - `bufferlen` + When `bufferlen` is set, it has to be the index of the last + element of `buffer`. + - `tables` + `tables` is a set to detect reference cycles. It is created + temporary when absent. Every table that is currently processed + is used as key, the value is `true`. + - `exception` + When `exception` is given, it will be called whenever the encoder + cannot encode a given value. + The parameters are `reason`, `value`, `state` and `defaultmessage`. + `reason` is either `"reference cycle"`, `"custom encoder failed"` or + `"unsupported type"`. `value` is the original value that caused the + exception, `state` is this state table, `defaultmessage` is the message + of the error that would usually be raised. + You can either return `true` and add directly to the buffer or you can + return the string directly. To keep raising an error return `nil` and + the desired error message. + An example implementation for an exception function is given in + `json.encodeexception`. + +When `state.buffer` was set, the return value will be `true` on +success. Without `state.buffer` the return value will be a string. + +`json.decode (string [, position [, null]])` +-------------------------------------------- + +Decode `string` starting at `position` or at 1 if `position` was +omitted. + +`null` is an optional value to be returned for null values. The +default is `nil`, but you could set it to `json.null` or any other +value. + +The return values are the object or `nil`, the position of the next +character that doesn't belong to the object, and in case of errors +an error message. + +Two metatables are created. Every array or object that is decoded gets +a metatable with the `__jsontype` field set to either `array` or +`object`. If you want to provide your own metatables use the syntax + + json.decode (string, position, null, objectmeta, arraymeta) + +To prevent the assigning of metatables pass `nil`: + + json.decode (string, position, null, nil) + +`.__jsonorder` +------------------------- + +`__jsonorder` can overwrite the `keyorder` for a specific table. + +`.__jsontype` +------------------------ + +`__jsontype` can be either `"array"` or `"object"`. This value is only +checked for empty tables. (The default for empty tables is `"array"`). + +`.__tojson (self, state)` +------------------------------------ + +You can provide your own `__tojson` function in a metatable. In this +function you can either add directly to the buffer and return true, +or you can return a string. On errors nil and a message should be +returned. + +`json.null` +----------- + +You can use this value for setting explicit `null` values. + +`json.version` +-------------- + +Set to `"dkjson 2.5"`. + +`json.quotestring (string)` +--------------------------- + +Quote a UTF-8 string and escape critical characters using JSON +escape sequences. This function is only necessary when you build +your own `__tojson` functions. + +`json.addnewline (state)` +------------------------- + +When `state.indent` is set, add a newline to `state.buffer` and spaces +according to `state.level`. + +`json.encodeexception (reason, value, state, defaultmessage)` +------------------------------------------------------------- + +This function can be used as value to the `exception` option. Instead of +raising an error this function encodes the error message as a string. This +can help to debug malformed input data. + + x = json.encode(value, { exception = json.encodeexception }) + +LPeg support +------------ + +When the local configuration variable `always_try_using_lpeg` is set, +this module tries to load LPeg to replace the `decode` function. The +speed increase is significant. You can get the LPeg module at + . +When LPeg couldn't be loaded, the pure Lua functions stay active. + +In case you don't want this module to require LPeg on its own, +disable the option `always_try_using_lpeg` in the options section at +the top of the module. + +In this case you can later load LPeg support using + +### `json.use_lpeg ()` + +Require the LPeg module and replace the functions `quotestring` and +and `decode` with functions that use LPeg patterns. +This function returns the module table, so you can load the module +using: + + json = require "dkjson".use_lpeg() + +Alternatively you can use `pcall` so the JSON module still works when +LPeg isn't found. + + json = require "dkjson" + pcall (json.use_lpeg) + +### `json.using_lpeg` + +This variable is set to `true` when LPeg was loaded successfully. + +--------------------------------------------------------------------- + +Contact +------- + +You can contact the author by sending an e-mail to 'david' at the +domain 'dkolf.de'. + +--------------------------------------------------------------------- + +*Copyright (C) 2010-2014 David Heiko Kolf* + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/versions.txt b/versions.txt index 14ce417..32b9273 100644 --- a/versions.txt +++ b/versions.txt @@ -1,3 +1,11 @@ +Version 2.5 (2014-04-28) +=========== + +Changes since version 2.4: + + * Added customizable exception handling. + * Decode input that contains JavaScript comments. + Version 2.4 (2013-09-28) ===========