sofie_cli/
seed_command.rs1use std::collections::BTreeMap;
2use std::fmt;
3use std::path::Path;
4
5use serde_json::Value;
6use sofie_model::App;
7use sofie_model::naming::file_stem;
8use sofie_values::Seed;
9
10use crate::gen_command::GenError;
11use crate::http_client::CookieJars;
12use crate::inputs::{clock_seed, valid_case};
13use crate::local_app::{LocalApp, bench_member};
14use crate::source_edits::{files_with_extension, read};
15use crate::targets::{Target, member_areas, targets};
16
17const SEEDS: &str = "seeds";
18const SHOWN_REFUSAL: usize = 300;
19
20#[derive(Clone, Debug, PartialEq, Eq)]
21pub struct SeedReport {
22 pub routes: Vec<SeededRoute>,
23}
24
25#[derive(Clone, Debug, PartialEq, Eq)]
26pub struct SeededRoute {
27 pub route: String,
28 pub created: usize,
29 pub refused: usize,
30 pub first_refusal: Option<String>,
31}
32
33pub fn seed(root: &Path, volume: usize) -> Result<SeedReport, GenError> {
38 let app = sofie_model::load(root).map_err(GenError::Model)?;
39 let creating: Vec<Target> = targets(&app)?
40 .targets
41 .into_iter()
42 .filter(Target::is_creating)
43 .collect();
44 let files = seed_files(&app, &creating)?;
45 let mut report = SeedReport { routes: Vec::new() };
46 if creating.is_empty() || (volume == 0 && files.is_empty()) {
47 return Ok(report);
48 }
49 let local = LocalApp::start(root, "seed")?;
50 let jars = sign_in(&local, &creating)?;
51 let mut seed = Seed::new(clock_seed());
54 for target in &creating {
55 let mut seeded = SeededRoute {
56 route: target.name.clone(),
57 created: 0,
58 refused: 0,
59 first_refusal: None,
60 };
61 let fixed = files.get(&target.name).map_or(&[][..], Vec::as_slice);
62 for body in fixed {
63 seeded.post(&local, &jars, target, &body.to_string());
64 }
65 for _ in 0..volume {
66 let case = valid_case(&target.fields, &mut seed);
67 seeded.post(&local, &jars, target, &case.body);
68 }
69 report.routes.push(seeded);
70 }
71 Ok(report)
72}
73
74fn sign_in(local: &LocalApp, creating: &[Target]) -> Result<CookieJars, GenError> {
75 let mut jars = CookieJars::default();
76 for (area, paths) in member_areas(creating) {
77 local.sign_in(&mut jars, area, paths, &bench_member(area))?;
78 }
79 Ok(jars)
80}
81
82impl SeededRoute {
83 fn post(&mut self, local: &LocalApp, jars: &CookieJars, target: &Target, body: &str) {
84 let cookie = jars.cookie_header(&target.area);
85 let refusal = match local.send_json("POST", &target.path, cookie, body) {
86 Ok(response) if response.status < 400 => {
87 self.created += 1;
88 return;
89 }
90 Ok(response) => format!(
91 "{}: {}",
92 response.status,
93 String::from_utf8_lossy(&response.body)
94 ),
95 Err(error) => error.to_string(),
96 };
97 self.refused += 1;
98 if self.first_refusal.is_none() {
99 let end = refusal.floor_char_boundary(SHOWN_REFUSAL);
100 self.first_refusal = Some(format!("{} for {body}", &refusal[..end]));
101 }
102 }
103}
104
105fn seed_files(app: &App, creating: &[Target]) -> Result<BTreeMap<String, Vec<Value>>, GenError> {
108 let mut files = BTreeMap::new();
109 for module in &app.modules {
110 let directory = module.krate.path.join(SEEDS);
111 for file in files_with_extension(&directory, "json")? {
112 let route = format!("{}.{}", module.krate.crate_name, file_stem(&file));
113 if !creating.iter().any(|target| target.name == route) {
114 return Err(GenError::App(format!(
115 "{}: no creating route `{route}`; a seed file is named \
116 `<area>.<handler>.json` after a POST route taking an input",
117 file.display()
118 )));
119 }
120 let text = read(&file)?;
121 let inputs: Vec<Value> = serde_json::from_str(&text).map_err(|error| {
122 GenError::App(format!(
123 "{}: not a JSON array of inputs: {error}",
124 file.display()
125 ))
126 })?;
127 files.insert(route, inputs);
128 }
129 }
130 Ok(files)
131}
132
133impl fmt::Display for SeedReport {
135 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
136 if self.routes.is_empty() {
137 return writeln!(
138 formatter,
139 "sofie seed: nothing to seed; no POST route without a path parameter takes an \
140 input, or there is no volume and no seed file"
141 );
142 }
143 for route in &self.routes {
144 write!(formatter, "{}: {} created", route.route, route.created)?;
145 if route.refused > 0 {
146 write!(formatter, ", {} refused", route.refused)?;
147 }
148 writeln!(formatter)?;
149 if let Some(refusal) = &route.first_refusal {
150 writeln!(formatter, " first refusal: {refusal}")?;
151 }
152 }
153 Ok(())
154 }
155}