rustploy-gui 0.1.0

Rustploy — desktop client (glacier-ui) for the Rustploy PaaS daemon
--!strict

-- net/api.luau — cliente HTTP/JSON da API do daemon.
--
-- Encapsula base_url + header Authorization e expõe `rpc(cmd)`: faz
-- POST /api/rpc com o Command em JSON e devolve a Response decodificada. O
-- `fetch` do prelúdio SUSPENDE a corrotina da ação (async/await) — a UI não
-- trava enquanto espera a resposta.
--
-- Convenção de Command (serde externally-tagged):
--   • variante unitária  → string:  "DaemonStatus", "StopAllManaged"
--   • variante com campos → tabela:  { ProjectDelete = { id = "..." } }
-- Convenção de Response (idem): { Projects = {...} }, { DaemonStatus = {...} }.

local M = {}
M.__index = M

--- Só o header `Authorization` (tabela vazia quando não há token). Função de
--- MÓDULO (ponto, não dois-pontos) de propósito: também serve quem NÃO tem um
--- Client nas mãos — as chamadas cruas de `sse` em `handlers/stream.luau` e
--- `log_window.luau`, que falam com o daemon sem passar por `rpc`. Assim o
--- formato "Bearer <token>" tem uma definição só, aqui.
function M.auth_headers(token: string?): { [string]: string }
    if token == nil or token == "" then
        return {}
    end

    return { Authorization = "Bearer " .. token }
end

function M:headers(): { [string]: string }
    local h = M.auth_headers(self.token)
    h["Content-Type"] = "application/json"

    return h
end

--- Executa um Command. Retorna (true, response_table) em sucesso HTTP, ou
--- (false, mensagem) em falha de transporte / corpo inválido.
function M:rpc(cmd: any): (boolean, any)
    local res = fetch(tostring(self.base_url) .. "/api/rpc", {
        method = "POST",
        body = json.encode(cmd),
        headers = self:headers(),
    })

    if not res.ok then
        local msg =
            (res.error ~= nil and res.error ~= "")
                and res.error
                or ("HTTP " .. tostring(res.status))

        return false, msg
    end

    local ok, decoded = pcall(json.decode, res.body)

    -- Aceita QUALQUER valor decodificado: uma Response de variante UNITÁRIA
    -- (ex.: `Response::Ok`) é serializada pelo serde como a string bare "Ok"
    -- (não um objeto). Só o parse falhando é resposta inválida.
    if not ok then
        return false, "resposta inválida do daemon"
    end

    return true, decoded
end

--- Como `rpc`, mas trata `Response::Err { code, message }` como falha.
function M:rpc_checked(cmd: any): (boolean, any)
    local ok, res = self:rpc(cmd)

    if not ok then
        return false, res
    end

    if type(res) == "table" and res.Err ~= nil then
        return false, tostring(res.Err.code) .. ": " .. tostring(res.Err.message)
    end

    return true, res
end

function M:upload_archive(service_id: string, filename: string, body_base64: string): (boolean, any)
    local h = self:headers()
    h["Content-Type"] = "application/zip"
    h["X-Rustploy-Filename"] = filename

    local res = fetch(tostring(self.base_url) .. "/api/services/" .. service_id .. "/archive", {
        method = "POST",
        body_base64 = body_base64,
        headers = h,
    })
    if not res.ok then
        return false, (res.error ~= nil and res.error ~= "") and res.error or ("HTTP " .. tostring(res.status))
    end
    local ok, decoded = pcall(json.decode, res.body)
    if not ok then
        return false, "resposta inválida do daemon"
    end
    if type(decoded) == "table" and decoded.Err ~= nil then
        return false, tostring(decoded.Err.code) .. ": " .. tostring(decoded.Err.message)
    end
    return true, decoded
end

export type Client = typeof(setmetatable(
    {} :: {
        base_url: string,
        token: string?,
    },
    M
))

function M.new(base_url: string, token: string?): Client
    return setmetatable({
        base_url = base_url,
        token = token ~= nil and token ~= "" and token or nil,
    }, M)
end

return M