config/src/schema.rs
159 lines
use crate::config_toml::ConfigToml;use crate::types::RawMcpServerConfig;use codex_features::FEATURES;use codex_features::legacy_feature_keys;use schemars::r#gen::SchemaGenerator;use schemars::r#gen::SchemaSettings;use schemars::schema::InstanceType;use schemars::schema::ObjectValidation;use schemars::schema::RootSchema;use schemars::schema::Schema;use schemars::schema::SchemaObject;use schemars::schema::SubschemaValidation;use serde_json::Map;use serde_json::Value;use std::path::Path;/// Schema for the `[features]` map with known + legacy keys only.pub fn features_schema(schema_gen: &mut SchemaGenerator) -> Schema { let mut object = SchemaObject { instance_type: Some(InstanceType::Object.into()), ..Default::default() }; let mut validation = ObjectValidation::default(); for feature in FEATURES { if feature.id == codex_features::Feature::Artifact { continue; } if feature.id == codex_features::Feature::CodeMode { validation.properties.insert( feature.key.to_string(), schema_gen.subschema_for::<codex_features::FeatureToml< codex_features::CodeModeConfigToml, >>(), ); continue; } if feature.id == codex_features::Feature::MultiAgentV2 { validation.properties.insert( feature.key.to_string(), schema_gen.subschema_for::<codex_features::FeatureToml< codex_features::MultiAgentV2ConfigToml, >>(), ); continue; } if feature.id == codex_features::Feature::AppsMcpPathOverride { validation.properties.insert( feature.key.to_string(), removed_apps_mcp_path_override_schema(schema_gen), ); continue; } if feature.id == codex_features::Feature::NetworkProxy { validation.properties.insert( feature.key.to_string(), schema_gen.subschema_for::<codex_features::FeatureToml< codex_features::NetworkProxyConfigToml, >>(), ); continue; } validation .properties .insert(feature.key.to_string(), schema_gen.subschema_for::<bool>()); } for legacy_key in legacy_feature_keys() { validation .properties .insert(legacy_key.to_string(), schema_gen.subschema_for::<bool>()); } validation.additional_properties = Some(Box::new(Schema::Bool(false))); object.object = Some(Box::new(validation)); Schema::Object(object)}fn removed_apps_mcp_path_override_schema(schema_gen: &mut SchemaGenerator) -> Schema { let mut config_validation = ObjectValidation::default(); config_validation .properties .insert("enabled".to_string(), schema_gen.subschema_for::<bool>()); config_validation .properties .insert("path".to_string(), schema_gen.subschema_for::<String>()); config_validation.additional_properties = Some(Box::new(Schema::Bool(false))); let config = Schema::Object(SchemaObject { instance_type: Some(InstanceType::Object.into()), object: Some(Box::new(config_validation)), ..Default::default() }); Schema::Object(SchemaObject { subschemas: Some(Box::new(SubschemaValidation { any_of: Some(vec![schema_gen.subschema_for::<bool>(), config]), ..Default::default() })), ..Default::default() })}/// Schema for the `[mcp_servers]` map using the raw input shape.pub fn mcp_servers_schema(schema_gen: &mut SchemaGenerator) -> Schema { let mut object = SchemaObject { instance_type: Some(InstanceType::Object.into()), ..Default::default() }; let validation = ObjectValidation { additional_properties: Some(Box::new(schema_gen.subschema_for::<RawMcpServerConfig>())), ..Default::default() }; object.object = Some(Box::new(validation)); Schema::Object(object)}/// Build the config schema for `config.toml`.pub fn config_schema() -> RootSchema { SchemaSettings::draft07() .with(|settings| { settings.option_add_null_type = false; }) .into_generator() .into_root_schema_for::<ConfigToml>()}/// Canonicalize a JSON value by sorting its keys.pub fn canonicalize(value: &Value) -> Value { match value { Value::Array(items) => Value::Array(items.iter().map(canonicalize).collect()), Value::Object(map) => { let mut entries: Vec<_> = map.iter().collect(); entries.sort_by_key(|(key, _)| *key); let mut sorted = Map::with_capacity(map.len()); for (key, child) in entries { sorted.insert(key.clone(), canonicalize(child)); } Value::Object(sorted) } _ => value.clone(), }}/// Render the config schema as pretty-printed JSON.pub fn config_schema_json() -> anyhow::Result<Vec<u8>> { let schema = config_schema(); let value = serde_json::to_value(schema)?; let value = canonicalize(&value); let json = serde_json::to_vec_pretty(&value)?; Ok(json)}/// Write the config schema fixture to disk.pub fn write_config_schema(out_path: &Path) -> anyhow::Result<()> { let json = config_schema_json()?; std::fs::write(out_path, json)?; Ok(())}