diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 9d4f5ab7cb..9948bc70d2 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -2137,7 +2137,7 @@ name = "codex-app-server-protocol" version = "0.0.0" dependencies = [ "anyhow", - "clap", + "codex-app-server-protocol-noop-macros", "codex-experimental-api-macros", "codex-extension-items", "codex-protocol", @@ -2159,8 +2159,13 @@ dependencies = [ "tracing", "ts-rs", "uuid", + "zstd", ] +[[package]] +name = "codex-app-server-protocol-noop-macros" +version = "0.0.0" + [[package]] name = "codex-app-server-test-client" version = "0.0.0" diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index a6db565517..aa80daee7f 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -13,6 +13,7 @@ members = [ "app-server-daemon", "app-server-client", "app-server-protocol", + "app-server-protocol-noop-macros", "app-server-test-client", "apply-patch", "arg0", @@ -152,6 +153,7 @@ codex-app-server-transport = { path = "app-server-transport" } codex-app-server-daemon = { path = "app-server-daemon" } codex-app-server-client = { path = "app-server-client" } codex-app-server-protocol = { path = "app-server-protocol" } +codex-app-server-protocol-noop-macros = { path = "app-server-protocol-noop-macros" } codex-app-server-test-client = { path = "app-server-test-client" } codex-apply-patch = { path = "apply-patch" } codex-arg0 = { path = "arg0" } diff --git a/codex-rs/app-server-protocol-noop-macros/BUILD.bazel b/codex-rs/app-server-protocol-noop-macros/BUILD.bazel new file mode 100644 index 0000000000..5d7f086efd --- /dev/null +++ b/codex-rs/app-server-protocol-noop-macros/BUILD.bazel @@ -0,0 +1,7 @@ +load("//:defs.bzl", "codex_rust_crate") + +codex_rust_crate( + name = "app-server-protocol-noop-macros", + crate_name = "codex_app_server_protocol_noop_macros", + proc_macro = True, +) diff --git a/codex-rs/app-server-protocol-noop-macros/Cargo.toml b/codex-rs/app-server-protocol-noop-macros/Cargo.toml new file mode 100644 index 0000000000..93a24b070b --- /dev/null +++ b/codex-rs/app-server-protocol-noop-macros/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "codex-app-server-protocol-noop-macros" +version.workspace = true +edition.workspace = true +license.workspace = true + +[lib] +proc-macro = true +test = false +doctest = false + +[lints] +workspace = true diff --git a/codex-rs/app-server-protocol-noop-macros/src/lib.rs b/codex-rs/app-server-protocol-noop-macros/src/lib.rs new file mode 100644 index 0000000000..74af71abc2 --- /dev/null +++ b/codex-rs/app-server-protocol-noop-macros/src/lib.rs @@ -0,0 +1,20 @@ +//! No-op schema derives for production app-server protocol builds. +//! +//! The real `ts-rs` and `schemars` derives are only needed when regenerating +//! the vendored protocol exports. Normal builds retain the annotations so the +//! protocol definitions stay readable, but use these derives to avoid +//! generating implementations that cannot be reached at runtime. + +use proc_macro::TokenStream; + +/// Accepts `#[schemars(...)]` helper attributes without generating an impl. +#[proc_macro_derive(JsonSchema, attributes(schemars))] +pub fn derive_json_schema(_input: TokenStream) -> TokenStream { + TokenStream::new() +} + +/// Accepts `#[ts(...)]` helper attributes without generating an impl. +#[proc_macro_derive(TS, attributes(ts))] +pub fn derive_ts(_input: TokenStream) -> TokenStream { + TokenStream::new() +} diff --git a/codex-rs/app-server-protocol/BUILD.bazel b/codex-rs/app-server-protocol/BUILD.bazel index af8c039688..eb6e1d280a 100644 --- a/codex-rs/app-server-protocol/BUILD.bazel +++ b/codex-rs/app-server-protocol/BUILD.bazel @@ -2,6 +2,7 @@ load("//:defs.bzl", "codex_rust_crate") codex_rust_crate( name = "app-server-protocol", + compile_data = glob(["schema/precomputed/**"]), crate_name = "codex_app_server_protocol", test_data_extra = glob( ["schema/**"], diff --git a/codex-rs/app-server-protocol/Cargo.toml b/codex-rs/app-server-protocol/Cargo.toml index 1f0cf450b3..debad694ab 100644 --- a/codex-rs/app-server-protocol/Cargo.toml +++ b/codex-rs/app-server-protocol/Cargo.toml @@ -14,14 +14,13 @@ workspace = true [dependencies] anyhow = { workspace = true } -clap = { workspace = true, features = ["derive"] } codex-experimental-api-macros = { workspace = true } +codex-app-server-protocol-noop-macros = { workspace = true } codex-extension-items = { workspace = true } codex-protocol = { workspace = true } codex-shell-command = { workspace = true } codex-utils-absolute-path = { workspace = true } codex-utils-path-uri = { workspace = true } -schemars = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } serde_with = { workspace = true } @@ -30,17 +29,24 @@ thiserror = { workspace = true } rmcp = { workspace = true, default-features = false, features = [ "base64", "macros", - "schemars", "server", ] } -ts-rs = { workspace = true } inventory = { workspace = true } tracing = { workspace = true } uuid = { workspace = true, features = ["serde", "v7"] } +zstd = { workspace = true } [dev-dependencies] anyhow = { workspace = true } codex-utils-cargo-bin = { workspace = true } pretty_assertions = { workspace = true } +rmcp = { workspace = true, default-features = false, features = [ + "base64", + "macros", + "schemars", + "server", +] } +schemars = { workspace = true } similar = { workspace = true } tempfile = { workspace = true } +ts-rs = { workspace = true } diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst new file mode 100644 index 0000000000..929386b1d6 Binary files /dev/null and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-experimental.json.zst differ diff --git a/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst new file mode 100644 index 0000000000..111bfd95e3 Binary files /dev/null and b/codex-rs/app-server-protocol/schema/precomputed/app-server-exports-stable.json.zst differ diff --git a/codex-rs/app-server-protocol/scripts/write_schema_fixtures.py b/codex-rs/app-server-protocol/scripts/write_schema_fixtures.py new file mode 100644 index 0000000000..69217206cc --- /dev/null +++ b/codex-rs/app-server-protocol/scripts/write_schema_fixtures.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 + +import argparse +import os +from pathlib import Path +import subprocess + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Regenerate vendored app-server schema fixtures" + ) + parser.add_argument( + "--schema-root", + type=Path, + help="root directory containing the schema fixtures", + ) + parser.add_argument( + "-p", + "--prettier", + type=Path, + help="optional Prettier executable used to format TypeScript files", + ) + parser.add_argument( + "--experimental", + action="store_true", + help="regenerate the precomputed experimental exports", + ) + args = parser.parse_args() + + workspace_root = Path(__file__).resolve().parents[2] + schema_root = args.schema_root or workspace_root / "app-server-protocol" / "schema" + + env = os.environ.copy() + env["CODEX_APP_SERVER_SCHEMA_ROOT"] = str(schema_root) + env["CODEX_APP_SERVER_SCHEMA_EXPERIMENTAL"] = ( + "1" if args.experimental else "0" + ) + if args.prettier: + env["CODEX_APP_SERVER_SCHEMA_PRETTIER"] = str(args.prettier) + + subprocess.run( + [ + "cargo", + "test", + "-p", + "codex-app-server-protocol", + "--lib", + "schema_fixtures_tests::write_schema_fixtures_from_env", + "--", + "--exact", + "--ignored", + ], + cwd=workspace_root, + env=env, + check=True, + ) + + +if __name__ == "__main__": + main() diff --git a/codex-rs/app-server-protocol/src/bin/export.rs b/codex-rs/app-server-protocol/src/bin/export.rs deleted file mode 100644 index 10541d1fcb..0000000000 --- a/codex-rs/app-server-protocol/src/bin/export.rs +++ /dev/null @@ -1,34 +0,0 @@ -use anyhow::Result; -use clap::Parser; -use std::path::PathBuf; - -#[derive(Parser, Debug)] -#[command( - about = "Generate TypeScript bindings and JSON Schemas for the Codex app-server protocol" -)] -struct Args { - /// Output directory where generated files will be written - #[arg(short = 'o', long = "out", value_name = "DIR")] - out_dir: PathBuf, - - /// Optional Prettier executable path to format generated TypeScript files - #[arg(short = 'p', long = "prettier", value_name = "PRETTIER_BIN")] - prettier: Option, - - /// Include experimental API methods and fields in generated output. - #[arg(long = "experimental")] - experimental: bool, -} - -fn main() -> Result<()> { - let args = Args::parse(); - codex_app_server_protocol::generate_ts_with_options( - &args.out_dir, - args.prettier.as_deref(), - codex_app_server_protocol::GenerateTsOptions { - experimental_api: args.experimental, - ..codex_app_server_protocol::GenerateTsOptions::default() - }, - )?; - codex_app_server_protocol::generate_json_with_experimental(&args.out_dir, args.experimental) -} diff --git a/codex-rs/app-server-protocol/src/bin/write_schema_fixtures.rs b/codex-rs/app-server-protocol/src/bin/write_schema_fixtures.rs deleted file mode 100644 index 789d30cea2..0000000000 --- a/codex-rs/app-server-protocol/src/bin/write_schema_fixtures.rs +++ /dev/null @@ -1,42 +0,0 @@ -use anyhow::Context; -use anyhow::Result; -use clap::Parser; -use std::path::PathBuf; - -#[derive(Parser, Debug)] -#[command(about = "Regenerate vendored app-server schema fixtures")] -struct Args { - /// Root directory containing `typescript/` and `json/`. - #[arg(long = "schema-root", value_name = "DIR")] - schema_root: Option, - - /// Optional path to the Prettier executable to format generated TypeScript files. - #[arg(short = 'p', long = "prettier", value_name = "PRETTIER_BIN")] - prettier: Option, - - /// Include experimental API methods and fields in generated fixtures. - #[arg(long = "experimental")] - experimental: bool, -} - -fn main() -> Result<()> { - let args = Args::parse(); - - let schema_root = args - .schema_root - .unwrap_or_else(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("schema")); - - codex_app_server_protocol::write_schema_fixtures_with_options( - &schema_root, - args.prettier.as_deref(), - codex_app_server_protocol::SchemaFixtureOptions { - experimental_api: args.experimental, - }, - ) - .with_context(|| { - format!( - "failed to regenerate schema fixtures under {}", - schema_root.display() - ) - }) -} diff --git a/codex-rs/app-server-protocol/src/export.rs b/codex-rs/app-server-protocol/src/export.rs index 03cfa8e632..ebd45a47c8 100644 --- a/codex-rs/app-server-protocol/src/export.rs +++ b/codex-rs/app-server-protocol/src/export.rs @@ -1,8 +1,10 @@ use crate::ClientNotification; use crate::ClientRequest; +use crate::JsonSchema; use crate::ServerNotification; use crate::ServerNotificationEnvelope; use crate::ServerRequest; +use crate::TS; use crate::experimental_api::experimental_fields; use crate::export_client_notification_schemas; use crate::export_client_param_schemas; @@ -22,7 +24,6 @@ use anyhow::Context; use anyhow::Result; use anyhow::anyhow; use codex_protocol::protocol::RolloutLine; -use schemars::JsonSchema; use schemars::schema_for; use serde::Serialize; use serde_json::Map; @@ -38,7 +39,6 @@ use std::path::Path; use std::path::PathBuf; use std::process::Command; use std::thread; -use ts_rs::TS; pub(crate) const GENERATED_TS_HEADER: &str = "// GENERATED CODE! DO NOT MODIFY BY HAND!\n\n"; const IGNORED_DEFINITIONS: &[&str] = &["Option<()>"]; @@ -87,11 +87,6 @@ impl GeneratedSchema { } type JsonSchemaEmitter = fn(&Path) -> Result; -pub fn generate_types(out_dir: &Path, prettier: Option<&Path>) -> Result<()> { - generate_ts(out_dir, prettier)?; - generate_json(out_dir)?; - Ok(()) -} #[derive(Clone, Copy, Debug)] pub struct GenerateTsOptions { @@ -112,10 +107,6 @@ impl Default for GenerateTsOptions { } } -pub fn generate_ts(out_dir: &Path, prettier: Option<&Path>) -> Result<()> { - generate_ts_with_options(out_dir, prettier, GenerateTsOptions::default()) -} - pub fn generate_ts_with_options( out_dir: &Path, prettier: Option<&Path>, diff --git a/codex-rs/app-server-protocol/src/lib.rs b/codex-rs/app-server-protocol/src/lib.rs index 26842d50dc..eb2b76de07 100644 --- a/codex-rs/app-server-protocol/src/lib.rs +++ b/codex-rs/app-server-protocol/src/lib.rs @@ -1,17 +1,26 @@ mod experimental_api; +#[cfg(test)] mod export; +mod precomputed_exports; +#[cfg(test)] +#[path = "precomputed_exports_tests.rs"] +mod precomputed_exports_tests; mod protocol; pub mod rpc; +#[cfg(test)] mod schema_fixtures; +#[cfg(test)] +#[path = "schema_fixtures_tests.rs"] +mod schema_fixtures_tests; pub use experimental_api::*; -pub use export::GenerateTsOptions; -pub use export::generate_internal_json_schema; -pub use export::generate_json; -pub use export::generate_json_with_experimental; -pub use export::generate_ts; -pub use export::generate_ts_with_options; -pub use export::generate_types; +pub use precomputed_exports::GenerateTsOptions; +pub use precomputed_exports::generate_internal_json_schema; +pub use precomputed_exports::generate_json; +pub use precomputed_exports::generate_json_with_experimental; +pub use precomputed_exports::generate_ts; +pub use precomputed_exports::generate_ts_with_options; +pub use precomputed_exports::generate_types; pub use protocol::common::*; pub use protocol::event_mapping::*; pub use protocol::item_builders::*; @@ -41,10 +50,22 @@ pub use protocol::v1::Tools; pub use protocol::v1::UserSavedConfig; pub use protocol::v2::*; pub use rpc::*; +#[cfg(test)] pub use schema_fixtures::SchemaFixtureOptions; -#[doc(hidden)] -pub use schema_fixtures::generate_typescript_schema_fixture_subtree_for_tests; +#[cfg(test)] pub use schema_fixtures::read_schema_fixture_subtree; +#[cfg(test)] pub use schema_fixtures::read_schema_fixture_tree; +#[cfg(test)] pub use schema_fixtures::write_schema_fixtures; +#[cfg(test)] pub use schema_fixtures::write_schema_fixtures_with_options; + +#[cfg(not(test))] +pub(crate) use codex_app_server_protocol_noop_macros::JsonSchema; +#[cfg(not(test))] +pub(crate) use codex_app_server_protocol_noop_macros::TS; +#[cfg(test)] +pub(crate) use schemars::JsonSchema; +#[cfg(test)] +pub(crate) use ts_rs::TS; diff --git a/codex-rs/app-server-protocol/src/precomputed_exports.rs b/codex-rs/app-server-protocol/src/precomputed_exports.rs new file mode 100644 index 0000000000..51af299a46 --- /dev/null +++ b/codex-rs/app-server-protocol/src/precomputed_exports.rs @@ -0,0 +1,214 @@ +use anyhow::Context; +use anyhow::Result; +use anyhow::bail; +use serde::Deserialize; +use std::collections::BTreeMap; +use std::ffi::OsStr; +use std::fs; +use std::io::Cursor; +use std::path::Component; +use std::path::Path; +use std::path::PathBuf; +use std::process::Command; + +pub(crate) const GENERATED_TS_HEADER: &str = "// GENERATED CODE! DO NOT MODIFY BY HAND!\n\n"; +const STABLE_EXPORTS: &[u8] = + include_bytes!("../schema/precomputed/app-server-exports-stable.json.zst"); +const EXPERIMENTAL_EXPORTS: &[u8] = + include_bytes!("../schema/precomputed/app-server-exports-experimental.json.zst"); + +#[derive(Clone, Copy, Debug)] +pub struct GenerateTsOptions { + pub generate_indices: bool, + pub ensure_headers: bool, + pub run_prettier: bool, + pub experimental_api: bool, +} + +impl Default for GenerateTsOptions { + fn default() -> Self { + Self { + generate_indices: true, + ensure_headers: true, + run_prettier: true, + experimental_api: false, + } + } +} + +#[derive(Deserialize)] +#[cfg_attr(test, derive(Debug, PartialEq, Eq, serde::Serialize))] +pub(crate) struct PrecomputedExports { + pub(crate) typescript: BTreeMap, + pub(crate) json_schema: BTreeMap, + pub(crate) internal_json_schema: BTreeMap, +} + +#[derive(Clone, Copy)] +enum ExportSet { + Stable, + Experimental, +} + +pub fn generate_types(out_dir: &Path, prettier: Option<&Path>) -> Result<()> { + generate_ts(out_dir, prettier)?; + generate_json(out_dir) +} + +pub fn generate_ts(out_dir: &Path, prettier: Option<&Path>) -> Result<()> { + generate_ts_with_options(out_dir, prettier, GenerateTsOptions::default()) +} + +pub fn generate_ts_with_options( + out_dir: &Path, + prettier: Option<&Path>, + options: GenerateTsOptions, +) -> Result<()> { + let export_set = if options.experimental_api { + ExportSet::Experimental + } else { + ExportSet::Stable + }; + let exports = load_exports(export_set)?; + let ts_files = write_typescript_exports(out_dir, &exports.typescript, options)?; + + if options.run_prettier + && let Some(prettier_bin) = prettier + && !ts_files.is_empty() + { + let status = Command::new(prettier_bin) + .arg("--write") + .arg("--log-level") + .arg("warn") + .args(ts_files.iter().map(PathBuf::as_path)) + .status() + .with_context(|| format!("Failed to invoke Prettier at {}", prettier_bin.display()))?; + if !status.success() { + bail!("Prettier failed with status {status}"); + } + } + + trim_trailing_whitespace_in_ts_files(&ts_files) +} + +pub fn generate_json(out_dir: &Path) -> Result<()> { + generate_json_with_experimental(out_dir, /*experimental_api*/ false) +} + +pub fn generate_json_with_experimental(out_dir: &Path, experimental_api: bool) -> Result<()> { + let export_set = if experimental_api { + ExportSet::Experimental + } else { + ExportSet::Stable + }; + let exports = load_exports(export_set)?; + write_exports(out_dir, &exports.json_schema)?; + Ok(()) +} + +pub fn generate_internal_json_schema(out_dir: &Path) -> Result<()> { + let exports = load_exports(ExportSet::Stable)?; + write_exports(out_dir, &exports.internal_json_schema)?; + Ok(()) +} + +fn load_exports(export_set: ExportSet) -> Result { + let compressed = match export_set { + ExportSet::Stable => STABLE_EXPORTS, + ExportSet::Experimental => EXPERIMENTAL_EXPORTS, + }; + let bytes = zstd::stream::decode_all(Cursor::new(compressed)) + .context("decompress precomputed app-server protocol exports")?; + serde_json::from_slice(&bytes).context("decode precomputed app-server protocol exports") +} + +fn write_typescript_exports( + out_dir: &Path, + exports: &BTreeMap, + options: GenerateTsOptions, +) -> Result> { + let mut written = Vec::new(); + for (relative_path, archived_contents) in exports { + let is_index = Path::new(relative_path).file_name() == Some(OsStr::new("index.ts")); + if is_index && !options.generate_indices { + continue; + } + + let contents = if options.ensure_headers || is_index { + archived_contents.as_str() + } else { + archived_contents + .strip_prefix(GENERATED_TS_HEADER) + .unwrap_or(archived_contents) + }; + written.push(write_export(out_dir, relative_path, contents)?); + } + Ok(written) +} + +fn write_exports(out_dir: &Path, exports: &BTreeMap) -> Result> { + exports + .iter() + .map(|(relative_path, contents)| write_export(out_dir, relative_path, contents)) + .collect() +} + +fn write_export(out_dir: &Path, relative_path: &str, contents: &str) -> Result { + let relative_path = validated_relative_path(relative_path)?; + let output_path = out_dir.join(relative_path); + let parent = output_path + .parent() + .context("precomputed export path should have a parent")?; + fs::create_dir_all(parent).with_context(|| format!("Failed to create {}", parent.display()))?; + fs::write(&output_path, contents) + .with_context(|| format!("Failed to write {}", output_path.display()))?; + Ok(output_path) +} + +fn validated_relative_path(path: &str) -> Result<&Path> { + let path = Path::new(path); + if path.as_os_str().is_empty() + || path + .components() + .any(|component| !matches!(component, Component::Normal(_))) + { + bail!("invalid precomputed export path: {}", path.display()); + } + Ok(path) +} + +fn trim_trailing_whitespace_in_ts_files(paths: &[PathBuf]) -> Result<()> { + for path in paths { + let content = fs::read_to_string(path) + .with_context(|| format!("Failed to read {}", path.display()))?; + let trimmed = trim_trailing_line_whitespace(&content); + if trimmed != content { + fs::write(path, trimmed) + .with_context(|| format!("Failed to write {}", path.display()))?; + } + } + Ok(()) +} + +fn trim_trailing_line_whitespace(content: &str) -> String { + let mut trimmed = String::with_capacity(content.len()); + for line in content.split_inclusive('\n') { + if let Some(line_without_newline) = line.strip_suffix('\n') { + trimmed.push_str(line_without_newline.trim_end_matches([' ', '\t'])); + trimmed.push('\n'); + } else { + trimmed.push_str(line.trim_end_matches([' ', '\t'])); + } + } + trimmed +} + +#[cfg(test)] +pub(crate) fn decode_precomputed_exports(experimental_api: bool) -> Result { + let export_set = if experimental_api { + ExportSet::Experimental + } else { + ExportSet::Stable + }; + load_exports(export_set) +} diff --git a/codex-rs/app-server-protocol/src/precomputed_exports_tests.rs b/codex-rs/app-server-protocol/src/precomputed_exports_tests.rs new file mode 100644 index 0000000000..8a18b9a634 --- /dev/null +++ b/codex-rs/app-server-protocol/src/precomputed_exports_tests.rs @@ -0,0 +1,69 @@ +use crate::GenerateTsOptions; +use crate::generate_internal_json_schema; +use crate::generate_json_with_experimental; +use crate::generate_ts_with_options; +use crate::precomputed_exports::GENERATED_TS_HEADER; +use crate::precomputed_exports::decode_precomputed_exports; +use crate::schema_fixtures::collect_export_files_recursive; +use anyhow::Context; +use anyhow::Result; +use pretty_assertions::assert_eq; +use std::path::Path; + +#[test] +fn precomputed_exports_are_written_to_disk() -> Result<()> { + let output_dir = tempfile::tempdir().context("create export temp dir")?; + let typescript_dir = output_dir.path().join("typescript"); + let json_dir = output_dir.path().join("json"); + let internal_json_dir = output_dir.path().join("internal-json"); + + generate_ts_with_options( + &typescript_dir, + /*prettier*/ None, + GenerateTsOptions::default(), + )?; + generate_json_with_experimental(&json_dir, /*experimental_api*/ false)?; + generate_internal_json_schema(&internal_json_dir)?; + + let exports = decode_precomputed_exports(/*experimental_api*/ false)?; + assert_eq!( + collect_export_files_recursive(&typescript_dir)?, + exports.typescript + ); + assert_eq!( + collect_export_files_recursive(&json_dir)?, + exports.json_schema + ); + assert_eq!( + collect_export_files_recursive(&internal_json_dir)?, + exports.internal_json_schema + ); + Ok(()) +} + +#[test] +fn typescript_export_options_preserve_generator_behavior() -> Result<()> { + let output_dir = tempfile::tempdir().context("create TypeScript export temp dir")?; + generate_ts_with_options( + output_dir.path(), + /*prettier*/ None, + GenerateTsOptions { + generate_indices: false, + ensure_headers: false, + ..GenerateTsOptions::default() + }, + )?; + + let files = collect_export_files_recursive(output_dir.path())?; + assert!(!files.keys().any(|path| { + Path::new(path) + .file_name() + .is_some_and(|name| name == "index.ts") + })); + assert!( + files + .values() + .all(|contents| !contents.starts_with(GENERATED_TS_HEADER)) + ); + Ok(()) +} diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 983c97b807..37f81e738c 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -1,19 +1,22 @@ +#[cfg(test)] use std::path::Path; use std::path::PathBuf; use crate::JSONRPCNotification; use crate::JSONRPCRequest; +use crate::JsonSchema; use crate::RequestId; +use crate::TS; +#[cfg(test)] use crate::export::GeneratedSchema; +#[cfg(test)] use crate::export::write_json_schema; use crate::protocol::v1; use crate::protocol::v2; use codex_experimental_api_macros::ExperimentalApi; -use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; use strum_macros::Display; -use ts_rs::TS; /// Authentication mode for OpenAI-backed providers. #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS)] @@ -90,6 +93,7 @@ macro_rules! experimental_reason_expr { }; } +#[cfg(test)] macro_rules! experimental_method_entry { (#[experimental($reason:expr)] => $wire:literal) => { $wire @@ -102,6 +106,7 @@ macro_rules! experimental_method_entry { }; } +#[cfg(test)] macro_rules! experimental_type_entry { (#[experimental($reason:expr)] $ty:ty) => { stringify!($ty) @@ -393,22 +398,26 @@ macro_rules! client_request_definitions { } } + #[cfg(test)] pub(crate) const EXPERIMENTAL_CLIENT_METHODS: &[&str] = &[ $( experimental_method_entry!($(#[experimental($reason)])? => $wire), )* ]; + #[cfg(test)] pub(crate) const EXPERIMENTAL_CLIENT_METHOD_PARAM_TYPES: &[&str] = &[ $( experimental_type_entry!($(#[experimental($reason)])? $params), )* ]; + #[cfg(test)] pub(crate) const EXPERIMENTAL_CLIENT_METHOD_RESPONSE_TYPES: &[&str] = &[ $( experimental_type_entry!($(#[experimental($reason)])? $response), )* ]; + #[cfg(test)] pub fn export_client_responses( out_dir: &::std::path::Path, ) -> ::std::result::Result<(), ::ts_rs::ExportError> { @@ -418,12 +427,14 @@ macro_rules! client_request_definitions { Ok(()) } + #[cfg(test)] pub(crate) fn visit_client_response_types(v: &mut impl ::ts_rs::TypeVisitor) { $( v.visit::<$response>(); )* } + #[cfg(test)] #[allow(clippy::vec_init_then_push)] pub fn export_client_response_schemas( out_dir: &::std::path::Path, @@ -435,6 +446,7 @@ macro_rules! client_request_definitions { Ok(schemas) } + #[cfg(test)] #[allow(clippy::vec_init_then_push)] pub fn export_client_param_schemas( out_dir: &::std::path::Path, @@ -1330,22 +1342,26 @@ macro_rules! server_request_definitions { } } + #[cfg(test)] pub(crate) const EXPERIMENTAL_SERVER_METHODS: &[&str] = &[ $( experimental_method_entry!($(#[experimental($reason)])? $(=> $wire)?), )* ]; + #[cfg(test)] pub(crate) const EXPERIMENTAL_SERVER_METHOD_PARAM_TYPES: &[&str] = &[ $( experimental_type_entry!($(#[experimental($reason)])? $params), )* ]; + #[cfg(test)] pub(crate) const EXPERIMENTAL_SERVER_METHOD_RESPONSE_TYPES: &[&str] = &[ $( experimental_type_entry!($(#[experimental($reason)])? $response), )* ]; + #[cfg(test)] pub fn export_server_responses( out_dir: &::std::path::Path, ) -> ::std::result::Result<(), ::ts_rs::ExportError> { @@ -1355,12 +1371,14 @@ macro_rules! server_request_definitions { Ok(()) } + #[cfg(test)] pub(crate) fn visit_server_response_types(v: &mut impl ::ts_rs::TypeVisitor) { $( v.visit::<$response>(); )* } + #[cfg(test)] #[allow(clippy::vec_init_then_push)] pub fn export_server_response_schemas( out_dir: &Path, @@ -1375,6 +1393,7 @@ macro_rules! server_request_definitions { Ok(schemas) } + #[cfg(test)] #[allow(clippy::vec_init_then_push)] pub fn export_server_param_schemas( out_dir: &Path, @@ -1438,6 +1457,7 @@ macro_rules! server_notification_definitions { } } + #[cfg(test)] #[allow(clippy::vec_init_then_push)] pub fn export_server_notification_schemas( out_dir: &::std::path::Path, @@ -1466,6 +1486,7 @@ macro_rules! client_notification_definitions { )* } + #[cfg(test)] pub fn export_client_notification_schemas( _out_dir: &::std::path::Path, ) -> ::anyhow::Result> { diff --git a/codex-rs/app-server-protocol/src/protocol/serde_helpers.rs b/codex-rs/app-server-protocol/src/protocol/serde_helpers.rs index 8d3cdbea61..57a2ce1004 100644 --- a/codex-rs/app-server-protocol/src/protocol/serde_helpers.rs +++ b/codex-rs/app-server-protocol/src/protocol/serde_helpers.rs @@ -5,6 +5,7 @@ use serde::Deserializer; use serde::Serialize; use serde::Serializer; +#[cfg(test)] pub(crate) fn nullable_string_schema( generator: &mut schemars::r#gen::SchemaGenerator, ) -> schemars::schema::Schema { diff --git a/codex-rs/app-server-protocol/src/protocol/v1.rs b/codex-rs/app-server-protocol/src/protocol/v1.rs index dccea51a36..bdd820e25e 100644 --- a/codex-rs/app-server-protocol/src/protocol/v1.rs +++ b/codex-rs/app-server-protocol/src/protocol/v1.rs @@ -1,6 +1,8 @@ use std::collections::HashMap; use std::path::PathBuf; +use crate::JsonSchema; +use crate::TS; use codex_protocol::ThreadId; use codex_protocol::config_types::ForcedLoginMethod; use codex_protocol::config_types::ReasoningSummary; @@ -16,10 +18,8 @@ use codex_protocol::protocol::SandboxPolicy; use codex_protocol::protocol::SessionSource; use codex_protocol::protocol::TurnAbortReason; use codex_utils_absolute_path::AbsolutePathBuf; -use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; -use ts_rs::TS; use crate::protocol::common::AuthMode; use crate::protocol::v2::ForcedChatgptWorkspaceIds; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/account.rs b/codex-rs/app-server-protocol/src/protocol/v2/account.rs index 1e7ac5088f..6b4e78e000 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/account.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/account.rs @@ -1,3 +1,5 @@ +use crate::JsonSchema; +use crate::TS; use crate::protocol::common::AuthMode; use codex_experimental_api_macros::ExperimentalApi; use codex_protocol::account::PlanType; @@ -7,11 +9,9 @@ use codex_protocol::protocol::RateLimitReachedType as CoreRateLimitReachedType; use codex_protocol::protocol::RateLimitSnapshot as CoreRateLimitSnapshot; use codex_protocol::protocol::RateLimitWindow as CoreRateLimitWindow; use codex_protocol::protocol::SpendControlLimitSnapshot as CoreSpendControlLimitSnapshot; -use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; use std::collections::HashMap; -use ts_rs::TS; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(tag = "type", rename_all = "camelCase")] diff --git a/codex-rs/app-server-protocol/src/protocol/v2/apps.rs b/codex-rs/app-server-protocol/src/protocol/v2/apps.rs index ea511183bd..2b36fa1746 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/apps.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/apps.rs @@ -1,9 +1,9 @@ use super::shared::default_enabled; -use schemars::JsonSchema; +use crate::JsonSchema; +use crate::TS; use serde::Deserialize; use serde::Serialize; use std::collections::HashMap; -use ts_rs::TS; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] #[serde(rename_all = "camelCase")] diff --git a/codex-rs/app-server-protocol/src/protocol/v2/attestation.rs b/codex-rs/app-server-protocol/src/protocol/v2/attestation.rs index ef8828b580..931539b3f1 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/attestation.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/attestation.rs @@ -1,7 +1,7 @@ -use schemars::JsonSchema; +use crate::JsonSchema; +use crate::TS; use serde::Deserialize; use serde::Serialize; -use ts_rs::TS; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, Default)] #[serde(rename_all = "camelCase")] diff --git a/codex-rs/app-server-protocol/src/protocol/v2/collaboration_mode.rs b/codex-rs/app-server-protocol/src/protocol/v2/collaboration_mode.rs index b013bc13d4..250719229e 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/collaboration_mode.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/collaboration_mode.rs @@ -1,10 +1,10 @@ +use crate::JsonSchema; +use crate::TS; use codex_protocol::config_types::CollaborationModeMask as CoreCollaborationModeMask; use codex_protocol::config_types::ModeKind; use codex_protocol::openai_models::ReasoningEffort; -use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; -use ts_rs::TS; /// EXPERIMENTAL - list collaboration mode presets. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] diff --git a/codex-rs/app-server-protocol/src/protocol/v2/command_exec.rs b/codex-rs/app-server-protocol/src/protocol/v2/command_exec.rs index 8f913b5bcf..15d747bd21 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/command_exec.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/command_exec.rs @@ -1,11 +1,11 @@ use super::SandboxPolicy; +use crate::JsonSchema; +use crate::TS; use codex_experimental_api_macros::ExperimentalApi; -use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; use std::collections::HashMap; use std::path::PathBuf; -use ts_rs::TS; /// PTY size in character cells for `command/exec` PTY sessions. #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] diff --git a/codex-rs/app-server-protocol/src/protocol/v2/config.rs b/codex-rs/app-server-protocol/src/protocol/v2/config.rs index aaafb255b2..82b4ca9fba 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/config.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/config.rs @@ -3,6 +3,8 @@ use super::AskForApproval; use super::SandboxMode; use super::WindowsSandboxSetupMode; use super::shared::default_enabled; +use crate::JsonSchema; +use crate::TS; use codex_experimental_api_macros::ExperimentalApi; use codex_protocol::config_types::AutoCompactTokenLimitScope; use codex_protocol::config_types::ForcedLoginMethod; @@ -13,14 +15,12 @@ use codex_protocol::config_types::WebSearchToolConfig; use codex_protocol::openai_models::ReasoningEffort; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_path_uri::PathUri; -use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; use serde_json::Value as JsonValue; use std::collections::BTreeMap; use std::collections::HashMap; use std::path::PathBuf; -use ts_rs::TS; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] #[serde(tag = "type", rename_all = "camelCase")] diff --git a/codex-rs/app-server-protocol/src/protocol/v2/current_time.rs b/codex-rs/app-server-protocol/src/protocol/v2/current_time.rs index 9fd23f7981..830b05b862 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/current_time.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/current_time.rs @@ -1,7 +1,7 @@ -use schemars::JsonSchema; +use crate::JsonSchema; +use crate::TS; use serde::Deserialize; use serde::Serialize; -use ts_rs::TS; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] diff --git a/codex-rs/app-server-protocol/src/protocol/v2/environment.rs b/codex-rs/app-server-protocol/src/protocol/v2/environment.rs index 5f8af6f26b..4351199224 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/environment.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/environment.rs @@ -1,8 +1,8 @@ +use crate::JsonSchema; +use crate::TS; use codex_utils_path_uri::PathUri; -use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; -use ts_rs::TS; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] diff --git a/codex-rs/app-server-protocol/src/protocol/v2/experimental_feature.rs b/codex-rs/app-server-protocol/src/protocol/v2/experimental_feature.rs index a16f45216b..1ea8fc7779 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/experimental_feature.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/experimental_feature.rs @@ -1,8 +1,8 @@ -use schemars::JsonSchema; +use crate::JsonSchema; +use crate::TS; use serde::Deserialize; use serde::Serialize; use std::collections::BTreeMap; -use ts_rs::TS; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)] #[serde(rename_all = "camelCase")] diff --git a/codex-rs/app-server-protocol/src/protocol/v2/feedback.rs b/codex-rs/app-server-protocol/src/protocol/v2/feedback.rs index fdddb3aaa7..4c5fae4059 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/feedback.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/feedback.rs @@ -1,9 +1,9 @@ -use schemars::JsonSchema; +use crate::JsonSchema; +use crate::TS; use serde::Deserialize; use serde::Serialize; use std::collections::BTreeMap; use std::path::PathBuf; -use ts_rs::TS; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] diff --git a/codex-rs/app-server-protocol/src/protocol/v2/fs.rs b/codex-rs/app-server-protocol/src/protocol/v2/fs.rs index 0132c6b284..951b7af9b6 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/fs.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/fs.rs @@ -1,8 +1,8 @@ +use crate::JsonSchema; +use crate::TS; use codex_utils_absolute_path::AbsolutePathBuf; -use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; -use ts_rs::TS; /// Read a file from the host filesystem. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] diff --git a/codex-rs/app-server-protocol/src/protocol/v2/hook.rs b/codex-rs/app-server-protocol/src/protocol/v2/hook.rs index 247b69e647..69984e8657 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/hook.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/hook.rs @@ -1,4 +1,6 @@ use super::shared::v2_enum_from_core; +use crate::JsonSchema; +use crate::TS; use codex_protocol::protocol::HookEventName as CoreHookEventName; use codex_protocol::protocol::HookExecutionMode as CoreHookExecutionMode; use codex_protocol::protocol::HookHandlerType as CoreHookHandlerType; @@ -10,10 +12,8 @@ use codex_protocol::protocol::HookScope as CoreHookScope; use codex_protocol::protocol::HookSource as CoreHookSource; use codex_protocol::protocol::HookTrustStatus as CoreHookTrustStatus; use codex_utils_absolute_path::AbsolutePathBuf; -use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; -use ts_rs::TS; v2_enum_from_core!( pub enum HookEventName from CoreHookEventName { diff --git a/codex-rs/app-server-protocol/src/protocol/v2/item.rs b/codex-rs/app-server-protocol/src/protocol/v2/item.rs index 009b7275d4..1099f710ae 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/item.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/item.rs @@ -8,6 +8,8 @@ use super::NetworkPolicyAmendment; use super::RequestPermissionProfile; use super::UserInput; use super::shared::v2_enum_from_core; +use crate::JsonSchema; +use crate::TS; use crate::protocol::item_builders::command_actions_for_path_uri; use crate::protocol::item_builders::convert_patch_changes; use crate::protocol::item_builders::review_output_text; @@ -44,7 +46,6 @@ use codex_protocol::protocol::SubAgentActivityKind as CoreSubAgentActivityKind; use codex_shell_command::parse_command::shlex_join; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_path_uri::LegacyAppPathString; -use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; use serde_json::Value as JsonValue; @@ -52,7 +53,6 @@ use serde_with::serde_as; use std::collections::HashMap; use std::io; use std::path::PathBuf; -use ts_rs::TS; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] diff --git a/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs b/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs index 95317c8d01..979d92ecb2 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/mcp.rs @@ -1,4 +1,6 @@ use super::shared::v2_enum_from_core; +use crate::JsonSchema; +use crate::TS; use codex_protocol::approvals::ElicitationRequest as CoreElicitationRequest; use codex_protocol::items::McpToolCallError as CoreMcpToolCallError; use codex_protocol::mcp::CallToolResult as CoreMcpCallToolResult; @@ -7,12 +9,10 @@ use codex_protocol::mcp::Resource as McpResource; pub use codex_protocol::mcp::ResourceContent as McpResourceContent; use codex_protocol::mcp::ResourceTemplate as McpResourceTemplate; use codex_protocol::mcp::Tool as McpTool; -use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; use serde_json::Value as JsonValue; use std::collections::BTreeMap; -use ts_rs::TS; v2_enum_from_core!( pub enum McpAuthStatus from codex_protocol::protocol::McpAuthStatus { diff --git a/codex-rs/app-server-protocol/src/protocol/v2/model.rs b/codex-rs/app-server-protocol/src/protocol/v2/model.rs index bb6c411124..7d9409136e 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/model.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/model.rs @@ -1,15 +1,15 @@ use super::shared::v2_enum_from_core; +use crate::JsonSchema; +use crate::TS; use codex_protocol::openai_models::InputModality; use codex_protocol::openai_models::ModelAvailabilityNux as CoreModelAvailabilityNux; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::openai_models::default_input_modalities; use codex_protocol::protocol::ModelRerouteReason as CoreModelRerouteReason; use codex_protocol::protocol::ModelVerification as CoreModelVerification; -use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; use serde_json::Value as JsonValue; -use ts_rs::TS; v2_enum_from_core!( pub enum ModelRerouteReason from CoreModelRerouteReason { diff --git a/codex-rs/app-server-protocol/src/protocol/v2/notification.rs b/codex-rs/app-server-protocol/src/protocol/v2/notification.rs index 8289cf5683..af2287627c 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/notification.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/notification.rs @@ -1,9 +1,9 @@ use super::TurnError; +use crate::JsonSchema; use crate::RequestId; -use schemars::JsonSchema; +use crate::TS; use serde::Deserialize; use serde::Serialize; -use ts_rs::TS; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] diff --git a/codex-rs/app-server-protocol/src/protocol/v2/permissions.rs b/codex-rs/app-server-protocol/src/protocol/v2/permissions.rs index 58c3735ba1..9360934aa4 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/permissions.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/permissions.rs @@ -1,4 +1,6 @@ use super::shared::v2_enum_from_core; +use crate::JsonSchema; +use crate::TS; use codex_protocol::approvals::ExecPolicyAmendment as CoreExecPolicyAmendment; use codex_protocol::approvals::NetworkApprovalContext as CoreNetworkApprovalContext; use codex_protocol::approvals::NetworkApprovalProtocol as CoreNetworkApprovalProtocol; @@ -19,13 +21,11 @@ use codex_protocol::request_permissions::RequestPermissionProfile as CoreRequest use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_path_uri::LegacyAppPathString; use codex_utils_path_uri::PathConvention; -use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; use std::io; use std::num::NonZeroUsize; use std::path::Path; -use ts_rs::TS; v2_enum_from_core! { pub enum NetworkApprovalProtocol from CoreNetworkApprovalProtocol { diff --git a/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs b/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs index 458a0247f5..e0f521b60f 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/plugin.rs @@ -3,17 +3,17 @@ use super::HookEventName; use super::HookHandlerType; use super::HookSource; use super::HookTrustStatus; +use crate::JsonSchema; +use crate::TS; use codex_protocol::protocol::SkillDependencies as CoreSkillDependencies; use codex_protocol::protocol::SkillInterface as CoreSkillInterface; use codex_protocol::protocol::SkillMetadata as CoreSkillMetadata; use codex_protocol::protocol::SkillScope as CoreSkillScope; use codex_protocol::protocol::SkillToolDependency as CoreSkillToolDependency; use codex_utils_absolute_path::AbsolutePathBuf; -use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; use std::path::PathBuf; -use ts_rs::TS; #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] diff --git a/codex-rs/app-server-protocol/src/protocol/v2/process.rs b/codex-rs/app-server-protocol/src/protocol/v2/process.rs index b70847165e..11a8d687d4 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/process.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/process.rs @@ -1,9 +1,9 @@ +use crate::JsonSchema; +use crate::TS; use codex_utils_absolute_path::AbsolutePathBuf; -use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; use std::collections::HashMap; -use ts_rs::TS; /// PTY size in character cells for `process/spawn` PTY sessions. #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] diff --git a/codex-rs/app-server-protocol/src/protocol/v2/realtime.rs b/codex-rs/app-server-protocol/src/protocol/v2/realtime.rs index cede06a43d..950590c366 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/realtime.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/realtime.rs @@ -1,3 +1,5 @@ +use crate::JsonSchema; +use crate::TS; use codex_protocol::protocol::CodexResponseHandoffMode; use codex_protocol::protocol::ConversationTextRole; use codex_protocol::protocol::RealtimeAudioFrame as CoreRealtimeAudioFrame; @@ -5,12 +7,10 @@ use codex_protocol::protocol::RealtimeConversationVersion; use codex_protocol::protocol::RealtimeOutputModality; use codex_protocol::protocol::RealtimeVoice; use codex_protocol::protocol::RealtimeVoicesList; -use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; use serde_json::Value as JsonValue; use std::collections::BTreeMap; -use ts_rs::TS; /// EXPERIMENTAL - thread realtime audio chunk. #[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS)] diff --git a/codex-rs/app-server-protocol/src/protocol/v2/remote_control.rs b/codex-rs/app-server-protocol/src/protocol/v2/remote_control.rs index f69bbdc008..f089126cf3 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/remote_control.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/remote_control.rs @@ -1,7 +1,7 @@ -use schemars::JsonSchema; +use crate::JsonSchema; +use crate::TS; use serde::Deserialize; use serde::Serialize; -use ts_rs::TS; #[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] diff --git a/codex-rs/app-server-protocol/src/protocol/v2/review.rs b/codex-rs/app-server-protocol/src/protocol/v2/review.rs index 82ec5b6f59..2896418e22 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/review.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/review.rs @@ -1,9 +1,9 @@ use super::Turn; use super::shared::v2_enum_from_core; -use schemars::JsonSchema; +use crate::JsonSchema; +use crate::TS; use serde::Deserialize; use serde::Serialize; -use ts_rs::TS; v2_enum_from_core!( pub enum ReviewDelivery from codex_protocol::protocol::ReviewDelivery { diff --git a/codex-rs/app-server-protocol/src/protocol/v2/shared.rs b/codex-rs/app-server-protocol/src/protocol/v2/shared.rs index 4cd580ed69..340c0ab496 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/shared.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/shared.rs @@ -1,3 +1,5 @@ +use crate::JsonSchema; +use crate::TS; use codex_experimental_api_macros::ExperimentalApi; use codex_protocol::config_types::ApprovalsReviewer as CoreApprovalsReviewer; use codex_protocol::config_types::SandboxMode as CoreSandboxMode; @@ -5,16 +7,20 @@ use codex_protocol::protocol::AskForApproval as CoreAskForApproval; use codex_protocol::protocol::CodexErrorInfo as CoreCodexErrorInfo; use codex_protocol::protocol::GranularApprovalConfig as CoreGranularApprovalConfig; use codex_protocol::protocol::NonSteerableTurnKind as CoreNonSteerableTurnKind; -use schemars::JsonSchema; +#[cfg(test)] use schemars::r#gen::SchemaGenerator; +#[cfg(test)] use schemars::schema::InstanceType; +#[cfg(test)] use schemars::schema::Metadata; +#[cfg(test)] use schemars::schema::Schema; +#[cfg(test)] use schemars::schema::SchemaObject; use serde::Deserialize; use serde::Serialize; +#[cfg(test)] use serde_json::Value as JsonValue; -use ts_rs::TS; // Macro to declare a camelCased API v2 enum mirroring a core enum which // tends to use either snake_case or kebab-case. @@ -236,6 +242,7 @@ pub enum ApprovalsReviewer { AutoReview, } +#[cfg(test)] impl JsonSchema for ApprovalsReviewer { fn schema_name() -> String { "ApprovalsReviewer".to_string() @@ -249,6 +256,7 @@ impl JsonSchema for ApprovalsReviewer { } } +#[cfg(test)] fn string_enum_schema_with_description(values: &[&str], description: &str) -> Schema { let mut schema = SchemaObject { instance_type: Some(InstanceType::String.into()), diff --git a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs index d8b02ac650..569593ac3f 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/thread.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/thread.rs @@ -12,6 +12,8 @@ use super::Turn; use super::TurnEnvironmentParams; use super::TurnItemsView; use super::shared::v2_enum_from_core; +use crate::JsonSchema; +use crate::TS; use codex_experimental_api_macros::ExperimentalApi; pub use codex_protocol::capabilities::CapabilityRootLocation; pub use codex_protocol::capabilities::SelectedCapabilityRoot; @@ -31,13 +33,11 @@ use codex_protocol::protocol::TokenUsageInfo as CoreTokenUsageInfo; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_path_uri::LegacyAppPathString; use codex_utils_path_uri::PathUri; -use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; use serde_json::Value as JsonValue; use std::collections::HashMap; use std::path::PathBuf; -use ts_rs::TS; #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] diff --git a/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs b/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs index 3b63fc3c87..4a55a10839 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/thread_data.rs @@ -2,20 +2,22 @@ use super::CodexErrorInfo; use super::ThreadItem; use super::ThreadStatus; use super::TurnStatus; +use crate::JsonSchema; +use crate::TS; use codex_experimental_api_macros::ExperimentalApi; use codex_protocol::protocol::SessionSource as CoreSessionSource; use codex_protocol::protocol::SubAgentSource as CoreSubAgentSource; use codex_protocol::protocol::ThreadHistoryMode as CoreThreadHistoryMode; use codex_protocol::protocol::ThreadSource as CoreThreadSource; use codex_utils_absolute_path::AbsolutePathBuf; -use schemars::JsonSchema; +#[cfg(test)] use schemars::r#gen::SchemaGenerator; +#[cfg(test)] use schemars::schema::Schema; use serde::Deserialize; use serde::Serialize; use std::path::PathBuf; use thiserror::Error; -use ts_rs::TS; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] @@ -103,6 +105,7 @@ pub enum ThreadSource { MemoryConsolidation, } +#[cfg(test)] impl JsonSchema for ThreadSource { fn schema_name() -> String { "ThreadSource".to_string() diff --git a/codex-rs/app-server-protocol/src/protocol/v2/turn.rs b/codex-rs/app-server-protocol/src/protocol/v2/turn.rs index cb8473d49a..0b2c092ec8 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/turn.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/turn.rs @@ -2,6 +2,8 @@ use super::ApprovalsReviewer; use super::AskForApproval; use super::SandboxPolicy; use super::Turn; +use crate::JsonSchema; +use crate::TS; use codex_experimental_api_macros::ExperimentalApi; use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::MultiAgentMode; @@ -16,13 +18,11 @@ use codex_protocol::user_input::TextElement as CoreTextElement; use codex_protocol::user_input::UserInput as CoreUserInput; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_path_uri::LegacyAppPathString; -use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; use serde_json::Value as JsonValue; use std::collections::HashMap; use std::path::PathBuf; -use ts_rs::TS; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] diff --git a/codex-rs/app-server-protocol/src/protocol/v2/windows_sandbox.rs b/codex-rs/app-server-protocol/src/protocol/v2/windows_sandbox.rs index 3e090c7bfd..1cc240a95e 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/windows_sandbox.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/windows_sandbox.rs @@ -1,8 +1,8 @@ +use crate::JsonSchema; +use crate::TS; use codex_utils_absolute_path::AbsolutePathBuf; -use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; -use ts_rs::TS; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(rename_all = "camelCase")] diff --git a/codex-rs/app-server-protocol/src/rpc.rs b/codex-rs/app-server-protocol/src/rpc.rs index 4e8858ce00..23920864dd 100644 --- a/codex-rs/app-server-protocol/src/rpc.rs +++ b/codex-rs/app-server-protocol/src/rpc.rs @@ -1,12 +1,12 @@ //! We do not do true JSON-RPC 2.0, as we neither send nor expect the //! "jsonrpc": "2.0" field. +use crate::JsonSchema; +use crate::TS; use codex_protocol::protocol::W3cTraceContext; -use schemars::JsonSchema; use serde::Deserialize; use serde::Serialize; use std::fmt; -use ts_rs::TS; pub const JSONRPC_VERSION: &str = "2.0"; diff --git a/codex-rs/app-server-protocol/src/schema_fixtures.rs b/codex-rs/app-server-protocol/src/schema_fixtures.rs index 6bf60e4a55..d0f36db800 100644 --- a/codex-rs/app-server-protocol/src/schema_fixtures.rs +++ b/codex-rs/app-server-protocol/src/schema_fixtures.rs @@ -3,10 +3,12 @@ use crate::ClientRequest; use crate::ServerNotification; use crate::ServerNotificationEnvelope; use crate::ServerRequest; +use crate::TS; use crate::export::GENERATED_TS_HEADER; use crate::export::filter_experimental_ts_tree; use crate::export::generate_index_ts_tree; use crate::export::trim_trailing_line_whitespace; +use crate::precomputed_exports::PrecomputedExports; use crate::protocol::common::visit_client_response_types; use crate::protocol::common::visit_server_response_types; use anyhow::Context; @@ -17,9 +19,9 @@ use std::any::TypeId; use std::cmp::Ordering; use std::collections::BTreeMap; use std::collections::HashSet; +use std::io::Cursor; use std::path::Path; use std::path::PathBuf; -use ts_rs::TS; use ts_rs::TypeVisitor; #[derive(Clone, Copy, Debug, Default)] @@ -81,7 +83,7 @@ pub fn generate_typescript_schema_fixture_subtree_for_tests() -> Result, options: SchemaFixtureOptions, ) -> Result<()> { + if options.experimental_api { + return write_experimental_precomputed_exports(schema_root, prettier); + } + let typescript_out_dir = schema_root.join("typescript"); let json_out_dir = schema_root.join("json"); ensure_empty_dir(&typescript_out_dir)?; ensure_empty_dir(&json_out_dir)?; - crate::generate_ts_with_options( + crate::export::generate_ts_with_options( &typescript_out_dir, prettier, - crate::GenerateTsOptions { - experimental_api: options.experimental_api, - ..crate::GenerateTsOptions::default() - }, + crate::export::GenerateTsOptions::default(), )?; - crate::generate_json_with_experimental(&json_out_dir, options.experimental_api)?; + crate::export::generate_json(&json_out_dir)?; + + let internal_dir = tempfile::tempdir().context("create internal schema temp dir")?; + crate::export::generate_internal_json_schema(internal_dir.path())?; + let exports = PrecomputedExports { + typescript: collect_export_files_recursive(&typescript_out_dir)?, + json_schema: collect_export_files_recursive(&json_out_dir)?, + internal_json_schema: collect_export_files_recursive(internal_dir.path())?, + }; + write_precomputed_exports(schema_root, "stable", &exports)?; Ok(()) } +fn write_experimental_precomputed_exports( + schema_root: &Path, + prettier: Option<&Path>, +) -> Result<()> { + let temp_dir = tempfile::tempdir().context("create experimental schema temp dir")?; + let typescript_out_dir = temp_dir.path().join("typescript"); + let json_out_dir = temp_dir.path().join("json"); + + crate::export::generate_ts_with_options( + &typescript_out_dir, + prettier, + crate::export::GenerateTsOptions { + experimental_api: true, + ..crate::export::GenerateTsOptions::default() + }, + )?; + crate::export::generate_json_with_experimental(&json_out_dir, /*experimental_api*/ true)?; + + let exports = PrecomputedExports { + typescript: collect_export_files_recursive(&typescript_out_dir)?, + json_schema: collect_export_files_recursive(&json_out_dir)?, + internal_json_schema: BTreeMap::new(), + }; + write_precomputed_exports(schema_root, "experimental", &exports) +} + +fn write_precomputed_exports( + schema_root: &Path, + name: &str, + exports: &PrecomputedExports, +) -> Result<()> { + let output_dir = schema_root.join("precomputed"); + std::fs::create_dir_all(&output_dir) + .with_context(|| format!("create {}", output_dir.display()))?; + let output_path = output_dir.join(format!("app-server-exports-{name}.json.zst")); + let json = serde_json::to_vec(exports).context("serialize precomputed protocol exports")?; + let compressed = zstd::stream::encode_all(Cursor::new(json), 19) + .context("compress precomputed protocol exports")?; + std::fs::write(&output_path, compressed) + .with_context(|| format!("write {}", output_path.display())) +} + fn ensure_empty_dir(dir: &Path) -> Result<()> { if dir.exists() { std::fs::remove_dir_all(dir) @@ -229,8 +283,31 @@ fn schema_array_item_sort_key(item: &Value) -> Option { } fn collect_files_recursive(root: &Path) -> Result>> { - let mut files = BTreeMap::new(); + files_recursive(root)? + .into_iter() + .map(|(relative_path, path)| Ok((relative_path, read_file_bytes(&path)?))) + .collect() +} +pub(crate) fn collect_export_files_recursive(root: &Path) -> Result> { + files_recursive(root)? + .into_iter() + .map(|(relative_path, path)| { + let relative_path = relative_path + .components() + .map(|component| component.as_os_str().to_string_lossy()) + .collect::>() + .join("/"); + let contents = std::fs::read_to_string(&path) + .with_context(|| format!("read UTF-8 export {}", path.display()))? + .replace("\r\n", "\n"); + Ok((relative_path, contents)) + }) + .collect() +} + +fn files_recursive(root: &Path) -> Result> { + let mut files = Vec::new(); let mut stack = vec![root.to_path_buf()]; while let Some(dir) = stack.pop() { for entry in std::fs::read_dir(&dir) @@ -262,10 +339,11 @@ fn collect_files_recursive(root: &Path) -> Result>> { })? .to_path_buf(); - files.insert(rel, read_file_bytes(&path)?); + files.push((rel, path)); } } + files.sort_by(|(left, _), (right, _)| left.cmp(right)); Ok(files) } diff --git a/codex-rs/app-server-protocol/tests/schema_fixtures.rs b/codex-rs/app-server-protocol/src/schema_fixtures_tests.rs similarity index 55% rename from codex-rs/app-server-protocol/tests/schema_fixtures.rs rename to codex-rs/app-server-protocol/src/schema_fixtures_tests.rs index 3fef9d6d0b..d87072b49c 100644 --- a/codex-rs/app-server-protocol/tests/schema_fixtures.rs +++ b/codex-rs/app-server-protocol/src/schema_fixtures_tests.rs @@ -1,8 +1,16 @@ +use crate::export::GenerateTsOptions; +use crate::export::generate_internal_json_schema; +use crate::export::generate_json_with_experimental; +use crate::export::generate_ts_with_options; +use crate::precomputed_exports::decode_precomputed_exports; +use crate::schema_fixtures::SchemaFixtureOptions; +use crate::schema_fixtures::collect_export_files_recursive; +use crate::schema_fixtures::generate_typescript_schema_fixture_subtree_for_tests; +use crate::schema_fixtures::read_schema_fixture_subtree; +use crate::schema_fixtures::write_schema_fixtures_with_options; use anyhow::Context; use anyhow::Result; -use codex_app_server_protocol::generate_json_with_experimental; -use codex_app_server_protocol::generate_typescript_schema_fixture_subtree_for_tests; -use codex_app_server_protocol::read_schema_fixture_subtree; +use pretty_assertions::assert_eq; use similar::TextDiff; use std::collections::BTreeMap; use std::path::Path; @@ -35,6 +43,81 @@ fn json_schema_fixtures_match_generated() -> Result<()> { }) } +#[test] +fn stable_precomputed_exports_match_schema_fixtures() -> Result<()> { + let schema_root = schema_root()?; + let exports = decode_precomputed_exports(/*experimental_api*/ false)?; + + assert_eq!( + exports.typescript, + collect_export_files_recursive(&schema_root.join("typescript"))? + ); + assert_eq!( + exports.json_schema, + collect_export_files_recursive(&schema_root.join("json"))? + ); + + let internal_dir = tempfile::tempdir().context("create internal schema temp dir")?; + generate_internal_json_schema(internal_dir.path())?; + assert_json_export_trees_match( + &exports.internal_json_schema, + &collect_export_files_recursive(internal_dir.path())?, + )?; + Ok(()) +} + +#[test] +fn experimental_precomputed_exports_match_generated() -> Result<()> { + let output_dir = tempfile::tempdir().context("create experimental schema temp dir")?; + let typescript_dir = output_dir.path().join("typescript"); + let json_dir = output_dir.path().join("json"); + generate_ts_with_options( + &typescript_dir, + /*prettier*/ None, + GenerateTsOptions { + experimental_api: true, + ..GenerateTsOptions::default() + }, + )?; + generate_json_with_experimental(&json_dir, /*experimental_api*/ true)?; + + let exports = decode_precomputed_exports(/*experimental_api*/ true)?; + assert_eq!( + exports.typescript, + collect_export_files_recursive(&typescript_dir)? + ); + assert_json_export_trees_match( + &exports.json_schema, + &collect_export_files_recursive(&json_dir)?, + )?; + assert_eq!(exports.internal_json_schema, BTreeMap::new()); + Ok(()) +} + +#[test] +#[ignore = "invoked by `just write-app-server-schema`"] +fn write_schema_fixtures_from_env() -> Result<()> { + let schema_root = std::env::var_os("CODEX_APP_SERVER_SCHEMA_ROOT") + .map(PathBuf::from) + .context("CODEX_APP_SERVER_SCHEMA_ROOT must be set")?; + let prettier = std::env::var_os("CODEX_APP_SERVER_SCHEMA_PRETTIER").map(PathBuf::from); + let experimental = std::env::var("CODEX_APP_SERVER_SCHEMA_EXPERIMENTAL") + .context("CODEX_APP_SERVER_SCHEMA_EXPERIMENTAL must be set")?; + let experimental_api = match experimental.as_str() { + "0" => false, + "1" => true, + value => { + anyhow::bail!("CODEX_APP_SERVER_SCHEMA_EXPERIMENTAL must be 0 or 1, got {value:?}") + } + }; + + write_schema_fixtures_with_options( + &schema_root, + prettier.as_deref(), + SchemaFixtureOptions { experimental_api }, + ) +} + fn assert_schema_fixtures_match_generated( label: &'static str, generate: impl FnOnce(&Path) -> Result<()>, @@ -52,9 +135,24 @@ fn assert_schema_fixtures_match_generated( })?; let generated_tree = read_tree(temp_dir.path(), label)?; + assert_schema_trees_match(label, &fixture_tree, &generated_tree) +} - assert_schema_trees_match(label, &fixture_tree, &generated_tree)?; - +fn assert_json_export_trees_match( + expected: &BTreeMap, + actual: &BTreeMap, +) -> Result<()> { + assert_eq!( + expected.keys().collect::>(), + actual.keys().collect::>() + ); + for (path, expected) in expected { + let expected: serde_json::Value = serde_json::from_str(expected) + .with_context(|| format!("parse precomputed JSON export {path}"))?; + let actual: serde_json::Value = serde_json::from_str(&actual[path]) + .with_context(|| format!("parse freshly generated JSON export {path}"))?; + assert_eq!(expected, actual, "JSON export differs: {path}"); + } Ok(()) } @@ -86,7 +184,6 @@ Run `just write-app-server-schema` to overwrite with your changes.\n\n{diff}" ); } - // If the file sets match, diff contents for each file for a nicer error. for (path, expected) in fixture_tree { let actual = generated_tree .get(path) @@ -113,8 +210,6 @@ Run `just write-app-server-schema` to overwrite with your changes.\n\n{diff}", } fn schema_root() -> Result { - // In Bazel runfiles (especially manifest-only mode), resolving directories is not - // reliable. Resolve a known file, then walk up to the schema root. let typescript_index = codex_utils_cargo_bin::find_resource!("schema/typescript/index.ts") .context("resolve TypeScript schema index.ts")?; let schema_root = typescript_index @@ -123,7 +218,6 @@ fn schema_root() -> Result { .context("derive schema root from schema/typescript/index.ts")? .to_path_buf(); - // Sanity check that the JSON fixtures resolve to the same schema root. let json_bundle = codex_utils_cargo_bin::find_resource!("schema/json/codex_app_server_protocol.schemas.json") .context("resolve JSON schema bundle")?; diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index 680e223e76..0ff7854162 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -2438,7 +2438,7 @@ For server-initiated request payloads, annotate the field the same way so schema ```bash just write-app-server-schema - # Include experimental API fields/methods in fixtures. + # Refresh the embedded exports that include experimental API fields/methods. just write-app-server-schema --experimental ```