aboutsummaryrefslogtreecommitdiffstats
path: root/lua/mason-core/lock/parser.lua
blob: b3b9c070dce62bce8ebd5cd528bc5c7a556f5df0 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
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,
}