1use callisto_model::{Version, VersionGrammar};
2use indexmap::IndexMap;
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5
6#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
7#[serde(rename_all = "camelCase")]
8pub struct PreState {
9 pub mode: PreMode,
10 pub tag: String,
11 #[schemars(with = "std::collections::BTreeMap<String, Version>")]
12 pub initial_versions: IndexMap<String, Version>,
13 pub changesets: Vec<String>,
14}
15
16impl PreState {
17 pub fn entering(
18 tag: impl Into<String>,
19 initial_versions: impl IntoIterator<Item = (String, Version)>,
20 ) -> Self {
21 let mut map = IndexMap::new();
22 for (pkg, ver) in initial_versions {
23 map.entry(pkg).or_insert(ver);
24 }
25 PreState {
26 mode: PreMode::Pre,
27 tag: tag.into(),
28 initial_versions: map,
29 changesets: Vec::new(),
30 }
31 }
32
33 pub fn exiting(mut self) -> Self {
34 self.mode = PreMode::Exit;
35 self
36 }
37}
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
40#[serde(rename_all = "lowercase")]
41pub enum PreMode {
42 Pre,
43 Exit,
44}
45
46pub fn parse_pre_json(input: &str) -> Result<PreState, PreJsonError> {
47 let clean_input = input.strip_prefix('\u{FEFF}').unwrap_or(input);
48 let val: serde_json::Value =
49 serde_json::from_str(clean_input).map_err(|e| PreJsonError::Malformed {
50 message: e.to_string(),
51 })?;
52
53 let obj = val.as_object().ok_or_else(|| PreJsonError::Malformed {
54 message: "expected a JSON object".to_string(),
55 })?;
56
57 let mode_val = obj
58 .get("mode")
59 .ok_or(PreJsonError::MissingField { field: "mode" })?;
60 let mode_str = mode_val
61 .as_str()
62 .ok_or(PreJsonError::WrongFieldType { field: "mode" })?;
63 let mode = match mode_str {
64 "pre" => PreMode::Pre,
65 "exit" => PreMode::Exit,
66 _ => {
67 return Err(PreJsonError::InvalidMode {
68 found: mode_str.to_string(),
69 })
70 }
71 };
72
73 let tag_val = obj
74 .get("tag")
75 .ok_or(PreJsonError::MissingField { field: "tag" })?;
76 let tag = tag_val
77 .as_str()
78 .ok_or(PreJsonError::WrongFieldType { field: "tag" })?
79 .to_string();
80
81 let init_val = obj
82 .get("initialVersions")
83 .ok_or(PreJsonError::MissingField {
84 field: "initialVersions",
85 })?;
86 let init_obj = init_val.as_object().ok_or(PreJsonError::WrongFieldType {
87 field: "initialVersions",
88 })?;
89
90 let mut initial_versions = IndexMap::new();
91 for (pkg, v_val) in init_obj {
92 let v_str = v_val.as_str().ok_or(PreJsonError::WrongFieldType {
93 field: "initialVersions",
94 })?;
95 let ver = Version::parse(v_str, VersionGrammar::SemVer).map_err(|source| {
96 PreJsonError::InvalidInitialVersion {
97 package: pkg.clone(),
98 raw: v_str.to_string(),
99 source,
100 }
101 })?;
102 initial_versions.insert(pkg.clone(), ver);
103 }
104
105 let cs_val = obj.get("changesets").ok_or(PreJsonError::MissingField {
106 field: "changesets",
107 })?;
108 let cs_arr = cs_val.as_array().ok_or(PreJsonError::WrongFieldType {
109 field: "changesets",
110 })?;
111
112 let mut changesets = Vec::new();
113 for (index, c_val) in cs_arr.iter().enumerate() {
114 let c_str = c_val
115 .as_str()
116 .ok_or(PreJsonError::InvalidChangesetId { index })?;
117 changesets.push(c_str.to_string());
118 }
119
120 Ok(PreState {
121 mode,
122 tag,
123 initial_versions,
124 changesets,
125 })
126}
127
128pub fn write_pre_json(state: &PreState) -> String {
129 let mut map = IndexMap::new();
130 map.insert(
131 "mode".to_string(),
132 serde_json::to_value(state.mode).unwrap(),
133 );
134 map.insert("tag".to_string(), serde_json::to_value(&state.tag).unwrap());
135
136 let mut init_map = IndexMap::new();
137 for (pkg, ver) in &state.initial_versions {
138 init_map.insert(pkg.clone(), serde_json::to_value(ver).unwrap());
139 }
140 map.insert(
141 "initialVersions".to_string(),
142 serde_json::to_value(init_map).unwrap(),
143 );
144 map.insert(
145 "changesets".to_string(),
146 serde_json::to_value(&state.changesets).unwrap(),
147 );
148
149 let mut out = serde_json::to_string_pretty(&map).unwrap();
150 out.push('\n');
151 out
152}
153
154#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
155#[non_exhaustive]
156pub enum PreJsonError {
157 #[error("pre.json is not a valid JSON object: {message}")]
158 Malformed { message: String },
159
160 #[error("pre.json is missing required field {field:?}")]
161 MissingField { field: &'static str },
162
163 #[error("pre.json field {field:?} has the wrong type")]
164 WrongFieldType { field: &'static str },
165
166 #[error("pre.json has mode {found:?}, expected \"pre\" or \"exit\"")]
167 InvalidMode { found: String },
168
169 #[error("pre.json initialVersions[{package:?}] = {raw:?} is not a valid version: {source}")]
170 InvalidInitialVersion {
171 package: String,
172 raw: String,
173 #[source]
174 source: callisto_model::VersionParseError,
175 },
176
177 #[error("pre.json changesets[{index}] is not a string")]
178 InvalidChangesetId { index: usize },
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn pre_json_round_trips() {
187 let json = r#"{
188 "mode": "pre",
189 "tag": "next",
190 "initialVersions": {
191 "foo": "1.0.0"
192 },
193 "changesets": [
194 "cool-dragons-fly"
195 ]
196}
197"#;
198 let state = parse_pre_json(json).unwrap();
199 assert_eq!(state.tag, "next");
200 let written = write_pre_json(&state);
201 assert_eq!(written, json);
202 }
203}