Files
codex/prs/bolinfest/study/PR-1715-study.md
2025-09-02 15:17:45 -07:00

5.3 KiB
Raw Blame History

DOs

  • Use strong types for API fields: prefer enums over strings for request/response parameters.
use codex_core::protocol::AskForApproval;
use codex_core::config_types::SandboxMode;

#[derive(Serialize, Deserialize)]
pub struct NewConversationArgs {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub prompt: Option<String>,
    pub model: String,
    pub cwd: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub approval_policy: Option<AskForApproval>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub sandbox: Option<SandboxMode>,
}
  • Newtype all identifiers: wrap raw IDs (e.g., u32/Uuid) so the representation can change without touching call sites.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ConversationId(pub u32); // swap to Uuid later without churn

#[derive(Serialize, Deserialize)]
pub struct ConnectArgs {
    pub conversation_id: ConversationId,
}
  • Convert ID representations at boundaries: accept one form, respond with another if desired.
// Example: accept Uuid on input, store/map to u32 internally, return ConversationId(u32)
fn register_conversation(external: uuid::Uuid) -> ConversationId {
    let short = id_map_insert_or_get_u32(external); // app logic, not shown
    ConversationId(short)
}
  • Co-locate requests with their results: keep related types adjacent and consistently named.
#[derive(Serialize, Deserialize)]
pub struct ConnectArgs {
    pub conversation_id: ConversationId,
}

#[derive(Serialize, Deserialize, Default)]
pub struct ConnectResult; // empty result lives next to ConnectArgs

#[derive(Serialize, Deserialize)]
#[serde(tag = "type", content = "data", rename_all = "snake_case")]
pub enum ToolCallResponseData {
    Connect(ConnectResult),
    // ...
}
  • Use consistent “Result” suffix for response payloads: avoid mixed naming like “Accepted.”
#[derive(Serialize, Deserialize)]
pub struct SendUserMessageResult {
    pub accepted: bool,
}

#[derive(Serialize, Deserialize)]
#[serde(tag = "type", content = "data", rename_all = "snake_case")]
pub enum ToolCallResponseData {
    SendUserMessage(SendUserMessageResult),
    // ...
}
  • Unify notifications under one enum and dispatch by a stable tag: one place to match on type.
#[derive(Serialize, Deserialize)]
#[serde(tag = "type", content = "data", rename_all = "snake_case")]
pub enum ServerNotification {
    InitialState(InitialStateNotificationParams),
    ConnectionRevoked(ConnectionRevokedNotificationParams),
    CodexEvent(CodexEventNotificationParams),
    Cancelled(CancelledNotificationParams),
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NotificationMeta {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub conversation_id: Option<ConversationId>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub request_id: Option<mcp_types::RequestId>,
}
  • Omit empties and nulls in JSON: use serde defaults and skip_serializing_if.
#[derive(Serialize, Deserialize)]
pub struct InitialStatePayload {
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub events: Vec<CodexEventNotificationParams>,
}

#[derive(Serialize, Deserialize)]
pub struct CancelledNotificationParams {
    pub id: mcp_types::RequestId,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub reason: Option<String>,
}
  • Favor clear test naming and ergonomics: use observed/expected and .expect(...).
#[test]
fn serialize_initial_state_minimal() {
    let params = InitialStateNotificationParams { /* ... */ };
    let observed = serde_json::to_value(&params)
        .expect("serialize InitialStateNotificationParams");
    let expected = serde_json::json!({ /* exact shape */ });
    assert_eq!(observed, expected);
}

DONTs

  • Dont use String for typed fields: avoid Option<String> for things like approval policy or sandbox.
// ❌ Avoid
pub struct NewConversationArgs {
    pub approval_policy: Option<String>,
    pub sandbox: Option<String>,
}
  • Dont pass raw primitives for IDs throughout the codebase: avoid leaking Uuid/u32 directly.
// ❌ Avoid
pub struct ConnectArgs {
    pub conversation_id: uuid::Uuid,
}
  • Dont mix response naming patterns: avoid one-offs like SendUserMessageAccepted among *Result types.
// ❌ Avoid
pub enum ToolCallResponseData {
    SendUserMessage(SendUserMessageAccepted),
}
  • Dont fragment notification handling across multiple enums: avoid scattering and ad-hoc dispatch.
// ❌ Avoid
pub enum ConversationNotificationParams { /* subset only */ }
pub enum SystemNotificationParams { /* another subset */ }
  • Dont write verbose test error handling or ambiguous variables: avoid got and manual match on results.
// ❌ Avoid
let got = match serde_json::to_value(&params) {
    Ok(v) => v,
    Err(e) => panic!("failed: {e}"),
};
  • Dont serialize empty/default fields: avoid emitting reason: null or empty arrays by default.
// ❌ Avoid
#[derive(Serialize)]
pub struct CancelledNotificationParams {
    pub id: mcp_types::RequestId,
    pub reason: Option<String>, // will serialize as null without skip_serializing_if
}