annotate Libs/AceSerializer-3.0/AceSerializer-3.0.lua @ 0:c6ff7ba0e8f6

Reasonably functional now. Cleaning up some stuff which might have to be reverted.
author Zerotorescue
date Thu, 07 Oct 2010 17:17:43 +0200
parents
children
rev   line source
Zerotorescue@0 1 --- **AceSerializer-3.0** can serialize any variable (except functions or userdata) into a string format,
Zerotorescue@0 2 -- that can be send over the addon comm channel. AceSerializer was designed to keep all data intact, especially
Zerotorescue@0 3 -- very large numbers or floating point numbers, and table structures. The only caveat currently is, that multiple
Zerotorescue@0 4 -- references to the same table will be send individually.
Zerotorescue@0 5 --
Zerotorescue@0 6 -- **AceSerializer-3.0** can be embeded into your addon, either explicitly by calling AceSerializer:Embed(MyAddon) or by
Zerotorescue@0 7 -- specifying it as an embeded library in your AceAddon. All functions will be available on your addon object
Zerotorescue@0 8 -- and can be accessed directly, without having to explicitly call AceSerializer itself.\\
Zerotorescue@0 9 -- It is recommended to embed AceSerializer, otherwise you'll have to specify a custom `self` on all calls you
Zerotorescue@0 10 -- make into AceSerializer.
Zerotorescue@0 11 -- @class file
Zerotorescue@0 12 -- @name AceSerializer-3.0
Zerotorescue@0 13 -- @release $Id: AceSerializer-3.0.lua 910 2010-02-11 21:54:24Z mikk $
Zerotorescue@0 14 local MAJOR,MINOR = "AceSerializer-3.0", 3
Zerotorescue@0 15 local AceSerializer, oldminor = LibStub:NewLibrary(MAJOR, MINOR)
Zerotorescue@0 16
Zerotorescue@0 17 if not AceSerializer then return end
Zerotorescue@0 18
Zerotorescue@0 19 -- Lua APIs
Zerotorescue@0 20 local strbyte, strchar, gsub, gmatch, format = string.byte, string.char, string.gsub, string.gmatch, string.format
Zerotorescue@0 21 local assert, error, pcall = assert, error, pcall
Zerotorescue@0 22 local type, tostring, tonumber = type, tostring, tonumber
Zerotorescue@0 23 local pairs, select, frexp = pairs, select, math.frexp
Zerotorescue@0 24 local tconcat = table.concat
Zerotorescue@0 25
Zerotorescue@0 26 -- quick copies of string representations of wonky numbers
Zerotorescue@0 27 local serNaN = tostring(0/0)
Zerotorescue@0 28 local serInf = tostring(1/0)
Zerotorescue@0 29 local serNegInf = tostring(-1/0)
Zerotorescue@0 30
Zerotorescue@0 31
Zerotorescue@0 32 -- Serialization functions
Zerotorescue@0 33
Zerotorescue@0 34 local function SerializeStringHelper(ch) -- Used by SerializeValue for strings
Zerotorescue@0 35 -- We use \126 ("~") as an escape character for all nonprints plus a few more
Zerotorescue@0 36 local n = strbyte(ch)
Zerotorescue@0 37 if n==30 then -- v3 / ticket 115: catch a nonprint that ends up being "~^" when encoded... DOH
Zerotorescue@0 38 return "\126\122"
Zerotorescue@0 39 elseif n<=32 then -- nonprint + space
Zerotorescue@0 40 return "\126"..strchar(n+64)
Zerotorescue@0 41 elseif n==94 then -- value separator
Zerotorescue@0 42 return "\126\125"
Zerotorescue@0 43 elseif n==126 then -- our own escape character
Zerotorescue@0 44 return "\126\124"
Zerotorescue@0 45 elseif n==127 then -- nonprint (DEL)
Zerotorescue@0 46 return "\126\123"
Zerotorescue@0 47 else
Zerotorescue@0 48 assert(false) -- can't be reached if caller uses a sane regex
Zerotorescue@0 49 end
Zerotorescue@0 50 end
Zerotorescue@0 51
Zerotorescue@0 52 local function SerializeValue(v, res, nres)
Zerotorescue@0 53 -- We use "^" as a value separator, followed by one byte for type indicator
Zerotorescue@0 54 local t=type(v)
Zerotorescue@0 55
Zerotorescue@0 56 if t=="string" then -- ^S = string (escaped to remove nonprints, "^"s, etc)
Zerotorescue@0 57 res[nres+1] = "^S"
Zerotorescue@0 58 res[nres+2] = gsub(v,"[%c \94\126\127]", SerializeStringHelper)
Zerotorescue@0 59 nres=nres+2
Zerotorescue@0 60
Zerotorescue@0 61 elseif t=="number" then -- ^N = number (just tostring()ed) or ^F (float components)
Zerotorescue@0 62 local str = tostring(v)
Zerotorescue@0 63 if tonumber(str)==v or str==serNaN or str==serInf or str==serNegInf then
Zerotorescue@0 64 -- translates just fine, transmit as-is
Zerotorescue@0 65 res[nres+1] = "^N"
Zerotorescue@0 66 res[nres+2] = str
Zerotorescue@0 67 nres=nres+2
Zerotorescue@0 68 else
Zerotorescue@0 69 local m,e = frexp(v)
Zerotorescue@0 70 res[nres+1] = "^F"
Zerotorescue@0 71 res[nres+2] = format("%.0f",m*2^53) -- force mantissa to become integer (it's originally 0.5--0.9999)
Zerotorescue@0 72 res[nres+3] = "^f"
Zerotorescue@0 73 res[nres+4] = tostring(e-53) -- adjust exponent to counteract mantissa manipulation
Zerotorescue@0 74 nres=nres+4
Zerotorescue@0 75 end
Zerotorescue@0 76
Zerotorescue@0 77 elseif t=="table" then -- ^T...^t = table (list of key,value pairs)
Zerotorescue@0 78 nres=nres+1
Zerotorescue@0 79 res[nres] = "^T"
Zerotorescue@0 80 for k,v in pairs(v) do
Zerotorescue@0 81 nres = SerializeValue(k, res, nres)
Zerotorescue@0 82 nres = SerializeValue(v, res, nres)
Zerotorescue@0 83 end
Zerotorescue@0 84 nres=nres+1
Zerotorescue@0 85 res[nres] = "^t"
Zerotorescue@0 86
Zerotorescue@0 87 elseif t=="boolean" then -- ^B = true, ^b = false
Zerotorescue@0 88 nres=nres+1
Zerotorescue@0 89 if v then
Zerotorescue@0 90 res[nres] = "^B" -- true
Zerotorescue@0 91 else
Zerotorescue@0 92 res[nres] = "^b" -- false
Zerotorescue@0 93 end
Zerotorescue@0 94
Zerotorescue@0 95 elseif t=="nil" then -- ^Z = nil (zero, "N" was taken :P)
Zerotorescue@0 96 nres=nres+1
Zerotorescue@0 97 res[nres] = "^Z"
Zerotorescue@0 98
Zerotorescue@0 99 else
Zerotorescue@0 100 error(MAJOR..": Cannot serialize a value of type '"..t.."'") -- can't produce error on right level, this is wildly recursive
Zerotorescue@0 101 end
Zerotorescue@0 102
Zerotorescue@0 103 return nres
Zerotorescue@0 104 end
Zerotorescue@0 105
Zerotorescue@0 106
Zerotorescue@0 107
Zerotorescue@0 108 local serializeTbl = { "^1" } -- "^1" = Hi, I'm data serialized by AceSerializer protocol rev 1
Zerotorescue@0 109
Zerotorescue@0 110 --- Serialize the data passed into the function.
Zerotorescue@0 111 -- Takes a list of values (strings, numbers, booleans, nils, tables)
Zerotorescue@0 112 -- and returns it in serialized form (a string).\\
Zerotorescue@0 113 -- May throw errors on invalid data types.
Zerotorescue@0 114 -- @param ... List of values to serialize
Zerotorescue@0 115 -- @return The data in its serialized form (string)
Zerotorescue@0 116 function AceSerializer:Serialize(...)
Zerotorescue@0 117 local nres = 1
Zerotorescue@0 118
Zerotorescue@0 119 for i=1,select("#", ...) do
Zerotorescue@0 120 local v = select(i, ...)
Zerotorescue@0 121 nres = SerializeValue(v, serializeTbl, nres)
Zerotorescue@0 122 end
Zerotorescue@0 123
Zerotorescue@0 124 serializeTbl[nres+1] = "^^" -- "^^" = End of serialized data
Zerotorescue@0 125
Zerotorescue@0 126 return tconcat(serializeTbl, "", 1, nres+1)
Zerotorescue@0 127 end
Zerotorescue@0 128
Zerotorescue@0 129 -- Deserialization functions
Zerotorescue@0 130 local function DeserializeStringHelper(escape)
Zerotorescue@0 131 if escape<"~\122" then
Zerotorescue@0 132 return strchar(strbyte(escape,2,2)-64)
Zerotorescue@0 133 elseif escape=="~\122" then -- v3 / ticket 115: special case encode since 30+64=94 ("^") - OOPS.
Zerotorescue@0 134 return "\030"
Zerotorescue@0 135 elseif escape=="~\123" then
Zerotorescue@0 136 return "\127"
Zerotorescue@0 137 elseif escape=="~\124" then
Zerotorescue@0 138 return "\126"
Zerotorescue@0 139 elseif escape=="~\125" then
Zerotorescue@0 140 return "\94"
Zerotorescue@0 141 end
Zerotorescue@0 142 error("DeserializeStringHelper got called for '"..escape.."'?!?") -- can't be reached unless regex is screwed up
Zerotorescue@0 143 end
Zerotorescue@0 144
Zerotorescue@0 145 local function DeserializeNumberHelper(number)
Zerotorescue@0 146 if number == serNaN then
Zerotorescue@0 147 return 0/0
Zerotorescue@0 148 elseif number == serNegInf then
Zerotorescue@0 149 return -1/0
Zerotorescue@0 150 elseif number == serInf then
Zerotorescue@0 151 return 1/0
Zerotorescue@0 152 else
Zerotorescue@0 153 return tonumber(number)
Zerotorescue@0 154 end
Zerotorescue@0 155 end
Zerotorescue@0 156
Zerotorescue@0 157 -- DeserializeValue: worker function for :Deserialize()
Zerotorescue@0 158 -- It works in two modes:
Zerotorescue@0 159 -- Main (top-level) mode: Deserialize a list of values and return them all
Zerotorescue@0 160 -- Recursive (table) mode: Deserialize only a single value (_may_ of course be another table with lots of subvalues in it)
Zerotorescue@0 161 --
Zerotorescue@0 162 -- The function _always_ works recursively due to having to build a list of values to return
Zerotorescue@0 163 --
Zerotorescue@0 164 -- Callers are expected to pcall(DeserializeValue) to trap errors
Zerotorescue@0 165
Zerotorescue@0 166 local function DeserializeValue(iter,single,ctl,data)
Zerotorescue@0 167
Zerotorescue@0 168 if not single then
Zerotorescue@0 169 ctl,data = iter()
Zerotorescue@0 170 end
Zerotorescue@0 171
Zerotorescue@0 172 if not ctl then
Zerotorescue@0 173 error("Supplied data misses AceSerializer terminator ('^^')")
Zerotorescue@0 174 end
Zerotorescue@0 175
Zerotorescue@0 176 if ctl=="^^" then
Zerotorescue@0 177 -- ignore extraneous data
Zerotorescue@0 178 return
Zerotorescue@0 179 end
Zerotorescue@0 180
Zerotorescue@0 181 local res
Zerotorescue@0 182
Zerotorescue@0 183 if ctl=="^S" then
Zerotorescue@0 184 res = gsub(data, "~.", DeserializeStringHelper)
Zerotorescue@0 185 elseif ctl=="^N" then
Zerotorescue@0 186 res = DeserializeNumberHelper(data)
Zerotorescue@0 187 if not res then
Zerotorescue@0 188 error("Invalid serialized number: '"..tostring(data).."'")
Zerotorescue@0 189 end
Zerotorescue@0 190 elseif ctl=="^F" then -- ^F<mantissa>^f<exponent>
Zerotorescue@0 191 local ctl2,e = iter()
Zerotorescue@0 192 if ctl2~="^f" then
Zerotorescue@0 193 error("Invalid serialized floating-point number, expected '^f', not '"..tostring(ctl2).."'")
Zerotorescue@0 194 end
Zerotorescue@0 195 local m=tonumber(data)
Zerotorescue@0 196 e=tonumber(e)
Zerotorescue@0 197 if not (m and e) then
Zerotorescue@0 198 error("Invalid serialized floating-point number, expected mantissa and exponent, got '"..tostring(m).."' and '"..tostring(e).."'")
Zerotorescue@0 199 end
Zerotorescue@0 200 res = m*(2^e)
Zerotorescue@0 201 elseif ctl=="^B" then -- yeah yeah ignore data portion
Zerotorescue@0 202 res = true
Zerotorescue@0 203 elseif ctl=="^b" then -- yeah yeah ignore data portion
Zerotorescue@0 204 res = false
Zerotorescue@0 205 elseif ctl=="^Z" then -- yeah yeah ignore data portion
Zerotorescue@0 206 res = nil
Zerotorescue@0 207 elseif ctl=="^T" then
Zerotorescue@0 208 -- ignore ^T's data, future extensibility?
Zerotorescue@0 209 res = {}
Zerotorescue@0 210 local k,v
Zerotorescue@0 211 while true do
Zerotorescue@0 212 ctl,data = iter()
Zerotorescue@0 213 if ctl=="^t" then break end -- ignore ^t's data
Zerotorescue@0 214 k = DeserializeValue(iter,true,ctl,data)
Zerotorescue@0 215 if k==nil then
Zerotorescue@0 216 error("Invalid AceSerializer table format (no table end marker)")
Zerotorescue@0 217 end
Zerotorescue@0 218 ctl,data = iter()
Zerotorescue@0 219 v = DeserializeValue(iter,true,ctl,data)
Zerotorescue@0 220 if v==nil then
Zerotorescue@0 221 error("Invalid AceSerializer table format (no table end marker)")
Zerotorescue@0 222 end
Zerotorescue@0 223 res[k]=v
Zerotorescue@0 224 end
Zerotorescue@0 225 else
Zerotorescue@0 226 error("Invalid AceSerializer control code '"..ctl.."'")
Zerotorescue@0 227 end
Zerotorescue@0 228
Zerotorescue@0 229 if not single then
Zerotorescue@0 230 return res,DeserializeValue(iter)
Zerotorescue@0 231 else
Zerotorescue@0 232 return res
Zerotorescue@0 233 end
Zerotorescue@0 234 end
Zerotorescue@0 235
Zerotorescue@0 236 --- Deserializes the data into its original values.
Zerotorescue@0 237 -- Accepts serialized data, ignoring all control characters and whitespace.
Zerotorescue@0 238 -- @param str The serialized data (from :Serialize)
Zerotorescue@0 239 -- @return true followed by a list of values, OR false followed by an error message
Zerotorescue@0 240 function AceSerializer:Deserialize(str)
Zerotorescue@0 241 str = gsub(str, "[%c ]", "") -- ignore all control characters; nice for embedding in email and stuff
Zerotorescue@0 242
Zerotorescue@0 243 local iter = gmatch(str, "(^.)([^^]*)") -- Any ^x followed by string of non-^
Zerotorescue@0 244 local ctl,data = iter()
Zerotorescue@0 245 if not ctl or ctl~="^1" then
Zerotorescue@0 246 -- we purposefully ignore the data portion of the start code, it can be used as an extension mechanism
Zerotorescue@0 247 return false, "Supplied data is not AceSerializer data (rev 1)"
Zerotorescue@0 248 end
Zerotorescue@0 249
Zerotorescue@0 250 return pcall(DeserializeValue, iter)
Zerotorescue@0 251 end
Zerotorescue@0 252
Zerotorescue@0 253
Zerotorescue@0 254 ----------------------------------------
Zerotorescue@0 255 -- Base library stuff
Zerotorescue@0 256 ----------------------------------------
Zerotorescue@0 257
Zerotorescue@0 258 AceSerializer.internals = { -- for test scripts
Zerotorescue@0 259 SerializeValue = SerializeValue,
Zerotorescue@0 260 SerializeStringHelper = SerializeStringHelper,
Zerotorescue@0 261 }
Zerotorescue@0 262
Zerotorescue@0 263 local mixins = {
Zerotorescue@0 264 "Serialize",
Zerotorescue@0 265 "Deserialize",
Zerotorescue@0 266 }
Zerotorescue@0 267
Zerotorescue@0 268 AceSerializer.embeds = AceSerializer.embeds or {}
Zerotorescue@0 269
Zerotorescue@0 270 function AceSerializer:Embed(target)
Zerotorescue@0 271 for k, v in pairs(mixins) do
Zerotorescue@0 272 target[v] = self[v]
Zerotorescue@0 273 end
Zerotorescue@0 274 self.embeds[target] = true
Zerotorescue@0 275 return target
Zerotorescue@0 276 end
Zerotorescue@0 277
Zerotorescue@0 278 -- Update embeds
Zerotorescue@0 279 for target, v in pairs(AceSerializer.embeds) do
Zerotorescue@0 280 AceSerializer:Embed(target)
Zerotorescue@0 281 end