1use std::collections::{BTreeMap, BTreeSet};
6use std::fmt::Display;
7use std::str::FromStr;
8
9use serde::{Deserialize, Serialize};
10
11use crate::config::Config;
12use crate::jigsaw::loaded::{SnapshotGap, StoreStatus};
13use crate::jigsaw::plan::{HostRelease, JigsawPlan, PlanError, PlanParts, TagSourceKind};
14use crate::jigsaw::releases::SeriesReleases;
15use crate::jigsaw::tags::OtpLine;
16use crate::model::names::{
17 ApplicationName, CommitSha, DependencyName, ExtractorVersion, HostFactsDigest, HostName,
18 ProjectName, TagName,
19};
20use crate::model::non_empty::NonEmpty;
21use crate::model::pin::Pin;
22use crate::model::release_version::{
23 ErlangVersionRange, HostSeries, HostTag, OtpLineVersion, OtpTag,
24};
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
28pub struct JigsawPlanPayload {
29 pub host: String,
30 pub host_project: String,
31 pub otp_project: String,
32 pub series: String,
33 pub tag_source: String,
34 pub extractor_version: String,
35 pub host_facts_digest: String,
36 pub releases: Vec<PlanReleasePayload>,
37 pub otp_lines: Vec<PlanOtpLinePayload>,
38 pub untracked_dependencies: Vec<String>,
39 pub excluded_otp_apps: Vec<String>,
40 pub required_snapshots: Vec<Pin>,
41 pub missing_snapshots: Vec<MissingSnapshotPayload>,
42 pub cache_key: String,
43 pub restore_key: String,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
48pub struct PlanReleasePayload {
49 pub tag: String,
50 pub commit: String,
51 pub pins: BTreeMap<String, String>,
52 pub erlang_support: String,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
57pub struct PlanOtpLinePayload {
58 pub line: String,
59 pub tags: Vec<String>,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
63#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
64pub struct MissingSnapshotPayload {
65 pub project: String,
66 pub tag: String,
67 pub reason: SnapshotGap,
68 pub detail: Option<String>,
69}
70
71fn wire<E: Display>(field: &'static str) -> impl Fn(E) -> PlanError {
72 move |error| PlanError::Wire {
73 field,
74 detail: error.to_string(),
75 }
76}
77
78fn parse<T: FromStr>(field: &'static str, value: &str) -> Result<T, PlanError>
79where
80 T::Err: Display,
81{
82 value.parse().map_err(wire(field))
83}
84
85impl JigsawPlan {
86 #[must_use]
87 pub fn to_wire(&self, status: &StoreStatus) -> JigsawPlanPayload {
88 let missing_snapshots = match status {
89 StoreStatus::Stocked => Vec::new(),
90 StoreStatus::Unstocked(unstocked) => unstocked
91 .gaps()
92 .into_iter()
93 .map(|(pin, gap)| MissingSnapshotPayload {
94 project: pin.project.to_string(),
95 tag: pin.tag.to_string(),
96 reason: gap,
97 detail: unstocked.detail(&pin),
98 })
99 .collect(),
100 };
101 JigsawPlanPayload {
102 host: self.host().to_string(),
103 host_project: self.host_project().to_string(),
104 otp_project: self.otp_project().to_string(),
105 series: self.series().to_string(),
106 tag_source: self.tag_source().to_string(),
107 extractor_version: self.extractor_version().to_string(),
108 host_facts_digest: self.host_facts().to_string(),
109 releases: self
110 .releases()
111 .iter()
112 .map(|release| PlanReleasePayload {
113 tag: release.tag.tag().to_string(),
114 commit: release.commit.to_string(),
115 pins: release
116 .pins
117 .iter()
118 .map(|(project, tag)| (project.to_string(), tag.to_string()))
119 .collect(),
120 erlang_support: release.erlang_support.to_string(),
121 })
122 .collect(),
123 otp_lines: self
124 .otp_lines()
125 .iter()
126 .map(|line| PlanOtpLinePayload {
127 line: line.line().to_string(),
128 tags: line
129 .tags()
130 .iter()
131 .map(|tag| tag.tag().to_string())
132 .collect(),
133 })
134 .collect(),
135 untracked_dependencies: self
136 .untracked_dependencies()
137 .iter()
138 .map(ToString::to_string)
139 .collect(),
140 excluded_otp_apps: self
141 .excluded_otp_apps()
142 .iter()
143 .map(ToString::to_string)
144 .collect(),
145 required_snapshots: self.required().into_iter().collect(),
146 missing_snapshots,
147 cache_key: self.cache_key().to_string(),
148 restore_key: self.cache_key().restore_prefix().to_owned(),
149 }
150 }
151
152 pub fn from_wire(payload: &JigsawPlanPayload, config: &Config) -> Result<Self, PlanError> {
155 let host_project_name: ProjectName = parse("host_project", &payload.host_project)?;
156 let otp_project_name: ProjectName = parse("otp_project", &payload.otp_project)?;
157 let host_project = config.project(&host_project_name)?;
158 let otp_project = config.project(&otp_project_name)?;
159 let releases = payload
160 .releases
161 .iter()
162 .map(|release| {
163 let tag: TagName = parse("releases.tag", &release.tag)?;
164 let pins = release
165 .pins
166 .iter()
167 .map(|(project, tag)| {
168 Ok((
169 parse::<ProjectName>("releases.pins", project)?,
170 parse::<TagName>("releases.pins", tag)?,
171 ))
172 })
173 .collect::<Result<_, PlanError>>()?;
174 Ok(HostRelease {
175 tag: HostTag::parse(host_project, tag).map_err(wire("releases.tag"))?,
176 commit: parse::<CommitSha>("releases.commit", &release.commit)?,
177 pins,
178 erlang_support: parse::<ErlangVersionRange>(
179 "releases.erlang_support",
180 &release.erlang_support,
181 )?,
182 })
183 })
184 .collect::<Result<Vec<_>, PlanError>>()?;
185 let releases = SeriesReleases::new(releases)?;
186 let series: HostSeries = parse("series", &payload.series)?;
187 if releases.series() != series {
188 return Err(PlanError::Wire {
189 field: "series",
190 detail: format!("releases are of series {}, not {series}", releases.series()),
191 });
192 }
193 let otp_lines = payload
194 .otp_lines
195 .iter()
196 .map(|line| {
197 let tags = line
198 .tags
199 .iter()
200 .map(|tag| {
201 OtpTag::parse(otp_project, parse("otp_lines.tags", tag)?)
202 .map_err(wire("otp_lines.tags"))
203 })
204 .collect::<Result<Vec<_>, PlanError>>()?;
205 Ok(OtpLine::new(
206 parse::<OtpLineVersion>("otp_lines.line", &line.line)?,
207 tags,
208 )?)
209 })
210 .collect::<Result<Vec<_>, PlanError>>()?;
211 let otp_lines = NonEmpty::try_from(otp_lines).map_err(wire("otp_lines"))?;
212 JigsawPlan::assemble(PlanParts {
213 host: parse::<HostName>("host", &payload.host)?,
214 host_project: host_project_name,
215 otp_project: otp_project_name,
216 releases,
217 otp_lines,
218 untracked_dependencies: parse_set::<DependencyName>(
219 "untracked_dependencies",
220 &payload.untracked_dependencies,
221 )?,
222 excluded_otp_apps: parse_set::<ApplicationName>(
223 "excluded_otp_apps",
224 &payload.excluded_otp_apps,
225 )?,
226 host_facts: parse::<HostFactsDigest>("host_facts_digest", &payload.host_facts_digest)?,
227 tag_source: parse::<TagSourceKind>("tag_source", &payload.tag_source)?,
228 extractor_version: parse::<ExtractorVersion>(
229 "extractor_version",
230 &payload.extractor_version,
231 )?,
232 })
233 }
234}
235
236fn parse_set<T: FromStr + Ord>(
237 field: &'static str,
238 values: &[String],
239) -> Result<BTreeSet<T>, PlanError>
240where
241 T::Err: Display,
242{
243 values.iter().map(|value| parse(field, value)).collect()
244}