Skip to main content

mkt_core/
provider.rs

1//! Core provider trait for the `mkt` marketing CLI.
2//!
3//! Every platform (Meta, Google, TikTok, LinkedIn) implements
4//! [`MarketingProvider`]. Methods return unified domain models from
5//! [`crate::models`], not platform-specific structs.
6//!
7//! This trait uses native `async fn` in traits (Rust 2024 edition RPITIT).
8//! Because RPITIT is not object-safe, the CLI uses enum dispatch
9//! (`AnyProvider`) instead of `dyn MarketingProvider`.
10
11use crate::error::{MktError, Result};
12use crate::models::{
13    Ad, AdSet, Audience, AudienceId, AudienceUpdateResult, AudienceUser, Campaign, CampaignFilters,
14    CampaignId, CreateAdSetInput, CreateAudienceInput, CreateCampaignInput, CreateCreativeInput,
15    CreateDarkPostInput, Creative, HttpMethod, InsightsQuery, InsightsReport, MediaAsset,
16    Paginated, Post, PostId, PromotePostInput, ProviderHealth, PublishPostInput,
17    UpdateCampaignInput, UploadImageInput, UploadVideoInput,
18};
19
20/// Describes the capabilities a provider supports.
21///
22/// Used by the CLI to show/hide commands dynamically and to provide
23/// helpful error messages when a user tries an unsupported feature.
24#[derive(Debug, Clone)]
25#[allow(clippy::struct_excessive_bools)] // capability flags are inherently boolean
26pub struct ProviderCapabilities {
27    /// Supports campaign CRUD.
28    pub campaigns: bool,
29    /// Supports ad set / ad group management.
30    pub adsets: bool,
31    /// Supports individual ad management.
32    pub ads: bool,
33    /// Supports creative asset management.
34    pub creatives: bool,
35    /// Supports audience management.
36    pub audiences: bool,
37    /// Supports insights / analytics queries.
38    pub insights: bool,
39    /// Supports organic post publishing.
40    pub organic_posts: bool,
41    /// Supports dark (unpublished) posts.
42    pub dark_posts: bool,
43    /// Supports video upload.
44    pub video_upload: bool,
45    /// Supports image upload.
46    pub image_upload: bool,
47    /// Supports workflow templates.
48    pub workflow_templates: bool,
49}
50
51/// The core abstraction. Every platform implements this trait.
52///
53/// Methods return unified domain models, not platform-specific structs.
54/// Optional capabilities have default implementations that return
55/// [`MktError::NotSupported`].
56///
57/// # Object Safety
58///
59/// This trait is **not** object-safe due to the use of native `async fn`
60/// (RPITIT). Use the `AnyProvider` enum in `mkt-cli` for dynamic dispatch.
61pub trait MarketingProvider: Send + Sync {
62    /// Short lowercase name used in CLI commands: `"meta"`, `"google"`, `"tiktok"`.
63    fn name(&self) -> &'static str;
64
65    /// Human-readable display name: `"Meta (Facebook/Instagram)"`.
66    fn display_name(&self) -> &'static str;
67
68    /// What this provider can do.
69    fn capabilities(&self) -> ProviderCapabilities;
70
71    // ── Campaigns ──────────────────────────────────────────
72
73    /// List campaigns matching the given filters.
74    fn list_campaigns(
75        &self,
76        filters: &CampaignFilters,
77    ) -> impl std::future::Future<Output = Result<Paginated<Campaign>>> + Send;
78
79    /// Get a single campaign by ID.
80    fn get_campaign(
81        &self,
82        id: &CampaignId,
83    ) -> impl std::future::Future<Output = Result<Campaign>> + Send;
84
85    /// Create a new campaign.
86    fn create_campaign(
87        &self,
88        input: &CreateCampaignInput,
89    ) -> impl std::future::Future<Output = Result<Campaign>> + Send;
90
91    /// Update an existing campaign.
92    fn update_campaign(
93        &self,
94        id: &CampaignId,
95        input: &UpdateCampaignInput,
96    ) -> impl std::future::Future<Output = Result<Campaign>> + Send;
97
98    /// Delete a campaign by ID.
99    fn delete_campaign(
100        &self,
101        id: &CampaignId,
102    ) -> impl std::future::Future<Output = Result<()>> + Send;
103
104    // ── Ad Sets / Ad Groups ────────────────────────────────
105
106    /// List ad sets for a campaign.
107    fn list_adsets(
108        &self,
109        _campaign_id: &CampaignId,
110    ) -> impl std::future::Future<Output = Result<Paginated<AdSet>>> + Send {
111        async { Err(MktError::not_supported(self.name(), "adsets")) }
112    }
113
114    /// Create a new ad set.
115    fn create_adset(
116        &self,
117        _input: &CreateAdSetInput,
118    ) -> impl std::future::Future<Output = Result<AdSet>> + Send {
119        async { Err(MktError::not_supported(self.name(), "adsets")) }
120    }
121
122    // ── Creatives ──────────────────────────────────────────
123
124    /// Create an ad creative.
125    fn create_creative(
126        &self,
127        _input: &CreateCreativeInput,
128    ) -> impl std::future::Future<Output = Result<Creative>> + Send {
129        async { Err(MktError::not_supported(self.name(), "creatives")) }
130    }
131
132    /// Create an unpublished (dark) post for use in ads.
133    fn create_dark_post(
134        &self,
135        _input: &CreateDarkPostInput,
136    ) -> impl std::future::Future<Output = Result<Creative>> + Send {
137        async { Err(MktError::not_supported(self.name(), "dark_posts")) }
138    }
139
140    // ── Audiences ──────────────────────────────────────────
141
142    /// List all audiences.
143    fn list_audiences(&self) -> impl std::future::Future<Output = Result<Vec<Audience>>> + Send {
144        async { Err(MktError::not_supported(self.name(), "audiences")) }
145    }
146
147    /// Create a new audience.
148    fn create_audience(
149        &self,
150        _input: &CreateAudienceInput,
151    ) -> impl std::future::Future<Output = Result<Audience>> + Send {
152        async { Err(MktError::not_supported(self.name(), "audiences")) }
153    }
154
155    /// Add users to an existing audience.
156    fn add_users_to_audience(
157        &self,
158        _id: &AudienceId,
159        _users: &[AudienceUser],
160    ) -> impl std::future::Future<Output = Result<AudienceUpdateResult>> + Send {
161        async { Err(MktError::not_supported(self.name(), "audience_users")) }
162    }
163
164    // ── Insights ───────────────────────────────────────────
165
166    /// Query analytics / insights.
167    fn get_insights(
168        &self,
169        _query: &InsightsQuery,
170    ) -> impl std::future::Future<Output = Result<InsightsReport>> + Send {
171        async { Err(MktError::not_supported(self.name(), "insights")) }
172    }
173
174    // ── Organic Posts ──────────────────────────────────────
175
176    /// Publish an organic post (Facebook Page, Instagram, etc.).
177    fn publish_post(
178        &self,
179        _input: &PublishPostInput,
180    ) -> impl std::future::Future<Output = Result<Post>> + Send {
181        async { Err(MktError::not_supported(self.name(), "organic_posts")) }
182    }
183
184    /// Promote an existing organic post as an ad.
185    fn promote_post(
186        &self,
187        _post_id: &PostId,
188        _input: &PromotePostInput,
189    ) -> impl std::future::Future<Output = Result<Ad>> + Send {
190        async { Err(MktError::not_supported(self.name(), "promote_post")) }
191    }
192
193    // ── Media Upload ───────────────────────────────────────
194
195    /// Upload an image asset.
196    fn upload_image(
197        &self,
198        _input: &UploadImageInput,
199    ) -> impl std::future::Future<Output = Result<MediaAsset>> + Send {
200        async { Err(MktError::not_supported(self.name(), "image_upload")) }
201    }
202
203    /// Upload a video asset.
204    fn upload_video(
205        &self,
206        _input: &UploadVideoInput,
207    ) -> impl std::future::Future<Output = Result<MediaAsset>> + Send {
208        async { Err(MktError::not_supported(self.name(), "video_upload")) }
209    }
210
211    // ── Raw Escape Hatch ───────────────────────────────────
212
213    /// Execute a raw API call, bypassing model mapping.
214    fn raw_request(
215        &self,
216        _method: HttpMethod,
217        _path: &str,
218        _params: &serde_json::Value,
219    ) -> impl std::future::Future<Output = Result<serde_json::Value>> + Send {
220        async { Err(MktError::not_supported(self.name(), "raw_request")) }
221    }
222
223    // ── Health Check ───────────────────────────────────────
224
225    /// Verify that credentials are valid and the API is reachable.
226    fn health_check(&self) -> impl std::future::Future<Output = Result<ProviderHealth>> + Send {
227        async { Err(MktError::not_supported(self.name(), "health_check")) }
228    }
229}