diff --git a/crates/stackless-integrations/src/providers/mod.rs b/crates/stackless-integrations/src/providers/mod.rs index 83eb55f..46aba29 100644 --- a/crates/stackless-integrations/src/providers/mod.rs +++ b/crates/stackless-integrations/src/providers/mod.rs @@ -46,6 +46,7 @@ pub mod sentry; pub mod steel; pub mod supabase; pub mod supermemory; +pub mod tabstack; pub mod turso; pub mod upstash; pub mod wix; @@ -63,7 +64,7 @@ mod tests { 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, schematic, - sentry, steel, supabase, supermemory, turso, upstash, wix, wordpress_com, workos, + sentry, steel, supabase, supermemory, tabstack, turso, upstash, wix, wordpress_com, workos, }; fn assert_outputs_match() { @@ -145,6 +146,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/tabstack/api.rs b/crates/stackless-integrations/src/providers/tabstack/api.rs new file mode 100644 index 0000000..213ebc6 --- /dev/null +++ b/crates/stackless-integrations/src/providers/tabstack/api.rs @@ -0,0 +1,137 @@ +//! `tabstack/api` 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-tabstack"; + +#[derive(Debug, Serialize)] +pub struct TabstackApiConfig { + #[serde(skip_serializing_if = "Option::is_none")] + pub plan: Option, +} + +impl CatalogService for TabstackApiConfig { + const REFERENCE: &'static str = "tabstack/api"; +} + +#[derive(Debug)] +pub struct TabstackApi; + +impl Hostable for TabstackApi { + const PROVIDER: &'static str = "tabstack"; + 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 TabstackApi { + type Config = TabstackApiConfig; + const PROVIDER_PREFIX: &'static str = "TABSTACK"; + // Provisional until pinned by `mise run discover tabstack/api`. + 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(TabstackApiConfig { + plan: super::interp_optional(ctx, &config, "plan")?, + }) + } +} + +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, &TabstackApiConfig { plan: None }); + assert!( + failures.is_empty(), + "tabstack/api 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_api","object":"v2.provisioning.provider_service_detail","provider_id":"prvdr_tabstack","provider_name":"Tabstack","service_id":"api","categories":["database"],"kind":"deployable","scope":"project","availability":"available","development":false,"livemode":true,"pricing":{"type":"paid"},"configuration_schema":{"properties":{"plan":{"type":"string","enum":["trial","individual","team","pro"],"description":"The plan tier to provision."}},"type":"object"}}]}}"##; + + fn test_def() -> StackDef { + StackDef::parse( + r#" +[stack] +name = "atto" +[stack.projects.stripe] +project = "project_1" +[integrations.res] +provider = "tabstack" +[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!({"TABSTACK_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 = TabstackApi + .provision( + &stripe.as_dyn(), + &test_def(), + dir.path(), + "demo", + "res", + "local", + false, + ) + .await + .unwrap(); + assert_eq!(resource.resource_kind, "integration-tabstack"); + 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/providers/tabstack/mod.rs b/crates/stackless-integrations/src/providers/tabstack/mod.rs new file mode 100644 index 0000000..5aeff60 --- /dev/null +++ b/crates/stackless-integrations/src/providers/tabstack/mod.rs @@ -0,0 +1,11 @@ +//! Tabstack catalog resources via Stripe Projects. +//! +//! Output envelopes are provisional until pinned by `xtask discover`. + +pub mod api; + +#[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/registry.rs b/crates/stackless-integrations/src/registry.rs index fc2bcd2..ee1de0f 100644 --- a/crates/stackless-integrations/src/registry.rs +++ b/crates/stackless-integrations/src/registry.rs @@ -134,6 +134,7 @@ register_providers! { (steel::browser, SteelBrowser), (supabase::project, SupabaseProject), (supermemory::memory, SupermemoryMemory), + (tabstack::api, TabstackApi), (turso::database, TursoDatabase), (upstash::qstash, UpstashQstash), (upstash::redis, UpstashRedis),