Skip to main content

fmp_rust_sdk/
plan.rs

1//! Subscription configuration and advisory limits for Financial Modeling Prep.
2//!
3//! FMP does not document a public API that returns an API key's exact billing
4//! plan. Supply the plan shown in the [account dashboard](https://site.financialmodelingprep.com/developer/docs/dashboard).
5//! Successful endpoint access does not identify the plan or establish access to
6//! other datasets. WebSocket access depends on the account's explicit cluster
7//! entitlements; confirm them with FMP rather than deriving them from a plan.
8
9use std::{error::Error, fmt, str::FromStr, time::Duration};
10
11use serde::{Deserialize, Serialize};
12
13/// A configured subscription plan.
14///
15/// This value describes configuration, not a plan verified by the FMP server.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "lowercase")]
18pub enum Plan {
19    /// FMP's Basic/free personal plan.
20    #[serde(alias = "free")]
21    Basic,
22    /// FMP's Starter personal plan.
23    Starter,
24    /// FMP's Premium personal plan.
25    Premium,
26    /// FMP's Ultimate personal plan.
27    Ultimate,
28    /// The Build plan retained in FMP's published bandwidth policy.
29    Build,
30    /// A commercial Enterprise agreement with negotiated limits and access.
31    Enterprise,
32}
33
34/// An advisory published request allowance, without additional account boosts.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct RateLimit {
37    /// The allowed request count within the published window.
38    pub requests: u32,
39    /// The published request counting window.
40    pub window: Duration,
41}
42
43impl Plan {
44    /// Returns the standard request allowance published on 2026-10-10.
45    ///
46    /// These are advisory values from FMP's [pricing page](https://site.financialmodelingprep.com/pricing-plans).
47    /// Add-ons and negotiated agreements can change the account's allowance.
48    /// Build has no confirmed standard request allowance; Enterprise is custom.
49    pub const fn published_rate_limit(self) -> Option<RateLimit> {
50        match self {
51            Self::Basic => Some(RateLimit {
52                requests: 250,
53                window: Duration::from_secs(86_400),
54            }),
55            Self::Starter => Some(RateLimit {
56                requests: 300,
57                window: Duration::from_secs(60),
58            }),
59            Self::Premium => Some(RateLimit {
60                requests: 750,
61                window: Duration::from_secs(60),
62            }),
63            Self::Ultimate => Some(RateLimit {
64                requests: 3_000,
65                window: Duration::from_secs(60),
66            }),
67            Self::Build | Self::Enterprise => None,
68        }
69    }
70}
71
72impl fmt::Display for Plan {
73    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
74        formatter.write_str(match self {
75            Self::Basic => "basic",
76            Self::Starter => "starter",
77            Self::Premium => "premium",
78            Self::Ultimate => "ultimate",
79            Self::Build => "build",
80            Self::Enterprise => "enterprise",
81        })
82    }
83}
84
85/// An unrecognized subscription plan name.
86#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct PlanParseError {
88    input: String,
89}
90
91impl PlanParseError {
92    /// Returns the unrecognized input supplied to the parser.
93    pub fn input(&self) -> &str {
94        &self.input
95    }
96}
97
98impl fmt::Display for PlanParseError {
99    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
100        write!(
101            formatter,
102            "unrecognized FMP subscription plan {:?}; expected basic (or free), starter, premium, ultimate, build, or enterprise",
103            self.input
104        )
105    }
106}
107
108impl Error for PlanParseError {}
109
110impl FromStr for Plan {
111    type Err = PlanParseError;
112
113    fn from_str(input: &str) -> Result<Self, Self::Err> {
114        match input.trim().to_ascii_lowercase().as_str() {
115            "basic" | "free" => Ok(Self::Basic),
116            "starter" => Ok(Self::Starter),
117            "premium" => Ok(Self::Premium),
118            "ultimate" => Ok(Self::Ultimate),
119            "build" => Ok(Self::Build),
120            "enterprise" => Ok(Self::Enterprise),
121            _ => Err(PlanParseError {
122                input: input.to_owned(),
123            }),
124        }
125    }
126}
127
128/// Explicit account grants for FMP's three WebSocket clusters.
129///
130/// Grants are supplied by the account owner. FMP's [FAQ](https://site.financialmodelingprep.com/faqs)
131/// describes cluster-specific entitlements and one simultaneous connection per
132/// cluster. The server remains authoritative about authentication and access.
133#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
134pub struct WebSocketEntitlements {
135    /// Access to the stocks WebSocket cluster.
136    pub stocks: bool,
137    /// Access to the cryptocurrency WebSocket cluster.
138    pub crypto: bool,
139    /// Access to the forex WebSocket cluster.
140    pub forex: bool,
141}
142
143impl WebSocketEntitlements {
144    /// Creates a declaration with no WebSocket cluster grants.
145    pub const fn none() -> Self {
146        Self {
147            stocks: false,
148            crypto: false,
149            forex: false,
150        }
151    }
152
153    /// Creates a declaration with grants for all three WebSocket clusters.
154    pub const fn all() -> Self {
155        Self {
156            stocks: true,
157            crypto: true,
158            forex: true,
159        }
160    }
161}
162
163/// Subscription details explicitly configured for an FMP account.
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165pub struct Subscription {
166    plan: Option<Plan>,
167    websocket_entitlements: WebSocketEntitlements,
168}
169
170impl Subscription {
171    /// Configures a known plan, or `None` when the billing plan is unknown.
172    ///
173    /// WebSocket entitlements are independent of the plan name and are supplied
174    /// explicitly to support legacy accounts and negotiated agreements.
175    pub const fn new(plan: Option<Plan>, websocket_entitlements: WebSocketEntitlements) -> Self {
176        Self {
177            plan,
178            websocket_entitlements,
179        }
180    }
181
182    /// Returns the configured billing plan, without claiming server verification.
183    pub const fn plan(&self) -> Option<Plan> {
184        self.plan
185    }
186
187    /// Returns the configured WebSocket cluster grants.
188    pub const fn websocket_entitlements(&self) -> WebSocketEntitlements {
189        self.websocket_entitlements
190    }
191}