use std::collections::{HashMap, HashSet};
fn load_openapi_schemas() -> HashMap<String, Vec<String>> {
let yaml_str =
std::fs::read_to_string("tests/openapi.yaml").expect("tests/openapi.yaml not found");
let yaml_str: String = yaml_str
.lines()
.filter(|line| !line.contains("9223372036854") && !line.contains("922337203685477"))
.collect::<Vec<_>>()
.join("\n");
let doc: serde_yaml::Value = serde_yaml::from_str(&yaml_str).expect("Invalid YAML");
let schemas = doc
.get("components")
.and_then(|c| c.get("schemas"))
.expect("No components.schemas in OpenAPI spec");
let mut result = HashMap::new();
if let serde_yaml::Value::Mapping(map) = schemas {
for (key, value) in map {
let name = key.as_str().unwrap_or("").to_string();
let mut fields = Vec::new();
if let Some(props) = value.get("properties") {
if let serde_yaml::Value::Mapping(props_map) = props {
for (field_key, _) in props_map {
if let Some(field_name) = field_key.as_str() {
fields.push(field_name.to_string());
}
}
}
}
if let Some(all_of) = value.get("allOf") {
if let serde_yaml::Value::Sequence(items) = all_of {
for item in items {
if let Some(props) = item.get("properties") {
if let serde_yaml::Value::Mapping(props_map) = props {
for (field_key, _) in props_map {
if let Some(field_name) = field_key.as_str() {
fields.push(field_name.to_string());
}
}
}
}
}
}
}
if !fields.is_empty() {
result.insert(name, fields);
}
}
}
result
}
fn fields_from_source(source_path: &str, struct_name: &str) -> HashSet<String> {
let content = std::fs::read_to_string(source_path).unwrap_or_default();
let mut fields = HashSet::new();
let mut in_struct = false;
let mut brace_depth = 0;
for line in content.lines() {
let trimmed = line.trim();
if trimmed.contains(&format!("pub struct {struct_name}")) {
in_struct = true;
if trimmed.contains('{') {
brace_depth = 1;
}
continue;
}
if in_struct {
brace_depth += trimmed.matches('{').count();
brace_depth = brace_depth.saturating_sub(trimmed.matches('}').count());
if brace_depth == 0 {
break;
}
if let Some(field) = trimmed.strip_prefix("pub ") {
if let Some(name) = field.split(':').next() {
let name = name.trim();
if !name.is_empty() && !name.contains('(') && !name.contains('<') {
let rename = content[..content.find(&format!("pub {name}:")).unwrap_or(0)]
.lines()
.rev()
.take(3)
.find(|l| l.contains("serde(rename"))
.and_then(|l| l.split('"').nth(1).map(|s| s.to_string()));
fields.insert(rename.unwrap_or_else(|| name.to_string()));
}
}
}
}
}
fields
}
fn check_coverage(schema_name: &str, spec_fields: &[String], rust_fields: &HashSet<String>) -> f64 {
if spec_fields.is_empty() {
return 100.0;
}
let matched: Vec<_> = spec_fields
.iter()
.filter(|f| rust_fields.contains(f.as_str()))
.collect();
let pct = (matched.len() as f64 / spec_fields.len() as f64) * 100.0;
let missing: Vec<_> = spec_fields
.iter()
.filter(|f| !rust_fields.contains(f.as_str()))
.collect();
eprintln!(
"[{schema_name}] {:.0}% ({}/{}) {}",
pct,
matched.len(),
spec_fields.len(),
if missing.is_empty() {
String::new()
} else {
format!("missing: {:?}", missing)
}
);
pct
}
#[test]
fn openapi_spec_loads() {
let schemas = load_openapi_schemas();
let expected = [
"CreateChatCompletionRequest",
"CreateChatCompletionResponse",
"CreateEmbeddingRequest",
"CreateImageRequest",
"CreateModerationRequest",
];
for name in &expected {
assert!(
schemas.contains_key(*name),
"OpenAPI spec missing schema: {name}"
);
}
eprintln!("OpenAPI spec loaded: {} schemas total", schemas.len());
}
#[test]
fn chat_completion_request_coverage() {
let schemas = load_openapi_schemas();
let spec = schemas
.get("CreateChatCompletionRequest")
.expect("schema not found");
let rust_fields =
fields_from_source("openai-types/src/chat/manual.rs", "ChatCompletionRequest");
eprintln!(" Rust fields found: {:?}", rust_fields);
let pct = check_coverage("CreateChatCompletionRequest", spec, &rust_fields);
assert!(pct >= 60.0, "coverage {pct:.0}% < 60%");
}
#[test]
fn chat_completion_response_deserializes_all_spec_fields() {
let fixture = serde_json::json!({
"id": "chatcmpl-abc123",
"object": "chat.completion",
"created": 1700000000_i64,
"model": "gpt-4o",
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": "Hello world",
"refusal": null,
"tool_calls": null
},
"finish_reason": "stop",
"logprobs": null
}],
"usage": {
"prompt_tokens": 10,
"completion_tokens": 5,
"total_tokens": 15,
"prompt_tokens_details": { "cached_tokens": 0, "audio_tokens": 0 },
"completion_tokens_details": { "reasoning_tokens": 0, "audio_tokens": 0 }
},
"system_fingerprint": "fp_abc123",
"service_tier": "default"
});
let result: Result<openai_oxide::types::chat::ChatCompletionResponse, _> =
serde_json::from_value(fixture);
assert!(
result.is_ok(),
"Failed to deserialize full response fixture: {:?}",
result.err()
);
}
#[test]
fn embedding_request_coverage() {
let schemas = load_openapi_schemas();
let spec = schemas
.get("CreateEmbeddingRequest")
.expect("schema not found");
let rust_fields = fields_from_source(
"openai-types/src/embedding/manual.rs",
"EmbeddingCreateRequest",
);
let pct = check_coverage("CreateEmbeddingRequest", spec, &rust_fields);
assert!(pct >= 80.0, "coverage {pct:.0}% < 80%");
}
#[test]
fn overall_coverage_report() {
let schemas = load_openapi_schemas();
let request_checks: Vec<(&str, HashSet<String>)> = vec![
(
"CreateChatCompletionRequest",
fields_from_source("openai-types/src/chat/manual.rs", "ChatCompletionRequest"),
),
(
"CreateEmbeddingRequest",
fields_from_source(
"openai-types/src/embedding/manual.rs",
"EmbeddingCreateRequest",
),
),
(
"CreateImageRequest",
fields_from_source("openai-types/src/image/manual.rs", "ImageGenerateRequest"),
),
(
"CreateModerationRequest",
fields_from_source("openai-types/src/moderation/manual.rs", "ModerationRequest"),
),
];
eprintln!("\n=== OpenAPI Coverage Report ===");
let mut total_spec = 0;
let mut total_matched = 0;
for (schema_name, rust_fields) in &request_checks {
if let Some(spec_fields) = schemas.get(*schema_name) {
let matched = spec_fields
.iter()
.filter(|f| rust_fields.contains(f.as_str()))
.count();
total_spec += spec_fields.len();
total_matched += matched;
check_coverage(schema_name, spec_fields, rust_fields);
}
}
let overall = if total_spec == 0 {
100.0
} else {
total_matched as f64 / total_spec as f64 * 100.0
};
eprintln!("=== Overall: {overall:.0}% ({total_matched}/{total_spec}) ===\n");
assert!(overall >= 80.0, "Overall coverage {overall:.0}% < 80%");
}
#[test]
fn chat_completion_response_coverage() {
let schemas = load_openapi_schemas();
let spec = schemas
.get("CreateChatCompletionResponse")
.expect("schema not found");
let rust_fields =
fields_from_source("openai-types/src/chat/manual.rs", "ChatCompletionResponse");
let pct = check_coverage("CreateChatCompletionResponse", spec, &rust_fields);
assert!(pct >= 80.0, "coverage {pct:.0}% < 80%");
}
#[test]
fn usage_details_deserialize() {
let json = serde_json::json!({
"prompt_tokens": 100,
"completion_tokens": 50,
"total_tokens": 150,
"prompt_tokens_details": {
"cached_tokens": 20,
"audio_tokens": 5
},
"completion_tokens_details": {
"reasoning_tokens": 10,
"audio_tokens": 3,
"accepted_prediction_tokens": 15,
"rejected_prediction_tokens": 2
}
});
let usage: openai_oxide::types::common::Usage = serde_json::from_value(json).unwrap();
assert_eq!(usage.prompt_tokens, Some(100));
let prompt_details = usage.prompt_tokens_details.unwrap();
assert_eq!(prompt_details.cached_tokens, Some(20));
assert_eq!(prompt_details.audio_tokens, Some(5));
let completion_details = usage.completion_tokens_details.unwrap();
assert_eq!(completion_details.reasoning_tokens, Some(10));
assert_eq!(completion_details.accepted_prediction_tokens, Some(15));
assert_eq!(completion_details.rejected_prediction_tokens, Some(2));
}
#[test]
fn chat_request_new_fields_serialize() {
use openai_oxide::types::chat::*;
let mut req = ChatCompletionRequest::new(
"gpt-4o",
vec![ChatCompletionMessageParam::User {
content: UserContent::Text("Hi".into()),
name: None,
}],
);
req.reasoning_effort = Some(openai_oxide::types::common::ReasoningEffort::High);
req.modalities = Some(vec!["text".into(), "audio".into()]);
req.audio = Some(ChatCompletionAudioParam {
format: "mp3".into(),
voice: "alloy".into(),
});
req.prediction = Some(PredictionContent {
type_: "content".into(),
content: serde_json::json!("predicted text"),
});
req.web_search_options = Some(WebSearchOptions {
search_context_size: Some(openai_oxide::types::common::SearchContextSize::Medium),
user_location: None,
});
req.max_tokens = Some(1000);
let json = serde_json::to_value(&req).unwrap();
assert_eq!(json["reasoning_effort"], "high");
assert_eq!(json["modalities"], serde_json::json!(["text", "audio"]));
assert_eq!(json["audio"]["format"], "mp3");
assert_eq!(json["audio"]["voice"], "alloy");
assert_eq!(json["prediction"]["type"], "content");
assert_eq!(json["web_search_options"]["search_context_size"], "medium");
assert_eq!(json["max_tokens"], 1000);
}
#[test]
fn file_object_status_enum_deserialize() {
use openai_oxide::types::file::{FilePurpose, FileStatus};
let json = serde_json::json!({
"id": "file-abc",
"object": "file",
"bytes": 100,
"created_at": 1700000000_i64,
"filename": "data.jsonl",
"purpose": "fine-tune",
"status": "processed"
});
let file: openai_oxide::types::file::FileObject = serde_json::from_value(json).unwrap();
assert_eq!(file.purpose, FilePurpose::FineTune);
assert_eq!(file.status, FileStatus::Processed);
}
#[test]
fn batch_status_enum_deserialize() {
use openai_oxide::types::batch::BatchStatus;
let json = serde_json::json!({
"id": "batch_abc",
"object": "batch",
"endpoint": "/v1/chat/completions",
"input_file_id": "file-abc",
"completion_window": "24h",
"status": "in_progress",
"created_at": 1700000000_i64
});
let batch: openai_oxide::types::batch::Batch = serde_json::from_value(json).unwrap();
assert_eq!(batch.status, BatchStatus::InProgress);
}
#[test]
fn upload_status_enum_deserialize() {
use openai_oxide::types::upload::UploadStatus;
let json = serde_json::json!({
"id": "upload_abc",
"object": "upload",
"bytes": 2000000,
"filename": "data.jsonl",
"purpose": "fine-tune",
"status": "completed",
"created_at": 1700000000_i64
});
let upload: openai_oxide::types::upload::Upload = serde_json::from_value(json).unwrap();
assert_eq!(upload.status, UploadStatus::Completed);
}
#[test]
fn fine_tuning_status_enum_deserialize() {
use openai_oxide::types::fine_tuning::FineTuningStatus;
let json = serde_json::json!({
"id": "ftjob-abc",
"object": "fine_tuning.job",
"created_at": 1700000000_i64,
"model": "gpt-4o-mini",
"training_file": "file-abc",
"status": "validating_files",
"organization_id": "org-123",
"result_files": [],
"seed": 42
});
let job: openai_oxide::types::fine_tuning::FineTuningJob =
serde_json::from_value(json).unwrap();
assert_eq!(job.status, FineTuningStatus::ValidatingFiles);
}
#[test]
fn image_enums_serialize() {
use openai_oxide::types::image::*;
let mut req = ImageGenerateRequest::new("test image");
req.quality = Some(ImageQuality::Hd);
req.size = Some(ImageSize::S1024x1024);
req.style = Some(ImageStyle::Natural);
req.output_format = Some(ImageOutputFormat::Webp);
req.background = Some(ImageBackground::Transparent);
req.moderation = Some(ImageModeration::Auto);
req.response_format = Some(ImageResponseFormat::B64Json);
let json = serde_json::to_value(&req).unwrap();
assert_eq!(json["quality"], "hd");
assert_eq!(json["size"], "1024x1024");
assert_eq!(json["style"], "natural");
assert_eq!(json["output_format"], "webp");
assert_eq!(json["background"], "transparent");
assert_eq!(json["moderation"], "auto");
assert_eq!(json["response_format"], "b64_json");
}
#[test]
fn audio_voice_enum_serialize() {
use openai_oxide::types::audio::*;
let req = SpeechRequest::new("Hello", "tts-1", AudioVoice::Shimmer);
let json = serde_json::to_value(&req).unwrap();
assert_eq!(json["voice"], "shimmer");
}
#[test]
fn encoding_format_enum_serialize() {
use openai_oxide::types::embedding::*;
let mut req = EmbeddingRequest::new("text-embedding-3-small", "hello");
req.encoding_format = Some(EncodingFormat::Base64);
let json = serde_json::to_value(&req).unwrap();
assert_eq!(json["encoding_format"], "base64");
}
#[test]
fn auto_or_fixed_round_trip() {
use openai_oxide::types::common::AutoOrFixed;
let auto: AutoOrFixed<i64> = serde_json::from_str(r#""auto""#).unwrap();
assert_eq!(auto, AutoOrFixed::Auto);
assert_eq!(serde_json::to_string(&auto).unwrap(), r#""auto""#);
let fixed: AutoOrFixed<i64> = serde_json::from_str("10").unwrap();
assert_eq!(fixed, AutoOrFixed::Fixed(10));
assert_eq!(serde_json::to_string(&fixed).unwrap(), "10");
let fixed_f: AutoOrFixed<f64> = serde_json::from_str("0.5").unwrap();
assert_eq!(fixed_f, AutoOrFixed::Fixed(0.5));
}
#[test]
fn max_response_tokens_round_trip() {
use openai_oxide::types::common::MaxResponseTokens;
let inf: MaxResponseTokens = serde_json::from_str(r#""inf""#).unwrap();
assert_eq!(inf, MaxResponseTokens::Inf);
assert_eq!(serde_json::to_string(&inf).unwrap(), r#""inf""#);
let fixed: MaxResponseTokens = serde_json::from_str("4096").unwrap();
assert_eq!(fixed, MaxResponseTokens::Fixed(4096));
assert_eq!(serde_json::to_string(&fixed).unwrap(), "4096");
}
#[test]
fn function_call_option_round_trip() {
use openai_oxide::types::chat::FunctionCallOption;
let mode: FunctionCallOption = serde_json::from_str(r#""auto""#).unwrap();
assert_eq!(serde_json::to_value(&mode).unwrap(), "auto");
let named: FunctionCallOption = serde_json::from_str(r#"{"name": "get_weather"}"#).unwrap();
assert_eq!(
serde_json::to_value(&named).unwrap(),
serde_json::json!({"name": "get_weather"})
);
}