local _ = require "mason-core.functional" local fs = require "mason-core.fs" ---@param str string ---@param char string local function split_once_left(str, char) for i = 1, #str do if str:sub(i, i) == char then local segment = str:sub(1, i - 1) return segment, str:sub(i + 1) end end return str end ---@param lockfile Lockfile local function validate(lockfile) assert(lockfile.header and lockfile.header.version, "Header and version missing.") assert(lockfile.header.version == "1", "Unknown lockfile version.") for pkg_name, metadata in pairs(lockfile.body) do assert(metadata.version, pkg_name .. " is missing version field.") assert(metadata.registry, pkg_name .. " is missing registry field.") local registry = metadata.registry if registry.proto == "github" then assert(registry.integrity, pkg_name .. " is missing registry.integrity field.") assert(registry.name, pkg_name .. " is missing registry.name field.") assert(registry.namespace, pkg_name .. " is missing registry.namespace field.") elseif registry.proto == "file" then assert(registry.path, pkg_name .. " is missing registry.path field.") elseif registry.proto == "lua" then assert(registry.mod, pkg_name .. " is missing registry.mod field.") else error "Unknown registry protocol." end end end ---@param contents string local function parse(contents) local header = nil local body = {} local cursor = { body } local lines = _.split("\n", contents) for line_no, line in ipairs(lines) do local indentation = #line:match "^%s*" local indent_level = indentation / 2 local current_indent_level = (#cursor - 1) if math.fmod(indentation, 2) ~= 0 or indent_level > current_indent_level then error(("Invalid indentation on line %s."):format(line_no)) end if _.matches("^%s*$", line) then -- empty line elseif _.matches("^%s*#", line) then -- comment elseif _.matches("^---$", line) then -- header assert(header == nil, ("Duplicate headers in document on line %s."):format(line_no)) header = body body = {} cursor = { body } else if indent_level < current_indent_level then cursor = _.take(indent_level + 1, cursor) end local key, val = split_once_left(line:sub(indentation + 1), " ") if val then cursor[#cursor][key] = val else cursor[#cursor][key] = {} cursor[#cursor + 1] = cursor[#cursor][key] end end end ---@type Lockfile local lockfile = { header = header, body = body, } validate(lockfile) return lockfile end ---@param file string local function deserialize_file(file) return parse(fs.sync.read_file(file)) end ---@param contents string local function deserialize(contents) return parse(contents) end ---@param lockfile Lockfile local function to_file(lockfile) validate(lockfile) local output = { "# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.", } local serialize serialize = _.curryN(function(depth, key, val) if type(val) == "string" then return ("%s%s %s"):format((" "):rep(depth), key, val) else assert(type(val) == "table", "Unknown value type.") return { (" "):rep(depth) .. key, _.compose(_.map(_.apply(serialize(depth + 1))), _.sort_by(_.head), _.to_pairs)(val), } end end, 3) -- Header for _, key in ipairs(_.sort_by(_.identity, _.keys(lockfile.header))) do output[#output + 1] = serialize(0, key, lockfile.header[key]) end output[#output + 1] = { "", "---", "" } -- Body for _, key in ipairs(_.sort_by(_.identity, _.keys(lockfile.body))) do output[#output + 1] = serialize(0, key, lockfile.body[key]) output[#output + 1] = { "" } end return _.join("\n", _.flatten(output)) end return { deserialize = deserialize, deserialize_file = deserialize_file, validate = validate, serialize = to_file, }