Precompute app-server protocol exports (#36212)

## Why

Normal app-server protocol builds do not need the `ts-rs` and `schemars`
implementations used to generate schema exports.

## What changed

- Embed compressed stable and experimental TypeScript and JSON schema exports,
  and serve the existing export APIs from those artifacts.
- Use no-op schema derives in non-test builds while keeping the real generators
  available for fixture regeneration and tests.
- Move schema fixture regeneration behind a Python helper and update
  `just write-app-server-schema --experimental` to refresh the experimental
  embedded exports.

## Testing

- Verify embedded exports match freshly generated stable and experimental
  schemas.
- Verify export options and on-disk output remain compatible.

GitOrigin-RevId: e8536338b457e6eec34bdf29ec0684144bd13734
This commit is contained in:
Michael Bolin
2026-07-30 19:47:30 +00:00
committed by copyberry
parent 0042b00986
commit acd540f158
50 changed files with 720 additions and 181 deletions
+6 -1
View File
@@ -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"
+2
View File
@@ -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" }
@@ -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,
)
@@ -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
@@ -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()
}
+1
View File
@@ -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/**"],
+10 -4
View File
@@ -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 }
@@ -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()
@@ -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<PathBuf>,
/// 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)
}
@@ -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<PathBuf>,
/// Optional path to the Prettier executable to format generated TypeScript files.
#[arg(short = 'p', long = "prettier", value_name = "PRETTIER_BIN")]
prettier: Option<PathBuf>,
/// 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()
)
})
}
+2 -11
View File
@@ -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<GeneratedSchema>;
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>,
+30 -9
View File
@@ -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;
@@ -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<String, String>,
pub(crate) json_schema: BTreeMap<String, String>,
pub(crate) internal_json_schema: BTreeMap<String, String>,
}
#[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<PrecomputedExports> {
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<String, String>,
options: GenerateTsOptions,
) -> Result<Vec<PathBuf>> {
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<String, String>) -> Result<Vec<PathBuf>> {
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<PathBuf> {
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<PrecomputedExports> {
let export_set = if experimental_api {
ExportSet::Experimental
} else {
ExportSet::Stable
};
load_exports(export_set)
}
@@ -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(())
}
@@ -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<Vec<GeneratedSchema>> {
@@ -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 {
@@ -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;
@@ -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")]
@@ -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")]
@@ -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")]
@@ -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)]
@@ -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)]
@@ -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")]
@@ -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")]
@@ -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")]
@@ -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")]
@@ -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")]
@@ -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)]
@@ -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 {
@@ -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")]
@@ -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 {
@@ -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 {
@@ -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")]
@@ -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 {
@@ -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")]
@@ -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)]
@@ -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)]
@@ -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")]
@@ -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 {
@@ -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()),
@@ -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")]
@@ -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()
@@ -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")]
@@ -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")]
+2 -2
View File
@@ -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";
@@ -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<BTreeMap
.collect())
}
/// Regenerates `schema/typescript/` and `schema/json/`.
/// Regenerates `schema/typescript/`, `schema/json/`, and the stable embedded exports.
///
/// This is intended to be used by tooling (e.g., `just write-app-server-schema`).
/// It deletes any previously generated files so stale artifacts are removed.
@@ -95,25 +97,77 @@ pub fn write_schema_fixtures_with_options(
prettier: Option<&Path>,
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<String> {
}
fn collect_files_recursive(root: &Path) -> Result<BTreeMap<PathBuf, Vec<u8>>> {
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<BTreeMap<String, String>> {
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::<Vec<_>>()
.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<Vec<(PathBuf, PathBuf)>> {
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<BTreeMap<PathBuf, Vec<u8>>> {
})?
.to_path_buf();
files.insert(rel, read_file_bytes(&path)?);
files.push((rel, path));
}
}
files.sort_by(|(left, _), (right, _)| left.cmp(right));
Ok(files)
}
@@ -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<String, String>,
actual: &BTreeMap<String, String>,
) -> Result<()> {
assert_eq!(
expected.keys().collect::<Vec<_>>(),
actual.keys().collect::<Vec<_>>()
);
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<PathBuf> {
// 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<PathBuf> {
.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")?;
+1 -1
View File
@@ -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
```