Skip to content

Commit

Permalink
feat: add provider interface (#601)
Browse files Browse the repository at this point in the history
  • Loading branch information
williamboman committed Oct 30, 2022
1 parent 311a14f commit 37c745f
Show file tree
Hide file tree
Showing 24 changed files with 419 additions and 56 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,15 @@ local DEFAULT_SETTINGS = {
download_url_template = "https://github.com/%s/releases/download/%s/%s",
},

-- The provider implementations to use for resolving package metadata (latest version, available versions, etc.).
-- Accepts multiple entries, where later entries will be used as fallback should prior providers fail.
-- Builtin providers are:
-- - mason.providers.registry-api (default) - uses the https://api.mason-registry.dev API
-- - mason.providers.client - uses only client-side tooling to resolve metadata
providers = {
"mason.providers.registry-api",
},

ui = {
-- Whether to automatically check for new versions when opening the :Mason window.
check_outdated_packages_on_open = true,
Expand Down
9 changes: 9 additions & 0 deletions doc/mason.txt
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,15 @@ Example:
download_url_template = "https://github.com/%s/releases/download/%s/%s",
},

-- The provider implementations to use for resolving package metadata (latest version, available versions, etc.).
-- Accepts multiple entries, where later entries will be used as fallback should prior providers fail.
-- Builtin providers are:
-- - mason.providers.registry-api (default) - uses the https://api.mason-registry.dev API
-- - mason.providers.client - uses only client-side tooling to resolve metadata
providers = {
"mason.providers.registry-api",
},

ui = {
-- Whether to automatically check for new versions when opening the :Mason window.
check_outdated_packages_on_open = true,
Expand Down
5 changes: 5 additions & 0 deletions lua/mason-core/functional/function.lua
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,9 @@ _.lazy = function(fn)
end
end

_.tap = _.curryN(function(fn, value)
fn(value)
return value
end, 2)

return _
3 changes: 3 additions & 0 deletions lua/mason-core/functional/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ _.T = fun.T
_.F = fun.F
_.memoize = fun.memoize
_.lazy = fun.lazy
_.tap = fun.tap

---@module "mason-core.functional.list"
local list = lazy_require "mason-core.functional.list"
Expand All @@ -45,6 +46,7 @@ _.prepend = list.prepend
_.zip_table = list.zip_table
_.nth = list.nth
_.head = list.head
_.last = list.last
_.length = list.length
_.flatten = list.flatten
_.sort_by = list.sort_by
Expand Down Expand Up @@ -80,6 +82,7 @@ _.dec = number.dec
---@module "mason-core.functional.string"
local string = lazy_require "mason-core.functional.string"
_.matches = string.matches
_.match = string.match
_.format = string.format
_.split = string.split
_.gsub = string.gsub
Expand Down
7 changes: 7 additions & 0 deletions lua/mason-core/functional/list.lua
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,13 @@ end, 2)

_.head = _.nth(1)

---@generic T
---@param list T[]
---@return T?
_.last = function(list)
return list[#list]
end

---@param value string|any[]
_.length = function(value)
return #value
Expand Down
4 changes: 4 additions & 0 deletions lua/mason-core/functional/string.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ _.matches = fun.curryN(function(pattern, str)
return str:match(pattern) ~= nil
end, 2)

_.match = fun.curryN(function(pattern, str)
return { str:match(pattern) }
end, 2)

---@param template string
---@param str string
_.format = fun.curryN(function(template, str)
Expand Down
13 changes: 3 additions & 10 deletions lua/mason-core/managers/github/client.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,10 @@ local _ = require "mason-core.functional"
local log = require "mason-core.log"
local fetch = require "mason-core.fetch"
local spawn = require "mason-core.spawn"
local api = require "mason-registry.api"
local providers = require "mason-core.providers"

local M = {}

---@alias GitHubReleaseAsset {url: string, id: integer, name: string, browser_download_url: string, created_at: string, updated_at: string, size: integer, download_count: integer}
---@alias GitHubRelease {tag_name: string, prerelease: boolean, draft: boolean, assets:GitHubReleaseAsset[]}
---@alias GitHubTag {name: string}
---@alias GitHubCommit {sha: string}

local stringify_params = _.compose(_.join "&", _.map(_.join "="), _.sort_by(_.head), _.to_pairs)
Expand Down Expand Up @@ -66,11 +63,7 @@ end
---@return Result # Result<GitHubRelease>
function M.fetch_latest_release(repo, opts)
opts = opts or { include_prerelease = false }
return api.repo.releases.latest({ repo = repo }, {
params = {
include_prerelease = opts.include_prerelease and "true" or "false",
},
})
return providers.github.get_latest_release(repo, { include_prerelease = opts.include_prerelease })
end

---@async
Expand All @@ -87,7 +80,7 @@ end
---@param repo string The GitHub repo ("username/repo").
---@return Result # Result<string> The latest tag name.
function M.fetch_latest_tag(repo)
return api.repo.tags.latest({ repo = repo }):map(_.prop "tag")
return providers.github.get_latest_tag(repo):map(_.prop "tag")
end

---@async
Expand Down
4 changes: 2 additions & 2 deletions lua/mason-core/managers/npm/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ local Result = require "mason-core.result"
local path = require "mason-core.path"
local _ = require "mason-core.functional"
local platform = require "mason-core.platform"
local api = require "mason-registry.api"
local providers = require "mason-core.providers"

local list_copy = _.list_copy

Expand Down Expand Up @@ -112,7 +112,7 @@ function M.check_outdated_primary_package(receipt, install_dir)
local primary_package = receipt.primary_source.package
return M.get_installed_primary_package_version(receipt, install_dir)
:and_then(function(installed_version)
return api.npm.versions.latest({ package = primary_package }):map(function(response)
return providers.npm.get_latest_version(primary_package):map(function(response)
return {
installed = installed_version,
latest = response.version,
Expand Down
48 changes: 20 additions & 28 deletions lua/mason-core/managers/pip3/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ local Optional = require "mason-core.optional"
local installer = require "mason-core.installer"
local Result = require "mason-core.result"
local spawn = require "mason-core.spawn"
local providers = require "mason-core.providers"

local VENV_DIR = "venv"

Expand Down Expand Up @@ -103,36 +104,27 @@ function M.check_outdated_primary_package(receipt, install_dir)
return Result.failure "Receipt does not have a primary source of type pip3"
end
local normalized_package = M.normalize_package(receipt.primary_source.package)
return spawn
.python({
"-m",
"pip",
"list",
"--outdated",
"--format=json",
cwd = install_dir,
with_paths = { M.venv_path(install_dir) },
})
:map_catching(function(result)
---@alias PipOutdatedPackage {name: string, version: string, latest_version: string}
---@type PipOutdatedPackage[]
local packages = vim.json.decode(result.stdout)

local outdated_primary_package = _.find_first(function(outdated_package)
return outdated_package.name == normalized_package
and outdated_package.version ~= outdated_package.latest_version
end, packages)

return Optional.of_nilable(outdated_primary_package)
:map(function(pkg)
return {
return M.get_installed_primary_package_version(receipt, install_dir):and_then(function(installed_version)
return providers.pypi
.get_latest_version(normalized_package)
:map(function(latest)
return {
current = installed_version,
latest = latest.version,
}
end)
:and_then(function(versions)
if versions.current ~= versions.latest then
return Result.success {
name = normalized_package,
current_version = assert(pkg.version, "missing current pip3 package version"),
latest_version = assert(pkg.latest_version, "missing latest pip3 package version"),
current_version = versions.current,
latest_version = versions.latest,
}
end)
:or_else_throw "Primary package is not outdated."
end)
else
return Result.failure "Primary package is not outdated."
end
end)
end)
end

---@async
Expand Down
8 changes: 6 additions & 2 deletions lua/mason-core/optional.lua
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,17 @@ function Optional:is_present()
return self._value ~= nil
end

---@param err fun(): any
---@param err (fun(): any)|string
function Optional:ok_or(err)
local Result = require "mason-core.result"
if self:is_present() then
return Result.success(self:get())
else
return Result.failure(err())
if type(err) == "string" then
return Result.failure(err)
else
return Result.failure(err())
end
end
end

Expand Down
77 changes: 77 additions & 0 deletions lua/mason-core/providers/init.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
local settings = require "mason.settings"
local log = require "mason-core.log"
local Result = require "mason-core.result"

---@alias GitHubReleaseAsset {url: string, id: integer, name: string, browser_download_url: string, created_at: string, updated_at: string, size: integer, download_count: integer}
---@alias GitHubRelease { tag_name: string, prerelease: boolean, draft: boolean, assets:GitHubReleaseAsset[] }
---@alias GitHubTag { name: string }

---@class GitHubProvider
---@field get_latest_release? async fun(repo: string, opts?: { include_prerelease?: boolean }): Result # Result<GitHubRelease>
---@field get_all_release_versions? async fun(repo: string): Result # Result<string[]>
---@field get_latest_tag? async fun(repo: string): Result # Result<GitHubTag>
---@field get_all_tags? async fun(repo: string): Result # Result<string[]>

---@alias NpmPackage { name: string, version: string }

---@class NpmProvider
---@field get_latest_version? async fun(pkg: string): Result # Result<NpmPackage>
---@field get_all_versions? async fun(pkg: string): Result # Result<string[]>

---@alias PyPiPackage { name: string, version: string }

---@class PyPiProvider
---@field get_latest_version? async fun(pkg: string): Result # Result<PyPiPackage>
---@field get_all_versions? async fun(pkg: string): Result # Result<string[]> # Sorting should not be relied upon due to "proprietary" sorting algo in pip that is difficult to replicate in mason-registry-api.

---@class Provider
---@field github? GitHubProvider
---@field npm? NpmProvider
---@field pypi? PyPiProvider

local function service_mt(service)
return setmetatable({}, {
__index = function(_, method)
return function(...)
if #settings.current.providers < 1 then
log.error "No providers configured."
return Result.failure "1 or more providers are required."
end
for _, provider_module in ipairs(settings.current.providers) do
local ok, provider = pcall(require, provider_module)
if ok and provider then
local impl = provider[service] and provider[service][method]
if impl then
---@type boolean, Result
local ok, result = pcall(impl, ...)
if ok and result:is_success() then
return result
else
if getmetatable(result) == Result then
log.fmt_error("Provider %s %s failed: %s", service, method, result:err_or_nil())
else
log.fmt_error("Provider %s %s errored: %s", service, method, result)
end
end
end
else
log.fmt_error("Unable to find provider %s is not registered. %s", provider_module, provider)
end
end
local err = ("No provider implementation found for %s.%s"):format(service, method)
log.error(err)
return Result.failure(err)
end
end,
})
end

---@type Provider
local providers = setmetatable({}, {
__index = function(tbl, service)
tbl[service] = service_mt(service)
return tbl[service]
end,
})

return providers
2 changes: 1 addition & 1 deletion lua/mason-core/spawn.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ local process = require "mason-core.process"
local platform = require "mason-core.platform"
local log = require "mason-core.log"

---@alias JobSpawn table<string, async fun(opts: JobSpawnOpts): Result>
---@alias JobSpawn table<string, async fun(opts: SpawnArgs): Result>
---@type JobSpawn
local spawn = {
_aliases = {
Expand Down
9 changes: 9 additions & 0 deletions lua/mason-registry/api.lua
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,13 @@ api.npm = {
},
}

api.pypi = {
versions = {
---@type ApiSignature<{ package: string }>
latest = get "/api/pypi/{package}/versions/latest",
---@type ApiSignature<{ package: string }>
all = get "/api/pypi/{package}/versions/all",
},
}

return api
43 changes: 43 additions & 0 deletions lua/mason/providers/client/gh.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
local spawn = require "mason-core.spawn"
local _ = require "mason-core.functional"
local Result = require "mason-core.result"
local Optional = require "mason-core.optional"

---@type GitHubProvider
return {
get_latest_release = function(repo, opts)
opts = opts or {}
if not opts.include_prerelease then
return spawn
.gh({ "api", ("repos/%s/releases/latest"):format(repo) })
:map(_.prop "stdout")
:map_catching(vim.json.decode)
else
return spawn
.gh({ "api", ("repos/%s/releases"):format(repo) })
:map(_.prop "stdout")
:map_catching(vim.json.decode)
:map(_.find_first(_.prop_eq("draft", false)))
:and_then(function(release)
return Optional.of_nilable(release):ok_or "Failed to find latest release."
end)
end
end,
get_all_release_versions = function(repo)
return spawn
.gh({ "api", ("repos/%s/releases"):format(repo) })
:map(_.prop "stdout")
:map_catching(vim.json.decode)
:map(_.map(_.prop "tag_name"))
end,
get_latest_tag = function(repo)
return Result.failure "Unimplemented"
end,
get_all_tags = function(repo)
return spawn
.gh({ "api", ("repos/%s/git/matching-refs/tags"):format(repo) })
:map(_.prop "stdout")
:map_catching(vim.json.decode)
:map(_.map(_.compose(_.gsub("^refs/tags/", ""), _.prop "ref")))
end,
}
6 changes: 6 additions & 0 deletions lua/mason/providers/client/init.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---@type Provider
return {
gh = require "mason.providers.client.gh",
npm = require "mason.providers.client.npm",
pypi = require "mason.providers.client.pypi",
}
16 changes: 16 additions & 0 deletions lua/mason/providers/client/npm.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
local spawn = require "mason-core.spawn"
local _ = require "mason-core.functional"

---@type NpmProvider
return {
get_latest_version = function(pkg)
return spawn
.npm({ "view", "--json", pkg .. "@latest" })
:map(_.prop "stdout")
:map_catching(vim.json.decode)
:map(_.pick { "name", "version" })
end,
get_all_versions = function(pkg)
return spawn.npm({ "view", pkg, "versions" }):map(_.prop "stdout"):map_catching(vim.json.decode)
end,
}
Loading

0 comments on commit 37c745f

Please sign in to comment.