1use serde::Serialize;
7use serde_json::{Map, Value};
8
9use super::package::PackageIssue;
10use super::records::is_record_kind;
11
12pub const RECORD_MIGRATIONS_MAX: usize = 64;
13pub const RECORD_MIGRATION_OPS_MAX: usize = 32;
14pub const MIGRATION_OP_NAMES: &[&str] = &["rename", "default", "drop", "map", "renameKind"];
15
16type Version = [u64; 3];
17
18fn issue(path: impl Into<String>, message: impl Into<String>) -> PackageIssue {
19 PackageIssue { path: path.into(), message: message.into() }
20}
21
22fn num_part(s: &str) -> Option<u64> {
23 if s == "0" || (!s.is_empty() && !s.starts_with('0') && s.bytes().all(|b| b.is_ascii_digit())) {
24 s.parse().ok()
25 } else {
26 None
27 }
28}
29
30fn version_of(v: &str) -> Option<Version> {
32 let parts: Vec<&str> = v.trim().split('.').collect();
33 if parts.len() != 3 {
34 return None;
35 }
36 Some([num_part(parts[0])?, num_part(parts[1])?, num_part(parts[2])?])
37}
38
39fn numeric_version(v: &str) -> Option<Version> {
41 let parts: Vec<&str> = v.split('.').collect();
42 if parts.is_empty() || parts.len() > 3 {
43 return None;
44 }
45 let mut out = [0u64; 3];
46 for (i, p) in parts.iter().enumerate() {
47 out[i] = num_part(p)?;
48 }
49 Some(out)
50}
51
52fn comparator_holds(token: &str, v: &Version) -> Option<bool> {
54 let (op, rest) = [">=", "<=", ">", "<", "=", "^", "~"].iter().find_map(|op| token.strip_prefix(op).map(|r| (*op, r))).unwrap_or(("", token));
55 if rest.is_empty() {
56 return None;
57 }
58 if op.is_empty() || op == "=" {
59 let parts: Vec<&str> = rest.split('.').collect();
60 if parts.len() > 3 {
61 return None;
62 }
63 let mut holds = true;
64 for (i, p) in parts.iter().enumerate() {
65 if *p == "x" || *p == "*" {
66 continue;
67 }
68 holds &= num_part(p)? == v[i];
69 }
70 return Some(holds);
71 }
72 let want = numeric_version(rest)?;
73 let c = v.cmp(&want);
74 Some(match op {
75 ">=" => c.is_ge(),
76 ">" => c.is_gt(),
77 "<=" => c.is_le(),
78 "<" => c.is_lt(),
79 "~" => v[0] == want[0] && v[1] == want[1] && c.is_ge(),
80 _ => {
81 if want[0] > 0 {
82 v[0] == want[0] && c.is_ge()
83 } else if want[1] > 0 {
84 v[0] == 0 && v[1] == want[1] && c.is_ge()
85 } else {
86 c.is_eq()
87 }
88 }
89 })
90}
91
92fn range_parts(range: &str) -> Option<Vec<Vec<&str>>> {
94 if range.trim().is_empty() {
95 return None;
96 }
97 let alts: Vec<Vec<&str>> = range.split("||").map(|a| a.split_whitespace().collect()).collect();
98 if alts.iter().any(Vec::is_empty) {
99 return None;
100 }
101 for a in &alts {
102 for t in a {
103 comparator_holds(t, &[0, 0, 0])?;
104 }
105 }
106 Some(alts)
107}
108
109pub fn is_version_range(range: &str) -> bool {
111 range_parts(range).is_some()
112}
113
114pub fn range_holds(version: &str, range: &str) -> bool {
116 let (Some(v), Some(alts)) = (version_of(version), range_parts(range)) else { return false };
117 alts.iter().any(|a| a.iter().all(|t| comparator_holds(t, &v) == Some(true)))
118}
119
120pub fn package_migrations(pkg: &Value) -> Vec<&Map<String, Value>> {
122 pkg.get("migrations")
123 .and_then(Value::as_array)
124 .into_iter()
125 .flatten()
126 .filter_map(Value::as_object)
127 .filter(|m| m.get("from").is_some_and(Value::is_string) && m.get("kind").is_some_and(Value::is_string) && m.get("ops").is_some_and(Value::is_array))
128 .collect()
129}
130
131fn op_name(op: &Value) -> Option<&'static str> {
132 let o = op.as_object()?;
133 if o.len() != 1 {
134 return None;
135 }
136 let key = o.keys().next()?;
137 MIGRATION_OP_NAMES.iter().copied().find(|n| n == key)
138}
139
140#[derive(Clone, Debug, PartialEq, Serialize)]
142#[serde(untagged)]
143pub enum MigrateResult {
144 Done { kind: String, data: Value, applied: usize },
145 Failed { error: String },
146}
147
148fn s<'a>(v: &'a Value, key: &str) -> &'a str {
149 v.get(key).and_then(Value::as_str).unwrap_or("")
150}
151
152pub fn migrate_record(migrations: &[&Map<String, Value>], version: &str, kind: &str, data: &Value, now: f64) -> MigrateResult {
154 let mut k = kind.to_string();
155 let mut d: Map<String, Value> = data.as_object().cloned().unwrap_or_default();
156 let mut applied = 0;
157 for (i, m) in migrations.iter().enumerate() {
158 if m["kind"].as_str() != Some(k.as_str()) || !range_holds(version, m["from"].as_str().unwrap_or("")) {
159 continue;
160 }
161 applied += 1;
162 for (j, op) in m["ops"].as_array().into_iter().flatten().enumerate() {
163 let at = format!("migration {i} op {j}");
164 match op_name(op) {
165 Some("rename") => {
166 let r = &op["rename"];
167 let (from, to) = (s(r, "from"), s(r, "to"));
168 if let Some(v) = d.remove(from) {
169 d.insert(to.to_string(), v);
170 }
171 }
172 Some("default") => {
173 let r = &op["default"];
174 let f = s(r, "field");
175 if d.get(f).is_none_or(Value::is_null) {
176 d.insert(f.to_string(), r.get("value").cloned().unwrap_or(Value::Null));
177 }
178 }
179 Some("drop") => {
180 d.remove(s(&op["drop"], "field"));
181 }
182 Some("map") => {
183 let r = &op["map"];
184 let ast = match crate::cel::parse_expression(s(r, "expr")) {
185 Ok(ast) => ast,
186 Err(e) => return MigrateResult::Failed { error: format!("{at}: {e}") },
187 };
188 let mut vars = d.clone();
189 for name in crate::cel::expression_names(&ast) {
190 vars.entry(name).or_insert(Value::Null);
191 }
192 let env = crate::cel::ExpressionEnv { now: Some(now), offset: None, max_steps: None };
193 match crate::cel::evaluate_node(&ast, &vars, &env) {
194 Ok(v) => {
195 d.insert(s(r, "field").to_string(), v);
196 }
197 Err(e) => return MigrateResult::Failed { error: format!("{at}: {e}") },
198 }
199 }
200 Some("renameKind") => k = op["renameKind"].as_str().unwrap_or("").to_string(),
201 _ => return MigrateResult::Failed { error: format!("{at}: unknown op") },
202 }
203 }
204 }
205 MigrateResult::Done { kind: k, data: Value::Object(d), applied }
206}
207
208fn sorted_keys(o: &Map<String, Value>) -> Vec<&String> {
209 let mut v: Vec<&String> = o.keys().collect();
210 v.sort_by(|a, b| a.encode_utf16().cmp(b.encode_utf16()));
211 v
212}
213
214pub fn validate_migrations(pkg: &Value) -> Vec<PackageIssue> {
216 let Some(list) = pkg.get("migrations") else { return Vec::new() };
217 let Some(list) = list.as_array() else { return vec![issue("/migrations", "migrations is a list of {from, kind, ops}")] };
218 let mut issues = Vec::new();
219 if list.len() > RECORD_MIGRATIONS_MAX {
220 issues.push(issue("/migrations", format!("at most {RECORD_MIGRATIONS_MAX} migrations")));
221 }
222 let empty = Map::new();
223 let records = pkg.get("records").and_then(Value::as_object).unwrap_or(&empty);
224 let field = |v: Option<&Value>| v.and_then(Value::as_str).is_some_and(|s| !s.is_empty());
225 for (i, m) in list.iter().enumerate() {
226 let at = format!("/migrations/{i}");
227 let Some(m) = m.as_object() else {
228 issues.push(issue(at, "a migration is {from, kind, ops}"));
229 continue;
230 };
231 for key in sorted_keys(m) {
232 if !["from", "kind", "ops"].contains(&key.as_str()) {
233 issues.push(issue(format!("{at}/{key}"), format!("unknown key {key}")));
234 }
235 }
236 if !m.get("from").and_then(Value::as_str).is_some_and(is_version_range) {
237 issues.push(issue(format!("{at}/from"), "from is a version or a range (1.2.3, 1.x, ^1.2.0, >=1.0.0 <2.0.0)"));
238 }
239 let mut kind: Option<String> = m.get("kind").and_then(Value::as_str).filter(|k| is_record_kind(k)).map(str::to_string);
240 if kind.is_none() {
241 issues.push(issue(format!("{at}/kind"), "kind is a kind name"));
242 }
243 let Some(ops) = m.get("ops").and_then(Value::as_array) else {
244 issues.push(issue(format!("{at}/ops"), "ops is a list"));
245 continue;
246 };
247 if ops.len() > RECORD_MIGRATION_OPS_MAX {
248 issues.push(issue(format!("{at}/ops"), format!("at most {RECORD_MIGRATION_OPS_MAX} ops")));
249 }
250 for (j, op) in ops.iter().enumerate() {
251 let op_at = format!("{at}/ops/{j}");
252 let Some(name) = op_name(op) else {
253 issues.push(issue(op_at, format!("an op is one of {}", MIGRATION_OP_NAMES.join(", "))));
254 continue;
255 };
256 let arg = &op[name];
257 let obj = arg.as_object();
258 let len = obj.map_or(0, Map::len);
259 let bad = |shape: &str| issue(format!("{op_at}/{name}"), format!("{name} is {shape}"));
260 match name {
261 "rename" if !(obj.is_some() && field(arg.get("from")) && field(arg.get("to")) && len == 2) => issues.push(bad("{from, to}")),
262 "default" if !(obj.is_some() && field(arg.get("field")) && arg.get("value").is_some() && len == 2) => issues.push(bad("{field, value}")),
263 "drop" if !(obj.is_some() && field(arg.get("field")) && len == 1) => issues.push(bad("{field}")),
264 "map" => {
265 if !(obj.is_some() && field(arg.get("field")) && arg.get("expr").is_some_and(Value::is_string) && len == 2) {
266 issues.push(bad("{field, expr}"));
267 } else if let Err(e) = crate::cel::parse_expression(s(arg, "expr")) {
268 issues.push(issue(format!("{op_at}/map/expr"), e));
269 }
270 }
271 "renameKind" => match arg.as_str().filter(|k| is_record_kind(k)) {
272 None => issues.push(bad("a kind name")),
273 Some(k) => kind = Some(k.to_string()),
274 },
275 _ => {}
276 }
277 }
278 if let Some(k) = kind {
279 if !records.contains_key(&k) {
280 issues.push(issue(format!("{at}/kind"), format!("the migration ends in {k}, which the package does not declare")));
281 }
282 }
283 }
284 issues
285}
286
287#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
289pub struct KindChange {
290 pub change: &'static str,
291 pub field: Option<String>,
292}
293
294fn props_of(kind: Option<&Value>) -> Map<String, Value> {
295 kind.and_then(|k| k.get("schema")).and_then(|s| s.get("properties")).and_then(Value::as_object).cloned().unwrap_or_default()
296}
297fn required_of(kind: Option<&Value>) -> Vec<String> {
298 kind.and_then(|k| k.get("schema")).and_then(|s| s.get("required")).and_then(Value::as_array).map(|l| l.iter().filter_map(Value::as_str).map(str::to_string).collect()).unwrap_or_default()
299}
300fn js_text(v: Option<&Value>) -> String {
301 match v {
302 None => "undefined".into(),
303 Some(Value::String(s)) => s.clone(),
304 Some(Value::Null) => "null".into(),
305 Some(Value::Bool(b)) => b.to_string(),
306 Some(Value::Number(n)) => n.to_string(),
307 Some(Value::Array(_)) => "[array]".into(),
308 Some(Value::Object(_)) => "[object Object]".into(),
309 }
310}
311fn type_of(f: &Value) -> String {
312 let t = f.get("type");
313 let items = if t.and_then(Value::as_str) == Some("array") { f.get("items").filter(|i| i.is_object()).map(|i| format!("<{}>", js_text(i.get("type")))) } else { None };
314 format!("{}{}", js_text(t), items.unwrap_or_default())
315}
316fn enum_of(f: &Value) -> Option<Vec<Value>> {
317 let e = if f.get("type").and_then(Value::as_str) == Some("array") && f.get("items").is_some_and(Value::is_object) { f["items"].get("enum") } else { f.get("enum") };
318 e.and_then(Value::as_array).cloned()
319}
320
321pub fn kind_changes(before: &Value, after: &Value) -> Vec<(String, Vec<KindChange>)> {
323 let empty = Map::new();
324 let b = before.get("records").and_then(Value::as_object).unwrap_or(&empty);
325 let a = after.get("records").and_then(Value::as_object).unwrap_or(&empty);
326 let mut out = Vec::new();
327 for kind in sorted_keys(b) {
328 let mut changes = Vec::new();
329 if !a.contains_key(kind) {
330 changes.push(KindChange { change: "removed", field: None });
331 } else {
332 let bp = props_of(b.get(kind));
333 let ap = props_of(a.get(kind));
334 let breq = required_of(b.get(kind));
335 for f in sorted_keys(&bp) {
336 let push = |c: &'static str| KindChange { change: c, field: Some(f.clone()) };
337 match ap.get(f) {
338 None => changes.push(push("drops")),
339 Some(af) if type_of(&bp[f]) != type_of(af) => changes.push(push("retypes")),
340 Some(af) => {
341 if let Some(ae) = enum_of(af) {
342 let narrows = match enum_of(&bp[f]) {
343 None => true,
344 Some(be) => be.iter().any(|v| !ae.contains(v)),
345 };
346 if narrows {
347 changes.push(push("narrows"));
348 }
349 }
350 }
351 }
352 }
353 let mut areq = required_of(a.get(kind));
354 areq.sort_by(|x, y| x.encode_utf16().cmp(y.encode_utf16()));
355 for f in areq {
356 if !breq.contains(&f) && ap.contains_key(&f) && !changes.iter().any(|c| c.field.as_deref() == Some(f.as_str())) {
357 changes.push(KindChange { change: "requires", field: Some(f) });
358 }
359 }
360 }
361 if !changes.is_empty() {
362 out.push((kind.clone(), changes));
363 }
364 }
365 out
366}
367
368pub fn kind_changes_json(before: &Value, after: &Value) -> Value {
370 Value::Object(kind_changes(before, after).into_iter().map(|(k, c)| (k, serde_json::to_value(c).unwrap_or(Value::Null))).collect())
371}
372
373fn covered(migrations: &[&Map<String, Value>], version: &str, kind: &str, c: &KindChange) -> bool {
374 let f = c.field.as_deref();
375 for m in migrations {
376 if m["kind"].as_str() != Some(kind) || !range_holds(version, m["from"].as_str().unwrap_or("")) {
377 continue;
378 }
379 for op in m["ops"].as_array().into_iter().flatten() {
380 let name = op_name(op);
381 let arg = |n: &str, k: &str| op.get(n).and_then(|a| a.get(k)).and_then(Value::as_str);
382 let hit = match c.change {
383 "removed" => name == Some("renameKind"),
384 "drops" => (name == Some("drop") && arg("drop", "field") == f) || (name == Some("rename") && arg("rename", "from") == f),
385 "retypes" | "narrows" => name == Some("map") && arg("map", "field") == f,
386 _ => (name == Some("default") && arg("default", "field") == f) || (name == Some("map") && arg("map", "field") == f) || (name == Some("rename") && arg("rename", "to") == f),
387 };
388 if hit {
389 return true;
390 }
391 }
392 }
393 false
394}
395
396fn change_text(c: &str) -> &'static str {
397 match c {
398 "removed" => "is removed",
399 "drops" => "drops",
400 "retypes" => "changes the type of",
401 "narrows" => "narrows the values of",
402 _ => "requires",
403 }
404}
405
406pub fn migration_refusal(before: &Value, after: &Value) -> Option<String> {
408 let bvs = before.get("version").and_then(Value::as_str)?;
409 let avs = after.get("version").and_then(Value::as_str)?;
410 let (bv, av) = (version_of(bvs)?, version_of(avs)?);
411 if av[0] <= bv[0] {
412 return None;
413 }
414 let migrations = package_migrations(after);
415 for (kind, changes) in kind_changes(before, after) {
416 for c in &changes {
417 if covered(&migrations, bvs, &kind, c) {
418 continue;
419 }
420 let what = match &c.field {
421 None => format!("{kind} {}", change_text(c.change)),
422 Some(f) => format!("{kind} {} {f}", change_text(c.change)),
423 };
424 return Some(format!("{what} in {avs}: add a migration from {bvs} that covers it"));
425 }
426 }
427 None
428}