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