From 455ed568a6ffb7b66f9425abc662ccceb7a9e5e6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 23:17:54 +0000 Subject: [PATCH] Rename the format plugin crates: forge-format-api and FormatSpec Formats are crates linked into the server, not plugins, so the names say so: - `crates/plugin-api` (`forge-plugin-api`) is `crates/format-api` (`forge-format-api`), and its trait `FormatPlugin` is `FormatSpec`. - `crates/plugin` (`forge-plugin`) is gone: its adapter, `PluginFormat`, is `forge_core::format::SpecFormat`, and its descriptor checks are `forge_core::format::descriptor`. forge-core already depended on the contract crate, and its tests no longer need a dev-dependency cycle. - The server's `plugins` module is `formats`, its registry's `plugin(s)`/`with_plugin(s)`/`images_plugin` are `spec(s)`, `with_spec(s)`, `images_spec`, and `tests/plugin_contract.rs` is `tests/format_contract.rs`. - Comments, docs and AGENTS.md say "format" or "format crate". The internal-error text and one log line follow ("format failed", the field `format`). Kept: the telemetry surfaces `plugin_npm`, `plugin_raw` and `plugin_other`, which dashboards and alerts select on, and the file name of docs/format-plugins.md (now titled "Format crates"), which CHANGELOG.md links to. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_012e6DJXVot5LKkJJWpxY1bT --- .github/codeql/codeql-config.yml | 3 +- AGENTS.md | 5 +- Cargo.lock | 33 +- Cargo.toml | 3 +- DEVELOPMENT.md | 14 +- README.md | 33 +- crates/core/Cargo.toml | 11 +- crates/core/src/domain.rs | 4 +- .../src => core/src/format}/descriptor.rs | 16 +- crates/core/src/{format.rs => format/mod.rs} | 31 +- .../src/format.rs => core/src/format/spec.rs} | 120 +++---- crates/core/src/lib.rs | 2 +- crates/core/src/repo/mod.rs | 10 +- crates/core/src/repo/proxy.rs | 12 +- crates/core/src/retention/worker.rs | 4 +- crates/core/tests/group_read.rs | 7 +- crates/core/tests/hosted_read.rs | 7 +- crates/core/tests/hosted_write.rs | 7 +- crates/core/tests/proxy_documents.rs | 4 +- crates/core/tests/proxy_read.rs | 25 +- crates/core/tests/retention_worker.rs | 7 +- crates/db/src/component_dao.rs | 2 +- crates/db/src/tag_dao.rs | 4 +- crates/{plugin-api => format-api}/Cargo.toml | 6 +- .../{plugin-api => format-api}/src/files.rs | 4 +- .../{plugin-api => format-api}/src/images.rs | 58 ++-- crates/{plugin-api => format-api}/src/lib.rs | 26 +- .../{plugin-api => format-api}/src/testing.rs | 54 ++-- .../{plugin-api => format-api}/src/types.rs | 168 +++++----- crates/formats/README.md | 19 +- crates/formats/apt/Cargo.toml | 6 +- crates/formats/apt/src/import.rs | 2 +- crates/formats/apt/src/index.rs | 2 +- crates/formats/apt/src/layout.rs | 4 +- crates/formats/apt/src/lib.rs | 14 +- crates/formats/apt/src/manifest.rs | 4 +- crates/formats/apt/src/proxy.rs | 8 +- crates/formats/docker/Cargo.toml | 6 +- crates/formats/docker/src/digest.rs | 4 +- crates/formats/docker/src/error.rs | 4 +- crates/formats/docker/src/lib.rs | 18 +- crates/formats/docker/src/manifest.rs | 2 +- crates/formats/docker/src/protocol.rs | 4 +- crates/formats/docker/src/upstream.rs | 6 +- crates/formats/npm/Cargo.toml | 6 +- .../formats/npm/examples/packument_memory.rs | 8 +- crates/formats/npm/src/lib.rs | 24 +- crates/formats/npm/src/protocol.rs | 8 +- crates/formats/npm/src/proxy.rs | 16 +- crates/formats/npm/tests/protocol.rs | 19 +- crates/formats/nuget/Cargo.toml | 6 +- crates/formats/nuget/src/lib.rs | 14 +- crates/formats/nuget/src/model.rs | 16 +- crates/formats/nuget/src/nuspec.rs | 2 +- crates/formats/nuget/src/protocol.rs | 2 +- crates/formats/nuget/src/upstream.rs | 8 +- crates/formats/nuget/tests/parse.rs | 2 +- crates/formats/nuget/tests/protocol.rs | 16 +- crates/formats/nuget/tests/upstream.rs | 10 +- crates/formats/raw/Cargo.toml | 6 +- crates/formats/raw/src/form.rs | 6 +- crates/formats/raw/src/lib.rs | 22 +- crates/formats/raw/src/listing.rs | 10 +- crates/formats/raw/src/sidecar.rs | 6 +- crates/formats/raw/tests/protocol.rs | 8 +- crates/plugin/Cargo.toml | 14 - crates/plugin/src/lib.rs | 21 -- crates/server/Cargo.toml | 6 +- crates/server/src/admin/formats.rs | 23 +- crates/server/src/admin/imports.rs | 2 +- crates/server/src/admin/mod.rs | 4 +- crates/server/src/admin/proxy_cache.rs | 8 +- crates/server/src/admin/repositories.rs | 54 ++-- crates/server/src/app.rs | 18 +- crates/server/src/auth/trusted_header.rs | 2 +- crates/server/src/browse.rs | 12 +- crates/server/src/browse_images.rs | 16 +- crates/server/src/cli_auth.rs | 6 +- crates/server/src/config.rs | 6 +- crates/server/src/dispatch.rs | 2 +- crates/server/src/feed.rs | 2 +- crates/server/src/files/listing.rs | 4 +- crates/server/src/files/mod.rs | 13 +- crates/server/src/files/proxy.rs | 28 +- crates/server/src/files/serve.rs | 2 +- crates/server/src/files/store.rs | 6 +- crates/server/src/files/web.rs | 2 +- crates/server/src/{plugins.rs => formats.rs} | 292 +++++++++--------- crates/server/src/images/auth.rs | 4 +- crates/server/src/images/dispatch.rs | 31 +- crates/server/src/images/group.rs | 2 +- crates/server/src/images/import.rs | 8 +- crates/server/src/images/mod.rs | 20 +- crates/server/src/images/proxy.rs | 10 +- crates/server/src/images/pull.rs | 2 +- crates/server/src/images/push.rs | 8 +- crates/server/src/import/feed.rs | 2 +- crates/server/src/import/mod.rs | 8 +- crates/server/src/import/raw.rs | 2 +- crates/server/src/indexes/mod.rs | 40 +-- crates/server/src/lib.rs | 2 +- crates/server/src/registry.rs | 6 +- crates/server/src/state.rs | 124 ++++---- crates/server/src/telemetry.rs | 2 +- crates/server/tests/common/mod.rs | 10 +- ...{plugin_contract.rs => format_contract.rs} | 31 +- crates/server/tests/nuget_publish.rs | 4 +- crates/server/tests/raw_import.rs | 4 +- crates/server/tests/registry.rs | 2 +- crates/telemetry/src/lib.rs | 12 +- docs/README.md | 4 +- docs/SUMMARY.md | 2 +- docs/apt.md | 2 +- docs/docker.md | 4 +- docs/format-plugins.md | 213 +++++++------ docs/npm.md | 2 +- docs/raw.md | 4 +- docs/retention.md | 2 +- web/src/api/repositories.ts | 2 +- web/src/routes/Browse.test.tsx | 2 +- .../admin/repositories/repositories.test.tsx | 4 +- 121 files changed, 1032 insertions(+), 1084 deletions(-) rename crates/{plugin/src => core/src/format}/descriptor.rs (96%) rename crates/core/src/{format.rs => format/mod.rs} (91%) rename crates/{plugin/src/format.rs => core/src/format/spec.rs} (84%) rename crates/{plugin-api => format-api}/Cargo.toml (53%) rename crates/{plugin-api => format-api}/src/files.rs (99%) rename crates/{plugin-api => format-api}/src/images.rs (89%) rename crates/{plugin-api => format-api}/src/lib.rs (95%) rename crates/{plugin-api => format-api}/src/testing.rs (80%) rename crates/{plugin-api => format-api}/src/types.rs (90%) delete mode 100644 crates/plugin/Cargo.toml delete mode 100644 crates/plugin/src/lib.rs rename crates/server/src/{plugins.rs => formats.rs} (90%) rename crates/server/tests/{plugin_contract.rs => format_contract.rs} (96%) diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml index e9febddb..ca6bcd02 100644 --- a/.github/codeql/codeql-config.yml +++ b/.github/codeql/codeql-config.yml @@ -15,8 +15,7 @@ paths-ignore: # instead, as a release build compiles it. - "**/tests" - crates/e2e - - crates/plugin/src/testing.rs - - crates/plugin-api/src/testing.rs + - crates/format-api/src/testing.rs # The web UI: unit tests next to the code, the Playwright suite with its # mock upstream, and the test runners' configuration. - "**/*.test.ts" diff --git a/AGENTS.md b/AGENTS.md index cbad603b..e1fbbf54 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -55,8 +55,9 @@ its pull request's description says why. A package format is a crate in `crates/formats//`, the directory named after its format id, linked into the server like `crates/formats/nuget` (Forge loads no WebAssembly plugins). Never add a format as a crate elsewhere -under `crates/` (no `crates/format-`) or as a module of the server: it -implements `FormatPlugin` with the layout +under `crates/` (no `crates/format-`: `crates/format-api` is the contract +the formats implement, not a format) or as a module of the server: it +implements `FormatSpec` with the layout [crates/formats/README.md](crates/formats/README.md) describes. Docker / OCI (`crates/formats/docker`) is one too, an images format: it speaks the registry protocol, and the server's image store (`crates/server/src/images`) diff --git a/Cargo.lock b/Cargo.lock index 0c4729d1..aef12e58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2176,9 +2176,8 @@ dependencies = [ "forge-cache", "forge-crypto", "forge-db", + "forge-format-api", "forge-format-nuget", - "forge-plugin", - "forge-plugin-api", "forge-storage", "forge-telemetry", "futures-util", @@ -2293,13 +2292,17 @@ dependencies = [ "zip", ] +[[package]] +name = "forge-format-api" +version = "2.1.0" + [[package]] name = "forge-format-apt" version = "2.1.0" dependencies = [ "chrono", "flate2", - "forge-plugin-api", + "forge-format-api", "hex", "lzma-rust2", "ruzstd", @@ -2315,7 +2318,7 @@ name = "forge-format-docker" version = "2.1.0" dependencies = [ "flate2", - "forge-plugin-api", + "forge-format-api", "hex", "serde", "serde_json", @@ -2331,7 +2334,7 @@ dependencies = [ "base64 0.23.1", "chrono", "flate2", - "forge-plugin-api", + "forge-format-api", "hex", "semver", "serde", @@ -2347,7 +2350,7 @@ version = "2.1.0" dependencies = [ "crc32fast", "flate2", - "forge-plugin-api", + "forge-format-api", "quick-xml", "serde", "serde_json", @@ -2360,7 +2363,7 @@ name = "forge-format-raw" version = "2.1.0" dependencies = [ "base64 0.23.1", - "forge-plugin-api", + "forge-format-api", "httpdate", "percent-encoding", "serde", @@ -2368,20 +2371,6 @@ dependencies = [ "url", ] -[[package]] -name = "forge-plugin" -version = "2.1.0" -dependencies = [ - "forge-core", - "forge-plugin-api", - "serde_json", - "tracing", -] - -[[package]] -name = "forge-plugin-api" -version = "2.1.0" - [[package]] name = "forge-server" version = "2.1.0" @@ -2401,12 +2390,12 @@ dependencies = [ "forge-core", "forge-crypto", "forge-db", + "forge-format-api", "forge-format-apt", "forge-format-docker", "forge-format-npm", "forge-format-nuget", "forge-format-raw", - "forge-plugin", "forge-storage", "forge-telemetry", "forge-traffic", diff --git a/Cargo.toml b/Cargo.toml index b17541dc..cf325ede 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,8 +8,7 @@ members = [ "crates/crypto", "crates/telemetry", "crates/traffic", - "crates/plugin-api", - "crates/plugin", + "crates/format-api", "crates/server", "crates/e2e", "crates/formats/apt", diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 7d014156..9a7a9b92 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -268,23 +268,23 @@ password: `docker login localhost:8421 -u admin@example.com`, then `docker push localhost:8421/docker-hosted/team/app:1`. See [docs/docker.md](docs/docker.md). -Raw repositories (`"format":"raw"`, the built-in plugin `crates/formats/raw`) +Raw repositories (`"format":"raw"`, the format crate `crates/formats/raw`) hold files under arbitrary paths, served under `/repository/`: `curl -u admin@example.com: --upload-file x.tgz localhost:8421/repository/binaries/tools/x.tgz`, a proxy of any HTTP(S) base URL (`"remote_url":"https://github.com///releases/download/"`), and groups. See [docs/raw.md](docs/raw.md). -### Repositories of other formats (format plugins) +### Repositories of other formats -Every format is a format plugin, a crate in +Every format is a crate in [`crates/formats/`](./crates/formats) linked into the server (see [docs/format-plugins.md](./docs/format-plugins.md)): Docker / OCI, NuGet, npm, raw and apt. A new format goes in `crates/formats/` too, never in a `crates/format-` crate or the server. Docker / OCI is an images format, -served under `/v2` over the server's image store. A repository of a plugin -format is created with `"format": ""` (hosted, proxy or group, the kinds -the format's descriptor lists: apt has no groups) and served under +served under `/v2` over the server's image store. A repository of a format +is created with `"format": ""` (hosted, proxy or group, the kinds the +format's descriptor lists: apt has no groups) and served under `/repository//`. Helm charts are pushed to docker repositories, see [docs/docker.md](docs/docker.md#helm-charts). @@ -1146,7 +1146,7 @@ URLs to local test servers are on purpose, and an alert for each would only be dismissed by hand. The configuration's `paths-ignore` leaves out the test directories (`**/tests`, `crates/e2e`, `web/e2e`), the web UI's `*.test.ts(x)` and test runner configuration, the `testing` helpers of -`crates/plugin` and `crates/plugin-api`, and `fixtures/`. The unit tests +`crates/format-api`, and `fixtures/`. The unit tests inside the Rust source files (`#[cfg(test)] mod tests`) cannot be left out by path; the Rust extractor compiles the code with `cfg(test)` off instead (`CODEQL_EXTRACTOR_RUST_OPTION_CARGO_CFG_OVERRIDES=-test`), as a release build diff --git a/README.md b/README.md index ee5015be..ff817fb3 100644 --- a/README.md +++ b/README.md @@ -43,11 +43,11 @@ token for a short-lived API key ([docs/trusted-publishing.md](docs/trusted-publishing.md), which also covers its limitations). A **retention** engine reaps cold versions and garbage-collects dereferenced -blobs. Every format is a **format plugin**: a crate built into the server -that speaks the format's protocol, while the server keeps all of the above -the same for every format ([docs/format-plugins.md](docs/format-plugins.md)). -The admin + browse UI is a **TanStack Router SPA** served by the same Rust -process on a single origin — there is no Node server in production. +blobs. Every format is a **format crate** built into the server: it speaks +the format's protocol, while the server keeps all of the above the same for +every format ([docs/format-plugins.md](docs/format-plugins.md)). The admin + +browse UI is a **TanStack Router SPA** served by the same Rust process on a +single origin — there is no Node server in production. ## Architecture @@ -61,22 +61,21 @@ A Cargo workspace of small crates plus the web SPA: | `crypto` | `forge-crypto` | Secret encryption at rest: the envelope encryption of stored secrets (proxy passwords, realm secrets, signing keys) under a root key. | | `telemetry` | `forge-telemetry` | OpenTelemetry metrics and the `tracing` spans that become traces. | | `traffic` | `forge-traffic` | Traffic statistics: requests per repository and client network per day, kept as Parquet files in the object store. | -| `core` | `forge-core` | Domain logic: repo dispatch, auth chain, authz/RBAC, sessions, OIDC, retention. | -| `plugin-api` | `forge-plugin-api` | The format-plugin contract: the `FormatPlugin` trait and its types. | -| `plugin` | `forge-plugin` | The host side of format plugins: the adapter of a plugin to the repository machinery, and the checks of its descriptor. | -| `formats/` | `forge-format-*` | Format plugins, built into the server: `apt`, `docker` (the distribution-spec `/v2` API, manifests, upstream registries), `nuget`, `npm`, `raw`. | -| `server` | `forge-server` | Axum HTTP server: the format plugins' feeds under `/repository/{repo}` (except Docker at `/v2`), the file store of files formats, the image store and registry tokens of images formats, `/admin` + `/auth` REST, background workers, static SPA serving. | +| `core` | `forge-core` | Domain logic: repo dispatch, the formats' adapter to it (`SpecFormat`), auth chain, authz/RBAC, sessions, OIDC, retention. | +| `format-api` | `forge-format-api` | The format contract: the `FormatSpec` trait every format implements, and its types. | +| `formats/` | `forge-format-*` | The formats, built into the server: `apt`, `docker` (the distribution-spec `/v2` API, manifests, upstream registries), `nuget`, `npm`, `raw`. | +| `server` | `forge-server` | Axum HTTP server: the formats' feeds under `/repository/{repo}` (except Docker at `/v2`), the file store of files formats, the image store and registry tokens of images formats, `/admin` + `/auth` REST, background workers, static SPA serving. | | `e2e` | `forge-e2e` | End-to-end suite (testcontainers + mock upstream/IdP). | | `xtask` | — | The task runner (`cargo xtask …`); replaces Make/Just. | | `web/` | — | TanStack Router SPA (pnpm + Vite), built into `web/dist/`. | -**Every format is a plugin in `crates/formats/`.** A format is a crate in -`crates/formats//` implementing `FormatPlugin`, linked into the server. -Docker / OCI is one too, an images format: the plugin speaks the protocol, +**Every format is a crate in `crates/formats/`.** A format is a crate in +`crates/formats//` implementing `FormatSpec`, linked into the server. +Docker / OCI is one too, an images format: the crate speaks the protocol, and the server's image store (`crates/server/src/images`) keeps the images. -**Single-origin serving.** `forge-server` serves every non-Docker format plugin +**Single-origin serving.** `forge-server` serves every non-Docker format at `/repository/{repo}`, the Docker / OCI registry at `/v2`, the admin/browse APIs (`/admin`, `/browse`, `/auth`) and then serves the built SPA from `web/dist/` via a `ServeDir` SPA fallback. The browser talks to the API on @@ -257,9 +256,9 @@ The guides below are also a site, with navigation and search: pipelines that cannot use workload identity: a read token and a release token of service accounts in GitLab's CI/CD variables, and how both methods run side by side. -- **[Format plugins](./docs/format-plugins.md)** — how every format is - built: the `FormatPlugin` contract, what the server does for each format, - and adding a new one. +- **[Format crates](./docs/format-plugins.md)** — how every format is built: + the `FormatSpec` contract, what the server does for each format, and + adding a new one. - **[Importing repositories](./docs/import.md)** — copy a hosted repository of Nexus, Artifactory, Verdaccio, a registry or another Forge (NuGet, npm, Docker, raw, apt) into Forge through the format's own protocol: dry run, diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index bf9410c1..55ac502b 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -13,9 +13,10 @@ forge-cache = { path = "../cache" } # Secret encryption at rest: the proxy upstream fetch decrypts the # upstream password through the envelope service (the single sanctioned site). forge-crypto = { path = "../crypto" } -# The format-plugin contract: the upstream conversation of proxy repositories -# (`Format::upstream`) speaks its types. Nothing but std. -forge-plugin-api = { path = "../plugin-api" } +# The contract every format implements (`FormatSpec`): the upstream +# conversation of proxy repositories (`Format::upstream`) speaks its types, and +# `format::SpecFormat` adapts a format to `Format`. Nothing but std. +forge-format-api = { path = "../format-api" } forge-telemetry = { path = "../telemetry" } tokio = { workspace = true } @@ -73,10 +74,8 @@ flate2 = { version = "1.1", default-features = false, features = ["zlib-rs"] } [dev-dependencies] tokio = { workspace = true, features = ["rt-multi-thread", "macros", "fs", "io-util", "time", "net", "test-util"] } -# NuGet, the format most integration tests use: the built-in plugin and its -# Format (a dev-only cycle through forge-plugin, which Cargo permits). +# NuGet, the format most integration tests use. forge-format-nuget = { path = "../formats/nuget" } -forge-plugin = { path = "../plugin" } argon2 = { workspace = true, features = ["password-hash"] } zip = { version = "9.0", default-features = false, features = ["deflate"] } testcontainers = "0.27" diff --git a/crates/core/src/domain.rs b/crates/core/src/domain.rs index 61fa0d0b..222f57b0 100644 --- a/crates/core/src/domain.rs +++ b/crates/core/src/domain.rs @@ -76,8 +76,8 @@ pub enum FormatError { Io(#[from] std::io::Error), } -/// The text of [`FormatError::InvalidVersion`]: a format plugin's reason -/// names the version itself, so it comes without the value. +/// The text of [`FormatError::InvalidVersion`]: a format's reason names the +/// version itself, so it comes without the value. fn invalid_version(value: &str, reason: &str) -> String { if value.is_empty() { format!("invalid version: {reason}") diff --git a/crates/plugin/src/descriptor.rs b/crates/core/src/format/descriptor.rs similarity index 96% rename from crates/plugin/src/descriptor.rs rename to crates/core/src/format/descriptor.rs index dba8d883..ef6170dd 100644 --- a/crates/plugin/src/descriptor.rs +++ b/crates/core/src/format/descriptor.rs @@ -1,12 +1,12 @@ //! What a format's [`Descriptor`] may say: a well-formed id that no other //! format and none of the server's names takes, its repository kinds once -//! each, plain asset names and content types. A test over the server's built-in formats -//! (`plugins::builtin`) checks them, so a bad descriptor fails `cargo test`. +//! each, plain asset names and content types. A test over the server's +//! formats (`formats::builtin`) checks them, so a bad descriptor fails +//! `cargo test`. -use forge_plugin_api::{Assets, Descriptor, OptionInput, Storage}; +use forge_format_api::{Assets, Descriptor, OptionInput, Storage}; -/// Format ids a plugin can never take: names the server routes or -/// reserves. +/// Ids a format can never take: names the server routes or reserves. const RESERVED_IDS: &[&str] = &["v2", "admin", "browse", "auth", "repository"]; /// Check the descriptors of the formats one server holds: each on its own @@ -151,7 +151,7 @@ fn validate_assets(assets: &Assets) -> Result<(), String> { #[cfg(test)] mod tests { - use forge_plugin_api::{RepositoryKind, Storage, UpstreamToken}; + use forge_format_api::{RepositoryKind, Storage, UpstreamToken}; use super::*; @@ -164,7 +164,7 @@ mod tests { assets: Assets::DEFAULT, upstream_token: UpstreamToken::None, options: &[], - prereleases: forge_plugin_api::Prereleases::Dash, + prereleases: forge_format_api::Prereleases::Dash, indexes: false, }; @@ -203,7 +203,7 @@ mod tests { #[test] fn checks_options_and_indexes() { - use forge_plugin_api::FormatOption; + use forge_format_api::FormatOption; const OPTION: FormatOption = FormatOption { name: "distribution", label: "Distribution", diff --git a/crates/core/src/format.rs b/crates/core/src/format/mod.rs similarity index 91% rename from crates/core/src/format.rs rename to crates/core/src/format/mod.rs index 26f6c85b..acce0aa0 100644 --- a/crates/core/src/format.rs +++ b/crates/core/src/format/mod.rs @@ -4,11 +4,23 @@ //! **object-safe**: no generic methods, no `Self`-typed returns. Parsing reads //! from a `&mut dyn ReadSeek` (formats such as ZIP-based NuGet packages need to //! seek), keeping the trait usable both with files and in-memory cursors. +//! +//! Every format is a crate under `crates/formats//` implementing +//! [`FormatSpec`](forge_format_api::FormatSpec) (crate `forge-format-api`): +//! its identity rules, its package parser and its client protocol. +//! [`SpecFormat`] adapts one to `Format`, so hosted publishing, reads, groups +//! and retention work for it unchanged, and [`descriptor::check`] checks +//! what the formats say about themselves. + +pub mod descriptor; +mod spec; + +pub use spec::{SpecFormat, UploadFormat}; use std::cmp::Ordering; -pub use forge_plugin_api::VersionClass; -use forge_plugin_api::{FetchResult, Remote, UpstreamOp, UpstreamStep}; +pub use forge_format_api::VersionClass; +use forge_format_api::{FetchResult, Remote, UpstreamOp, UpstreamStep}; use crate::domain::{FormatError, ParsedUpload}; @@ -66,8 +78,9 @@ pub trait Format: Send + Sync { /// [`Self::parse_upload`] for a package staged on disk at `path` (what /// the hosted publish path parses). The default opens the file and reads - /// it through `parse_upload`; a format that needs the file itself (a - /// format plugin lends it to its sandbox) overrides it. + /// it through `parse_upload`; a format that needs the file itself + /// ([`SpecFormat`], through `FormatSpec::parse_package_file`) overrides + /// it. fn parse_upload_file(&self, path: &std::path::Path) -> Result { let mut file = std::fs::File::open(path)?; self.parse_upload(&mut file) @@ -94,7 +107,7 @@ pub trait Format: Send + Sync { /// Retention: the class of a **normalized** version (release, prerelease /// or snapshot). The default is SemVer's rule: a version with a `-` is a /// prerelease (build metadata is gone once normalized), any other a - /// release. See [`forge_plugin_api::FormatPlugin::version_class`]. + /// release. See [`forge_format_api::FormatSpec::version_class`]. fn version_class(&self, version: &str) -> VersionClass { if version.contains('-') { VersionClass::Prerelease @@ -105,7 +118,7 @@ pub trait Format: Send + Sync { /// Retention: the partition of package `id`'s versions within which the /// keep-N rules count; `None` (the default) keeps the whole package one - /// partition. See [`forge_plugin_api::FormatPlugin::retention_partition`]. + /// partition. See [`forge_format_api::FormatSpec::retention_partition`]. fn retention_partition(&self, id: &str, version: &str) -> Option { let _ = (id, version); None @@ -145,9 +158,9 @@ pub trait Format: Send + Sync { /// The fields of [`Self::manifest_summary`] of which a hosted repository /// holds a bounded number of distinct values - /// ([`forge_plugin_api::FormatPlugin::summary_caps`]): a publish or an + /// ([`forge_format_api::FormatSpec::summary_caps`]): a publish or an /// import that would add a value past one is refused. None by default. - fn summary_caps(&self) -> &'static [forge_plugin_api::SummaryCap] { + fn summary_caps(&self) -> &'static [forge_format_api::SummaryCap] { &[] } @@ -205,7 +218,7 @@ pub trait Format: Send + Sync { } /// One step of the upstream side of a proxy repository - /// ([`forge_plugin_api::FormatPlugin::upstream`]): what to fetch from + /// ([`forge_format_api::FormatSpec::upstream`]): what to fetch from /// `remote` for `op`, or what came of it. The proxy read path /// ([`crate::repo::proxy::ProxyRepo`]) performs the fetches and calls /// again with everything fetched so far. The default has no upstream: a diff --git a/crates/plugin/src/format.rs b/crates/core/src/format/spec.rs similarity index 84% rename from crates/plugin/src/format.rs rename to crates/core/src/format/spec.rs index cc27d0cc..d3418fb3 100644 --- a/crates/plugin/src/format.rs +++ b/crates/core/src/format/spec.rs @@ -1,60 +1,60 @@ -//! [`forge_core::Format`] for a format plugin. +//! [`Format`] for a [`FormatSpec`]. use std::cmp::Ordering; use std::path::Path; use std::sync::Arc; -use forge_core::domain::{Dependency, DependencyGroup, FormatError, ParsedUpload}; +use crate::domain::{Dependency, DependencyGroup, FormatError, ParsedUpload}; use std::sync::OnceLock; -use forge_core::format::{Format, ReadSeek}; -use forge_plugin_api::{self as api, Assets, FormatPlugin, Storage, UpstreamToken}; +use crate::format::{Format, ReadSeek}; +use forge_format_api::{self as api, Assets, FormatSpec, Storage, UpstreamToken}; -/// The longest version the host takes from a format plugin, in bytes: a -/// version to normalize as given, its normalized form, and a parsed -/// package's version as written and normalized. Versions are stored in the +/// The longest version the host takes from a format, in bytes: a version to +/// normalize as given, its normalized form, and a parsed package's version +/// as written and normalized. Versions are stored in the /// `UNIQUE (repository_id, name, version)` indexes of `component` and /// `proxy_cached_version`, and PostgreSQL refuses a B-tree index entry -/// larger than about 2,700 bytes: a plugin without a limit of its own let a +/// larger than about 2,700 bytes: a format without a limit of its own let a /// multi-kilobyte version through to that insert, so a push answered 500 /// instead of 400, and so did a proxy asked for such a version. 256 is above -/// NuGet's own limit (64 characters) and, -/// with [`MAX_ID_LENGTH`], keeps an entry far below PostgreSQL's. +/// NuGet's own limit (64 characters) and, with [`MAX_ID_LENGTH`], keeps an +/// entry far below PostgreSQL's. const MAX_VERSION_LENGTH: usize = 256; -/// The longest package id the host takes from a format plugin, in bytes: -/// an id to validate, and a parsed package's id as written and canonical. -/// Ids share the indexes of [`MAX_VERSION_LENGTH`]. 512 is above NuGet's -/// own limit (100 characters, at most 400 bytes in UTF-8). +/// The longest package id the host takes from a format, in bytes: an id to +/// validate, and a parsed package's id as written and canonical. Ids share +/// the indexes of [`MAX_VERSION_LENGTH`]. 512 is above NuGet's own limit +/// (100 characters, at most 400 bytes in UTF-8). const MAX_ID_LENGTH: usize = 512; -/// A format plugin as a [`Format`]: what the hosted write path, the read +/// A [`FormatSpec`] as a [`Format`]: what the hosted write path, the read /// ops, the proxy read path and retention work with. #[derive(Clone)] -pub struct PluginFormat { +pub struct SpecFormat { id: &'static str, - plugin: Arc, + spec: Arc, assets: Assets, storage: Storage, upstream_token: UpstreamToken, } -impl PluginFormat { - /// Adapt `plugin`. - pub fn new(plugin: Arc) -> Self { - let descriptor = plugin.descriptor(); - PluginFormat { +impl SpecFormat { + /// Adapt `spec`. + pub fn new(spec: Arc) -> Self { + let descriptor = spec.descriptor(); + SpecFormat { id: descriptor.id, assets: descriptor.assets, storage: descriptor.storage, upstream_token: descriptor.upstream_token, - plugin, + spec, } } - /// The format of one publish through the plugin's protocol: the package + /// The format of one publish through the format's protocol: the package /// is parsed with the document it came in, when there is one - /// ([`FormatPlugin::parse_upload`]; the host sets + /// ([`FormatSpec::parse_upload`]; the host sets /// [`UploadFormat::document`] once the body is read, before the staged /// package is parsed), must have the canonical id `expected_id`, when /// one is given, and its identity is recorded in @@ -75,9 +75,9 @@ impl PluginFormat { } } -/// See [`PluginFormat::upload`]. +/// See [`SpecFormat::upload`]. pub struct UploadFormat { - format: PluginFormat, + format: SpecFormat, /// The rest of the document the package came in, for an /// [`api::Upload::Document`]. pub document: Option>>>, @@ -120,9 +120,9 @@ impl Format for UploadFormat { document, publisher: self.publisher.as_deref(), }; - self.format.plugin.parse_upload(&upload, &mut file) + self.format.spec.parse_upload(&upload, &mut file) } - None => self.format.plugin.parse_package(&mut file), + None => self.format.spec.parse_package(&mut file), } .map_err(format_error) .and_then(parsed_upload)?; @@ -210,7 +210,7 @@ fn check_version(version: &str) -> Result<(), FormatError> { Ok(()) } -/// A package the plugin parsed, as the host stores it, unless its id or +/// A package the format parsed, as the host stores it, unless its id or /// version is longer than the host takes ([`MAX_ID_LENGTH`], /// [`MAX_VERSION_LENGTH`]): a push of it is refused (400). fn parsed_upload(parsed: api::ParsedPackage) -> Result { @@ -243,7 +243,7 @@ fn parsed_upload(parsed: api::ParsedPackage) -> Result &'static str { self.id } @@ -274,55 +274,55 @@ impl Format for PluginFormat { fn parse_upload(&self, mut file: &mut dyn ReadSeek) -> Result { file.rewind()?; - self.plugin + self.spec .parse_package(&mut file) .map_err(format_error) .and_then(parsed_upload) } fn parse_upload_file(&self, path: &Path) -> Result { - self.plugin + self.spec .parse_package_file(path) .map_err(format_error) .and_then(parsed_upload) } - /// The plugin's normalized form of `raw`, refused when either is longer + /// The format's normalized form of `raw`, refused when either is longer /// than [`MAX_VERSION_LENGTH`] (a read naming it answers 404). fn normalize_version(&self, raw: &str) -> Result { - // Before the plugin, so an oversized version never reaches it. + // Before the format, so an oversized version never reaches it. check_version(raw)?; - let normalized = self.plugin.normalize_version(raw).map_err(format_error)?; + let normalized = self.spec.normalize_version(raw).map_err(format_error)?; check_version(&normalized)?; Ok(normalized) } fn compare_versions(&self, a: &str, b: &str) -> Ordering { - self.plugin.compare_versions(a, b) + self.spec.compare_versions(a, b) } fn has_prereleases(&self) -> bool { - self.plugin.descriptor().prereleases != api::Prereleases::None + self.spec.descriptor().prereleases != api::Prereleases::None } fn lower_id(&self, id: &str) -> String { - self.plugin.canonical_id(id) + self.spec.canonical_id(id) } - fn version_class(&self, version: &str) -> forge_core::format::VersionClass { - self.plugin.version_class(version) + fn version_class(&self, version: &str) -> crate::format::VersionClass { + self.spec.version_class(version) } fn retention_partition(&self, id: &str, version: &str) -> Option { - self.plugin.retention_partition(id, version) + self.spec.retention_partition(id, version) } - /// Whether the plugin takes `id`, which is never longer than + /// Whether the format takes `id`, which is never longer than /// [`MAX_ID_LENGTH`]. fn validate_id(&self, id: &str) -> Result<(), FormatError> { - // Before the plugin, so an oversized id never reaches it. + // Before the format, so an oversized id never reaches it. check_id(id)?; - self.plugin.validate_id(id).map_err(format_error) + self.spec.validate_id(id).map_err(format_error) } fn upstream( @@ -331,7 +331,7 @@ impl Format for PluginFormat { op: &api::UpstreamOp, fetched: &[api::FetchResult], ) -> Result { - self.plugin + self.spec .upstream(remote, op, fetched) .map_err(format_error) } @@ -341,20 +341,20 @@ impl Format for PluginFormat { } fn readme(&self, manifest: &[u8]) -> Option { - self.plugin.readme(manifest) + self.spec.readme(manifest) } fn reserved_prefix(&self, prefix: &str) -> Result { - self.plugin.reserved_prefix(prefix).map_err(format_error) + self.spec.reserved_prefix(prefix).map_err(format_error) } fn manifest_summary(&self, manifest: &[u8]) -> Option { - let text = self.plugin.manifest_summary(manifest)?; + let text = self.spec.manifest_summary(manifest)?; match serde_json::from_str::(&text) { Ok(summary @ serde_json::Value::Object(_)) => Some(summary), _ => { tracing::warn!( - plugin = self.id, + format = self.id, "manifest-summary returned something other than a JSON object; not stored" ); None @@ -363,7 +363,7 @@ impl Format for PluginFormat { } fn summary_caps(&self) -> &'static [api::SummaryCap] { - self.plugin.summary_caps() + self.spec.summary_caps() } } @@ -388,11 +388,11 @@ mod tests { assets: Assets::DEFAULT, upstream_token: UpstreamToken::None, options: &[], - prereleases: forge_plugin_api::Prereleases::Dash, + prereleases: forge_format_api::Prereleases::Dash, indexes: false, }; - impl FormatPlugin for Unlimited { + impl FormatSpec for Unlimited { fn descriptor(&self) -> &'static api::Descriptor { &UNLIMITED } @@ -455,8 +455,8 @@ mod tests { } /// [`Unlimited`] as the host sees it. - fn unlimited(normalized: Option<&str>, parsed: api::ParsedPackage) -> PluginFormat { - PluginFormat::new(Arc::new(Unlimited { + fn unlimited(normalized: Option<&str>, parsed: api::ParsedPackage) -> SpecFormat { + SpecFormat::new(Arc::new(Unlimited { normalized: normalized.map(str::to_string), parsed, })) @@ -528,11 +528,11 @@ mod tests { } } - /// A plugin with retention hooks of its own: Maven-like snapshots, + /// A format with retention hooks of its own: Maven-like snapshots, /// counted per base version. struct Snapshots; - impl FormatPlugin for Snapshots { + impl FormatSpec for Snapshots { fn descriptor(&self) -> &'static api::Descriptor { &UNLIMITED } @@ -565,14 +565,14 @@ mod tests { #[test] fn retention_hooks_reach_the_host() { - use forge_core::format::VersionClass; + use crate::format::VersionClass; // The defaults: SemVer's prereleases, one partition per package. let plain = unlimited(None, package("demo", "1.0.0")); assert_eq!(plain.version_class("1.0.0"), VersionClass::Release); assert_eq!(plain.version_class("1.0.0-ci.1"), VersionClass::Prerelease); assert_eq!(plain.retention_partition("demo", "1.0.0-ci.1"), None); - // A plugin's own. - let snapshots = PluginFormat::new(Arc::new(Snapshots)); + // A format's own. + let snapshots = SpecFormat::new(Arc::new(Snapshots)); assert_eq!(snapshots.version_class("33.0.0-jre"), VersionClass::Release); assert_eq!( snapshots.version_class("1.0-SNAPSHOT"), diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 06f428a5..4ff663bf 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -1,6 +1,6 @@ //! `forge-core`: format-agnostic domain logic for Forge. //! -//! This crate sits above [`forge_db`] and below the format plugins +//! This crate sits above [`forge_db`] and below the format crates //! (`crates/formats/*`) and the server. It defines: //! //! * [`domain`] — neutral types ([`domain::ParsedUpload`]) produced by parsing diff --git a/crates/core/src/repo/mod.rs b/crates/core/src/repo/mod.rs index 8c993fa1..c29cbd44 100644 --- a/crates/core/src/repo/mod.rs +++ b/crates/core/src/repo/mod.rs @@ -2,7 +2,7 @@ //! //! The [`hosted`], [`proxy`] and [`group`] read paths live in their submodules. //! The neutral DTOs in this module are the contract between the storage-backed -//! implementations and the format plugins (served by the server's plugin +//! implementations and the formats (served by the server's format //! dispatcher): they are format-agnostic and carry exactly what a feed needs //! to render its JSON responses, **plus** the raw //! manifest bytes so the format layer can extract dependency groups itself @@ -75,9 +75,9 @@ pub enum RepoError { /// upstream or the proxy changes → 502. #[error("bad upstream: {0}")] BadUpstream(String), - /// The repository's format is not installed on this server (a format - /// plugin missing from the configuration) → 503: nothing of it is served - /// until the format is back, and it is never read as another format. + /// The repository's format is not one this server has (a format a + /// later build no longer links) → 503: nothing of it is served until the + /// format is back, and it is never read as another format. #[error("format {0:?} is not installed")] FormatUnavailable(String), } @@ -571,7 +571,7 @@ pub trait RepoReadOps: Send + Sync { /// [`Self::registration`] with every version's publish time, even where /// that costs more: a proxy asks its upstream with - /// [`forge_plugin_api::UpstreamOp::Dating`] (npm: the full packument, + /// [`forge_format_api::UpstreamOp::Dating`] (npm: the full packument, /// not the abbreviated one installs read). The default is /// [`Self::registration`], whose times hosted repositories know. async fn dated_registration( diff --git a/crates/core/src/repo/proxy.rs b/crates/core/src/repo/proxy.rs index b8db77a6..f3f99f5d 100644 --- a/crates/core/src/repo/proxy.rs +++ b/crates/core/src/repo/proxy.rs @@ -2,8 +2,8 @@ //! //! A proxy repository mirrors the upstream named by `repository.remote_url`. //! What the upstream speaks is its format's business: the format's upstream -//! protocol ([`Format::upstream`], a format plugin's -//! [`forge_plugin_api::FormatPlugin::upstream`]) says which documents to fetch +//! protocol ([`Format::upstream`], a format's +//! [`forge_format_api::FormatSpec::upstream`]) says which documents to fetch //! for an operation and reads them; this module performs every request, //! content-addresses package bytes through the [`Cas`], and records a //! `proxy_cached_version` row so future reads are served locally. Everything @@ -27,8 +27,8 @@ //! `remote_url` or to an origin an absolute URL names in a document fetched //! from the upstream for the same conversation, or while resolving its //! index ([`crate::net::origins`]). Anything else is not sent: a format -//! plugin may be code from elsewhere, and must not turn the proxy into a -//! way to read the services around Forge (#233). +//! may be code from elsewhere, and must not turn the proxy into a way to +//! read the services around Forge (#233). //! * **NO advisory locks, no DB transaction across network I/O** — first-time //! fetch singleflight is a plain row lock: //! [`proxy_dao::insert_unconfirmed_cached_version`] (INSERT … ON CONFLICT DO NOTHING) to @@ -156,7 +156,7 @@ use serde_json::Value as JsonValue; use sha2::{Digest, Sha256}; use sqlx::PgPool; -use forge_plugin_api as api; +use forge_format_api as api; use forge_telemetry::proxy::{DocumentSource, Kind as LookupKind, Lookup, Outcome}; use tracing::Instrument; @@ -655,7 +655,7 @@ impl ProxyRepo { /// far, until the format answers. `index` is the resolved upstream index /// (`None` while resolving it). Bounded to [`MAX_UPSTREAM_ROUNDS`] rounds /// and [`MAX_UPSTREAM_FETCHES`] fetches; the format runs off the async - /// workers, as a format plugin may take its time. + /// workers, as a format may take its time. /// /// The documents fetched are kept up to [`CONVERSATION_BUDGET_DOCUMENTS`] /// × `proxy.max_metadata_bytes` in all: a document past that is dropped diff --git a/crates/core/src/retention/worker.rs b/crates/core/src/retention/worker.rs index 351f3cfa..3537a456 100644 --- a/crates/core/src/retention/worker.rs +++ b/crates/core/src/retention/worker.rs @@ -319,8 +319,8 @@ impl RetentionWorker { .map_err(|e| WorkerError::Policy(format!("parse retention_policy: {e}")))?; validate_policy(&policy, kind) .map_err(|e| WorkerError::Policy(format!("invalid retention_policy: {e}")))?; - // A repository of a format that is not installed (a plugin removed - // from the configuration) cannot order its versions: fail the run + // A repository of a format this server does not have (one a later + // build no longer links) cannot order its versions: fail the run // without mutating anything until the format is back. let format = self.formats.resolve(&repo.format).ok_or_else(|| { WorkerError::Policy(format!("format {:?} is not installed", repo.format)) diff --git a/crates/core/tests/group_read.rs b/crates/core/tests/group_read.rs index 0dc7ec6f..22368ab3 100644 --- a/crates/core/tests/group_read.rs +++ b/crates/core/tests/group_read.rs @@ -37,10 +37,9 @@ use forge_db::repository_dao::{self, NewRepository}; use forge_db::user_dao::{self, NewUser}; use sqlx::PgPool; -/// NuGet, the built-in format plugin, as the `Format` the repository code -/// works with. -fn nuget() -> forge_plugin::PluginFormat { - forge_plugin::PluginFormat::new(std::sync::Arc::new(forge_format_nuget::Nuget::default())) +/// The NuGet format, as the `Format` the repository code works with. +fn nuget() -> forge_core::format::SpecFormat { + forge_core::format::SpecFormat::new(std::sync::Arc::new(forge_format_nuget::Nuget::default())) } /// In-memory blob byte store: maps `s3_key -> bytes`. diff --git a/crates/core/tests/hosted_read.rs b/crates/core/tests/hosted_read.rs index 7ed52527..22732937 100644 --- a/crates/core/tests/hosted_read.rs +++ b/crates/core/tests/hosted_read.rs @@ -18,10 +18,9 @@ use forge_db::repository_dao::{self, NewRepository}; use forge_db::{blob_dao, model::Repository}; use sqlx::PgPool; -/// NuGet, the built-in format plugin, as the `Format` the repository code -/// works with. -fn nuget() -> forge_plugin::PluginFormat { - forge_plugin::PluginFormat::new(std::sync::Arc::new(forge_format_nuget::Nuget::default())) +/// The NuGet format, as the `Format` the repository code works with. +fn nuget() -> forge_core::format::SpecFormat { + forge_core::format::SpecFormat::new(std::sync::Arc::new(forge_format_nuget::Nuget::default())) } /// In-memory blob byte store: maps `s3_key -> bytes`. diff --git a/crates/core/tests/hosted_write.rs b/crates/core/tests/hosted_write.rs index 8718769d..a65a0d8d 100644 --- a/crates/core/tests/hosted_write.rs +++ b/crates/core/tests/hosted_write.rs @@ -24,10 +24,9 @@ use forge_storage::{BlobStore, Cas}; use sqlx::PgPool; use tokio::sync::Mutex; -/// NuGet, the built-in format plugin, as the `Format` the repository code -/// works with. -fn nuget() -> forge_plugin::PluginFormat { - forge_plugin::PluginFormat::new(std::sync::Arc::new(forge_format_nuget::Nuget::default())) +/// The NuGet format, as the `Format` the repository code works with. +fn nuget() -> forge_core::format::SpecFormat { + forge_core::format::SpecFormat::new(std::sync::Arc::new(forge_format_nuget::Nuget::default())) } const MAX_UPLOAD: u64 = 64 * 1024 * 1024; diff --git a/crates/core/tests/proxy_documents.rs b/crates/core/tests/proxy_documents.rs index 4e419b18..9c806078 100644 --- a/crates/core/tests/proxy_documents.rs +++ b/crates/core/tests/proxy_documents.rs @@ -37,8 +37,8 @@ use serde_json::{json, Value}; use sqlx::PgPool; use tokio::net::TcpListener; -fn nuget() -> forge_plugin::PluginFormat { - forge_plugin::PluginFormat::new(std::sync::Arc::new(forge_format_nuget::Nuget::default())) +fn nuget() -> forge_core::format::SpecFormat { + forge_core::format::SpecFormat::new(std::sync::Arc::new(forge_format_nuget::Nuget::default())) } // ── An upstream feed with conditional listings ────────────────────────────── diff --git a/crates/core/tests/proxy_read.rs b/crates/core/tests/proxy_read.rs index 0cfa6084..10ba451e 100644 --- a/crates/core/tests/proxy_read.rs +++ b/crates/core/tests/proxy_read.rs @@ -27,8 +27,8 @@ //! versions not downloaded yet, bounded page fetches, flat-view fallback. //! * **where requests go** — a format's fetches and package URLs reach the //! `remote_url`'s origin and the origins the upstream's documents name, -//! nothing else (#233): a scripted format plays a plugin that wants to read -//! an internal service. +//! nothing else (#233): a scripted format plays one that wants to read an +//! internal service. mod common; @@ -47,17 +47,16 @@ use forge_core::repo::proxy::ProxyRepo; use forge_core::repo::{RepoError, RepoReadOps}; use forge_core::{Format, FormatError, ParsedUpload, ReadSeek}; use forge_db::repository_dao::{self, NewRepository}; -use forge_plugin_api::{ +use forge_format_api::{ Fetch, FetchResult, Fetched, Freshness, Remote, UpstreamAnswer, UpstreamOp, UpstreamStep, }; use sqlx::PgPool; use tokio::net::TcpListener; use tokio::sync::Notify; -/// NuGet, the built-in format plugin, as the `Format` the repository code -/// works with. -fn nuget() -> forge_plugin::PluginFormat { - forge_plugin::PluginFormat::new(std::sync::Arc::new(forge_format_nuget::Nuget::default())) +/// The NuGet format, as the `Format` the repository code works with. +fn nuget() -> forge_core::format::SpecFormat { + forge_core::format::SpecFormat::new(std::sync::Arc::new(forge_format_nuget::Nuget::default())) } // ── Mock upstream NuGet V3 feed ───────────────────────────────────────────── @@ -3834,10 +3833,10 @@ type Script = dyn Fn(&dyn Format, &Remote, &UpstreamOp, &[FetchResult]) -> Resul + Send + Sync; -/// NuGet with a scripted upstream side: a format plugin that wants to read -/// what Forge's network position reaches. +/// NuGet with a scripted upstream side: a format that wants to read what +/// Forge's network position reaches. struct Scripted { - nuget: forge_plugin::PluginFormat, + nuget: forge_core::format::SpecFormat, script: Box