Skip to content

Commit fd9ea49

Browse files
committed
make with_replacement patch easier to use and less error prone
1 parent 4758654 commit fd9ea49

2 files changed

Lines changed: 56 additions & 28 deletions

File tree

src/example/patches.rs

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -35,26 +35,26 @@ fn get_all_patches() -> PatchMap {
3535
(
3636
// The simple example with electricity priced using marginal costs
3737
"simple_marginal",
38-
vec![FilePatch::new("commodities.csv").with_replacement(
39-
"id,description,type,time_slice_level,pricing_strategy,units\n\
40-
GASPRD,Gas produced,sed,season,shadow,PJ\n\
41-
GASNAT,Natural gas,sed,season,shadow,PJ\n\
42-
ELCTRI,Electricity,sed,daynight,marginal,PJ\n\
43-
RSHEAT,Residential heating,svd,daynight,shadow,PJ\n\
44-
CO2EMT,CO2 emitted,oth,annual,unpriced,ktCO2\n",
45-
)],
38+
vec![FilePatch::new("commodities.csv").with_replacement(&[
39+
"id,description,type,time_slice_level,pricing_strategy,units",
40+
"GASPRD,Gas produced,sed,season,shadow,PJ",
41+
"GASNAT,Natural gas,sed,season,shadow,PJ",
42+
"ELCTRI,Electricity,sed,daynight,marginal,PJ",
43+
"RSHEAT,Residential heating,svd,daynight,shadow,PJ",
44+
"CO2EMT,CO2 emitted,oth,annual,unpriced,ktCO2",
45+
])],
4646
),
4747
(
4848
// The simple example with gas commodities priced using full costs
4949
"simple_full",
50-
vec![FilePatch::new("commodities.csv").with_replacement(
51-
"id,description,type,time_slice_level,pricing_strategy,units\n\
52-
GASPRD,Gas produced,sed,season,full,PJ\n\
53-
GASNAT,Natural gas,sed,season,full,PJ\n\
54-
ELCTRI,Electricity,sed,daynight,shadow,PJ\n\
55-
RSHEAT,Residential heating,svd,daynight,shadow,PJ\n\
56-
CO2EMT,CO2 emitted,oth,annual,unpriced,ktCO2\n",
57-
)],
50+
vec![FilePatch::new("commodities.csv").with_replacement(&[
51+
"id,description,type,time_slice_level,pricing_strategy,units",
52+
"GASPRD,Gas produced,sed,season,full,PJ",
53+
"GASNAT,Natural gas,sed,season,full,PJ",
54+
"ELCTRI,Electricity,sed,daynight,shadow,PJ",
55+
"RSHEAT,Residential heating,svd,daynight,shadow,PJ",
56+
"CO2EMT,CO2 emitted,oth,annual,unpriced,ktCO2",
57+
])],
5858
),
5959
]
6060
.into_iter()

src/patch.rs

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,12 @@ impl FilePatch {
156156
self
157157
}
158158

