codex-app-server-sdk 0.5.1

Tokio Rust SDK for Codex App Server
Documentation
use schemars::JsonSchema;
use schemars::generate::SchemaSettings;
use serde::Serialize;
use serde::de::DeserializeOwned;
use serde_json::Value;

pub trait OpenAiSerializable: Sized {
    fn openai_output_schema() -> Value;

    fn to_openai_value(&self) -> serde_json::Result<Value>
    where
        Self: Serialize,
    {
        serialize_openai_value(self)
    }

    fn from_openai_value(value: Value) -> serde_json::Result<Self>
    where
        Self: DeserializeOwned,
    {
        deserialize_openai_value(value)
    }
}

pub fn openai_json_schema_for<T>() -> Value
where
    T: JsonSchema,
{
    let schema = SchemaSettings::default()
        .with(|settings| settings.meta_schema = None)
        .into_generator()
        .into_root_schema_for::<T>();
    let mut schema =
        serde_json::to_value(schema).expect("serializing generated schema should not fail");
    enforce_openai_object_constraints(&mut schema);
    schema
}

pub fn serialize_openai_value<T>(value: &T) -> serde_json::Result<Value>
where
    T: Serialize,
{
    serde_json::to_value(value)
}

pub fn deserialize_openai_value<T>(value: Value) -> serde_json::Result<T>
where
    T: DeserializeOwned,
{
    serde_json::from_value(value)
}

fn enforce_openai_object_constraints(value: &mut Value) {
    match value {
        Value::Object(map) => {
            for nested in map.values_mut() {
                enforce_openai_object_constraints(nested);
            }

            let is_object_schema = map.get("type").and_then(Value::as_str) == Some("object");
            if is_object_schema && !map.contains_key("additionalProperties") {
                map.insert("additionalProperties".to_string(), Value::Bool(false));
            }
        }
        Value::Array(values) => {
            for nested in values {
                enforce_openai_object_constraints(nested);
            }
        }
        _ => {}
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use schemars::JsonSchema;
    use serde::{Deserialize, Serialize};

    #[derive(
        Debug,
        Clone,
        PartialEq,
        Eq,
        Serialize,
        Deserialize,
        JsonSchema,
        codex_app_server_sdk::OpenAiSerializable,
    )]
    struct SummarySchema {
        answer: String,
    }

    #[test]
    fn derives_openai_schema_without_meta_schema() {
        let schema = SummarySchema::openai_output_schema();
        assert!(schema.get("$schema").is_none());
        assert_eq!(
            schema.get("type"),
            Some(&Value::String("object".to_string()))
        );
        assert_eq!(
            schema.get("additionalProperties"),
            Some(&Value::Bool(false))
        );
        let properties = schema
            .get("properties")
            .and_then(Value::as_object)
            .expect("schema should include properties object");
        assert!(properties.contains_key("answer"));
    }

    #[test]
    fn helper_serializes_and_deserializes_struct_values() {
        let expected = SummarySchema {
            answer: "ok".to_string(),
        };
        let value = expected
            .to_openai_value()
            .expect("serialize should succeed");
        let parsed = SummarySchema::from_openai_value(value).expect("deserialize should succeed");
        assert_eq!(parsed, expected);
    }

    #[test]
    fn schema_helper_accepts_json_schema_types_directly() {
        let schema = openai_json_schema_for::<SummarySchema>();
        assert_eq!(
            schema.get("type"),
            Some(&Value::String("object".to_string()))
        );
    }
}