#![expect( clippy::disallowed_methods, reason = "build script: runs at compile time outside any runtime" )] use std::path::{Path, PathBuf}; use std::{env, fs}; use progenitor::{GenerationSettings, Generator, InterfaceStyle, TypeImpl}; /// Recursively convert OpenAPI 3.1 `type: "null"` patterns to 3.0 `nullable: /// true`. /// /// Handles two patterns: /// - `oneOf: [{...}, {type: "null"}]` → the non-null schema with `nullable: /// true` /// - `type: [T1, ..., "null"]` → the remaining types with `nullable: true` fn patch_nullable(value: &mut serde_json::Value) { match value { serde_json::Value::Object(map) => { // Pattern: oneOf with a {type: "null"} variant if let Some(one_of) = map.get_mut("oneOf") { if let Some(variants) = one_of.as_array_mut() { let null_idx = variants.iter().position(|v| { v.get("type").and_then(serde_json::Value::as_str) == Some("null") }); if let Some(idx) = null_idx { variants.remove(idx); if variants.len() == 1 { // Collapse single-variant oneOf into the schema itself let mut inner = variants.remove(0); if inner.get("$ref").is_some() { inner = serde_json::json!({ "allOf": [inner], "nullable": true, }); } else { inner .as_object_mut() .expect("oneOf collapse should leave an object schema") .insert("nullable".to_string(), serde_json::Value::Bool(true)); } patch_nullable(&mut inner); *value = inner; return; } map.insert("nullable".to_string(), serde_json::Value::Bool(true)); } } } // Pattern: type array containing "null" let needs_nullable_from_type = map .get("type") .and_then(|v| v.as_array()) .is_some_and(|arr| arr.iter().any(|v| v.as_str() == Some("null"))); if needs_nullable_from_type { if let Some(type_val) = map.get_mut("type") { if let Some(arr) = type_val.as_array_mut() { arr.retain(|v| v.as_str() != Some("null")); if arr.len() == 1 { *type_val = arr.remove(0); } } } map.insert("nullable".to_string(), serde_json::Value::Bool(true)); } for v in map.values_mut() { patch_nullable(v); } } serde_json::Value::Array(arr) => { for v in arr { patch_nullable(v); } } _ => {} } } /// Progenitor currently panics when an operation advertises more than one /// request-body media type. /// /// Keep the source OpenAPI spec accurate for docs, but collapse the /// generated-client view down to a single preferred media type so code /// generation can proceed. fn patch_codegen_request_body_media_types(value: &mut serde_json::Value) { let Some(paths) = value .get_mut("paths") .and_then(serde_json::Value::as_object_mut) else { return; }; for path_item in paths.values_mut() { let Some(item) = path_item.as_object_mut() else { continue; }; for method in ["get", "put", "post", "delete", "patch"] { let Some(operation) = item .get_mut(method) .and_then(serde_json::Value::as_object_mut) else { continue; }; let Some(content) = operation .get_mut("requestBody") .and_then(|request_body| request_body.get_mut("content")) .and_then(serde_json::Value::as_object_mut) else { continue; }; if content.len() <= 1 { continue; } let preferred = content .get("application/octet-stream") .cloned() .map(|value| ("application/octet-stream".to_string(), value)) .or_else(|| { content .iter() .next() .map(|(key, value)| (key.clone(), value.clone())) }); if let Some((key, value)) = preferred { content.clear(); content.insert(key, value); } } } } /// Progenitor currently panics when an operation advertises a typed success /// response alongside a no-content success response. Keep the source spec /// accurate for docs and other generators, but preserve the existing Rust /// client shape for `DELETE /runs/{id}`: success with no body. fn patch_codegen_delete_run_responses(value: &mut serde_json::Value) { let Some(responses) = value .get_mut("paths") .and_then(|paths| paths.get_mut("/api/v1/runs/{id}")) .and_then(|path| path.get_mut("delete")) .and_then(|operation| operation.get_mut("responses")) .and_then(serde_json::Value::as_object_mut) else { return; }; responses.remove("200"); } fn spec_path_from_manifest_dir(manifest_dir: &Path) -> PathBuf { manifest_dir .ancestors() .nth(3) .expect("fabro-api manifest dir should be nested under /lib/foundation/fabro-api") .join("docs/public/api-reference/fabro-api.yaml") } fn main() { let manifest_dir = env::var_os("CARGO_MANIFEST_DIR") .map(PathBuf::from) .expect("CARGO_MANIFEST_DIR should be set for build scripts"); let spec_path = spec_path_from_manifest_dir(&manifest_dir); println!("cargo::rerun-if-changed={}", spec_path.display()); let spec_text = fs::read_to_string(&spec_path) .unwrap_or_else(|e| panic!("failed to read {}: {e}", spec_path.display())); let mut spec_value: serde_json::Value = serde_yaml::from_str(&spec_text).unwrap_or_else(|e| panic!("failed to parse YAML: {e}")); // TODO: Remove 3.1→3.0 patch when progenitor supports OpenAPI 3.1. // Progenitor only supports OpenAPI 3.0.x; our spec uses 3.1.0 but doesn't // rely on any 3.1-only features that affect codegen. spec_value["openapi"] = serde_json::Value::String("3.0.3".to_string()); patch_nullable(&mut spec_value); patch_codegen_request_body_media_types(&mut spec_value); patch_codegen_delete_run_responses(&mut spec_value); let spec: openapiv3::OpenAPI = serde_json::from_value(spec_value).expect("failed to deserialize OpenAPI spec"); let mut settings = GenerationSettings::default(); settings.with_interface(InterfaceStyle::Builder); let replacements: &[(&str, &str, &[TypeImpl])] = &[ ("RunStatus", "fabro_types::status::RunStatus", &[]), ("PendingReason", "fabro_types::status::PendingReason", &[]), ("SuccessReason", "fabro_types::status::SuccessReason", &[]), ("FailureReason", "fabro_types::status::FailureReason", &[]), ("FailureCategory", "fabro_types::FailureCategory", &[]), ("FailureSignature", "fabro_types::FailureSignature", &[]), ("FailureDetail", "fabro_types::FailureDetail", &[]), ("RunFailure", "fabro_types::RunFailure", &[]), ("BlockedReason", "fabro_types::status::BlockedReason", &[]), ( "RunControlAction", "fabro_types::status::RunControlAction", &[], ), ("Run", "fabro_types::Run", &[]), ("RunApproval", "fabro_types::RunApproval", &[]), ("RunApprovalState", "fabro_types::RunApprovalState", &[]), ("RunRunnableSource", "fabro_types::RunRunnableSource", &[]), ("RunSize", "fabro_types::RunSize", &[]), ("DiffSummary", "fabro_types::DiffSummary", &[]), ("RepositoryRef", "fabro_types::RepositoryRef", &[]), ("WorkflowSettings", "fabro_types::WorkflowSettings", &[]), // Run-level GitHub integration settings reuse the canonical resolved // types instead of generating parallel API DTOs; the wire shape is // identical (InterpString serializes as its source string). ( "RunIntegrationsSettings", "fabro_types::settings::run::RunIntegrationsSettings", &[], ), ( "RunIntegrationsGithubSettings", "fabro_types::settings::run::RunIntegrationsGithubSettings", &[], ), ("ServerSettings", "fabro_types::ServerSettings", &[]), ( "ServerNamespace", "fabro_types::settings::ServerNamespace", &[], ), ( "ServerListenSettings", "fabro_types::settings::server::ServerListenSettings", &[], ), ( "ServerApiSettings", "fabro_types::settings::server::ServerApiSettings", &[], ), ( "ServerWebSettings", "fabro_types::settings::server::ServerWebSettings", &[], ), ( "ServerAuthSettings", "fabro_types::settings::server::ServerAuthSettings", &[], ), ( "ServerAuthMethod", "fabro_types::settings::server::ServerAuthMethod", &[], ), ( "ServerAuthGithubSettings", "fabro_types::settings::server::ServerAuthGithubSettings", &[], ), ( "RunModelSettings", "fabro_types::settings::run::RunModelSettings", &[], ), ( "RunModelControls", "fabro_types::settings::run::RunModelControls", &[], ), ( "ServerSandboxSettings", "fabro_types::settings::server::ServerSandboxSettings", &[], ), ( "ServerSandboxProvidersSettings", "fabro_types::settings::server::ServerSandboxProvidersSettings", &[], ), ( "ServerSandboxProviderSettings", "fabro_types::settings::server::ServerSandboxProviderSettings", &[], ), ( "ServerStorageSettings", "fabro_types::settings::server::ServerStorageSettings", &[], ), ( "ServerArtifactsSettings", "fabro_types::settings::server::ServerArtifactsSettings", &[], ), ( "ServerSlateDbSettings", "fabro_types::settings::server::ServerSlateDbSettings", &[], ), ( "ObjectStoreSettings", "fabro_types::settings::server::ObjectStoreSettings", &[], ), ( "ServerSchedulerSettings", "fabro_types::settings::server::ServerSchedulerSettings", &[], ), ( "ServerLoggingSettings", "fabro_types::settings::server::ServerLoggingSettings", &[], ), ( "LogDestination", "fabro_types::settings::server::LogDestination", &[], ), ( "ServerIntegrationsSettings", "fabro_types::settings::server::ServerIntegrationsSettings", &[], ), ( "GithubIntegrationSettings", "fabro_types::settings::server::GithubIntegrationSettings", &[], ), ( "GithubIntegrationStrategy", "fabro_types::settings::server::GithubIntegrationStrategy", &[], ), ( "SlackIntegrationSettings", "fabro_types::settings::server::SlackIntegrationSettings", &[], ), ( "IntegrationWebhooksSettings", "fabro_types::settings::server::IntegrationWebhooksSettings", &[], ), ( "WebhookStrategy", "fabro_types::settings::server::WebhookStrategy", &[], ), ("AuthMethod", "fabro_types::AuthMethod", &[]), ("IdpIdentity", "fabro_types::IdpIdentity", &[]), ( "RunClientProvenance", "fabro_types::RunClientProvenance", &[], ), ("RunProvenance", "fabro_types::RunProvenance", &[]), ( "RunServerProvenance", "fabro_types::RunServerProvenance", &[], ), ("Principal", "fabro_types::Principal", &[]), ("PrincipalUser", "fabro_types::UserPrincipal", &[]), ("SystemActorKind", "fabro_types::SystemActorKind", &[]), ("QuestionType", "fabro_types::QuestionType", &[]), ("StageCompletion", "fabro_types::StageCompletion", &[]), ("Conclusion", "fabro_types::Conclusion", &[]), ("StageOutcome", "fabro_types::StageOutcome", &[]), ("StageId", "fabro_types::StageId", &[]), ("ParallelBranchId", "fabro_types::ParallelBranchId", &[]), ("StageHandler", "fabro_types::StageHandler", &[]), ("StageState", "fabro_types::StageState", &[]), ("AgentControlState", "fabro_types::AgentControlState", &[]), ("CommandTermination", "fabro_types::CommandTermination", &[]), ("StageModelUsage", "fabro_types::StageModelUsage", &[]), ( "ParallelBranchResult", "fabro_types::ParallelBranchResult", &[], ), ("StageProjection", "fabro_types::StageProjection", &[]), ( "StageInferenceProjection", "fabro_types::StageInferenceProjection", &[], ), ( "StageToolBatchProjection", "fabro_types::StageToolBatchProjection", &[], ), ("LlmOutputKind", "fabro_types::LlmOutputKind", &[]), ("PermissionLevel", "fabro_types::PermissionLevel", &[]), ( "AgentSessionActivatedProps", "fabro_types::run_event::AgentSessionActivatedProps", &[], ), ("TodoListProjection", "fabro_types::TodoListProjection", &[]), ("SubAgentProjection", "fabro_types::SubAgentProjection", &[]), ("SubAgentStatus", "fabro_types::SubAgentStatus", &[]), ("SkillsProjection", "fabro_types::SkillsProjection", &[]), ("ActivatedSkill", "fabro_types::ActivatedSkill", &[]), ("AgentSkillSummary", "fabro_types::AgentSkillSummary", &[]), ( "AgentSkillActivationSource", "fabro_types::AgentSkillActivationSource", &[], ), ("AgentToolSummary", "fabro_types::AgentToolSummary", &[]), ("AgentToolSource", "fabro_types::AgentToolSource", &[]), ("AgentToolCategory", "fabro_types::AgentToolCategory", &[]), ( "AgentToolsAvailableProps", "fabro_types::AgentToolsAvailableProps", &[], ), ( "McpServerProjection", "fabro_types::McpServerProjection", &[], ), ("McpServerStatus", "fabro_types::McpServerStatus", &[]), ( "AgentMcpToolSummary", "fabro_types::AgentMcpToolSummary", &[], ), ( "McpHttpProtocol", "fabro_types::settings::run::McpHttpProtocol", &[], ), ( "McpTransport", "fabro_types::settings::run::McpTransport", &[], ), ("McpTransportView", "fabro_types::McpTransportView", &[]), ("StageContextWindow", "fabro_types::StageContextWindow", &[]), ( "StageContextWindowProjection", "fabro_types::StageContextWindowProjection", &[], ), ( "StageContextWindowBreakdownItem", "fabro_types::StageContextWindowBreakdownItem", &[], ), ( "StageContextWindowCategory", "fabro_types::StageContextWindowCategory", &[], ), ( "StageContextWindowCountMethod", "fabro_types::StageContextWindowCountMethod", &[], ), ( "StageContextWindowStaleness", "fabro_types::StageContextWindowStaleness", &[], ), ( "StageContextWindowUnavailableReason", "fabro_types::StageContextWindowUnavailableReason", &[], ), ( "StageContextWindowWarning", "fabro_types::StageContextWindowWarning", &[], ), ("SecretMetadata", "fabro_types::SecretMetadata", &[]), ("Variable", "fabro_types::Variable", &[]), ( "VariableListResponse", "fabro_types::VariableListResponse", &[], ), ( "CreateVariableRequest", "fabro_types::CreateVariableRequest", &[], ), ( "UpdateVariableRequest", "fabro_types::UpdateVariableRequest", &[], ), ("InterviewOption", "fabro_types::InterviewOption", &[]), ("ReviewTarget", "fabro_types::ReviewTarget", &[]), ("ReviewTargetKind", "fabro_types::ReviewTargetKind", &[]), ( "InterviewQuestionRecord", "fabro_types::InterviewQuestionRecord", &[], ), ( "PendingInterviewRecord", "fabro_types::PendingInterviewRecord", &[], ), ("CompletionUsage", "fabro_model::TokenCounts", &[]), ("BilledTokenCounts", "fabro_types::BilledTokenCounts", &[]), ("BillingModelRef", "fabro_model::ModelRef", &[]), ("BillingSpeed", "fabro_model::Speed", &[]), ("ExecOutputTail", "fabro_types::ExecOutputTail", &[]), ("StageTiming", "fabro_types::StageTiming", &[]), ("RunTiming", "fabro_types::RunTiming", &[]), ("ProviderId", "fabro_model::ProviderId", &[]), ("Model", "fabro_model::Model", &[]), ("Provider", "fabro_model::Provider", &[]), ("ModelLimits", "fabro_model::ModelLimits", &[]), ( "ReasoningEffortFeature", "fabro_model::ReasoningEffortFeature", &[], ), ("ReasoningEffort", "fabro_model::ReasoningEffort", &[]), ("ModelFeatures", "fabro_model::ModelFeatures", &[]), ("ModelControls", "fabro_model::ModelControls", &[]), ("ModelCosts", "fabro_model::ModelCosts", &[]), ("ModelTestMode", "fabro_model::ModelTestMode", &[]), ("RunProjection", "fabro_types::RunProjection", &[]), ("RunEvent", "fabro_types::RunEvent", &[]), ("PairId", "fabro_types::PairId", &[]), ("PairMessageId", "fabro_types::PairMessageId", &[]), ("PairStatus", "fabro_types::PairStatus", &[]), ("PairTarget", "fabro_types::PairTarget", &[]), ("PairRecord", "fabro_types::PairRecord", &[]), ( "RunPairStatusResponse", "fabro_types::RunPairStatusResponse", &[], ), ("PairStartRequest", "fabro_types::PairStartRequest", &[]), ("PairMessageRequest", "fabro_types::PairMessageRequest", &[]), ("PairMessageRecord", "fabro_types::PairMessageRecord", &[]), ( "PairTranscriptResponse", "fabro_types::PairTranscriptResponse", &[], ), ( "PairTranscriptEntry", "fabro_types::PairTranscriptEntry", &[], ), ( "RunEventDetailResponse", "fabro_types::RunEventDetailResponse", &[], ), ("EventEnvelope", "fabro_types::EventEnvelope", &[]), ("PullRequest", "fabro_types::PullRequest", &[]), ("PullRequestLink", "fabro_types::PullRequestLink", &[]), ( "PullRequestCreationId", "fabro_types::PullRequestCreationId", &[], ), ( "PullRequestCreationStatus", "fabro_types::PullRequestCreationStatus", &[], ), ( "PullRequestCreation", "fabro_types::PullRequestCreation", &[], ), ("PullRequestDetails", "fabro_types::PullRequestDetails", &[]), ("PullRequestMeta", "fabro_types::PullRequestMeta", &[]), ( "PullRequestResponse", "fabro_types::PullRequestResponse", &[], ), ( "PullRequestDetailsStatus", "fabro_types::PullRequestDetailsStatus", &[], ), ( "PullRequestDetailsUnavailableReason", "fabro_types::PullRequestDetailsUnavailableReason", &[], ), ("RunSandboxRuntime", "fabro_types::RunSandboxRuntime", &[]), ("RunSandboxKind", "fabro_types::RunSandboxKind", &[]), ("RunSandboxPlan", "fabro_types::RunSandboxPlan", &[]), ("RunSandboxInstance", "fabro_types::RunSandboxInstance", &[]), ("RunSandboxFailure", "fabro_types::RunSandboxFailure", &[]), ("PullRequestUser", "fabro_types::PullRequestUser", &[]), ("PullRequestRef", "fabro_types::PullRequestRef", &[]), ( "MergeMethod", "fabro_types::settings::run::MergeStrategy", &[], ), ("SecretType", "fabro_types::SecretType", &[]), ("DiffStats", "fabro_types::DiffStats", &[]), ("DirtyStatus", "fabro_types::DirtyStatus", &[]), ("GitContext", "fabro_types::GitContext", &[]), ( "SandboxProviderKind", "fabro_types::SandboxProviderKind", &[], ), ( "SystemIntegrationsResponse", "fabro_types::SystemIntegrationsResponse", &[], ), ( "SystemIntegrationStatus", "fabro_types::SystemIntegrationStatus", &[], ), ( "IntegrationProvider", "fabro_types::IntegrationProvider", &[], ), ("IntegrationStatus", "fabro_types::IntegrationStatus", &[]), ( "IntegrationConnectionStatus", "fabro_types::IntegrationConnectionStatus", &[], ), ( "IntegrationConnectionKind", "fabro_types::IntegrationConnectionKind", &[], ), ( "IntegrationConnectionState", "fabro_types::IntegrationConnectionState", &[], ), ("RunSandbox", "fabro_types::RunSandbox", &[]), ("SandboxDetails", "fabro_types::SandboxDetails", &[]), ("SandboxInfo", "fabro_types::SandboxInfo", &[]), ( "SandboxProviderLookupError", "fabro_types::SandboxProviderLookupError", &[], ), ("SandboxListMeta", "fabro_types::SandboxListMeta", &[]), ( "SandboxListResponse", "fabro_types::SandboxListResponse", &[], ), ("SandboxNetwork", "fabro_types::SandboxNetwork", &[]), ( "SandboxNetworkPolicy", "fabro_types::SandboxNetworkPolicy", &[], ), ( "SandboxNetworkPolicyMode", "fabro_types::SandboxNetworkPolicyMode", &[], ), ("SandboxService", "fabro_types::SandboxService", &[]), ( "SandboxServiceDiscoverySource", "fabro_types::SandboxServiceDiscoverySource", &[], ), ( "SandboxServiceListMeta", "fabro_types::SandboxServiceListMeta", &[], ), ( "SandboxServiceListResponse", "fabro_types::SandboxServiceListResponse", &[], ), ("SandboxState", "fabro_types::SandboxState", &[]), ("SandboxResources", "fabro_types::SandboxResources", &[]), ("SandboxTimestamps", "fabro_types::SandboxTimestamps", &[]), ("AskFabro", "fabro_types::AskFabro", &[]), ("Automation", "fabro_automation::Automation", &[]), ( "AutomationGitWorkflowSource", "fabro_types::GitRunTarget", &[], ), ("AutomationRef", "fabro_types::AutomationRef", &[]), ( "AutomationTrigger", "fabro_automation::AutomationTrigger", &[], ), ( "CreateAutomationRequest", "fabro_automation::AutomationDraft", &[], ), ( "ReplaceAutomationRequest", "fabro_automation::AutomationReplace", &[], ), // MCP server catalog wire types reuse the domain model rather than // generating parallel DTOs. The integer formats in the spec generate as // `i64`/`i32`, but the domain types use `u64`/`u16`; the replacements // ensure the signed widths never leak into the public client. // (`McpTransport`/`McpTransportView`/`McpHttpProtocol` are mapped above.) // // Read responses use the value-omitting `McpServerView`/`McpTransportView` // projection (env/header values are never returned); write requests carry // the full transport, so they reuse the draft/replace domain types. ("McpServer", "fabro_types::McpServerView", &[]), ("CreateMcpServerRequest", "fabro_types::McpServerDraft", &[]), ( "ReplaceMcpServerRequest", "fabro_types::McpServerReplace", &[], ), ("Environment", "fabro_environment::Environment", &[]), ("SessionId", "fabro_types::SessionId", &[]), ("TurnId", "fabro_types::TurnId", &[]), ("SessionStatus", "fabro_types::SessionStatus", &[]), ("SessionTurn", "fabro_types::SessionTurn", &[]), ("SessionMessage", "fabro_types::SessionMessage", &[]), ("SessionRecord", "fabro_types::SessionRecord", &[]), ("SessionSummary", "fabro_types::SessionSummary", &[]), ("SessionDetail", "fabro_types::SessionDetail", &[]), ("ReasoningOutput", "fabro_types::ReasoningOutput", &[]), ("CompletionMessage", "fabro_types::Message", &[]), ("CompletionMessageRole", "fabro_types::Role", &[]), ("CompletionContentPart", "fabro_types::ContentPart", &[]), ("WorkflowVersion", "fabro_types::WorkflowVersion", &[]), ("RunIntent", "fabro_types::RunIntent", &[]), ("RunIntentArgs", "fabro_types::RunIntentArgs", &[]), ("RunTarget", "fabro_types::RunTarget", &[]), ("GitRunTarget", "fabro_types::GitRunTarget", &[]), ("WorkflowPath", "fabro_types::WorkflowPath", &[]), ("WorkflowVersionId", "fabro_types::WorkflowVersionId", &[]), ("BlobHash", "fabro_types::BlobHash", &[]), ("CostSource", "fabro_model::CostSource", &[]), ]; for (name, path, impls) in replacements { settings.with_replacement(*name, *path, impls.iter().copied()); } let mut generator = Generator::new(&settings); let tokens = generator .generate_tokens(&spec) .expect("failed to generate tokens from OpenAPI spec"); let syntax_tree = syn::parse2::(tokens).expect("failed to parse generated tokens"); let formatted = prettyplease::unparse(&syntax_tree); let out_dir = env::var("OUT_DIR").expect("OUT_DIR should be set for build scripts"); let out_path = Path::new(&out_dir).join("codegen.rs"); fs::write(&out_path, formatted).expect("failed to write generated code"); }