Skip to main content

rusty_fmp/cli/
args.rs

1//! Clap command and argument definitions.
2
3use crate::cli::{groups, help};
4
5use clap::{Args, Parser, Subcommand, ValueEnum};
6
7use crate::endpoint::{ANNUAL_PERIOD, QUARTER_PERIOD};
8
9/// Validates that a date string is in `YYYY-MM-DD` format.
10///
11/// Used as a Clap [`value_parser`] so that invalid dates are caught at
12/// parse time and produce human-readable usage errors instead of
13/// structured JSON runtime errors.
14fn parse_date(s: &str) -> Result<String, clap::Error> {
15    jiff::civil::Date::strptime("%Y-%m-%d", s)
16        .map(|_| s.to_string())
17        .map_err(|_| {
18            clap::Error::raw(
19                clap::error::ErrorKind::ValueValidation,
20                format!("invalid date '{s}': expected YYYY-MM-DD"),
21            )
22        })
23}
24
25const DEFAULT_BASE_URL: &str = "https://financialmodelingprep.com/stable/";
26
27/// Financial Modeling Prep CLI configuration.
28#[derive(Debug, Parser)]
29#[command(
30    name = "fmp-agent",
31    version,
32    about = help::CLI_ABOUT,
33    after_help = help::EXIT_CODES,
34    disable_help_subcommand = true
35)]
36pub struct Cli {
37    /// FMP API key.
38    #[arg(long, global = true, env = "FMP_API_KEY", hide_env_values = true, help = help::API_KEY)]
39    pub api_key: Option<String>,
40
41    /// FMP stable API base URL.
42    #[arg(long, global = true, env = "FMP_BASE_URL", default_value = DEFAULT_BASE_URL, help = help::BASE_URL)]
43    pub base_url: String,
44
45    /// Increase log verbosity (-v for INFO, -vv for DEBUG, -vvv for TRACE).
46    #[arg(short = 'v', long = "verbose", action = clap::ArgAction::Count)]
47    pub verbose: u8,
48
49    /// Fail symbol lookups that return an empty JSON result.
50    #[arg(long, global = true, help = help::STRICT_EMPTY)]
51    pub strict_empty: bool,
52
53    /// Command to execute.
54    #[command(subcommand)]
55    pub(crate) command: Command,
56}
57
58/// Supported FMP endpoint commands.
59#[derive(Debug, Subcommand)]
60pub(crate) enum Command {
61    /// Git-like help topic command group.
62    #[command(
63        about = help::HELP_GROUP_ABOUT,
64        long_about = help::HELP_GROUP_LONG,
65        disable_help_subcommand = true
66    )]
67    Help {
68        /// Help topic to show.
69        #[command(subcommand)]
70        command: Option<HelpTopic>,
71    },
72
73    /// Search command.
74    #[command(about = help::SEARCH_ABOUT, long_about = help::SEARCH_LONG)]
75    Search {
76        /// Search query.
77        #[arg(help = help::SEARCH_QUERY)]
78        query: String,
79    },
80
81    /// Schema dump command.
82    #[command(about = help::SCHEMA_ABOUT, long_about = help::SCHEMA_LONG)]
83    Schema,
84
85    /// Commands list command.
86    #[command(about = help::COMMANDS_ABOUT, long_about = help::COMMANDS_LONG)]
87    Commands {
88        /// Group commands by category instead of flat sorted output.
89        #[arg(long, help = help::COMMANDS_GROUPED)]
90        grouped: bool,
91    },
92
93    /// Shell completions command.
94    #[command(about = help::COMPLETIONS_ABOUT, long_about = help::COMPLETIONS_LONG)]
95    Completions {
96        /// Target shell.
97        #[arg(value_enum)]
98        shell: clap_complete::Shell,
99    },
100
101    /// Local configuration diagnostic command.
102    #[command(about = help::DOCTOR_ABOUT, long_about = help::DOCTOR_LONG)]
103    Doctor,
104
105    /// Top-level alias for `market quote`.
106    #[command(about = help::QUOTE_ALIAS_ABOUT, long_about = help::QUOTE_ALIAS_LONG)]
107    Quote(SymbolArgs),
108
109    /// Top-level alias for `market historical`.
110    #[command(
111        about = help::HISTORICAL_ALIAS_ABOUT,
112        long_about = help::HISTORICAL_ALIAS_LONG
113    )]
114    Historical(SymbolDateRangeArgs),
115
116    /// Top-level alias for `company profile`.
117    #[command(about = help::PROFILE_ALIAS_ABOUT, long_about = help::PROFILE_ALIAS_LONG)]
118    Profile(SymbolArgs),
119
120    /// Top-level alias for `calendar earnings`.
121    #[command(about = help::EARNINGS_ALIAS_ABOUT, long_about = help::EARNINGS_ALIAS_LONG)]
122    Earnings(DateRangeArgs),
123
124    /// Company data command group.
125    #[command(about = help::COMPANY_GROUP_ABOUT, long_about = help::COMPANY_GROUP_LONG)]
126    Company {
127        /// Company subcommand to run.
128        #[command(subcommand)]
129        command: Option<groups::company::Cmd>,
130    },
131
132    /// Market data command group.
133    #[command(about = help::MARKET_GROUP_ABOUT, long_about = help::MARKET_GROUP_LONG)]
134    Market {
135        /// Market subcommand to run.
136        #[command(subcommand)]
137        command: Option<groups::market::Cmd>,
138    },
139
140    /// Fundamentals command group.
141    #[command(
142        about = help::FUNDAMENTALS_GROUP_ABOUT,
143        long_about = help::FUNDAMENTALS_GROUP_LONG
144    )]
145    Fundamentals {
146        /// Fundamentals subcommand to run.
147        #[command(subcommand)]
148        command: Option<groups::fundamentals::Cmd>,
149    },
150
151    /// Analyst command group.
152    #[command(about = help::ANALYST_GROUP_ABOUT, long_about = help::ANALYST_GROUP_LONG)]
153    Analyst {
154        /// Analyst subcommand to run.
155        #[command(subcommand)]
156        command: Option<groups::analyst::Cmd>,
157    },
158
159    /// Insider command group.
160    #[command(about = help::INSIDER_GROUP_ABOUT, long_about = help::INSIDER_GROUP_LONG)]
161    Insider {
162        /// Insider subcommand to run.
163        #[command(subcommand)]
164        command: Option<groups::insider::Cmd>,
165    },
166
167    /// Calendar command group.
168    #[command(about = help::CALENDAR_GROUP_ABOUT, long_about = help::CALENDAR_GROUP_LONG)]
169    Calendar {
170        /// Calendar subcommand to run.
171        #[command(subcommand)]
172        command: Option<groups::calendar::Cmd>,
173    },
174
175    /// Macro command group.
176    #[command(
177        name = "macro",
178        about = help::MACRO_GROUP_ABOUT,
179        long_about = help::MACRO_GROUP_LONG
180    )]
181    MacroEcon {
182        /// Macro subcommand to run.
183        #[command(subcommand)]
184        command: Option<groups::macro_econ::Cmd>,
185    },
186
187    /// Technical analysis command group.
188    #[command(about = help::TECHNICAL_GROUP_ABOUT, long_about = help::TECHNICAL_GROUP_LONG)]
189    Technical {
190        /// Technical subcommand to run.
191        #[command(subcommand)]
192        command: Option<groups::technical::Cmd>,
193    },
194
195    /// SEC command group.
196    #[command(about = help::SEC_GROUP_ABOUT, long_about = help::SEC_GROUP_LONG)]
197    Sec {
198        /// SEC subcommand to run.
199        #[command(subcommand)]
200        command: Option<groups::sec::Cmd>,
201    },
202
203    /// ETF command group.
204    #[command(about = help::ETF_GROUP_ABOUT, long_about = help::ETF_GROUP_LONG)]
205    Etf {
206        /// ETF subcommand to run.
207        #[command(subcommand)]
208        command: Option<groups::etf::Cmd>,
209    },
210
211    /// Cryptocurrency command group.
212    #[command(about = help::CRYPTO_GROUP_ABOUT, long_about = help::CRYPTO_GROUP_LONG)]
213    Crypto {
214        /// Cryptocurrency subcommand to run.
215        #[command(subcommand)]
216        command: Option<groups::crypto::Cmd>,
217    },
218
219    /// Forex command group.
220    #[command(about = help::FOREX_GROUP_ABOUT, long_about = help::FOREX_GROUP_LONG)]
221    Forex {
222        /// Forex subcommand to run.
223        #[command(subcommand)]
224        command: Option<groups::forex::Cmd>,
225    },
226
227    /// News command group.
228    #[command(about = help::NEWS_GROUP_ABOUT, long_about = help::NEWS_GROUP_LONG)]
229    News {
230        /// News subcommand to run.
231        #[command(subcommand)]
232        command: Option<groups::news::Cmd>,
233    },
234}
235
236/// Git-like help topics for operational guidance.
237#[derive(Debug, Subcommand)]
238pub(crate) enum HelpTopic {
239    /// Environment and configuration help topic.
240    #[command(about = help::HELP_ENVIRONMENT_ABOUT, long_about = help::HELP_ENVIRONMENT_LONG)]
241    Environment,
242
243    /// Exit code and stderr format help topic.
244    #[command(about = help::HELP_EXIT_CODES_ABOUT, long_about = help::HELP_EXIT_CODES_LONG)]
245    ExitCodes,
246
247    /// Schema and tool-calling help topic.
248    #[command(about = help::HELP_SCHEMA_ABOUT, long_about = help::HELP_SCHEMA_LONG)]
249    Schema,
250
251    /// Troubleshooting help topic.
252    #[command(
253        about = help::HELP_TROUBLESHOOTING_ABOUT,
254        long_about = help::HELP_TROUBLESHOOTING_LONG
255    )]
256    Troubleshooting,
257
258    /// Representative examples help topic.
259    #[command(about = help::HELP_EXAMPLES_ABOUT, long_about = help::HELP_EXAMPLES_LONG)]
260    Examples,
261}
262
263/// Shared command symbol argument.
264#[derive(Debug, Args)]
265pub struct SymbolArgs {
266    /// Command symbol.
267    #[arg(help = help::SYMBOL)]
268    pub symbol: String,
269}
270
271/// Shared multi-symbol positional argument for batch-style commands.
272#[derive(Debug, Args)]
273pub struct SymbolsArgs {
274    /// One or more stock ticker symbols (e.g., AAPL MSFT GOOGL).
275    #[arg(required = true, num_args = 1.., value_name = "SYMBOLS", help = help::SYMBOLS)]
276    pub symbols: Vec<String>,
277}
278
279/// Shared symbol plus row limit arguments.
280#[derive(Debug, Args)]
281pub struct SymbolLimitArgs {
282    /// Stock ticker symbol.
283    #[arg(help = help::STOCK_SYMBOL)]
284    pub symbol: String,
285
286    /// Maximum number of rows to return.
287    #[arg(long, default_value_t = 10, help = help::LIMIT_ROWS)]
288    pub limit: u16,
289}
290
291/// Annual report form arguments.
292#[derive(Debug, Args)]
293pub struct AnnualReportFormArgs {
294    /// Stock ticker symbol.
295    #[arg(help = help::STOCK_SYMBOL)]
296    pub symbol: String,
297
298    /// Fiscal year.
299    #[arg(long, help = help::YEAR)]
300    pub year: u16,
301
302    /// Fiscal period.
303    #[arg(long, default_value = "FY", help = help::PERIOD)]
304    pub period: String,
305}
306
307/// Shared symbol and date range arguments.
308#[derive(Debug, Args)]
309pub struct SymbolDateRangeArgs {
310    /// Symbol accepted by this command.
311    #[arg(help = help::SYMBOL)]
312    pub symbol: String,
313
314    /// Inclusive start date.
315    #[arg(long, help = help::FROM, value_parser = parse_date)]
316    pub from: Option<String>,
317
318    /// Inclusive end date.
319    #[arg(long, help = help::TO, value_parser = parse_date)]
320    pub to: Option<String>,
321}
322
323/// Shared date range arguments.
324#[derive(Debug, Args)]
325pub struct DateRangeArgs {
326    /// Inclusive start date.
327    #[arg(long, help = help::FROM, value_parser = parse_date)]
328    pub from: Option<String>,
329
330    /// Inclusive end date.
331    #[arg(long, help = help::TO, value_parser = parse_date)]
332    pub to: Option<String>,
333}
334
335/// Shared indicator name and date range arguments.
336#[derive(Debug, Args)]
337pub struct NameDateRangeArgs {
338    /// Economic indicator name.
339    #[arg(help = help::ECONOMIC_INDICATOR_NAME)]
340    pub name: String,
341
342    /// Inclusive start date.
343    #[arg(long, help = help::FROM, value_parser = parse_date)]
344    pub from: Option<String>,
345
346    /// Inclusive end date.
347    #[arg(long, help = help::TO, value_parser = parse_date)]
348    pub to: Option<String>,
349}
350
351/// Shared annual endpoint arguments.
352#[derive(Debug, Args)]
353pub struct AnnualArgs {
354    /// Stock ticker symbol.
355    #[arg(help = help::STOCK_SYMBOL)]
356    pub symbol: String,
357
358    /// Maximum number of annual rows to return.
359    #[arg(long, default_value_t = 5, help = help::ANNUAL_LIMIT)]
360    pub limit: u16,
361}
362
363/// Shared statement endpoint arguments.
364#[derive(Debug, Args)]
365pub struct StatementArgs {
366    /// Stock ticker symbol.
367    #[arg(help = help::STOCK_SYMBOL)]
368    pub symbol: String,
369
370    /// Fiscal period to request.
371    #[arg(long, value_enum, default_value_t = StatementPeriod::Annual, help = help::STATEMENT_PERIOD)]
372    pub period: StatementPeriod,
373
374    /// Maximum number of statement rows to return.
375    #[arg(long, default_value_t = 5, help = help::ANNUAL_LIMIT)]
376    pub limit: u16,
377}
378
379/// Supported statement periods for statement-style fundamentals endpoints.
380#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
381pub enum StatementPeriod {
382    /// Annual fiscal period rows.
383    #[default]
384    Annual,
385    /// Quarterly fiscal period rows.
386    Quarter,
387}
388
389impl StatementPeriod {
390    /// Returns the FMP API query value for this period.
391    #[must_use]
392    pub const fn as_str(self) -> &'static str {
393        match self {
394            Self::Annual => ANNUAL_PERIOD,
395            Self::Quarter => QUARTER_PERIOD,
396        }
397    }
398}
399
400impl std::fmt::Display for StatementPeriod {
401    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
402        formatter.write_str(self.as_str())
403    }
404}
405
406/// Simple moving average arguments.
407#[derive(Debug, Args)]
408pub struct TechnicalSmaArgs {
409    /// Stock ticker symbol.
410    #[arg(help = help::STOCK_SYMBOL)]
411    pub symbol: String,
412
413    /// Number of periods in the moving average window.
414    #[arg(long, default_value_t = 10, help = help::PERIOD_LENGTH)]
415    pub period_length: u16,
416
417    /// FMP candle timeframe.
418    #[arg(long, default_value = "1day", help = help::TIMEFRAME)]
419    pub timeframe: String,
420}
421
422/// Stock news arguments.
423#[derive(Debug, Args)]
424pub struct StockNewsArgs {
425    /// Stock ticker symbol.
426    #[arg(help = help::STOCK_SYMBOL)]
427    pub symbol: String,
428
429    /// Maximum number of news items to return.
430    #[arg(long, default_value_t = 10, help = help::LIMIT_ROWS)]
431    pub limit: u16,
432}
433
434/// Shared paginated endpoint arguments.
435#[derive(Debug, Args)]
436pub struct PagedArgs {
437    /// Zero-based result page.
438    #[arg(long, default_value_t = 0, help = help::PAGE)]
439    pub page: u16,
440
441    /// Maximum number of items to return.
442    #[arg(long, default_value_t = 10, help = help::PAGE_LIMIT)]
443    pub limit: u16,
444}
445
446#[cfg(test)]
447mod tests {
448    use super::StatementPeriod;
449
450    #[test]
451    fn statement_period_displays_api_values() {
452        assert_eq!(StatementPeriod::Annual.to_string(), "annual");
453        assert_eq!(StatementPeriod::Quarter.to_string(), "quarter");
454    }
455}