diff --git a/Cargo.lock b/Cargo.lock index b79de82..b1fecba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -562,18 +562,21 @@ dependencies = [ [[package]] name = "comline-codegen" version = "0.1.0" -source = "git+https://github.com/ComlineProject/generation?rev=b116831182c32e70fb4452ff9fce4b463ce4bd99#b116831182c32e70fb4452ff9fce4b463ce4bd99" +source = "git+https://github.com/ComlineProject/generation?rev=de7ceb4c266d1fb5d6dc3c918d84cb226003f6b6#de7ceb4c266d1fb5d6dc3c918d84cb226003f6b6" dependencies = [ "comline-core", "eyre", + "miette", + "schemars", "serde", "serde_json", + "toml", ] [[package]] name = "comline-codegen-rust" version = "0.1.0" -source = "git+https://github.com/ComlineProject/comline-rust?rev=0e5c60ebdfab9cd172f23691a18084d651bfb33a#0e5c60ebdfab9cd172f23691a18084d651bfb33a" +source = "git+https://github.com/ComlineProject/comline-rust?rev=5346678b82fc275fca741bf5ca0cfad1e8edf075#5346678b82fc275fca741bf5ca0cfad1e8edf075" dependencies = [ "comline-codegen", "comline-core", @@ -583,7 +586,7 @@ dependencies = [ [[package]] name = "comline-codegen-typescript" version = "0.1.0" -source = "git+https://github.com/ComlineProject/comline-typescript?rev=765fc2d1177d0044498b00160000ac5084540af2#765fc2d1177d0044498b00160000ac5084540af2" +source = "git+https://github.com/ComlineProject/comline-typescript?rev=c2096cd500f7cd3a83d495fbbbf5df72bc12ecc7#c2096cd500f7cd3a83d495fbbbf5df72bc12ecc7" dependencies = [ "comline-codegen", "comline-core", @@ -593,7 +596,7 @@ dependencies = [ [[package]] name = "comline-core" version = "0.1.0" -source = "git+https://github.com/ComlineProject/core?rev=98d64faedfc48ab4d2c93722e2b099debedd503c#98d64faedfc48ab4d2c93722e2b099debedd503c" +source = "git+https://github.com/ComlineProject/core?rev=2ae998634bad418e4a03ce7494e673275dafcc01#2ae998634bad418e4a03ce7494e673275dafcc01" dependencies = [ "ariadne", "blake3", @@ -621,7 +624,7 @@ dependencies = [ [[package]] name = "comline-core-stdlib" version = "0.1.0" -source = "git+https://github.com/ComlineProject/core?rev=98d64faedfc48ab4d2c93722e2b099debedd503c#98d64faedfc48ab4d2c93722e2b099debedd503c" +source = "git+https://github.com/ComlineProject/core?rev=2ae998634bad418e4a03ce7494e673275dafcc01#2ae998634bad418e4a03ce7494e673275dafcc01" [[package]] name = "comline-runtime" @@ -968,6 +971,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + [[package]] name = "either" version = "1.19.0" @@ -2098,6 +2107,26 @@ dependencies = [ "bitflags 2.13.1", ] +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + [[package]] name = "regalloc2" version = "0.15.2" @@ -2284,6 +2313,31 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schemars" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab508826f74a77ca9d5aba6ff19b522583ee3eaf28a19384ff3d0e5835fadf6e" +dependencies = [ + "dyn-clone", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1c3a92094fa7d61aa124645844facb6b554dfc797136d0f5fd1f890e2bffc69" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 3.0.9", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -2330,6 +2384,17 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "serde_derive_internals" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + [[package]] name = "serde_json" version = "1.0.149" diff --git a/Cargo.toml b/Cargo.toml index d938378..4a0cf10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,15 +32,15 @@ eyre = "0.6.8" # needs real I/O (git fetch, filesystem), so it's off by default for # `comline-core`'s wasm32 consumers (the playground editor, the language # server); the CLI is the one consumer that actually runs `comline build`. -comline-core = { git = "https://github.com/ComlineProject/core", rev = "98d64faedfc48ab4d2c93722e2b099debedd503c", features = ["deps"] } +comline-core = { git = "https://github.com/ComlineProject/core", rev = "2ae998634bad418e4a03ce7494e673275dafcc01", features = ["deps"] } # All three pinned to revs that agree on one `comline-codegen`, and bumped # together: comline-rust and comline-typescript's codegen crates consume # `comline-codegen`'s types, so the rev below must be byte-identical to the # one *they* pin - Cargo only unifies a git dependency across identical rev # strings, and two copies of `GenRequest` are two incompatible types. -comline-codegen = { git = "https://github.com/ComlineProject/generation", rev = "b116831182c32e70fb4452ff9fce4b463ce4bd99" } -comline-codegen-rust = { git = "https://github.com/ComlineProject/comline-rust", rev = "0e5c60ebdfab9cd172f23691a18084d651bfb33a", optional = true } -comline-codegen-typescript = { git = "https://github.com/ComlineProject/comline-typescript", rev = "765fc2d1177d0044498b00160000ac5084540af2", optional = true } +comline-codegen = { git = "https://github.com/ComlineProject/generation", rev = "de7ceb4c266d1fb5d6dc3c918d84cb226003f6b6" } +comline-codegen-rust = { git = "https://github.com/ComlineProject/comline-rust", rev = "5346678b82fc275fca741bf5ca0cfad1e8edf075", optional = true } +comline-codegen-typescript = { git = "https://github.com/ComlineProject/comline-typescript", rev = "c2096cd500f7cd3a83d495fbbbf5df72bc12ecc7", optional = true } # `comline conformance`: the conformance runner (corpus loading, judging, # profile proofs) and its case format, a public spec. Pinned by git rev. comline-runtime-conformance = { git = "https://github.com/ComlineProject/runtime", rev = "6f8f73a247f0aa4ac8b7041b2fdc4b02bce0e0d6" } diff --git a/docs/cli.md b/docs/cli.md index 428c41f..5a48bc2 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -233,7 +233,7 @@ none, from `code_generation.languages` in `config.idp`. [generate] out = "generated" # output root, relative to comline.toml layout = "{{language}}/{{namespace}}.{{ext}}" # path of each file under the root -mode = "code" # code | lib | dylib (only `code` today) +mode = "code" # code (loose source) | lib (a buildable package) external_std = false # reference comline_std instead of inlining it (rust only) package_versions = "latest" # latest | all | ["0.3.0", "0.4.0"] @@ -248,6 +248,20 @@ out = "src/generated" variables: `{{language}}`, `{{namespace}}` (`/`-joined), `{{ext}}`, `{{lang_version}}`, `{{spec_version}}`, `{{package_version}}`. +**Editor support.** `comline.toml` has a JSON Schema, generated from the +same types `comline generate` parses it with, at +`https://comlineproject.github.io/docs/schemas/comline-toml/0.x.json`. A +`comline new` project's `comline.toml` starts with a line naming it: + +```toml +#:schema https://comlineproject.github.io/docs/schemas/comline-toml/0.x.json +``` + +It's a TOML comment, so `comline` ignores it; editors using Taplo (Even +Better TOML in VS Code) read it, and complete keys, describe them, and flag +an unknown key or a wrong value (`mode = "dylib"`) as you type. Add the line +to an existing `comline.toml` to get the same. + **`layout` only applies to `code` mode.** A `lib`-mode target ignores it entirely (`comline generate` warns if you set one) and always writes the crate's fixed internal shape (`Cargo.toml`, `src/lib.rs`, …) at diff --git a/src/commands/clean.rs b/src/commands/clean.rs index 8676384..12ffd7a 100644 --- a/src/commands/clean.rs +++ b/src/commands/clean.rs @@ -10,8 +10,9 @@ use comline_core::package::config::ir::frozen::FrozenUnit as ConfigUnit; use miette::{IntoDiagnostic, Result, WrapErr}; use crate::commands::ensure_project; -use crate::gen_config::{self, ComlineToml, DeclaredLang, Overrides}; +use crate::gen_config::Destination; use crate::ui; +use comline_codegen::config::{self as gen_config, ComlineToml, DeclaredLang, Overrides}; pub fn run(work_dir: &Path, dry_run: bool) -> Result<()> { ensure_project(work_dir)?; diff --git a/src/commands/generate.rs b/src/commands/generate.rs index 1012c4d..2a71c08 100644 --- a/src/commands/generate.rs +++ b/src/commands/generate.rs @@ -26,9 +26,12 @@ use miette::{miette, IntoDiagnostic, Result, WrapErr}; use crate::commands::{core_err, ensure_project}; use crate::external::{self, External}; -use crate::gen_config::{self, ComlineToml, DeclaredLang, Overrides, VersionSpec}; +use crate::gen_config::Destination; use crate::lang_targets::{self, TargetError}; use crate::{history, ui, watch}; +use comline_codegen::config::{ + self as gen_config, ComlineToml, DeclaredLang, Overrides, VersionSpec, +}; /// The generator registry, composed from the per-language `comline-codegen-*` /// crates this CLI was built with — one `register()` per crate, each behind its diff --git a/src/commands/new.rs b/src/commands/new.rs index d318eec..6279c71 100644 --- a/src/commands/new.rs +++ b/src/commands/new.rs @@ -33,6 +33,7 @@ const GITIGNORE: &str = "\ /// only says where each lands. Everything here is commented by default, so a /// new project writes to `generated/{{language}}/{{namespace}}.{{ext}}`. const COMLINE_TOML: &str = "\ +#:schema https://comlineproject.github.io/docs/schemas/comline-toml/0.x.json # comline.toml — where `comline generate` writes code, and how. # # Which languages are generated comes from `config.idp`'s diff --git a/src/external.rs b/src/external.rs index 0b9818f..63e6232 100644 --- a/src/external.rs +++ b/src/external.rs @@ -35,7 +35,7 @@ use miette::{miette, Result}; /// How long one call to a generator may take, unless the target sets /// `timeout` (in seconds). -pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(60); +pub const DEFAULT_TIMEOUT: Duration = comline_codegen::config::DEFAULT_TIMEOUT; /// The most a generator may write to stdout: one encoded response. pub const STDOUT_CAP: usize = 256 * 1024 * 1024; /// The most generated file contents one response may carry, all files diff --git a/src/gen_config.rs b/src/gen_config.rs index 39f7935..3986736 100644 --- a/src/gen_config.rs +++ b/src/gen_config.rs @@ -1,313 +1,17 @@ -//! `comline.toml` — the consumer-owned code-generation config. +//! `comline.toml`, the CLI's side of it. //! -//! `config.idp` (the congregation) declares *what* a package can be generated as -//! (`code_generation.languages`); it is frozen into the package and must not -//! carry output paths. `comline.toml` is the other half: it belongs to whoever -//! *consumes* the package and says where generated code lands, in what layout, -//! and in what form. It is plain committed source — never frozen, never in CAS. -//! -//! Field precedence, lowest to highest: built-in defaults → `[generate]` → -//! `[[generate.target]]` → `COMLINE_GENERATE_*` env → CLI flags. +//! The file's types, parsing and resolution live in +//! [`comline_codegen::config`], shared with the language server and the +//! JSON Schema editors validate it with. What stays here needs the +//! compiler: rendering a target's `layout` into a path, with core's template +//! engine. -use std::path::{Path, PathBuf}; +use std::path::PathBuf; -use comline_codegen::validation::ValidateOnDecode; -use comline_codegen::versions::LangVersion; -use comline_core::schema::idl::annotations; +use comline_codegen::config::ResolvedTarget; use comline_core::utils::templating::recurse_render; -use miette::{miette, IntoDiagnostic, Result, WrapErr}; -use serde::{Deserialize, Serialize}; - -/// File looked for in the project directory. -pub const FILE_NAME: &str = "comline.toml"; - -/// Default output root, relative to `comline.toml`. -pub const DEFAULT_OUT: &str = "generated"; -/// Default on-disk layout under a target's root. -pub const DEFAULT_LAYOUT: &str = "{{language}}/{{namespace}}.{{ext}}"; -/// Default emit form. `"code"` and `"lib"` are implemented; `"dylib"` is not. -pub const DEFAULT_MODE: &str = "code"; - -/// Whether `std` is referenced as its own package rather than inlined, when -/// nothing says either way. -/// -/// It follows the mode, because the two modes differ in whether there is -/// anywhere to put the dependency. `lib` writes the package manifest, so the -/// generator adds `comline_std` to it and the crate links one shared std. -/// `code` emits loose source files with no manifest to add anything to - all -/// a generator can do there is leave a comment naming the dependency line to -/// write by hand, so inlining stays the honest default. -fn default_external_std(mode: &str) -> bool { - mode == "lib" -} - -/// Which package versions to generate bindings for. -/// -/// TOML: `package_versions = "latest"` (default) | `"all"` | `"0.3.0"` | -/// `["0.3.0", "0.4.0"]`. `latest` is the working tree; anything else reads -/// committed versions from the CAS chain: `all` every release, a list any -/// version named, a build (`0.3.0-dev.2`) included. -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub enum VersionSpec { - /// The working tree (no CAS read). - #[default] - Latest, - /// Every committed version in the chain. - All, - /// Specific committed versions (each a version string or commit hash). - List(Vec), -} - -impl<'de> Deserialize<'de> for VersionSpec { - fn deserialize>(d: D) -> std::result::Result { - #[derive(Deserialize)] - #[serde(untagged)] - enum Raw { - One(String), - Many(Vec), - } - Ok(match Raw::deserialize(d)? { - Raw::One(s) if s.eq_ignore_ascii_case("latest") => VersionSpec::Latest, - Raw::One(s) if s.eq_ignore_ascii_case("all") => VersionSpec::All, - Raw::One(s) => VersionSpec::List(vec![s]), - Raw::Many(v) => VersionSpec::List(v), - }) - } -} - -/// Parsed `comline.toml`. An absent file parses as [`Self::default`]. -#[derive(Debug, Default, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct ComlineToml { - #[serde(default)] - pub generate: Generate, -} - -/// The `[generate]` table. -#[derive(Debug, Default, Deserialize)] -#[serde(default, deny_unknown_fields)] -pub struct Generate { - /// Narrow generation to these languages, out of the ones `config.idp` - /// declares. Empty (the default) means every declared language. - /// - /// The consumer's half of the split: `config.idp` says which languages - /// the *package* is for, and this says which of them *this checkout* - /// wants built. A name the congregation doesn't declare is an error - /// rather than a silent no-op, so a typo here can't quietly stop - /// generating something. - pub only: Vec, - /// Output root shared by every target unless overridden. - pub out: Option, - /// Layout template shared by every target unless overridden. - pub layout: Option, - /// Emit form shared by every target unless overridden. - pub mode: Option, - /// Version selection shared by every target unless overridden. - pub package_versions: Option, - /// Moved to config.idp's `wire = { framing = "..." }`: the framing is part - /// of the schema, which both ends must agree on. Read only to say so. - pub default_framing: Option, - /// `true`: reference `comline_std` as its own crate instead of inlining - /// it, shared by every target unless overridden. Only a generator that - /// knows how (today, Rust's) acts on it. Default `false`. - pub external_std: Option, - /// Whether decoding a value runs its `@validators` checks: `off` (the - /// default), `collect` (fail with every failure) or `first` (fail with - /// the first). Shared by every target unless overridden. - pub validate_on_decode: Option, - /// `true`: protocols on a connection get async handlers and senders - /// (Rust's `AsyncConnection`), shared by every target unless overridden. - /// A generator whose output is async anyway ignores it. Default `false`. - #[serde(rename = "async")] - pub async_api: Option, - /// Which type implements each encoding (format, framing, handshake) in - /// the generated code, by encoding name: - /// `encodings = { "com.acme.lines@1" = "acme::LinesFraming" }`, or with - /// the package it comes from, `{ type = "...", package = "...", version - /// = "..." }`. Overrides the declaring package's suggested mapping, and - /// maps an encoding that has none. A target's own `encodings` override - /// these, name by name. - pub encodings: EncodingMap, - /// `[[generate.target]]` blocks. - #[serde(rename = "target")] - pub targets: Vec, -} - -/// `encodings = { ... }`, by encoding name as written. -pub type EncodingMap = std::collections::BTreeMap; - -/// One entry of `encodings`: the type alone, or with its package. -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] -#[serde(untagged)] -pub enum EncodingEntry { - /// `"acme::LinesFraming"`. - Type(String), - /// `{ type = "LinesFraming", package = "acme-encodings", version = "^0.2" }`. - Full(EncodingTable), -} - -#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct EncodingTable { - #[serde(rename = "type")] - pub ty: String, - pub package: Option, - pub version: Option, -} - -impl EncodingEntry { - pub fn mapping(&self) -> comline_codegen::protocol::EncodingMapping { - match self { - EncodingEntry::Type(ty) => comline_codegen::protocol::EncodingMapping { - type_path: ty.clone(), - package: None, - version: None, - }, - EncodingEntry::Full(t) => comline_codegen::protocol::EncodingMapping { - type_path: t.ty.clone(), - package: t.package.clone(), - version: t.version.clone(), - }, - } - } -} - -/// `[generate]`'s `encodings` with a target's over them, by canonical -/// name, so `jsonrpc@1` in one and `json-rpc` in the other are the same -/// entry and the target's wins. -fn merge_encodings( - section: &EncodingMap, - target: &EncodingMap, -) -> std::collections::BTreeMap { - let mut merged = std::collections::BTreeMap::new(); - for (name, entry) in section.iter().chain(target) { - merged.insert(comline_codegen::encodings::canonical(name), entry.mapping()); - } - merged -} - -/// One `[[generate.target]]` block. -#[derive(Debug, Clone, Deserialize)] -#[serde(deny_unknown_fields)] -pub struct Target { - /// Must match a language the congregation declared under - /// `code_generation.languages`. With `generator`, it also picks among the - /// languages that generator offers, and may be left out when it offers - /// one; [`crate::commands::generate`] fills it in before [`resolve`]. - #[serde(default)] - pub language: String, - /// A generator outside this binary: a path to a `.wasm` (run sandboxed - /// under wasmtime) or to an executable, relative to `comline.toml`'s - /// directory. Unset, the built-in generator for `language` is used. - pub generator: Option, - /// The wall-time limit for each call to `generator`, in seconds. - /// Default 60. - pub timeout: Option, - /// Language/toolchain version selecting the generator (e.g. `1.88.0`). - /// Falls back to the version the congregation declared for this language, - /// and may raise it but never lower it. - pub lang_version: Option, - pub out: Option, - pub layout: Option, - pub mode: Option, - pub package_versions: Option, - pub default_framing: Option, - pub external_std: Option, - pub validate_on_decode: Option, - #[serde(rename = "async")] - pub async_api: Option, - /// See [`Generate::encodings`]; these override those, name by name. - #[serde(default)] - pub encodings: EncodingMap, - /// `lib` mode only: write the crate straight into `out` instead of - /// `//`. Target-only (no `[generate]` default, no flag/env) - /// — a shared `out` across several `lib` targets needs the per-language - /// split to avoid two crates colliding in one directory, so flattening - /// only makes sense once a target already has `out` to itself. - #[serde(default)] - pub flatten: bool, -} - -/// A non-empty `COMLINE_GENERATE_*` env var, if set. -fn env_var(name: &str) -> Option { - std::env::var(name).ok().filter(|s| !s.is_empty()) -} - -/// Same as [`env_var`], parsed as a bool (`"true"`/`"1"` ⇒ on, -/// `"false"`/`"0"` ⇒ off; anything else unset, same as absent). -fn env_var_bool(name: &str) -> Option { - match env_var(name)?.as_str() { - "true" | "1" => Some(true), - "false" | "0" => Some(false), - _ => None, - } -} - -impl ComlineToml { - /// Load `/comline.toml`; a missing file yields defaults. - pub fn load(work_dir: &Path) -> Result { - let path = work_dir.join(FILE_NAME); - match std::fs::read_to_string(&path) { - Ok(text) => toml::from_str(&text) - .into_diagnostic() - .wrap_err_with(|| format!("failed to parse `{}`", path.display())), - Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Self::default()), - Err(e) => Err(e) - .into_diagnostic() - .wrap_err_with(|| format!("failed to read `{}`", path.display())), - } - } -} - -/// A language the congregation declared, `name#version` already split. -#[derive(Debug, Clone)] -pub struct DeclaredLang { - pub language: String, - pub lang_version: String, -} - -/// CLI flag overrides for `comline generate`. -#[derive(Debug, Default)] -pub struct Overrides<'a> { - pub target: Option<&'a str>, - pub out: Option<&'a str>, - pub layout: Option<&'a str>, - pub mode: Option<&'a str>, - /// `Some(true)` from `--external-std`, `Some(false)` from - /// `--no-external-std`, `None` when neither was passed. Both spellings - /// exist because the default is no longer a fixed `false` - it follows - /// the mode (see [`default_external_std`]), so either direction can be - /// the one a given run needs to override. - pub external_std: Option, - /// `--validate-on-decode`. - pub validate_on_decode: Option<&'a str>, -} - -/// A fully-resolved target: every field decided, `out` made absolute. -#[derive(Debug)] -pub struct ResolvedTarget { - pub language: String, - /// See [`Target::generator`], made absolute. - pub generator: Option, - /// See [`Target::timeout`]. - pub timeout: std::time::Duration, - pub lang_version: String, - pub out: PathBuf, - pub layout: String, - pub mode: String, - pub versions: VersionSpec, - /// `lib` mode only — see [`Target::flatten`]. - pub flatten: bool, - /// See [`Generate::external_std`]. - pub external_std: bool, - /// See [`Generate::validate_on_decode`]. - pub validate_on_decode: ValidateOnDecode, - /// See [`Generate::async_api`]. - pub async_api: bool, - /// [`Generate::encodings`] with the target's over them, by canonical - /// encoding name. - pub encodings: std::collections::BTreeMap, -} +use miette::{miette, Result}; +use serde::Serialize; /// Variables a `layout` template can reference. #[derive(Serialize)] @@ -320,12 +24,23 @@ struct LayoutVars<'a> { package_version: &'a str, } -impl ResolvedTarget { +/// Where a resolved target writes each file. +pub trait Destination { /// Render `layout` for one schema namespace into an absolute path under - /// `self.out`. `ext` is the generator's file extension (e.g. `rs`); + /// `out`. `ext` is the generator's file extension (e.g. `rs`); /// `package_version` is the version being generated (empty for an unbuilt /// working tree). - pub fn dest_for( + fn dest_for( + &self, + namespace: &str, + ext: &str, + spec_version: &str, + package_version: &str, + ) -> Result; +} + +impl Destination for ResolvedTarget { + fn dest_for( &self, namespace: &str, ext: &str, @@ -345,769 +60,3 @@ impl ResolvedTarget { Ok(self.out.join(rel)) } } - -/// Merge `comline.toml`, the congregation's declared languages and CLI flags -/// into the concrete set of targets `generate` should emit. -/// -/// **The declared languages are the set.** `config.idp`'s -/// `code_generation.languages` says which languages this package is generated -/// for - that is the package's own contract, committed alongside it - and a -/// `[[generate.target]]` block says where and how one of them lands. A block -/// used to *replace* the list outright, so adding one to redirect `rust`'s -/// output silently stopped generating every other declared language; the -/// docs described the block as settings all along ("one block per target you -/// want to pin or override"), and this is the code catching up. -/// -/// A language with no block takes the `[generate]` defaults. Several blocks -/// for one language give several targets - two `rust` blocks writing to -/// different `out` is a real thing to want. A block naming a language the -/// congregation doesn't declare is an error, not a silent no-op: that would -/// be the same footgun inverted. -/// -/// With no declared languages at all, the blocks are the set - a -/// `comline.toml`-only project with no `code_generation` in its congregation -/// still works. -/// -/// `[generate] only` narrows the declared set persistently; `--target` -/// narrows it for one run (case-insensitive). The `--out` / `--layout` / -/// `--mode` flags apply to the single remaining target, or to the -/// `--target`-named one; using them with several targets and no `--target` is an -/// error. `COMLINE_GENERATE_{OUT,LAYOUT,MODE}` sit just below the flags but apply -/// to every target regardless (a deliberate global for CI). -pub fn resolve( - cfg: &ComlineToml, - declared: &[DeclaredLang], - work_dir: &Path, - ov: &Overrides, -) -> Result> { - let moved = std::iter::once(&cfg.generate.default_framing) - .chain(cfg.generate.targets.iter().map(|t| &t.default_framing)); - if let Some(written) = moved.flatten().next() { - return Err(moved_default_framing(written)); - } - - if let Some(t) = cfg.generate.targets.iter().find(|t| t.language.is_empty()) { - return Err(match &t.generator { - Some(g) => miette!("`[[generate.target]]` with `generator = \"{g}\"` needs `language`"), - None => miette!("a `[[generate.target]]` sets no `language`"), - }); - } - - let declares = |language: &str| { - declared - .iter() - .any(|d| d.language.eq_ignore_ascii_case(language)) - }; - - // A block for a language nobody declared is a mistake worth naming - - // it would otherwise configure a target that is never built. - if !declared.is_empty() { - if let Some(stray) = cfg.generate.targets.iter().find(|t| !declares(&t.language)) { - return Err(miette!( - "`[[generate.target]]` names `{}`, which config.idp does not declare \ - - add `{}# = {{}}` to `code_generation.languages`, or drop \ - the block", - stray.language, - stray.language, - )); - } - } - if let Some(stray) = cfg - .generate - .only - .iter() - .find(|l| !declared.is_empty() && !declares(l)) - { - return Err(miette!( - "`[generate] only` names `{stray}`, which config.idp does not declare", - )); - } - - let base: Vec = if declared.is_empty() { - // No congregation-declared languages: the blocks are all there is. - cfg.generate.targets.clone() - } else { - declared - .iter() - .filter(|d| { - cfg.generate.only.is_empty() - || cfg - .generate - .only - .iter() - .any(|l| l.eq_ignore_ascii_case(&d.language)) - }) - .flat_map(|d| { - let blocks: Vec = cfg - .generate - .targets - .iter() - .filter(|t| t.language.eq_ignore_ascii_case(&d.language)) - .cloned() - .collect(); - if blocks.is_empty() { - // Declared but unconfigured: the `[generate]` defaults. - vec![Target { - language: d.language.clone(), - generator: None, - timeout: None, - lang_version: Some(d.lang_version.clone()), - out: None, - layout: None, - mode: None, - package_versions: None, - default_framing: None, - external_std: None, - validate_on_decode: None, - async_api: None, - encodings: Default::default(), - flatten: false, - }] - } else { - blocks - } - }) - .collect() - }; - - let selected: Vec<&Target> = base - .iter() - .filter(|t| match ov.target { - Some(want) => want.eq_ignore_ascii_case(&t.language), - None => true, - }) - .collect(); - - if selected.is_empty() { - return match ov.target { - Some(want) => Err(miette!("no code-generation target matches `{want}`")), - None if !cfg.generate.only.is_empty() => { - Err(miette!("`[generate] only` leaves nothing to generate",)) - } - None => Err(miette!( - "nothing to generate: add `[[generate.target]]` to {} \ - or `code_generation.languages` to config.idp", - FILE_NAME - )), - }; - } - - let field_override = ov.out.is_some() - || ov.layout.is_some() - || ov.mode.is_some() - || ov.external_std.is_some() - || ov.validate_on_decode.is_some(); - if field_override && selected.len() > 1 && ov.target.is_none() { - return Err(miette!( - "`--out` / `--layout` / `--mode` / `--external-std` / `--validate-on-decode` \ - need `--target ` when more than one target is configured ({} here)", - selected.len() - )); - } - // A flag binds to a target when it was explicitly named, or when it is the - // only one left. `COMLINE_*` env vars are a deliberate global — they apply to - // every target regardless. - let apply_flag = ov.target.is_some() || selected.len() == 1; - let env_out = env_var("COMLINE_GENERATE_OUT"); - let env_layout = env_var("COMLINE_GENERATE_LAYOUT"); - let env_mode = env_var("COMLINE_GENERATE_MODE"); - let env_external_std = env_var_bool("COMLINE_GENERATE_EXTERNAL_STD"); - let env_validate_on_decode = env_var("COMLINE_GENERATE_VALIDATE_ON_DECODE"); - - let mut resolved = Vec::with_capacity(selected.len()); - for t in selected { - // Precedence, highest first: flag → env → [[target]] → [generate] → default. - let pick = |flag: Option<&str>, - env: &Option, - target: &Option, - section: &Option, - default: &str| { - flag.filter(|_| apply_flag) - .map(str::to_owned) - .or_else(|| env.clone()) - .or_else(|| target.clone()) - .or_else(|| section.clone()) - .unwrap_or_else(|| default.to_owned()) - }; - - // Same precedence, for a bool field. - let pick_bool = |flag: Option, - env: Option, - target: Option, - section: Option, - default: bool| { - flag.filter(|_| apply_flag) - .or(env) - .or(target) - .or(section) - .unwrap_or(default) - }; - - let out = pick(ov.out, &env_out, &t.out, &cfg.generate.out, DEFAULT_OUT); - let layout = pick( - ov.layout, - &env_layout, - &t.layout, - &cfg.generate.layout, - DEFAULT_LAYOUT, - ); - let mode = pick( - ov.mode, - &env_mode, - &t.mode, - &cfg.generate.mode, - DEFAULT_MODE, - ); - let external_std = pick_bool( - ov.external_std, - env_external_std, - t.external_std, - cfg.generate.external_std, - default_external_std(&mode), - ); - let validate_on_decode = pick( - ov.validate_on_decode, - &env_validate_on_decode, - &t.validate_on_decode, - &cfg.generate.validate_on_decode, - ValidateOnDecode::Off.name(), - ); - let validate_on_decode = ValidateOnDecode::parse(&validate_on_decode).ok_or_else(|| { - miette!( - "`validate_on_decode = \"{validate_on_decode}\"` for target `{}` - expected one of \ - {}", - t.language, - ValidateOnDecode::NAMES.map(|n| format!("`{n}`")).join(", ") - ) - })?; - - let declared_version = declared - .iter() - .find(|d| d.language.eq_ignore_ascii_case(&t.language)) - .map(|d| d.lang_version.clone()); - // `comline.toml` may raise the version, never lower it: config.idp's - // is the oldest toolchain the package was proven against. A version - // that doesn't parse is left for the registry to name. - if let (Some(raised), Some(declared_version)) = (&t.lang_version, &declared_version) { - if let (Some(r), Some(d)) = ( - LangVersion::parse(raised), - LangVersion::parse(declared_version), - ) { - if r < d { - return Err(miette!( - "target `{}` sets `lang_version = \"{raised}\"`, below config.idp's `{}#{declared_version}` \ - - {} can raise a target's version, never lower it: the package is only proven \ - from `{declared_version}` on", - t.language, - t.language, - FILE_NAME - )); - } - } - } - let lang_version = t.lang_version.clone().or(declared_version).ok_or_else(|| { - miette!( - "target `{}` sets no `lang_version` and config.idp does not \ - declare that language under `code_generation.languages`", - t.language - ) - })?; - - let versions = t - .package_versions - .clone() - .or_else(|| cfg.generate.package_versions.clone()) - .unwrap_or_default(); - - if t.timeout == Some(0) { - return Err(miette!( - "target `{}`: `timeout = 0` - it's in seconds, and must be at least 1", - t.language - )); - } - if t.timeout.is_some() && t.generator.is_none() { - return Err(miette!( - "target `{}`: `timeout` limits an external `generator`, and this target uses the \ - built-in one", - t.language - )); - } - resolved.push(ResolvedTarget { - language: t.language.clone(), - generator: t.generator.as_ref().map(|g| work_dir.join(g)), - timeout: t - .timeout - .map(std::time::Duration::from_secs) - .unwrap_or(crate::external::DEFAULT_TIMEOUT), - lang_version, - out: work_dir.join(out), - layout, - mode, - versions, - flatten: t.flatten, - external_std, - validate_on_decode, - async_api: t.async_api.or(cfg.generate.async_api).unwrap_or(false), - encodings: merge_encodings(&cfg.generate.encodings, &t.encodings), - }); - } - - Ok(resolved) -} - -/// `default_framing` left `comline.toml`: the framing is part of the schema, -/// which both ends of a connection must agree on, so the package declares it. -fn moved_default_framing(written: &str) -> miette::Report { - let framing = annotations::canonical_value("framing", written).unwrap_or(written); - miette!( - "`default_framing` moved from {FILE_NAME} to config.idp\n \ - help: drop it here, and declare the package's framing in config.idp:\n \ - wire = {{ framing = \"{framing}\" }}\n \ - Protocols with no `@framing` default to `datagram@2` without it." - ) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn parse(toml_src: &str) -> ComlineToml { - toml::from_str(toml_src).unwrap() - } - - #[test] - fn version_spec_forms() { - assert_eq!( - parse("[generate]\npackage_versions = \"latest\"") - .generate - .package_versions, - Some(VersionSpec::Latest) - ); - assert_eq!( - parse("[generate]\npackage_versions = \"ALL\"") - .generate - .package_versions, - Some(VersionSpec::All) - ); - assert_eq!( - parse("[generate]\npackage_versions = \"0.3.0\"") - .generate - .package_versions, - Some(VersionSpec::List(vec!["0.3.0".into()])) - ); - assert_eq!( - parse("[generate]\npackage_versions = [\"0.3.0\", \"0.4.0\"]") - .generate - .package_versions, - Some(VersionSpec::List(vec!["0.3.0".into(), "0.4.0".into()])) - ); - assert_eq!(parse("[generate]").generate.package_versions, None); - } - - #[test] - fn target_package_versions_wins_over_section() { - let cfg = parse( - "[generate]\npackage_versions = \"all\"\n\n\ - [[generate.target]]\nlanguage = \"rust\"\nlang_version = \"1.85.0\"\n\ - package_versions = \"latest\"\n", - ); - let declared = []; - let resolved = resolve(&cfg, &declared, Path::new("/x"), &Overrides::default()).unwrap(); - assert_eq!(resolved[0].versions, VersionSpec::Latest); - } - - fn two_langs() -> [DeclaredLang; 2] { - [ - DeclaredLang { - language: "rust".into(), - lang_version: "1.85.0".into(), - }, - DeclaredLang { - language: "typescript".into(), - lang_version: "5.0".into(), - }, - ] - } - - /// The bug this rule replaced: one block existed only to redirect - /// `rust`'s output, and every other declared language silently stopped - /// being generated. - #[test] - fn a_block_configures_its_language_without_dropping_the_others() { - let cfg = parse("[[generate.target]]\nlanguage = \"rust\"\nout = \"../test-cl-rs/\"\n"); - let resolved = resolve(&cfg, &two_langs(), Path::new("/x"), &Overrides::default()).unwrap(); - - let langs: Vec<&str> = resolved.iter().map(|t| t.language.as_str()).collect(); - assert_eq!(langs, vec!["rust", "typescript"]); - assert_eq!( - resolved[0].out, - Path::new("/x/../test-cl-rs/"), - "the block's out" - ); - assert_eq!( - resolved[1].out, - Path::new("/x/generated"), - "the [generate] default" - ); - // A language with no block still gets the version its congregation declared. - assert_eq!(resolved[1].lang_version, "5.0"); - } - - #[test] - fn a_block_may_be_matched_case_insensitively() { - let cfg = parse("[[generate.target]]\nlanguage = \"Rust\"\nout = \"elsewhere\"\n"); - let resolved = resolve(&cfg, &two_langs(), Path::new("/x"), &Overrides::default()).unwrap(); - assert_eq!(resolved[0].out, Path::new("/x/elsewhere")); - } - - /// Two outputs for one language is a real thing to want, and survives - /// the declared set becoming the base. - #[test] - fn several_blocks_for_one_language_give_several_targets() { - let cfg = parse( - "[[generate.target]]\nlanguage = \"rust\"\nout = \"a\"\n\n\ - [[generate.target]]\nlanguage = \"rust\"\nout = \"b\"\n", - ); - let declared = [DeclaredLang { - language: "rust".into(), - lang_version: "1.85.0".into(), - }]; - let resolved = resolve(&cfg, &declared, Path::new("/x"), &Overrides::default()).unwrap(); - assert_eq!(resolved.len(), 2); - assert_eq!(resolved[0].out, Path::new("/x/a")); - assert_eq!(resolved[1].out, Path::new("/x/b")); - } - - /// Silently ignoring a block someone wrote would be the same footgun - /// inverted, so it names the fix instead. - #[test] - fn a_block_for_an_undeclared_language_is_an_error() { - let cfg = parse("[[generate.target]]\nlanguage = \"python\"\nlang_version = \"3.11\"\n"); - let declared = [DeclaredLang { - language: "rust".into(), - lang_version: "1.85.0".into(), - }]; - let err = resolve(&cfg, &declared, Path::new("/x"), &Overrides::default()) - .unwrap_err() - .to_string(); - assert!(err.contains("config.idp does not declare"), "{err}"); - assert!(err.contains("code_generation.languages"), "{err}"); - } - - /// A congregation with no `code_generation` at all: the blocks are all - /// there is, as they always were. - #[test] - fn blocks_stand_alone_when_nothing_is_declared() { - let cfg = parse("[[generate.target]]\nlanguage = \"rust\"\nlang_version = \"1.85.0\"\n"); - let resolved = resolve(&cfg, &[], Path::new("/x"), &Overrides::default()).unwrap(); - assert_eq!(resolved.len(), 1); - assert_eq!(resolved[0].language, "rust"); - } - - #[test] - fn only_narrows_the_declared_set() { - let cfg = parse("[generate]\nonly = [\"rust\"]\n"); - let resolved = resolve(&cfg, &two_langs(), Path::new("/x"), &Overrides::default()).unwrap(); - let langs: Vec<&str> = resolved.iter().map(|t| t.language.as_str()).collect(); - assert_eq!(langs, vec!["rust"]); - } - - /// A typo in `only` would otherwise quietly stop generating something. - #[test] - fn only_naming_an_undeclared_language_is_an_error() { - let cfg = parse("[generate]\nonly = [\"rustt\"]\n"); - let err = resolve(&cfg, &two_langs(), Path::new("/x"), &Overrides::default()) - .unwrap_err() - .to_string(); - assert!(err.contains("does not declare"), "{err}"); - } - - #[test] - fn only_still_lets_a_block_configure_what_survives() { - let cfg = parse( - "[generate]\nonly = [\"rust\"]\n\n\ - [[generate.target]]\nlanguage = \"rust\"\nout = \"here\"\n\n\ - [[generate.target]]\nlanguage = \"typescript\"\nout = \"there\"\n", - ); - let resolved = resolve(&cfg, &two_langs(), Path::new("/x"), &Overrides::default()).unwrap(); - assert_eq!(resolved.len(), 1); - assert_eq!(resolved[0].out, Path::new("/x/here")); - } - - /// `lib` writes the package manifest, so the generator can add - /// `comline_std` to it; `code` emits loose files with nowhere to put a - /// dependency, so inlining stays the honest default there. - #[test] - fn external_std_follows_the_mode() { - for (mode, expected) in [("code", false), ("lib", true)] { - let cfg = parse(&format!( - "[[generate.target]]\nlanguage = \"rust\"\nlang_version = \"1.85.0\"\n\ - mode = \"{mode}\"\n", - )); - let resolved = resolve(&cfg, &[], Path::new("/x"), &Overrides::default()).unwrap(); - assert_eq!(resolved[0].external_std, expected, "mode {mode}"); - } - } - - /// Following the mode is a *default*, not a rule: a crate that wants a - /// self-contained copy of std can still say so. - #[test] - fn an_explicit_false_still_inlines_std_in_lib_mode() { - let cfg = parse( - "[[generate.target]]\nlanguage = \"rust\"\nlang_version = \"1.85.0\"\n\ - mode = \"lib\"\nexternal_std = false\n", - ); - let resolved = resolve(&cfg, &[], Path::new("/x"), &Overrides::default()).unwrap(); - assert!(!resolved[0].external_std); - } - - #[test] - fn the_section_default_applies_to_a_code_target_too() { - let cfg = parse( - "[generate]\nexternal_std = true\n\n\ - [[generate.target]]\nlanguage = \"rust\"\nlang_version = \"1.85.0\"\n", - ); - let resolved = resolve(&cfg, &[], Path::new("/x"), &Overrides::default()).unwrap(); - assert!(resolved[0].external_std, "`code`, but asked for explicitly"); - } - - /// Both flag spellings exist because the default is no longer a fixed - /// `false` - either direction can be the one a run needs to override. - #[test] - fn either_flag_spelling_overrides_the_mode_default() { - let cfg = parse( - "[[generate.target]]\nlanguage = \"rust\"\nlang_version = \"1.85.0\"\n\ - mode = \"lib\"\n", - ); - let off = Overrides { - external_std: Some(false), - ..Default::default() - }; - let resolved = resolve(&cfg, &[], Path::new("/x"), &off).unwrap(); - assert!(!resolved[0].external_std, "--no-external-std"); - - let cfg = parse("[[generate.target]]\nlanguage = \"rust\"\nlang_version = \"1.85.0\"\n"); - let on = Overrides { - external_std: Some(true), - ..Default::default() - }; - let resolved = resolve(&cfg, &[], Path::new("/x"), &on).unwrap(); - assert!(resolved[0].external_std, "--external-std"); - } - - #[test] - fn a_target_may_raise_the_declared_version_but_not_lower_it() { - let declared = [DeclaredLang { - language: "rust".into(), - lang_version: "1.88.0".into(), - }]; - let raised = parse("[[generate.target]]\nlanguage = \"rust\"\nlang_version = \"1.90\"\n"); - let resolved = resolve(&raised, &declared, Path::new("/x"), &Overrides::default()).unwrap(); - assert_eq!(resolved[0].lang_version, "1.90"); - - let lowered = - parse("[[generate.target]]\nlanguage = \"rust\"\nlang_version = \"1.85.0\"\n"); - let err = resolve(&lowered, &declared, Path::new("/x"), &Overrides::default()).unwrap_err(); - let message = err.to_string(); - assert!( - message.contains("below config.idp's `rust#1.88.0`"), - "{message}" - ); - assert!(message.contains("never lower it"), "{message}"); - } - - #[test] - fn flatten_defaults_to_false_and_is_target_only() { - let cfg = parse( - "[[generate.target]]\nlanguage = \"rust\"\nlang_version = \"1.85.0\"\n\ - mode = \"lib\"\nflatten = true\n", - ); - let declared = []; - let resolved = resolve(&cfg, &declared, Path::new("/x"), &Overrides::default()).unwrap(); - assert!(resolved[0].flatten); - - let cfg = parse( - "[[generate.target]]\nlanguage = \"rust\"\nlang_version = \"1.85.0\"\nmode = \"lib\"\n", - ); - let resolved = resolve(&cfg, &declared, Path::new("/x"), &Overrides::default()).unwrap(); - assert!(!resolved[0].flatten); - } - - #[test] - fn async_is_off_unless_asked_for_and_a_target_overrides_the_section() { - let declared = [DeclaredLang { - language: "rust".into(), - lang_version: "1.85.0".into(), - }]; - let off = resolve( - &parse("[generate]\n"), - &declared, - Path::new("/x"), - &Overrides::default(), - ) - .unwrap(); - assert!(!off[0].async_api); - - let on = resolve( - &parse("[generate]\nasync = true\n"), - &declared, - Path::new("/x"), - &Overrides::default(), - ) - .unwrap(); - assert!(on[0].async_api); - - let overridden = parse( - "[generate]\nasync = true\n\n[[generate.target]]\nlanguage = \"rust\"\nasync = false\n", - ); - let resolved = resolve( - &overridden, - &declared, - Path::new("/x"), - &Overrides::default(), - ) - .unwrap(); - assert!(!resolved[0].async_api); - } - - #[test] - fn external_std_defaults_to_false() { - let cfg = parse("[generate]\n"); - let declared = [DeclaredLang { - language: "rust".into(), - lang_version: "1.80".into(), - }]; - let resolved = resolve(&cfg, &declared, Path::new("/x"), &Overrides::default()).unwrap(); - assert!(!resolved[0].external_std); - } - - #[test] - fn external_std_section_applies_to_every_target() { - let cfg = parse("[generate]\nexternal_std = true\n"); - let declared = [DeclaredLang { - language: "rust".into(), - lang_version: "1.80".into(), - }]; - let resolved = resolve(&cfg, &declared, Path::new("/x"), &Overrides::default()).unwrap(); - assert!(resolved[0].external_std); - } - - #[test] - fn target_external_std_wins_over_section() { - let cfg = parse( - "[generate]\nexternal_std = true\n\n\ - [[generate.target]]\nlanguage = \"rust\"\nlang_version = \"1.85.0\"\n\ - external_std = false\n", - ); - let declared = []; - let resolved = resolve(&cfg, &declared, Path::new("/x"), &Overrides::default()).unwrap(); - assert!(!resolved[0].external_std); - } - - #[test] - fn flag_wins_over_everything_for_external_std() { - let cfg = parse("[generate]\nexternal_std = false\n"); - let declared = [DeclaredLang { - language: "rust".into(), - lang_version: "1.80".into(), - }]; - let ov = Overrides { - external_std: Some(true), - ..Default::default() - }; - let resolved = resolve(&cfg, &declared, Path::new("/x"), &ov).unwrap(); - assert!(resolved[0].external_std); - } - - #[test] - fn validate_on_decode_defaults_to_off() { - let cfg = parse("[[generate.target]]\nlanguage = \"rust\"\nlang_version = \"1.85.0\"\n"); - let resolved = resolve(&cfg, &[], Path::new("/x"), &Overrides::default()).unwrap(); - assert_eq!(resolved[0].validate_on_decode, ValidateOnDecode::Off); - } - - #[test] - fn validate_on_decode_target_wins_over_section_and_the_flag_over_both() { - let cfg = parse( - "[generate]\nvalidate_on_decode = \"collect\"\n\n\ - [[generate.target]]\nlanguage = \"rust\"\nlang_version = \"1.85.0\"\n\ - validate_on_decode = \"first\"\n", - ); - let resolved = resolve(&cfg, &[], Path::new("/x"), &Overrides::default()).unwrap(); - assert_eq!(resolved[0].validate_on_decode, ValidateOnDecode::First); - - let ov = Overrides { - validate_on_decode: Some("off"), - ..Default::default() - }; - let resolved = resolve(&cfg, &[], Path::new("/x"), &ov).unwrap(); - assert_eq!(resolved[0].validate_on_decode, ValidateOnDecode::Off); - } - - #[test] - fn default_framing_says_it_moved_to_config_idp() { - let cfg = parse("[generate]\ndefault_framing = \"json-rpc\"\n"); - let declared = [DeclaredLang { - language: "rust".into(), - lang_version: "1.80".into(), - }]; - let error = resolve(&cfg, &declared, Path::new("/x"), &Overrides::default()).unwrap_err(); - let message = error.to_string(); - assert!( - message.contains("moved from comline.toml to config.idp"), - "{message}" - ); - // The hint is ready to paste, in the canonical spelling. - assert!( - message.contains("wire = { framing = \"jsonrpc\" }"), - "{message}" - ); - } - - #[test] - fn a_target_default_framing_says_it_moved_too() { - let cfg = parse( - "[[generate.target]]\nlanguage = \"rust\"\nlang_version = \"1.85.0\"\n\ - default_framing = \"datagram\"\n", - ); - let error = resolve(&cfg, &[], Path::new("/x"), &Overrides::default()).unwrap_err(); - assert!( - error - .to_string() - .contains("wire = { framing = \"datagram\" }"), - "{error}" - ); - } - - #[test] - fn an_unknown_validate_on_decode_names_the_accepted_values() { - let cfg = parse( - "[generate]\nvalidate_on_decode = \"always\"\n\n\ - [[generate.target]]\nlanguage = \"rust\"\nlang_version = \"1.85.0\"\n", - ); - let error = resolve(&cfg, &[], Path::new("/x"), &Overrides::default()).unwrap_err(); - assert_eq!( - error.to_string(), - "`validate_on_decode = \"always\"` for target `rust` - expected one of `off`, `collect`, `first`" - ); - } - - #[test] - fn a_targets_encodings_override_the_sections_by_canonical_name() { - let cfg: ComlineToml = toml::from_str( - "[generate]\n\ - encodings = { \"jsonrpc@1\" = \"section::Rpc\", \"com.acme.cbor@1\" = \"section::Cbor\" }\n\ - [[generate.target]]\n\ - language = \"rust\"\n\ - encodings = { \"json-rpc\" = { type = \"mine::Rpc\", package = \"mine\", version = \"1\" } }\n", - ) - .unwrap(); - let merged = merge_encodings(&cfg.generate.encodings, &cfg.generate.targets[0].encodings); - let keys: Vec<&str> = merged.keys().map(String::as_str).collect(); - assert_eq!(keys, ["com.acme.cbor@1", "jsonrpc"]); - assert_eq!(merged["jsonrpc"].type_path, "mine::Rpc"); - assert_eq!(merged["jsonrpc"].package.as_deref(), Some("mine")); - assert_eq!(merged["com.acme.cbor@1"].type_path, "section::Cbor"); - assert_eq!(merged["com.acme.cbor@1"].package, None); - } -} diff --git a/src/lang_targets.rs b/src/lang_targets.rs index 40682b1..3a4ee6b 100644 --- a/src/lang_targets.rs +++ b/src/lang_targets.rs @@ -29,7 +29,7 @@ use comline_core::schema::ir::frozen::unit::FrozenUnit; use comline_core::settings::effective::package_settings; use comline_core::settings::SettingsSpans; -use crate::gen_config::DeclaredLang; +use comline_codegen::config::DeclaredLang; /// A target declared or raised past the newest version its generator has /// verified. A warning: the wire isn't affected, only whoever compiles the diff --git a/src/main.rs b/src/main.rs index 963f5e0..e46840d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -130,7 +130,7 @@ fn run(cli: Cli) -> Result<()> { list, watch, } => { - let overrides = gen_config::Overrides { + let overrides = comline_codegen::config::Overrides { target: target.as_deref(), out: out.as_deref().map(|p| p.to_str().unwrap_or_default()), layout: layout.as_deref(), diff --git a/tests/cli/generate.rs b/tests/cli/generate.rs index 1a5134d..60edef0 100644 --- a/tests/cli/generate.rs +++ b/tests/cli/generate.rs @@ -940,6 +940,13 @@ fn acme_and_app(root: &std::path::Path, map: &str) -> std::path::PathBuf { ) .unwrap(); fs::write(acme.join("lines.json"), LINES_PROFILE).unwrap(); + // Vectors beside the profile: without them, a protocol over the framing + // gives up encoding conformance and needs `@unverified` (core#148). + fs::write( + acme.join("lines.vectors.json"), + include_str!("../fixtures/conformance_package/profiles/lines.vectors.json"), + ) + .unwrap(); fs::write( acme.join("src/models.ids"), "struct Line {\n text: string\n}\n", diff --git a/tests/cli/new.rs b/tests/cli/new.rs index 1f88b33..769e0fb 100644 --- a/tests/cli/new.rs +++ b/tests/cli/new.rs @@ -19,7 +19,13 @@ fn scaffolds_a_buildable_project() { let root = temp.path().join("my_api"); assert!(root.join("config.idp").exists()); - assert!(root.join("comline.toml").exists()); + // An editor validates comline.toml against its schema from the first line. + let comline_toml = fs::read_to_string(root.join("comline.toml")).unwrap(); + assert_eq!( + comline_toml.lines().next(), + Some("#:schema https://comlineproject.github.io/docs/schemas/comline-toml/0.x.json"), + "{comline_toml}" + ); assert!(root.join("src/greeting.ids").exists()); // The version history is committed; only fetched dependencies are ignored. let gitignore = fs::read_to_string(root.join(".gitignore")).unwrap(); diff --git a/tests/fixtures/generators/Cargo.lock b/tests/fixtures/generators/Cargo.lock index 580b467..2e3ff69 100644 --- a/tests/fixtures/generators/Cargo.lock +++ b/tests/fixtures/generators/Cargo.lock @@ -73,18 +73,21 @@ dependencies = [ [[package]] name = "comline-codegen" version = "0.1.0" -source = "git+https://github.com/ComlineProject/generation?rev=b116831182c32e70fb4452ff9fce4b463ce4bd99#b116831182c32e70fb4452ff9fce4b463ce4bd99" +source = "git+https://github.com/ComlineProject/generation?rev=de7ceb4c266d1fb5d6dc3c918d84cb226003f6b6#de7ceb4c266d1fb5d6dc3c918d84cb226003f6b6" dependencies = [ "comline-core", "eyre", + "miette", + "schemars", "serde", "serde_json", + "toml", ] [[package]] name = "comline-codegen-rust" version = "0.1.0" -source = "git+https://github.com/ComlineProject/comline-rust?rev=0e5c60ebdfab9cd172f23691a18084d651bfb33a#0e5c60ebdfab9cd172f23691a18084d651bfb33a" +source = "git+https://github.com/ComlineProject/comline-rust?rev=5346678b82fc275fca741bf5ca0cfad1e8edf075#5346678b82fc275fca741bf5ca0cfad1e8edf075" dependencies = [ "comline-codegen", "comline-core", @@ -94,7 +97,7 @@ dependencies = [ [[package]] name = "comline-core" version = "0.1.0" -source = "git+https://github.com/ComlineProject/core?rev=98d64faedfc48ab4d2c93722e2b099debedd503c#98d64faedfc48ab4d2c93722e2b099debedd503c" +source = "git+https://github.com/ComlineProject/core?rev=2ae998634bad418e4a03ce7494e673275dafcc01#2ae998634bad418e4a03ce7494e673275dafcc01" dependencies = [ "blake3", "ciborium", @@ -111,7 +114,7 @@ dependencies = [ [[package]] name = "comline-core-stdlib" version = "0.1.0" -source = "git+https://github.com/ComlineProject/core?rev=98d64faedfc48ab4d2c93722e2b099debedd503c#98d64faedfc48ab4d2c93722e2b099debedd503c" +source = "git+https://github.com/ComlineProject/core?rev=2ae998634bad418e4a03ce7494e673275dafcc01#2ae998634bad418e4a03ce7494e673275dafcc01" [[package]] name = "comline-test-generators" @@ -142,6 +145,18 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + [[package]] name = "eyre" version = "0.6.14" @@ -170,12 +185,28 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + [[package]] name = "indenter" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "itoa" version = "1.0.18" @@ -203,6 +234,28 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "miette" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" +dependencies = [ + "cfg-if", + "miette-derive", + "unicode-width", +] + +[[package]] +name = "miette-derive" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -233,6 +286,51 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "schemars" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab508826f74a77ca9d5aba6ff19b522583ee3eaf28a19384ff3d0e5835fadf6e" +dependencies = [ + "dyn-clone", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1c3a92094fa7d61aa124645844facb6b554dfc797136d0f5fd1f890e2bffc69" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 3.0.9", +] + [[package]] name = "semver" version = "1.0.28" @@ -269,6 +367,17 @@ dependencies = [ "syn 3.0.9", ] +[[package]] +name = "serde_derive_internals" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + [[package]] name = "serde_json" version = "1.0.154" @@ -282,6 +391,15 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "shlex" version = "2.0.1" @@ -310,6 +428,47 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "tracing" version = "0.1.44" @@ -353,6 +512,21 @@ version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + [[package]] name = "zerocopy" version = "0.8.63" diff --git a/tests/fixtures/generators/Cargo.toml b/tests/fixtures/generators/Cargo.toml index cbb2da1..9eb1857 100644 --- a/tests/fixtures/generators/Cargo.toml +++ b/tests/fixtures/generators/Cargo.toml @@ -12,5 +12,5 @@ publish = false [dependencies] # The same revs the CLI pins, so the real generator's output is comparable # byte for byte with the CLI's built-in one. -comline-codegen = { git = "https://github.com/ComlineProject/generation", rev = "b116831182c32e70fb4452ff9fce4b463ce4bd99" } -comline-codegen-rust = { git = "https://github.com/ComlineProject/comline-rust", rev = "0e5c60ebdfab9cd172f23691a18084d651bfb33a" } +comline-codegen = { git = "https://github.com/ComlineProject/generation", rev = "de7ceb4c266d1fb5d6dc3c918d84cb226003f6b6" } +comline-codegen-rust = { git = "https://github.com/ComlineProject/comline-rust", rev = "5346678b82fc275fca741bf5ca0cfad1e8edf075" }