diff --git a/crates/stackless-integrations/src/providers/mod.rs b/crates/stackless-integrations/src/providers/mod.rs index b7e5cf5..1cfb048 100644 --- a/crates/stackless-integrations/src/providers/mod.rs +++ b/crates/stackless-integrations/src/providers/mod.rs @@ -41,6 +41,7 @@ pub mod railway; pub mod render_db; pub mod revenuecat; pub mod runloop; +pub mod schematic; pub mod sentry; pub mod supabase; pub mod supermemory; @@ -60,8 +61,8 @@ mod tests { chatbase, chroma, clickhouse, cloudflare, composio, customerio, daytona, depot, e2b, elevenlabs, exa, firecrawl, flyio, gitlab, heygen, huggingface, inngest, kernel, laravel_cloud, metronome, mixpanel, neon, openrouter, parallel, planetscale, postalform, - posthog, prisma, privy, pydantic, railway, render_db, revenuecat, runloop, sentry, - supabase, supermemory, turso, upstash, wix, wordpress_com, workos, + posthog, prisma, privy, pydantic, railway, render_db, revenuecat, runloop, schematic, + sentry, supabase, supermemory, turso, upstash, wix, wordpress_com, workos, }; fn assert_outputs_match() { @@ -137,6 +138,7 @@ mod tests { assert_outputs_match::(); assert_outputs_match::(); assert_outputs_match::(); + assert_outputs_match::(); assert_outputs_match::(); assert_outputs_match::(); assert_outputs_match::(); diff --git a/crates/stackless-integrations/src/providers/schematic/mod.rs b/crates/stackless-integrations/src/providers/schematic/mod.rs new file mode 100644 index 0000000..ebc5e80 --- /dev/null +++ b/crates/stackless-integrations/src/providers/schematic/mod.rs @@ -0,0 +1,11 @@ +//! Schematic catalog resources via Stripe Projects. +//! +//! Output envelopes are provisional until pinned by `xtask discover`. + +pub mod schematic_environment; + +#[allow(unused_imports)] +pub(crate) use crate::resource::{ + CatalogResource as FamilyResource, bool_optional, bool_required, int_optional, int_required, + integration_config, interp_optional, interp_required, +}; diff --git a/crates/stackless-integrations/src/providers/schematic/schematic_environment.rs b/crates/stackless-integrations/src/providers/schematic/schematic_environment.rs new file mode 100644 index 0000000..ba75138 --- /dev/null +++ b/crates/stackless-integrations/src/providers/schematic/schematic_environment.rs @@ -0,0 +1,147 @@ +//! `schematic/schematic-environment` integration. + +use std::collections::BTreeMap; + +use serde::Serialize; +use stackless_stripe_projects::catalog::verify::CatalogService; +use stackless_stripe_projects::provision::ProvisionContext; + +use super::FamilyResource; +use crate::error::IntegrationError; +use crate::hostable::{ConfigScope, Hostable, IntegrationHosting}; + +pub const RESOURCE_KIND: &str = "integration-schematic"; + +#[derive(Debug, Serialize)] +pub struct SchematicEnvironmentConfig { + #[serde(skip_serializing_if = "Option::is_none")] + pub environment_type: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, +} + +impl CatalogService for SchematicEnvironmentConfig { + const REFERENCE: &'static str = "schematic/schematic-environment"; +} + +#[derive(Debug)] +pub struct SchematicEnvironment; + +impl Hostable for SchematicEnvironment { + const PROVIDER: &'static str = "schematic"; + const HOSTING: IntegrationHosting = IntegrationHosting::Managed; + const CONFIG_SCOPE: ConfigScope = ConfigScope::GlobalOnly; + const RESOURCE_KIND: &'static str = RESOURCE_KIND; + const OUTPUTS: &'static [&'static str] = &["api_key"]; +} + +impl FamilyResource for SchematicEnvironment { + type Config = SchematicEnvironmentConfig; + const PROVIDER_PREFIX: &'static str = "SCHEMATIC"; + // Provisional until pinned by `mise run discover schematic/schematic-environment`. + const OUTPUT_FIELDS: &'static [(&'static str, &'static str, bool)] = + &[("API_KEY", "api_key", true)]; + + fn build_config( + ctx: &ProvisionContext<'_>, + ) -> Result { + let config = super::integration_config(ctx)?; + Ok(SchematicEnvironmentConfig { + environment_type: super::interp_optional(ctx, &config, "environment_type")?, + name: super::interp_optional(ctx, &config, "name")?, + }) + } +} + +pub fn validate_config( + name: &str, + config: &BTreeMap, +) -> Result<(), IntegrationError> { + let _ = (name, config); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ProviderOps; + use crate::resource::ResourcePayload; + use stackless_core::def::StackDef; + use stackless_stripe_projects::stripe::StripeProjects; + use stackless_stripe_projects::test_support; + + #[test] + fn config_matches_catalog() { + const FIXTURE: &str = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../stackless-stripe-projects/tests/fixtures/catalog.json" + )); + let catalog = stackless_stripe_projects::Catalog::from_json_envelope(FIXTURE).unwrap(); + let failures = stackless_stripe_projects::verify_service( + &catalog, + &SchematicEnvironmentConfig { + environment_type: None, + name: None, + }, + ); + assert!( + failures.is_empty(), + "schematic/schematic-environment catalog gaps:\n{}", + failures.join("\n") + ); + } + + const CATALOG_ENVELOPE: &str = r##"{"ok":true,"command":"projects catalog","data":{"last_updated":"2026-07-11T00:00:00Z","services":[{"id":"prvsvc_schematic_environment","object":"v2.provisioning.provider_service_detail","provider_id":"prvdr_schematic","provider_name":"Schematic","service_id":"schematic-environment","categories":["database"],"kind":"deployable","scope":"project","availability":"available","development":false,"livemode":true,"pricing":{"type":"component"},"configuration_schema":{"properties":{"environment_type":{"type":"string","enum":["development","production"]},"name":{"type":"string"}},"type":"object"}}]}}"##; + + fn test_def() -> StackDef { + StackDef::parse( + r#" +[stack] +name = "atto" +[stack.projects.stripe] +project = "project_1" +[integrations.res] +provider = "schematic" +[services.api] +source = { repo = "r", ref = "main" } +env = { OUT = "${integrations.res.api_key}" } +health = { path = "/health" } +[services.api.local] +run = "true" +"#, + ) + .unwrap() + } + + #[tokio::test] + async fn provision_records_outputs() { + let runner = test_support::provision_script( + CATALOG_ENVELOPE, + serde_json::json!({"SCHEMATIC_API_KEY": "val_api_key"}), + 0, + ); + let dir = tempfile::tempdir().unwrap(); + std::fs::write( + dir.path().join("stackless.toml"), + "[stack]\nname=\"atto\"\n", + ) + .unwrap(); + let stripe = StripeProjects::new(&runner, dir.path()); + + let resource = SchematicEnvironment + .provision( + &stripe.as_dyn(), + &test_def(), + dir.path(), + "demo", + "res", + "local", + false, + ) + .await + .unwrap(); + assert_eq!(resource.resource_kind, "integration-schematic"); + let payload: ResourcePayload = serde_json::from_str(&resource.payload).unwrap(); + assert_eq!(payload.outputs["api_key"], "val_api_key"); + } +} diff --git a/crates/stackless-integrations/src/registry.rs b/crates/stackless-integrations/src/registry.rs index abe81b8..64abd13 100644 --- a/crates/stackless-integrations/src/registry.rs +++ b/crates/stackless-integrations/src/registry.rs @@ -128,6 +128,7 @@ register_providers! { (render_db::postgres, RenderPostgres), (revenuecat::app, RevenuecatApp), (runloop::sandbox, RunloopSandbox), + (schematic::schematic_environment, SchematicEnvironment), (sentry::project, SentryProject), (sentry::seer, SentrySeer), (supabase::project, SupabaseProject),