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}