Skip to content

Commit 28ca4db

Browse files
committed
Reimplement upgrade-quality as a json walk
1 parent c98f5e9 commit 28ca4db

4 files changed

Lines changed: 263 additions & 70 deletions

File tree

src/blueprint/upgrade_quality.rs

Lines changed: 137 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use serde_json::json;
1+
use crate::json_walk::{WalkAction, walk_json};
22

33
const QUALITIES: [&str; 6] = [
44
"normal",
@@ -9,82 +9,106 @@ const QUALITIES: [&str; 6] = [
99
"legendary",
1010
];
1111

12-
fn upgrade_quality(thing: &mut serde_json::Value) {
13-
if let Some(quality) = thing.get_mut("quality") {
14-
let s = quality.as_str().unwrap();
15-
let new_quality = QUALITIES
16-
.windows(2)
17-
.find(|pair| pair[0] == s)
18-
.unwrap_or_else(|| panic!("can't find quality {s}"))[1];
19-
*quality = json!(new_quality);
20-
}
12+
fn upgrade_quality(quality: &mut String) {
13+
let new_quality = QUALITIES
14+
.windows(2)
15+
.find(|pair| pair[0] == quality)
16+
.unwrap_or_else(|| panic!("can't find quality {quality}"))[1];
17+
*quality = new_quality.to_owned();
2118
}
2219

23-
fn upgrade_circuit_condition(circuit_condition: &mut serde_json::Value) {
24-
if let Some(first_signal) = circuit_condition.get_mut("first_signal") {
25-
upgrade_quality(first_signal);
26-
}
27-
if let Some(second_signal) = circuit_condition.get_mut("second_signal") {
28-
upgrade_quality(second_signal);
29-
}
30-
}
20+
#[rustfmt::skip]
21+
const UPGRADE_PATHS: &[&[&str]] = &[
22+
&["blueprint", "entities", "[]", "filter", "quality"],
23+
&["blueprint", "entities", "[]", "filters", "[]", "quality"],
24+
&["blueprint", "entities", "[]", "recipe_quality"],
25+
&["blueprint", "entities", "[]", "control_behavior", "circuit_condition", "first_signal", "quality"],
26+
&["blueprint", "entities", "[]", "control_behavior", "circuit_condition", "second_signal", "quality"],
27+
&["blueprint", "entities", "[]", "control_behavior", "logistic_condition", "first_signal", "quality"],
28+
&["blueprint", "entities", "[]", "control_behavior", "logistic_condition", "second_signal", "quality"],
29+
// requester chest with sections
30+
&["blueprint", "entities", "[]", "request_filters", "sections", "[]", "filters", "[]", "quality"],
31+
// constant combinator with sections
32+
&["blueprint", "entities", "[]", "control_behavior", "sections", "sections", "[]", "filters", "[]", "quality"],
33+
// decider combinator
34+
&["blueprint", "entities", "[]", "control_behavior", "decider_conditions", "conditions", "[]", "first_signal", "quality"],
35+
&["blueprint", "entities", "[]", "control_behavior", "decider_conditions", "conditions", "[]", "second_signal", "quality"],
36+
&["blueprint", "entities", "[]", "control_behavior", "decider_conditions", "outputs", "[]", "signal", "quality"],
37+
// arithmetic combinator
38+
&["blueprint", "entities", "[]", "control_behavior", "arithmetic_conditions", "first_signal", "quality"],
39+
&["blueprint", "entities", "[]", "control_behavior", "arithmetic_conditions", "second_signal", "quality"],
40+
&["blueprint", "entities", "[]", "control_behavior", "arithmetic_conditions", "output_signal", "quality"],
41+
42+
// selector combinator index signal
43+
&["blueprint", "entities", "[]", "control_behavior", "index_signal", "quality"],
44+
// selector combinator count signal
45+
&["blueprint", "entities", "[]", "control_behavior", "count_signal", "quality"],
46+
// selector combinator quality transfer static quality
47+
&["blueprint", "entities", "[]", "control_behavior", "quality_source_static", "name"],
48+
// selector combinator quality filter
49+
&["blueprint", "entities", "[]", "control_behavior", "quality_filter", "quality"],
50+
// selector combinator quality filter destination signal
51+
&["blueprint", "entities", "[]", "control_behavior", "quality_destination_signal", "quality"],
52+
53+
54+
];
55+
56+
#[rustfmt::skip]
57+
const IGNORE_PATHS: &[&[&str]] = &[
58+
// static quality is under quality_source_static.name, listed above
59+
&["blueprint", "entities", "[]", "control_behavior", "quality_source_static"],
60+
// quality filter quality is under quality_filter.quality
61+
&["blueprint", "entities", "[]", "control_behavior", "quality_filter"],
62+
// quality transfer destination signal (quality under quality_destination_signal.quality)
63+
&["blueprint", "entities", "[]", "control_behavior", "quality_destination_signal"],
64+
// quality transfer source signal (never has a quality because it picks the quality from the biggest)
65+
&["blueprint", "entities", "[]", "control_behavior", "quality_source_signal"],
66+
];
67+
68+
#[rustfmt::skip]
69+
const NO_UPGRADE_PATHS: &[&[&str]] = &[
70+
// Don't upgrade modules
71+
&["blueprint", "entities", "[]", "items", "[]", "id", "quality"],
72+
// Don't upgrade entities
73+
&["blueprint", "entities", "[]", "quality"],
74+
// Usually icons match entities not recipes, although there's really no way to know
75+
&["blueprint", "icons", "[]", "signal", "quality"],
76+
// Just a boolean picking between quality_source_signal and quality_source_static
77+
&["blueprint", "entities", "[]", "control_behavior", "select_quality_from_signal"],
78+
];
3179

3280
pub(crate) fn upgrade(mut json: serde_json::Value) -> serde_json::Value {
33-
let bp = json
34-
.get_mut("blueprint")
35-
.expect("blueprint books not supported yet");
36-
let entities = bp.get_mut("entities").unwrap().as_array_mut().unwrap();
37-
for entity in entities {
38-
if let Some(control_behavior) = entity.get_mut("control_behavior") {
39-
if let Some(circuit_condition) = control_behavior.get_mut("circuit_condition") {
40-
upgrade_circuit_condition(circuit_condition);
41-
}
42-
if let Some(logistic_condition) = control_behavior.get_mut("logistic_condition") {
43-
upgrade_circuit_condition(logistic_condition);
44-
}
45-
if let Some(serde_json::Value::Object(sections)) = control_behavior.get_mut("sections")
46-
&& let Some(serde_json::Value::Array(sections)) = sections.get_mut("sections")
81+
walk_json(&mut json, &mut |path, value| {
82+
if UPGRADE_PATHS.contains(&path) {
83+
let serde_json::Value::String(s) = value else {
84+
panic!("can't upgrade quality at {path:?}, expected string, got {value}");
85+
};
86+
upgrade_quality(s);
87+
WalkAction::Enter
88+
} else if IGNORE_PATHS.contains(&path) {
89+
WalkAction::Enter
90+
} else if NO_UPGRADE_PATHS.contains(&path) {
91+
WalkAction::Break
92+
} else {
93+
if let Some(last) = path.last()
94+
&& last.contains("quality")
4795
{
48-
for section in sections {
49-
if let Some(serde_json::Value::Array(filters)) = section.get_mut("filters") {
50-
for filter in filters {
51-
upgrade_quality(filter);
52-
}
53-
}
54-
}
55-
}
56-
}
57-
if let Some(filter) = entity.get_mut("filter") {
58-
upgrade_quality(filter);
59-
}
60-
if let Some(filters) = entity.get_mut("filters") {
61-
for filter in filters.as_array_mut().unwrap() {
62-
upgrade_quality(filter);
63-
}
64-
}
65-
if let Some(quality) = entity.get_mut("recipe_quality") {
66-
let s = quality.as_str().unwrap();
67-
let new_quality = QUALITIES.windows(2).find(|pair| pair[0] == s).unwrap()[1];
68-
*quality = json!(new_quality);
69-
}
70-
if let Some(request_filters) = entity.get_mut("request_filters") {
71-
if let Some(sections) = request_filters.get_mut("sections") {
72-
for section in sections.as_array_mut().unwrap() {
73-
if let Some(filters) = section.get_mut("filters") {
74-
for filter in filters.as_array_mut().unwrap() {
75-
upgrade_quality(filter);
76-
}
77-
}
78-
}
96+
panic!(
97+
"Unhandled quality in blueprint; not sure whether we should upgrade:\n &{path:?},"
98+
);
99+
} else {
100+
WalkAction::Enter
79101
}
80102
}
81-
}
103+
});
104+
// upgrade_old(json)
82105
json
83106
}
84107

85108
#[cfg(test)]
86109
mod tests {
87110
use super::*;
111+
use serde_json::json;
88112
use std::str::FromStr;
89113

90114
macro_rules! test_bp {
@@ -774,9 +798,7 @@ mod tests {
774798
)
775799
}
776800

777-
// TODO: implement arithmetic combinators
778801
#[test]
779-
#[should_panic = "wrong signals"]
780802
fn test_arithmetic_combinator() {
781803
let bp = test_bp!(
782804
bp: "0eNqNkc1ugzAQhN9lrzVVwk8jeJWqQsZsm5XwT9d21Aj53bvAIYdKVXxjduebMV5hWjIGJpdgWIGMdxGG9xUifTm9bJrTFmEAzZSuFhOZyng7kdPJMxQF5Gb8geFcPhSgS5QID8T+cR9dthOyLKj/UQqCj+L2bksVYnM+KbjDUNX16bWTpJkYzbHQKpCqif0yTnjVNxKAuB7kUcbzTovb4JM4pvHPpYwPAbnyjBL/nfUihUXOTnpZyZHQiBvpaStrEcTmZaiPrvAiGz6nkJ9vgIEMFDnyUymhFenxUApuyHFnd2913/Z9d2m6pr3UpfwCsbKgAQ==",
@@ -850,8 +872,6 @@ mod tests {
850872
}
851873

852874
#[test]
853-
// TODO: implement for decider combinators
854-
#[should_panic = "wrong qualities"]
855875
fn test_decider_combinator() {
856876
let bp = test_bp!(
857877
bp: "0eNqdkutOwzAMhd/FvzPELmVqXwVNUdp6YKl1Qi4T1ZR3x2kngQSMjZ859Tn+juoztENC54kjNGegznKA5vkMgV7YDEVjMyI00GNHPfpVZ8eW2ETrISsg7vEdmnU+KECOFAkX//yYNKexRS8D6kqOAmeDWC2XfRK3XT8qmKBZbdb1QyVrevLYLQM7BQIZvR10i6/mRBIgrkuslm/9HBWK+vUlUEfyIepv1TrrnBBZj4Lylswg5CInFsZRdgpAwBJ1s9UbEXJW965cfHevQ0ddsQmvkwQdJ1emDfdwhYG85X+V/tF4qSx3YFN0Kf5yRn82cJP8w8RRH70dNbFEQXM0Q8BS5TaWJS0f8sxDEUcRPw9dwQl9mK+petrUu7qu9ttqu9tvcv4Afs8QFg==",
@@ -1061,8 +1081,55 @@ mod tests {
10611081
})
10621082
);
10631083
let upgraded = upgrade(bp);
1064-
// TODO implement and add assertions
1065-
drop(upgraded)
1084+
let select_combinator = jaq_one(
1085+
r#".blueprint.entities[] | select(.control_behavior.operation == "select")"#,
1086+
upgraded.clone(),
1087+
);
1088+
assert_eq!(
1089+
jaq_one(".control_behavior.index_signal.quality", select_combinator),
1090+
json!("legendary"),
1091+
);
1092+
let count_combinator = jaq_one(
1093+
r#".blueprint.entities[] | select(.control_behavior.operation == "count")"#,
1094+
upgraded.clone(),
1095+
);
1096+
assert_eq!(
1097+
jaq_one(".control_behavior.count_signal.quality", count_combinator),
1098+
json!("epic"),
1099+
);
1100+
let static_quality_transfer_combinator = jaq_one(
1101+
r#".blueprint.entities[] | select(.control_behavior.operation == "quality-transfer" and .control_behavior.quality_source_static)"#,
1102+
upgraded.clone(),
1103+
);
1104+
assert_eq!(
1105+
jaq_one(
1106+
".control_behavior.quality_source_static.name",
1107+
static_quality_transfer_combinator
1108+
),
1109+
json!("epic"),
1110+
);
1111+
let quality_from_signal_transfer_combinator = jaq_one(
1112+
r#".blueprint.entities[] | select(.control_behavior.operation == "quality-transfer" and .control_behavior.select_quality_from_signal == true)"#,
1113+
upgraded.clone(),
1114+
);
1115+
assert_eq!(
1116+
jaq_one(
1117+
".control_behavior.quality_destination_signal.quality",
1118+
quality_from_signal_transfer_combinator
1119+
),
1120+
json!("legendary"),
1121+
);
1122+
let quality_filter_combinator = jaq_one(
1123+
r#".blueprint.entities[] | select(.control_behavior.operation == "quality-filter")"#,
1124+
upgraded.clone(),
1125+
);
1126+
assert_eq!(
1127+
jaq_one(
1128+
".control_behavior.quality_filter.quality",
1129+
quality_filter_combinator
1130+
),
1131+
json!("epic"),
1132+
);
10661133
}
10671134

10681135
// #[test]

src/json_walk.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
use serde_json::Value;
2+
use string_stack::StringStack;
3+
4+
#[derive(Debug, PartialEq, Eq)]
5+
pub enum WalkAction {
6+
Enter,
7+
Break,
8+
}
9+
10+
mod string_stack;
11+
12+
pub fn walk_json(to_walk: &mut Value, cb: &mut impl FnMut(&[&str], &mut Value) -> WalkAction) {
13+
let mut storage = vec![];
14+
let path = StringStack::new(&mut storage);
15+
walk_json_inner(path, to_walk, cb);
16+
}
17+
18+
fn walk_json_inner<'a, 'parent>(
19+
mut path: StringStack<'a, 'parent>,
20+
to_walk: &mut Value,
21+
cb: &mut impl FnMut(&[&str], &mut Value) -> WalkAction,
22+
) {
23+
match (cb)(path.as_slice(), to_walk) {
24+
WalkAction::Enter => {}
25+
WalkAction::Break => return,
26+
}
27+
match to_walk {
28+
Value::Array(values) => {
29+
for value in values {
30+
let new_path = path.push("[]");
31+
walk_json_inner(new_path, value, cb)
32+
}
33+
}
34+
Value::Object(map) => {
35+
for (key, value) in map {
36+
let new_path = path.push(key);
37+
walk_json_inner(new_path, value, cb)
38+
}
39+
}
40+
Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {
41+
//nothing to walk
42+
}
43+
}
44+
}

src/json_walk/string_stack.rs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
use std::marker::PhantomData;
2+
3+
pub struct StringStack<'a, 'parent> {
4+
// unsafe field: must only access the first len elements, must not mutate the strings, strings only valid for 'a.
5+
// other strings can be pushed but their validity can only be proved locally.
6+
storage: &'parent mut Vec<*const str>,
7+
// unsafe field; indicates which elements are safe to read from for 'a
8+
len: usize,
9+
phantom: PhantomData<&'a str>,
10+
}
11+
12+
impl<'parent> StringStack<'static, 'parent> {
13+
pub fn new(storage: &'parent mut Vec<*const str>) -> Self {
14+
Self {
15+
// SAFETY: we don't need any of the elements to be valid because we start with len=0
16+
storage,
17+
// SAFETY: len=0 trivially valid because it is always safe to read the first 0 elements
18+
len: 0,
19+
phantom: Default::default(),
20+
}
21+
}
22+
}
23+
24+
impl<'a, 'parent> StringStack<'a, 'parent> {
25+
pub fn as_slice<'s>(&'s self) -> &'s [&'a str] {
26+
let slice: &'s [*const str] = &self.storage[..self.len];
27+
// SAFETY: it is safe to read the first len elements for 'a.
28+
// Using a lifetime of 's for the slice ensures we will not mutate the storage while this slice is alive.
29+
unsafe {
30+
std::slice::from_raw_parts::<'s, &'a str>(slice.as_ptr() as *const &'a str, slice.len())
31+
}
32+
}
33+
34+
#[must_use]
35+
pub fn push<'s, 'b>(&'s mut self, s: &'b str) -> StringStack<'b, 's>
36+
where
37+
'a: 'b,
38+
{
39+
assert!(self.len <= self.storage.len());
40+
// SAFETY: assert proves we're only shrinking here, so new len will be within capacity and all the elements will already be initialized.
41+
unsafe {
42+
self.storage.set_len(self.len);
43+
}
44+
// SAFETY: we're allowed to push to storage; this new string will be valid for lifetime 'b
45+
self.storage.push(s as *const str);
46+
47+
// demonstration: the existing strings from this stack are valid `&'b str`s:
48+
let _: &[&'b str] = self.as_slice();
49+
50+
// SAFETY: The new StringStack will allow access to:
51+
// - the existing strings from this stack for lifetime `'b` (see demonstration, above)
52+
// - plus the new one we just pushed, which is also valid for `'b`.
53+
// Because the new StringStack is reborrowing the storage from this one, we won't
54+
// accidentally overwrite the new element in storage with one that isn't valid for
55+
// `'b` until it's no longer relevant.
56+
StringStack {
57+
storage: self.storage,
58+
len: self.len + 1,
59+
phantom: Default::default(),
60+
}
61+
}
62+
}
63+
64+
#[cfg(test)]
65+
mod tests {
66+
use super::*;
67+
68+
#[test]
69+
fn test_string_stack() {
70+
let mut s = vec![];
71+
let mut stack = StringStack::new(&mut s);
72+
let mut stack = stack.push("a");
73+
{
74+
let b = "b".to_owned();
75+
let stack = stack.push(&b);
76+
assert_eq!(stack.as_slice(), ["a", "b"]);
77+
};
78+
assert_eq!(stack.as_slice(), ["a"]);
79+
let _: &'static str = stack.as_slice()[0];
80+
}
81+
}

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
mod blueprint;
2+
mod json_walk;
23

34
use std::{
45
fmt::Write as _,

0 commit comments

Comments
 (0)