1use std::{env, path::Path};
4
5use crate::{Client, Error, Plan, Result, Subscription, WebSocketEntitlements};
6
7struct ConfigValues {
8 api_key: Option<String>,
9 plan: Option<String>,
10}
11
12impl Client {
13 pub fn from_env(websocket_entitlements: WebSocketEntitlements) -> Result<Self> {
18 let values = ConfigValues {
19 api_key: environment_value("FMP_API_KEY")?,
20 plan: environment_value("FMP_PLAN")?,
21 };
22 configured_client(values, "process environment", websocket_entitlements)
23 }
24
25 pub fn from_dotenv(path: &Path, websocket_entitlements: WebSocketEntitlements) -> Result<Self> {
40 let mut entries =
41 dotenvy::from_path_iter(path).map_err(|error| dotenv_error(path, error))?;
42 let values = entries.try_fold(
43 ConfigValues {
44 api_key: None,
45 plan: None,
46 },
47 |values, entry| {
48 let (name, value) = entry.map_err(|error| dotenv_error(path, error))?;
49 match name.as_str() {
50 "FMP_API_KEY" => {
51 if values.api_key.is_some() {
52 return Err(duplicate_setting(path, "FMP_API_KEY"));
53 }
54 Ok(ConfigValues {
55 api_key: Some(value),
56 ..values
57 })
58 }
59 "FMP_PLAN" => {
60 if values.plan.is_some() {
61 return Err(duplicate_setting(path, "FMP_PLAN"));
62 }
63 Ok(ConfigValues {
64 plan: Some(value),
65 ..values
66 })
67 }
68 _ => Ok(values),
69 }
70 },
71 )?;
72 configured_client(
73 values,
74 &format!("dotenv file '{}'", path.display()),
75 websocket_entitlements,
76 )
77 }
78}
79
80fn environment_value(name: &str) -> Result<Option<String>> {
81 match env::var(name) {
82 Ok(value) => Ok(Some(value)),
83 Err(env::VarError::NotPresent) => Ok(None),
84 Err(env::VarError::NotUnicode(_)) => Err(Error::Configuration {
85 reason: format!("{name} in the process environment must contain valid Unicode"),
86 }),
87 }
88}
89
90fn configured_client(
91 values: ConfigValues,
92 source: &str,
93 websocket_entitlements: WebSocketEntitlements,
94) -> Result<Client> {
95 let api_key = values.api_key.ok_or_else(|| Error::Configuration {
96 reason: format!(
97 "FMP_API_KEY is missing from {source}; add the key from your FMP dashboard"
98 ),
99 })?;
100 let plan = values
101 .plan
102 .filter(|value| !value.trim().is_empty())
103 .map(|value| {
104 value.parse::<Plan>().map_err(|_| Error::Configuration {
105 reason: format!(
106 "FMP_PLAN in {source} is unrecognized; use basic (or free), starter, premium, ultimate, build, enterprise, or leave it blank for the free plan"
107 ),
108 })
109 })
110 .transpose()?
111 .unwrap_or(Plan::Basic);
112 Client::new(
113 &api_key,
114 Subscription::new(Some(plan), websocket_entitlements),
115 )
116 .map_err(|error| match error {
117 Error::Configuration { reason } => Error::Configuration {
118 reason: format!("cannot configure FMP_API_KEY from {source}: {reason}"),
119 },
120 error => error,
121 })
122}
123
124fn duplicate_setting(path: &Path, name: &str) -> Error {
125 Error::Configuration {
126 reason: format!(
127 "dotenv file '{}' contains duplicate {name} assignments; keep exactly one",
128 path.display()
129 ),
130 }
131}
132
133fn dotenv_error(path: &Path, error: dotenvy::Error) -> Error {
135 let reason = match error {
136 dotenvy::Error::LineParse(_, index) => format!(
137 "invalid dotenv syntax at byte index {index} within an assignment; check KEY=VALUE syntax and matching quotes"
138 ),
139 dotenvy::Error::Io(error) => format!("file could not be read: {error}"),
140 dotenvy::Error::EnvVar(env::VarError::NotPresent) => {
141 "a referenced environment variable is missing".to_owned()
142 }
143 dotenvy::Error::EnvVar(env::VarError::NotUnicode(_)) => {
144 "a referenced environment variable is not valid Unicode".to_owned()
145 }
146 _ => "the dotenv parser returned an unsupported error variant".to_owned(),
147 };
148 Error::Configuration {
149 reason: format!("cannot read dotenv file '{}': {reason}", path.display()),
150 }
151}