159-
/// Set full replacement content for this file.
160-
pub fn with_replacement(mut self, content: impl Into<String>) -> Self {
159+
/// Set full replacement content for this file from a slice of lines.
160+
///
161+
/// Each line is joined with newlines, and a trailing newline is added.
162+
/// All lines must have the same number of columns (commas).
163+
/// Example: `with_replacement(&["header1,header2", "value1,value2"])`
164+
pub fn with_replacement(mut self, lines: &[&str]) -> Self {
161165
assert!(
162166
self.header_row.is_none(),
163167
"Cannot set replacement content when header is set for this FilePatch",
@@ -170,7 +174,21 @@ impl FilePatch {
170174
self.replacement_content.is_none(),
171175
"Replacement content already set for this FilePatch",
172176
);
173-
self.replacement_content = Some(content.into());
177+
178+
// Validate that all lines have the same number of columns
179+
if !lines.is_empty() {
180+
let first_col_count = lines[0].matches(',').count() + 1;
181+
for (idx, line) in lines.iter().enumerate() {
182+
let col_count = line.matches(',').count() + 1;
183+
assert_eq!(
184+
col_count, first_col_count,
185+
"Line {idx} has {col_count} columns but line 0 has {first_col_count}: {line:?}"
186+
);
187+
}
188+
}
189+
190+
let content = lines.join("\n") + "\n";
191+
self.replacement_content = Some(content);
174192
self
175193
}
176194

@@ -208,7 +226,8 @@ impl FilePatch {
208226
base_path.display()
209227
);
210228

211-
// nothing further to do if this patch is a full replacement
229+
// If this patch is a full replacement, validate the base file exists
230+
// (checked above) and return the replacement content
212231
if let Some(content) = &self.replacement_content {
213232
return Ok(content.clone());
214233
}
@@ -430,16 +449,18 @@ mod tests {
430449

431450
#[test]
432451
fn file_patch_with_replacement() {
433-
let replacement = "col1,col2\nnew1,new2\n";
452+
let expected = "col1,col2\nnew1,new2\n";
434453

435454
let model_dir = ModelPatch::from_example("simple")
436-
.with_file_patch(FilePatch::new("assets.csv").with_replacement(replacement))
455+
.with_file_patch(
456+
FilePatch::new("assets.csv").with_replacement(&["col1,col2", "new1,new2"]),
457+
)
437458
.build_to_tempdir()
438459
.unwrap();
439460

440461
let assets_path = model_dir.path().join("assets.csv");
441462
let assets_content = std::fs::read_to_string(assets_path).unwrap();
442-
assert_eq!(assets_content, replacement);
463+
assert_eq!(assets_content, expected);
443464
}
444465

445466
#[test]
@@ -449,7 +470,7 @@ mod tests {
449470
fn file_patch_replacement_after_header_panics() {
450471
let _ = FilePatch::new("assets.csv")
451472
.with_header("col1,col2")
452-
.with_replacement("col1,col2\na,b\n");
473+
.with_replacement(&["col1,col2", "a,b"]);
453474
}
454475

455476
#[test]
@@ -459,28 +480,35 @@ mod tests {
459480
fn file_patch_replacement_after_addition_panics() {
460481
let _ = FilePatch::new("assets.csv")
461482
.with_addition("a,b")
462-
.with_replacement("col1,col2\na,b\n");
483+
.with_replacement(&["col1,col2", "a,b"]);
463484
}
464485

465486
#[test]
466487
#[should_panic(expected = "Cannot add rows when replacement content is set for this FilePatch")]
467488
fn file_patch_addition_after_replacement_panics() {
468489
let _ = FilePatch::new("assets.csv")
469-
.with_replacement("col1,col2\na,b\n")
490+
.with_replacement(&["col1,col2", "a,b"])
470491
.with_addition("c,d");
471492
}
472493

473494
#[test]
474495
fn file_patch_with_replacement_missing_base_file_fails() {
475-
let model_patch = ModelPatch::from_example("simple")
476-
.with_file_patch(FilePatch::new("not_a_real_file.csv").with_replacement("x,y\n1,2\n"));
496+
let model_patch = ModelPatch::from_example("simple").with_file_patch(
497+
FilePatch::new("not_a_real_file.csv").with_replacement(&["x,y", "1,2"]),
498+
);
477499

478500
assert_error!(
479501
model_patch.build_to_tempdir(),
480502
"Base file for patching does not exist: examples/simple/not_a_real_file.csv"
481503
);
482504
}
483505

506+
#[test]
507+
#[should_panic(expected = "Line 1 has 2 columns but line 0 has 3")]
508+
fn file_patch_replacement_column_count_mismatch_panics() {
509+
let _ = FilePatch::new("test.csv").with_replacement(&["col1,col2,col3", "a,b"]);
510+
}
511+
484512
#[test]
485513
fn toml_patch() {
486514
// Patch to add an extra milestone year (2050)

0 commit comments

Comments
 (0)