diff options
Diffstat (limited to 'lib/lyaml/init.lua')
-rw-r--r-- | lib/lyaml/init.lua | 534 |
1 files changed, 534 insertions, 0 deletions
diff --git a/lib/lyaml/init.lua b/lib/lyaml/init.lua new file mode 100644 index 000000000000..95e4036ea7c9 --- /dev/null +++ b/lib/lyaml/init.lua @@ -0,0 +1,534 @@ +-- Transform between YAML 1.1 streams and Lua table representations. +-- Written by Gary V. Vaughan, 2013 +-- +-- Copyright(C) 2013-2022 Gary V. Vaughan +-- +-- 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. +-- +-- Portions of this software were inspired by an earlier LibYAML binding +-- by Andrew Danforth <acd@weirdness.net> + +--- @module lyaml + + +local explicit = require 'lyaml.explicit' +local functional = require 'lyaml.functional' +local implicit = require 'lyaml.implicit' +local yaml = require 'yaml' + +local NULL = functional.NULL +local anyof = functional.anyof +local find = string.find +local format = string.format +local gsub = string.gsub +local id = functional.id +local isnull = functional.isnull +local match = string.match + + +local TAG_PREFIX = 'tag:yaml.org,2002:' + + +local function tag(name) + return TAG_PREFIX .. name +end + + +local default = { + -- Tag table to lookup explicit scalar conversions. + explicit_scalar = { + [tag 'bool'] = explicit.bool, + [tag 'float'] = explicit.float, + [tag 'int'] = explicit.int, + [tag 'null'] = explicit.null, + [tag 'str'] = explicit.str, + }, + -- Order is important, so we put most likely and fastest nearer + -- the top to reduce average number of comparisons and funcalls. + implicit_scalar = anyof { + implicit.null, + implicit.octal, -- subset of decimal, must come earlier + implicit.decimal, + implicit.float, + implicit.bool, + implicit.inf, + implicit.nan, + implicit.hexadecimal, + implicit.binary, + implicit.sexagesimal, + implicit.sexfloat, + id, + }, +} + + +-- Metatable for Dumper objects. +local dumper_mt = { + __index = { + -- Emit EVENT to the LibYAML emitter. + emit = function(self, event) + return self.emitter.emit(event) + end, + + -- Look up an anchor for a repeated document element. + get_anchor = function(self, value) + local r = self.anchors[value] + if r then + self.aliased[value], self.anchors[value] = self.anchors[value], nil + end + return r + end, + + -- Look up an already anchored repeated document element. + get_alias = function(self, value) + return self.aliased[value] + end, + + -- Dump ALIAS into the event stream. + dump_alias = function(self, alias) + return self:emit { + type = 'ALIAS', + anchor = alias, + } + end, + + -- Dump MAP into the event stream. + dump_mapping = function(self, map) + local alias = self:get_alias(map) + if alias then + return self:dump_alias(alias) + end + + self:emit { + type = 'MAPPING_START', + anchor = self:get_anchor(map), + style = 'BLOCK', + } + for k, v in pairs(map) do + self:dump_node(k) + self:dump_node(v) + end + return self:emit {type='MAPPING_END'} + end, + + -- Dump SEQUENCE into the event stream. + dump_sequence = function(self, sequence) + local alias = self:get_alias(sequence) + if alias then + return self:dump_alias(alias) + end + + self:emit { + type = 'SEQUENCE_START', + anchor = self:get_anchor(sequence), + style = 'BLOCK', + } + for _, v in ipairs(sequence) do + self:dump_node(v) + end + return self:emit {type='SEQUENCE_END'} + end, + + -- Dump a null into the event stream. + dump_null = function(self) + return self:emit { + type = 'SCALAR', + value = '~', + plain_implicit = true, + quoted_implicit = true, + style = 'PLAIN', + } + end, + + -- Dump VALUE into the event stream. + dump_scalar = function(self, value) + local alias = self:get_alias(value) + if alias then + return self:dump_alias(alias) + end + + local anchor = self:get_anchor(value) + local itsa = type(value) + local style = 'PLAIN' + if itsa == 'string' and self.implicit_scalar(value) ~= value then + -- take care to round-trip strings that look like scalars + style = 'SINGLE_QUOTED' + elseif value == math.huge then + value = '.inf' + elseif value == -math.huge then + value = '-.inf' + elseif value ~= value then + value = '.nan' + elseif itsa == 'number' or itsa == 'boolean' then + value = tostring(value) + elseif itsa == 'string' and find(value, '\n') then + style = 'LITERAL' + end + return self:emit { + type = 'SCALAR', + anchor = anchor, + value = value, + plain_implicit = true, + quoted_implicit = true, + style = style, + } + end, + + -- Decompose NODE into a stream of events. + dump_node = function(self, node) + local itsa = type(node) + if isnull(node) then + return self:dump_null() + elseif itsa == 'string' or itsa == 'boolean' or itsa == 'number' then + return self:dump_scalar(node) + elseif itsa == 'table' then + -- Something is only a sequence if its keys start at 1 + -- and are consecutive integers without any jumps. + local prior_key = 0 + local is_pure_sequence = true + local i, v = next(node, nil) + while i and is_pure_sequence do + if type(i) ~= "number" or (prior_key + 1 ~= i) then + is_pure_sequence = false -- breaks the loop + else + prior_key = i + i, v = next(node, prior_key) + end + end + if is_pure_sequence then + -- Only sequentially numbered integer keys starting from 1. + return self:dump_sequence(node) + else + -- Table contains non sequential integer keys or mixed keys. + return self:dump_mapping(node) + end + else -- unsupported Lua type + error("cannot dump object of type '" .. itsa .. "'", 2) + end + end, + + -- Dump DOCUMENT into the event stream. + dump_document = function(self, document) + self:emit {type='DOCUMENT_START'} + self:dump_node(document) + return self:emit {type='DOCUMENT_END'} + end, + }, +} + + +-- Emitter object constructor. +local function Dumper(opts) + local anchors = {} + for k, v in pairs(opts.anchors) do + anchors[v] = k + end + local object = { + aliased = {}, + anchors = anchors, + emitter = yaml.emitter(), + implicit_scalar = opts.implicit_scalar, + } + return setmetatable(object, dumper_mt) +end + + +--- Dump options table. +-- @table dumper_opts +-- @tfield table anchors map initial anchor names to values +-- @tfield function implicit_scalar parse implicit scalar values + + +--- Dump a list of Lua tables to an equivalent YAML stream. +-- @tparam table documents a sequence of Lua tables. +-- @tparam[opt] dumper_opts opts initialisation options +-- @treturn string equivalest YAML stream +local function dump(documents, opts) + opts = opts or {} + + -- backwards compatibility + if opts.anchors == nil and opts.implicit_scalar == nil then + opts = {anchors=opts} + end + + local dumper = Dumper { + anchors = opts.anchors or {}, + implicit_scalar = opts.implicit_scalar or default.implicit_scalar, + } + + dumper:emit {type='STREAM_START', encoding='UTF8'} + for _, document in ipairs(documents) do + dumper:dump_document(document) + end + local ok, stream = dumper:emit {type='STREAM_END'} + return stream +end + + +-- We save anchor types that will match the node type from expanding +-- an alias for that anchor. +local alias_type = { + MAPPING_END = 'MAPPING_END', + MAPPING_START = 'MAPPING_END', + SCALAR = 'SCALAR', + SEQUENCE_END = 'SEQUENCE_END', + SEQUENCE_START = 'SEQUENCE_END', +} + + +-- Metatable for Parser objects. +local parser_mt = { + __index = { + -- Return the type of the current event. + type = function(self) + return tostring(self.event.type) + end, + + -- Raise a parse error. + error = function(self, errmsg, ...) + error(format('%d:%d: ' .. errmsg, self.mark.line, + self.mark.column, ...), 0) + end, + + -- Save node in the anchor table for reference in future ALIASes. + add_anchor = function(self, node) + if self.event.anchor ~= nil then + self.anchors[self.event.anchor] = { + type = alias_type[self.event.type], + value = node, + } + end + end, + + -- Fetch the next event. + parse = function(self) + local ok, event = pcall(self.next) + if not ok then + -- if ok is nil, then event is a parser error from libYAML + self:error(gsub(event, ' at document: .*$', '')) + end + self.event = event + self.mark = { + line = self.event.start_mark.line + 1, + column = self.event.start_mark.column + 1, + } + return self:type() + end, + + -- Construct a Lua hash table from following events. + load_map = function(self) + local map = {} + self:add_anchor(map) + while true do + local key = self:load_node() + local tag = self.event.tag + if tag then + tag = match(tag, '^' .. TAG_PREFIX .. '(.*)$') + end + if key == nil then + break + end + if key == '<<' or tag == 'merge' then + tag = self.event.tag or key + local node, event = self:load_node() + if event == 'MAPPING_END' then + for k, v in pairs(node) do + if map[k] == nil then + map[k] = v + end + end + + elseif event == 'SEQUENCE_END' then + for i, merge in ipairs(node) do + if type(merge) ~= 'table' then + self:error("invalid '%s' sequence element %d: %s", + tag, i, tostring(merge)) + end + for k, v in pairs(merge) do + if map[k] == nil then + map[k] = v + end + end + end + + else + if event == 'SCALAR' then + event = tostring(node) + end + self:error("invalid '%s' merge event: %s", tag, event) + end + else + local value, event = self:load_node() + if value == nil then + self:error('unexpected %s event', self:type()) + end + map[key] = value + end + end + return map, self:type() + end, + + -- Construct a Lua array table from following events. + load_sequence = function(self) + local sequence = {} + self:add_anchor(sequence) + while true do + local node = self:load_node() + if node == nil then + break + end + sequence[#sequence + 1] = node + end + return sequence, self:type() + end, + + -- Construct a primitive type from the current event. + load_scalar = function(self) + local value = self.event.value + local tag = self.event.tag + local explicit = self.explicit_scalar[tag] + + -- Explicitly tagged values. + if explicit then + value = explicit(value) + if value == nil then + self:error("invalid '%s' value: '%s'", tag, self.event.value) + end + + -- Otherwise, implicit conversion according to value content. + elseif self.event.style == 'PLAIN' then + value = self.implicit_scalar(self.event.value) + end + self:add_anchor(value) + return value, self:type() + end, + + load_alias = function(self) + local anchor = self.event.anchor + local event = self.anchors[anchor] + if event == nil then + self:error('invalid reference: %s', tostring(anchor)) + end + return event.value, event.type + end, + + load_node = function(self) + local dispatch = { + SCALAR = self.load_scalar, + ALIAS = self.load_alias, + MAPPING_START = self.load_map, + SEQUENCE_START = self.load_sequence, + MAPPING_END = function() end, + SEQUENCE_END = function() end, + DOCUMENT_END = function() end, + } + + local event = self:parse() + if dispatch[event] == nil then + self:error('invalid event: %s', self:type()) + end + return dispatch[event](self) + end, + }, +} + + +-- Parser object constructor. +local function Parser(s, opts) + local object = { + anchors = {}, + explicit_scalar = opts.explicit_scalar, + implicit_scalar = opts.implicit_scalar, + mark = {line=0, column=0}, + next = yaml.parser(s), + } + return setmetatable(object, parser_mt) +end + + +--- Load options table. +-- @table loader_opts +-- @tfield boolean all load all documents from the stream +-- @tfield table explicit_scalar map full tag-names to parser functions +-- @tfield function implicit_scalar parse implicit scalar values + + +--- Load a YAML stream into a Lua table. +-- @tparam string s YAML stream +-- @tparam[opt] loader_opts opts initialisation options +-- @treturn table Lua table equivalent of stream *s* +local function load(s, opts) + opts = opts or {} + local documents = {} + local all = false + + -- backwards compatibility + if opts == true then + opts = {all=true} + end + + local parser = Parser(s, { + explicit_scalar = opts.explicit_scalar or default.explicit_scalar, + implicit_scalar = opts.implicit_scalar or default.implicit_scalar, + }) + + if parser:parse() ~= 'STREAM_START' then + error('expecting STREAM_START event, but got ' .. parser:type(), 2) + end + + while parser:parse() ~= 'STREAM_END' do + local document = parser:load_node() + if document == nil then + error('unexpected ' .. parser:type() .. ' event') + end + + if parser:parse() ~= 'DOCUMENT_END' then + error('expecting DOCUMENT_END event, but got ' .. parser:type(), 2) + end + + -- save document + documents[#documents + 1] = document + + -- reset anchor table + parser.anchors = {} + end + + return opts.all and documents or documents[1] +end + + +--[[ ----------------- ]]-- +--[[ Public Interface. ]]-- +--[[ ----------------- ]]-- + + +--- @export +return { + dump = dump, + load = load, + + --- `lyaml.null` value. + -- @table null + null = NULL, + + --- Version number from yaml C binding. + -- @table _VERSION + _VERSION = yaml.version, +} |