haste_health/commands/
artifacts.rs1use clap::Subcommand;
2use haste_artifact_patcher::{Build, Change};
3use haste_fhir_model::r4::generated::terminology::IssueType;
4use haste_fhir_operation_error::OperationOutcomeError;
5use serde_json::Value;
6use similar::{ChangeTag, TextDiff};
7use std::{
8 fmt::Write,
9 path::{Path, PathBuf},
10};
11
12#[derive(Subcommand, Debug)]
14pub(crate) enum ArtifactCommands {
15 Build {
17 #[arg(required = true)]
19 manifest: Vec<PathBuf>,
20 #[arg(long)]
22 check: bool,
23 },
24 Diff {
26 #[arg(required = true)]
28 manifest: Vec<PathBuf>,
29 #[arg(short, long)]
31 resource: Option<String>,
32 #[arg(short, long)]
34 origin: Option<String>,
35 #[arg(long)]
37 summary: bool,
38 },
39}
40
41fn error(message: String) -> OperationOutcomeError {
42 OperationOutcomeError::error(IssueType::exception(), message)
43}
44
45fn build(manifest: &Path) -> Result<Build, OperationOutcomeError> {
46 haste_artifact_patcher::build(manifest).map_err(|e| error(e.to_string()))
47}
48
49fn render(value: Option<&Value>) -> String {
50 match value {
51 None => "(absent)".to_string(),
52 Some(Value::String(text)) => text.clone(),
53 Some(other) => other.to_string(),
54 }
55}
56
57fn print_value(prefix: char, value: Option<&Value>) {
58 for line in render(value).lines() {
59 println!(" {prefix} {line}");
60 }
61}
62
63fn print_text_diff(before: &str, after: &str) {
66 let mut marked = String::new();
67 for change in TextDiff::from_words(before, after).iter_all_changes() {
68 match change.tag() {
69 ChangeTag::Equal => marked.push_str(change.value()),
70 ChangeTag::Delete => {
71 let _ = write!(marked, "[-{}-]", change.value());
72 }
73 ChangeTag::Insert => {
74 let _ = write!(marked, "{{+{}+}}", change.value());
75 }
76 }
77 }
78 for line in marked
79 .lines()
80 .filter(|l| l.contains("[-") || l.contains("{+"))
81 {
82 println!(" ~ {line}");
83 }
84}
85
86fn print_changes(changes: &[&Change]) {
87 let mut current = None;
88 for change in changes {
89 let heading = (&change.resource, &change.origin);
90 if current != Some(heading) {
91 println!(
92 "\n{} [{}] ({})",
93 change.resource,
94 change.origin,
95 change.source.display()
96 );
97 current = Some(heading);
98 }
99 println!(" {}", change.pointer);
100 if let (Some(Value::String(before)), Some(Value::String(after))) =
101 (&change.before, &change.after)
102 {
103 print_text_diff(before, after);
104 } else {
105 print_value('-', change.before.as_ref());
106 print_value('+', change.after.as_ref());
107 }
108 }
109}
110
111fn print_summary(changes: &[&Change]) {
112 let mut counts: Vec<(String, usize)> = Vec::new();
113 for change in changes {
114 let origin = change.origin.to_string();
115 match counts.iter_mut().find(|(name, _)| *name == origin) {
116 Some((_, count)) => *count += 1,
117 None => counts.push((origin, 1)),
118 }
119 }
120 for (origin, count) in counts {
121 println!("{count:>8} {origin}");
122 }
123}
124
125pub(crate) async fn run(command: &ArtifactCommands) -> Result<(), OperationOutcomeError> {
127 match command {
128 ArtifactCommands::Build { manifest, check } => {
129 let mut stale = Vec::new();
130 for manifest in manifest {
131 let result = build(manifest)?;
132 for output in result.outputs {
133 let current = tokio::fs::read_to_string(&output.path).await.ok();
134 if current.as_deref() == Some(output.contents.as_str()) {
135 continue;
136 }
137 if *check {
138 stale.push(output.path.display().to_string());
139 } else {
140 tokio::fs::write(&output.path, output.contents)
141 .await
142 .map_err(|e| error(format!("{}: {e}", output.path.display())))?;
143 println!("Wrote {}", output.path.display());
144 }
145 }
146 println!(
147 "{}: {} change(s) from patches and rules",
148 manifest.display(),
149 result.changes.len()
150 );
151 }
152
153 if stale.is_empty() {
154 Ok(())
155 } else {
156 Err(error(format!(
157 "Out of date, run `cargo run artifacts build` to regenerate: {}",
158 stale.join(", ")
159 )))
160 }
161 }
162 ArtifactCommands::Diff {
163 manifest,
164 resource,
165 origin,
166 summary,
167 } => {
168 for manifest in manifest {
169 let result = build(manifest)?;
170 let changes = result
171 .changes
172 .iter()
173 .filter(|c| {
174 resource
175 .as_ref()
176 .is_none_or(|r| c.resource.contains(r.as_str()))
177 })
178 .filter(|c| {
179 origin
180 .as_ref()
181 .is_none_or(|o| c.origin.to_string().contains(o.as_str()))
182 })
183 .collect::<Vec<_>>();
184
185 println!("# {} ({} change(s))", manifest.display(), changes.len());
186 if *summary {
187 print_summary(&changes);
188 } else {
189 print_changes(&changes);
190 }
191 }
192 Ok(())
193 }
194 }
195}