haste_health/commands/
subscription.rs1use clap::Subcommand;
2use haste_fhir_model::r4::generated::terminology::IssueType;
3use haste_fhir_operation_error::OperationOutcomeError;
4use std::path::{Path, PathBuf};
5
6const DEFAULT_OUTPUT: &str = "../frontend/packages/website/static/pricing/tiers.json";
8
9#[derive(Subcommand, Debug)]
11pub(crate) enum SubscriptionCommands {
12 Export {
14 #[arg(short, long, default_value = DEFAULT_OUTPUT)]
16 output: PathBuf,
17 #[arg(long)]
19 check: bool,
20 },
21 Show,
23}
24
25fn error(message: String) -> OperationOutcomeError {
26 OperationOutcomeError::error(IssueType::exception(), message)
27}
28
29fn rendered() -> Result<String, OperationOutcomeError> {
32 let mut json = serde_json::to_string_pretty(&haste_subscription::export::build())
33 .map_err(|e| error(format!("Failed to serialize the subscription tiers: {e}")))?;
34 json.push('\n');
35
36 Ok(json)
37}
38
39async fn export(output: &Path, check: bool) -> Result<(), OperationOutcomeError> {
40 let contents = rendered()?;
41
42 if check {
43 let current = tokio::fs::read_to_string(output)
44 .await
45 .map_err(|e| error(format!("{}: {e}", output.display())))?;
46
47 return if current == contents {
48 println!("{} is up to date", output.display());
49 Ok(())
50 } else {
51 Err(error(format!(
52 "Out of date, run `cargo run subscription export` to regenerate: {}",
53 output.display()
54 )))
55 };
56 }
57
58 if let Some(parent) = output.parent() {
59 tokio::fs::create_dir_all(parent)
60 .await
61 .map_err(|e| error(format!("{}: {e}", parent.display())))?;
62 }
63
64 tokio::fs::write(output, contents)
65 .await
66 .map_err(|e| error(format!("{}: {e}", output.display())))?;
67 println!("Wrote {}", output.display());
68
69 Ok(())
70}
71
72fn show() {
73 let export = haste_subscription::export::build();
74
75 println!(
76 "Request costs: read {}, search {}, history {}, invocation {}, write {} points (budget per {})\n",
77 export.operation_points.read,
78 export.operation_points.search,
79 export.operation_points.history,
80 export.operation_points.invocation,
81 export.operation_points.write,
82 export.window,
83 );
84
85 for tier in &export.tiers {
86 let tier_id: String = tier.limits.tier.clone().into();
87
88 println!("{} ({})", tier.limits.display_name, tier_id);
89 println!(
90 " {}{} - {}",
91 tier.limits.price,
92 tier.limits.cadence.unwrap_or(""),
93 tier.limits.audience,
94 );
95 println!(" Request budget {}", tier.request_budget_label);
96 println!(" Total resources {}", tier.total_resources_label);
97 println!(" Support {}", tier.limits.support);
98 println!(" Uptime SLA {}", tier.limits.uptime_sla);
99 println!(
100 " Customization {}",
101 if tier.limits.tenant_customization {
102 "allowed"
103 } else {
104 "not allowed"
105 }
106 );
107 println!(
108 " BAA {}",
109 if tier.limits.baa_available {
110 "available"
111 } else {
112 "not available"
113 }
114 );
115 println!();
116 }
117}
118
119pub(crate) async fn run(command: &SubscriptionCommands) -> Result<(), OperationOutcomeError> {
120 match command {
121 SubscriptionCommands::Export { output, check } => export(output, *check).await,
122 SubscriptionCommands::Show => {
123 show();
124 Ok(())
125 }
126 }
127}