providers-kit 0.0.2

Portable enum-dispatch Kit over multiple model provider APIs.
Documentation
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

struct CompileCase {
    name: &'static str,
    source: &'static str,
    expected: &'static [&'static str],
}

impl CompileCase {
    fn run(&self) {
        let project = tempfile::tempdir().expect("compile-contract temp directory should build");
        fs::create_dir(project.path().join("src"))
            .expect("compile-contract src directory should build");
        fs::write(project.path().join("src/main.rs"), self.source)
            .expect("compile-contract source should write");
        fs::write(
            project.path().join("Cargo.toml"),
            format!(
                "[package]\nname = \"provider-contract-{}\"\nversion = \"0.0.0\"\nedition = \"2024\"\n\n[workspace]\n\n[dependencies]\nproviders-kit = {{ path = \"{}\" }}\n",
                self.name,
                Self::escaped_path(&Self::manifest_dir())
            ),
        )
        .expect("compile-contract manifest should write");
        fs::create_dir(project.path().join(".cargo"))
            .expect("compile-contract cargo directory should build");
        fs::write(
            project.path().join(".cargo/config.toml"),
            "[net]\noffline = true\n",
        )
        .expect("compile-contract cargo config should write");

        let output = Command::new("cargo")
            .args(["check", "--quiet", "--offline"])
            .current_dir(project.path())
            .env("CARGO_TARGET_DIR", Self::target_dir())
            .output()
            .expect("compile-contract cargo check should run");
        assert!(
            !output.status.success(),
            "{} unexpectedly compiled",
            self.name
        );
        let stderr = String::from_utf8_lossy(&output.stderr);
        for fragment in self.expected {
            assert!(
                stderr.contains(fragment),
                "{} stderr omitted `{fragment}`:\n{stderr}",
                self.name
            );
        }
    }

    fn manifest_dir() -> PathBuf {
        PathBuf::from(env!("CARGO_MANIFEST_DIR"))
    }

    fn target_dir() -> PathBuf {
        Self::manifest_dir()
            .parent()
            .expect("workspace root should exist")
            .join("target/compile-contracts")
    }

    fn escaped_path(path: &Path) -> String {
        path.display().to_string().replace('\\', "\\\\")
    }
}

#[test]
fn removed_ingress_routers_mutable_fields_and_synthetic_sessions_are_rejected() {
    for case in [
        CompileCase {
            name: "private-core",
            source: "use providers_kit::core; fn main() {}\n",
            expected: &["core", "private"],
        },
        CompileCase {
            name: "provider-ingress",
            source: "fn main() { let _ = providers_kit::Provider::from_backend(()); }\n",
            expected: &["Provider", "from_backend"],
        },
        CompileCase {
            name: "model-ingress",
            source: "fn main() { let _ = providers_kit::Model::from_extension(()); }\n",
            expected: &["Model", "from_extension"],
        },
        CompileCase {
            name: "session-ingress",
            source: "fn main() { let _ = providers_kit::Session::from_extension(()); }\n",
            expected: &["Session", "from_extension"],
        },
        CompileCase {
            name: "implementation-identity-type",
            source: "fn main() { let _ = providers_kit::ProviderImplementationId::new(\"custom\"); }\n",
            expected: &["ProviderImplementationId"],
        },
        CompileCase {
            name: "advanced-root-dtos",
            source: "use providers_kit::{GenerateRequest, ProviderKind}; fn main() {}\n",
            expected: &["GenerateRequest", "ProviderKind"],
        },
        CompileCase {
            name: "removed-router-envelopes",
            source: "use providers_kit::models::ModelsRequest; use providers_kit::files::FilesRequest; use providers_kit::batch::BatchRequest; use providers_kit::runtime::RuntimeRequest; fn main() {}\n",
            expected: &[
                "ModelsRequest",
                "FilesRequest",
                "BatchRequest",
                "RuntimeRequest",
            ],
        },
        CompileCase {
            name: "removed-synthetic-session-types",
            source: "use providers_kit::session::{SessionKind, SessionOpenRequest, SessionState}; fn main() {}\n",
            expected: &["SessionKind", "SessionOpenRequest", "SessionState"],
        },
        CompileCase {
            name: "removed-session-decomposition",
            source: "use providers_kit::session::SessionConfig; fn main() { let _ = SessionConfig::default().into_parts(); }\n",
            expected: &["SessionConfig", "into_parts"],
        },
        CompileCase {
            name: "removed-session-generation",
            source: "use providers_kit::session::SessionBackend; fn forbidden<B: SessionBackend>(session: &mut B) { let _ = session.generate; } fn main() {}\n",
            expected: &["generate"],
        },
        CompileCase {
            name: "private-request-fields",
            source: "use providers_kit::generation::{GenerateRequest, Message}; fn main() { let mut request = GenerateRequest::<()>::new(vec![Message::user_text(\"hi\").unwrap()]).unwrap(); request.messages.clear(); }\n",
            expected: &["messages", "private"],
        },
        CompileCase {
            name: "private-message-fields",
            source: "use providers_kit::generation::Message; fn main() { let mut message = Message::<()>::user_text(\"hi\").unwrap(); message.content.clear(); }\n",
            expected: &["content", "private"],
        },
        CompileCase {
            name: "private-generation-config-fields",
            source: "use providers_kit::generation::GenerationConfig; fn main() { let mut config = GenerationConfig::default(); config.temperature = Some(0.5); }\n",
            expected: &["temperature", "private"],
        },
        CompileCase {
            name: "removed-content-and-hosted-options",
            source: "use providers_kit::generation::ContentPart; use providers_kit::hosted::HostedTool; fn main() { let _ = ContentPart::Binary; let _ = ContentPart::ImageBase64; let _ = ContentPart::AudioBase64; let _ = ContentPart::FileRef; let _ = HostedTool::ImageGeneration; let _ = HostedTool::LocationGrounding; }\n",
            expected: &[
                "Binary",
                "ImageBase64",
                "AudioBase64",
                "FileRef",
                "ImageGeneration",
                "LocationGrounding",
            ],
        },
        CompileCase {
            name: "opaque-runtime-error",
            source: "fn main() { let _ = providers_kit::KitError {}; }\n",
            expected: &["KitError", "private"],
        },
        CompileCase {
            name: "opaque-remote-failure",
            source: "fn main() { let _ = providers_kit::error::RemoteFailure {}; }\n",
            expected: &["RemoteFailure", "private"],
        },
        CompileCase {
            name: "removed-error-categories",
            source: "fn main() { let _ = providers_kit::KitError::NotImplemented(\"x\".to_string()); let _ = providers_kit::KitError::Streaming(\"x\".to_string()); let _ = providers_kit::KitError::WebSocket(\"x\".to_string()); let _ = providers_kit::KitError::Serialization(\"x\".to_string()); let _ = providers_kit::KitError::Io(\"x\".to_string()); }\n",
            expected: &[
                "NotImplemented",
                "Streaming",
                "WebSocket",
                "Serialization",
                "Io",
            ],
        },
        CompileCase {
            name: "trait-import-required",
            source: "use providers_kit::{ModelId, Provider}; fn main() { let provider: Provider = todo!(); let _ = provider.model(ModelId::new(\"m\").unwrap()); }\n",
            expected: &["model", "ProviderBackend"],
        },
        CompileCase {
            name: "session-not-clone",
            source: "fn assert_clone<T: Clone>() {} fn main() { assert_clone::<providers_kit::Session>(); }\n",
            expected: &["Session", "Clone"],
        },
    ] {
        case.run();
    }
}