local util = require("util") -- any -- string, boolean, number -- base64 -- int, uint -- option[x] or ?x -- array[key] OR [key] -- struct { hi : string, bye: blah } OR { hi: string, bye: blah }, {} -- map[key, value] -- set[key] -- enum[a,b,c,d] -- union { foo: array[string] } local function stream_match(stream, pat) local _, pos, match = stream.data:find("^"..pat, stream.pos) if not pos then return end stream.pos = pos+1 return match or true end local function stream_end(stream) return stream_match(stream, "%s*$") ~= nil end local function stream_get(stream, pat) return stream_match(stream, "%s*("..pat..")") end local function stream_ident(stream) return stream_get(stream, "[%w_]+") end local function stream_any(stream) return stream_get(stream, "%S") end local function stream_char(stream, chars) local special = "["..("^$()%.[]*+-?"):gsub(".", function(c) return "%"..c end).."]" local pat = "["..chars:gsub(special, function(c) return "%"..c end).."]" return stream_get(stream, pat) end local parse_type local function expect_ident(stream) local ident = stream_ident(stream) if not ident then return nil, "expected identifier, got " .. (stream_any(stream) or "EOF") end return ident end local function expect_char(stream, chars) local c = stream_char(stream, chars) if c then return c end local list = chars:gsub(".", function(c) return "'"..c.."'"..", " end):sub(1,-3) if #chars > 1 then list = "one of " .. list end return nil, "expected " .. list .. ", got " .. (stream_any(stream) or "EOF") end local function expect_type(stream) local type, err = parse_type(stream) if err then return nil, err end if not type then return nil, "expected type, got EOF" end return type end local function parse_struct_tail(stream) local fields = {} while true do local key = stream_ident(stream) if not key then break end local _, err = expect_char(stream, ":") if err then return nil, err end local value, err = expect_type(stream) if err then return nil, err end table.insert(fields, { key = key, value = value }) if not stream_char(stream, ",") then break end end local _, err = expect_char(stream, "}") if err then return nil, err end return { kind = "struct", fields = fields } end local function parse_array_tail(stream) local inner, err = expect_type(stream) if err then return nil, err end local _, err = expect_char(stream, "]") if err then return nil, err end return { kind = "array", inner = inner } end parse_type = function(stream) if stream_end(stream) then return end local ident = stream_ident(stream) if ident then if ident == "any" then return { kind = "any" } elseif ident == "string" or ident == "boolean" or ident == "number" then return { kind = "simple", type = ident } elseif ident == "int" or ident == "uint" then return { kind = "integer", unsigned = ident == "uint" } elseif ident == "base64" then return { kind = "base64" } elseif ident == "uuidv4" then return { kind = "uuidv4" } elseif ident == "option" then local _, err = expect_char(stream, "[") if err then return nil, err end local inner, err = expect_type(stream) if err then return nil, err end local _, err = expect_char(stream, "]") if err then return nil, err end return { kind = "option", inner = inner } elseif ident == "array" then local _, err = expect_char(stream, "[") if err then return nil, err end return parse_array_tail(stream) elseif ident == "struct" then local _, err = expect_char(stream, "{") if err then return nil, err end if err then return nil, err end return parse_struct_tail(stream) elseif ident == "map" then local _, err = expect_char(stream, "[") if err then return nil, err end local key, err = expect_type(stream) if err then return nil, err end local _, err = expect_char(stream, ",") if err then return nil, err end local value, err = expect_type(stream) if err then return nil, err end local _, err = expect_char(stream, "]") if err then return nil, err end return { kind = "map", key = key, value = value } elseif ident == "set" then local _, err = expect_char(stream, "[") if err then return nil, err end local inner, err = expect_type(stream) if err then return nil, err end local _, err = expect_char(stream, "]") if err then return nil, err end return { kind = "set", inner = inner } elseif ident == "enum" then local _, err = expect_char(stream, "[") if err then return nil, err end local variants = {} local strings = {} while true do local ident = stream_ident(stream) if not ident then break end table.insert(strings, ident) variants[ident] = true if not stream_char(stream, ",") then break end end local _, err = expect_char(stream, "]") if err then return nil, err end return { kind = "enum", variants = variants, expect_variant = "one of [" .. table.concat(strings, ", ") .. "]" } elseif ident == "union" then local _, err = expect_char(stream, "{") if err then return nil, err end local variants = {} local strings = {} while true do local ident = stream_ident(stream) if not ident then break end local variant if stream_char(stream, ":") then local err variant, err = expect_type(stream) if err then return nil, err end else variant = { kind = "struct", fields = {} } end table.insert(strings, ident) variants[ident] = variant if not stream_char(stream, ",") then break end end local _, err = expect_char(stream, "}") if err then return nil, err end return { kind = "union", variants = variants, expect_variant = "one of [" .. table.concat(strings, ", ") .. "]" } else return nil, "expected type, got " .. ident end end local char, err = expect_char(stream, "?[{$") if err then return nil, err end if char == "?" then local inner, err = expect_type(stream) if err then return nil, err end return { kind = "option", inner = inner } elseif char == "[" then return parse_array_tail(stream) elseif char == "{" then return parse_struct_tail(stream) elseif char == "$" then local ident, err = expect_ident(stream) if err then return nil, err end local var = stream.dict and stream.dict[ident] if not var then return nil, "no such schema: " .. ident end return var end end local function parse(stream) local type, err = expect_type(stream) if err then return nil, err end if not stream_end(stream) then return nil, "" end return type end local function parse_throw(name, input, dict) local stream = { data = input, pos = 1, dict = dict } local type, err = parse(stream) if err then error("error while parsing " .. name .. " schema at " .. stream.pos .. ": " .. err) end return { kind = "named", name = name, inner = type } end local function parse_dict(...) local dict = {} local args = { ... } for i = 1, #args, 2 do local type, err = parse_throw(args[i], args[i+1], dict) if err then return nil, err end dict[type.name] = type.inner end return dict end local function fail(path, expect, got_val, got_type) return nil, { path = path, expect = expect, got = { val = got_val, type = got_type } } end local function wrap(path, val, err) if err then return nil, { path = path .. err.path, expect = err.expect, got = err.got } end return val end local function deserialize(schema, data) if schema.kind == "named" then return wrap(schema.name, deserialize(schema.inner, data)) elseif schema.kind == "any" then return data elseif schema.kind == "simple" then if type(data) ~= schema.type then return fail("", schema.type, data, type(data)) end return data elseif schema.kind == "base64" then if type(data) ~= "string" then return fail("", "string", data, type(data)) end local dec = util.base64_dec(data) if not dec then return fail("", "base64", data) end return dec elseif schema.kind == "uuidv4" then if type(data) ~= "string" then return fail("", "string", data, type(data)) end if not util.validate_uuidv4(data) then return fail("", "uuidv4", data) end return data elseif schema.kind == "integer" then if type(data) ~= "number" then return fail("", "number", data, type(data)) elseif data ~= data then return fail("", "number", data, "nan") elseif data == math.huge then return fail("", "integer", data, "inf") elseif math.floor(data) ~= data then return fail("", "integer", data, "float") elseif schema.unsigned and data < 0 then return fail("", "unsigned", data, "negative") end return data elseif schema.kind == "option" then if data ~= nil then return wrap("?", deserialize(schema.inner, data)) end return nil elseif schema.kind == "array" then if type(data) ~= "table" then return fail("", "table", data, type(data)) end local arr = {} for i, x in ipairs(data) do local elem, err = deserialize(schema.inner, x) if err then return wrap("["..i.."]", nil, err) end arr[i] = elem end return arr elseif schema.kind == "struct" then if type(data) ~= "table" then return fail("", "table", data, type(data)) end local struct = {} for _, field in ipairs(schema.fields) do local val, err = deserialize(field.value, data[field.key]) if err then return wrap("."..field.key, nil, err) end struct[field.key] = val end return struct elseif schema.kind == "map" then local map = {} for k, v in pairs(data) do local key, err = deserialize(schema.key, k) if err then return wrap(".(key)", nil, err) end local val, err = deserialize(schema.value, v) if err then return wrap("["..util.display(k).."]", nil, err) end map[key] = val end return map elseif schema.kind == "set" then local inner, err = deserialize({ kind = "array", inner = schema.inner }) if err then return nil, err end local set = {} for _, v in ipairs(inner) do set[v] = true end return set elseif schema.kind == "enum" then if type(data) ~= "string" then return fail(".type", "string", data, type(data)) end if not schema.variants[data] then return fail(".type", schema.expect_variant, data) end return data elseif schema.kind == "union" then if type(data) ~= "table" then return fail("", "table", data, type(data)) elseif type(data.type) ~= "string" then return fail(".type", "string", data.type, type(data.type)) end local variant = schema.variants[data.type] if not variant then return fail(".type", schema.expect_variant, data.type) end local val, err = deserialize(variant, data) if err then return wrap("@"..data.type, nil, err) end val.type = data.type return val else error(schema.kind) end end local function deserialize_string(schema, json) local data = util.json_dec(json) if not data then return nil, "invalid JSON: " .. util.display(json) end local data, err = deserialize(schema, data) if err then local got = util.display(err.got.val) if err.got.type then got = err.got.type .. " (" .. got .. ")" end return nil, err.path .. ": expected " .. err.expect .. ", got " .. got end return data end return { parse = parse_throw, parse_dict = parse_dict, deserialize = deserialize_string, }