1use crate::error::CliError;
14use crate::report::{Diagnostic, Envelope, Severity, UrlSource, redact_text};
15use serde::Serialize;
16use siderite_orm::{Db, TransactionSupport};
17use std::future::Future;
18use std::pin::Pin;
19use std::sync::Arc;
20
21#[cfg(test)]
22mod tests;
23
24pub type SeedError = Box<dyn std::error::Error + Send + Sync>;
27
28pub(crate) type SeedFuture = Pin<Box<dyn Future<Output = Seeded> + Send>>;
30
31type Seeded = Result<(), SeedError>;
32
33type Callback = Arc<dyn Fn(Seeding) -> SeedFuture + Send + Sync>;
34
35#[derive(Debug, Clone)]
37pub struct Seeding {
38 pub db: Db,
41 pub alias: String,
43 pub dataset: String,
45}
46
47#[derive(Clone)]
49pub(crate) struct Dataset {
50 name: String,
51 summary: String,
52 run: Callback,
53}
54
55impl std::fmt::Debug for Dataset {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 f.debug_struct("Dataset")
58 .field("name", &self.name)
59 .finish_non_exhaustive()
60 }
61}
62
63impl Dataset {
64 pub(crate) fn new<F, Fut>(name: &str, summary: &str, run: F) -> Self
66 where
67 F: Fn(Seeding) -> Fut + Send + Sync + 'static,
68 Fut: Future<Output = Result<(), SeedError>> + Send + 'static,
69 {
70 let run: Callback = Arc::new(move |s| Box::pin(run(s)));
71 Self {
72 name: name.to_owned(),
73 summary: summary.to_owned(),
74 run,
75 }
76 }
77}
78
79#[derive(Debug, Clone, PartialEq, Eq)]
81pub(crate) enum Request {
82 List,
84 Load(String),
86}
87
88const USAGE: &str = "usage: seed --dataset NAME | seed --list";
89
90pub(crate) fn parse(rest: &[String]) -> Result<Request, CliError> {
96 let mut args = rest.iter().skip(1).map(String::as_str);
97 let request = match (args.next(), args.next(), args.next()) {
98 (Some("--list"), None, _) => Request::List,
99 (Some("--dataset"), Some(name), None) if !name.starts_with('-') => {
100 Request::Load(name.to_owned())
101 }
102 (Some(flag), None, _) if flag.starts_with("--dataset=") => {
103 Request::Load(flag["--dataset=".len()..].to_owned())
104 }
105 _ => return Err(CliError::usage(USAGE)),
106 };
107 if request == Request::Load(String::new()) {
108 return Err(CliError::usage(USAGE));
109 }
110 Ok(request)
111}
112
113fn names(datasets: &[Dataset]) -> String {
115 let names: Vec<&str> = datasets.iter().map(|d| d.name.as_str()).collect();
116 if names.is_empty() {
117 "none is registered".to_owned()
118 } else {
119 format!("registered: {}", names.join(", "))
120 }
121}
122
123type Found<'a> = Result<&'a Dataset, CliError>;
124
125pub(crate) fn find<'a>(datasets: &'a [Dataset], name: &str) -> Found<'a> {
131 let mut found = datasets.iter().filter(|d| d.name == name);
132 match (found.next(), found.next()) {
133 (Some(dataset), None) => Ok(dataset),
134 (Some(_), Some(_)) => Err(CliError::usage(format!(
135 "dataset `{name}` is registered more than once"
136 ))),
137 _ => Err(CliError::usage(format!(
138 "no dataset `{name}` ({})",
139 names(datasets)
140 ))),
141 }
142}
143
144#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
146pub(crate) struct DatasetInfo {
147 pub name: String,
149 pub summary: String,
151}
152
153#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
155pub(crate) struct Loaded {
156 pub dataset: String,
158 pub database: String,
160 pub url_source: UrlSource,
162 pub url: String,
164 pub transaction: bool,
166}
167
168#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
170pub(crate) struct SeedData {
171 pub datasets: Vec<DatasetInfo>,
173 pub loaded: Option<Loaded>,
175}
176
177pub(crate) fn list(datasets: &[Dataset]) -> Envelope<SeedData> {
179 let info = datasets.iter().map(|d| DatasetInfo {
180 name: d.name.clone(),
181 summary: d.summary.clone(),
182 });
183 let data = SeedData {
184 datasets: info.collect(),
185 loaded: None,
186 };
187 Envelope::new("seed", data, Vec::new())
188}
189
190#[derive(Debug, Clone)]
192pub(crate) struct Target {
193 pub db: Db,
195 pub alias: String,
197 pub source: UrlSource,
199 pub url: String,
201}
202
203pub(crate) async fn load(
209 datasets: &[Dataset],
210 dataset: &Dataset,
211 target: Target,
212) -> Envelope<SeedData> {
213 let support = target.db.capabilities().transactions;
214 let transaction = support != TransactionSupport::None;
215 let seeding = |db| Seeding {
216 db,
217 alias: target.alias.clone(),
218 dataset: dataset.name.clone(),
219 };
220 let run = &dataset.run;
221 let outcome = if transaction {
222 let body = |tx| run(seeding(tx));
223 target.db.transaction(body).await
224 } else {
225 run(seeding(target.db.clone())).await
226 };
227 let diags = outcome
228 .err()
229 .map(|err| failed(&dataset.name, &*err, transaction));
230 let mut report = list(datasets);
231 report.data.loaded = Some(Loaded {
232 dataset: dataset.name.clone(),
233 database: target.alias.clone(),
234 url_source: target.source,
235 url: redact_text(&target.url),
236 transaction,
237 });
238 let diags: Vec<Diagnostic> = diags.into_iter().collect();
239 Envelope::new("seed", report.data, diags)
240}
241
242fn failed(
243 name: &str,
244 err: &(dyn std::error::Error + Send + Sync),
245 transaction: bool,
246) -> Diagnostic {
247 let after = if transaction {
248 "nothing was written (the transaction was rolled back)"
249 } else {
250 "writes made before the failure remain (no transaction)"
251 };
252 let message = redact_text(&format!("dataset `{name}` failed: {err}"));
253 let diag = Diagnostic::new("seed.E001", Severity::Error, &message);
254 diag.with_hint(after)
255}
256
257pub(crate) fn render(report: &Envelope<SeedData>) -> String {
259 use std::fmt::Write;
260 let mut out = String::new();
261 let data = &report.data;
262 if let Some(loaded) = &data.loaded {
263 let how = if loaded.transaction {
264 "in one transaction"
265 } else {
266 "without a transaction"
267 };
268 let verb = if report.ok { "loaded" } else { "failed" };
269 let _ = writeln!(
270 out,
271 "dataset `{}` {verb} into database `{}` ({}) {how}",
272 loaded.dataset, loaded.database, loaded.url
273 );
274 } else if data.datasets.is_empty() {
275 let _ = writeln!(out, "no datasets; register one with AppCli::seed");
276 }
277 if data.loaded.is_none() {
278 for info in &data.datasets {
279 let _ = writeln!(out, "{:<20} {}", info.name, info.summary);
280 }
281 }
282 for diag in &report.diagnostics {
283 let line = crate::report::text::diagnostic_line(diag);
284 let _ = writeln!(out, "{line}");
285 if let Some(hint) = diag.hint() {
286 let _ = writeln!(out, " {hint}");
287 }
288 }
289 out
290}