• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kaspar030 / laze / 18686477427

21 Oct 2025 02:03PM UTC coverage: 80.168% (-0.01%) from 80.182%
18686477427

Pull #805

github

web-flow
Merge 6f0f54a8b into f1a88ed7d
Pull Request #805: fix: Prevent panic on flattening empty list

3347 of 4175 relevant lines covered (80.17%)

99.49 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.23
/src/nested_env/mod.rs
1
use std::borrow::Cow;
2

3
use anyhow::{anyhow, Context, Error};
4
use evalexpr::EvalexprError;
5
use im::{hashmap::Entry, vector, Vector};
6
use itertools::join;
7
use serde::{Deserialize, Serialize};
8

9
mod expand;
10
mod expr;
11
pub use expr::Eval;
12

13
pub use expand::{expand, expand_eval, IfMissing};
14

15
pub type EnvMap<'a> = std::collections::HashMap<&'a str, Cow<'a, str>>;
16

17
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Hash)]
18
pub struct Env {
19
    #[serde(flatten)]
20
    inner: im::HashMap<String, EnvKey>,
21
}
22

23
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, Hash)]
24
#[serde(untagged, expecting = "expected single value or array of values")]
25
pub enum EnvKey {
26
    Single(String),
27
    List(Vector<String>),
28
}
29

30
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone, Default)]
31
pub struct MergeOption {
32
    from: Option<String>,
33
    joiner: Option<String>,
34
    prefix: Option<String>,
35
    suffix: Option<String>,
36
    start: Option<String>,
37
    end: Option<String>,
38
}
39

40
impl EnvKey {
41
    fn merge(&self, other: &EnvKey) -> EnvKey {
367✔
42
        match self {
367✔
43
            EnvKey::Single(_) => other.clone(),
326✔
44
            EnvKey::List(self_values) => match other {
41✔
45
                EnvKey::Single(_) => other.clone(),
×
46
                EnvKey::List(other_values) => {
41✔
47
                    let mut combined = self_values.clone();
41✔
48
                    combined.append(other_values.clone());
41✔
49
                    EnvKey::List(combined)
41✔
50
                }
51
            },
52
        }
53
    }
367✔
54

55
    fn flatten(&self) -> Result<Cow<'_, str>, EvalexprError> {
4,974✔
56
        match self {
4,974✔
57
            EnvKey::Single(s) => Ok(Cow::from(s)),
4,302✔
58
            EnvKey::List(list) => Ok(join(list, " ").into()),
672✔
59
        }
60
    }
4,974✔
61

62
    fn flatten_with_opts(&self, opts: &MergeOption) -> Result<Cow<'_, str>, EvalexprError> {
22✔
63
        eprintln!("Flattening {:?}", self);
22✔
64
        let mut res = String::new();
22✔
65
        if let Some(start) = &opts.start {
22✔
66
            res.push_str(start);
×
67
        }
22✔
68

69
        match self {
22✔
70
            EnvKey::Single(s) => {
×
71
                if let Some(prefix) = &opts.prefix {
×
72
                    res.push_str(prefix);
×
73
                }
×
74

75
                res.push_str(s);
×
76

77
                if let Some(suffix) = &opts.suffix {
×
78
                    res.push_str(suffix);
×
79
                }
×
80
            }
81
            EnvKey::List(list) if !list.is_empty() => {
22✔
82
                let joiner = match &opts.joiner {
22✔
83
                    Some(joiner) => joiner,
×
84
                    None => " ",
22✔
85
                };
86
                let last = list.len() - 1;
22✔
87
                for (pos, s) in list.iter().enumerate() {
46✔
88
                    if s.is_empty() {
46✔
89
                        continue;
×
90
                    }
46✔
91
                    if let Some(prefix) = &opts.prefix {
46✔
92
                        res.push_str(prefix);
46✔
93
                    }
46✔
94

95
                    res.push_str(s);
46✔
96

97
                    if let Some(suffix) = &opts.suffix {
46✔
98
                        res.push_str(suffix);
×
99
                    }
46✔
100
                    if pos != last {
46✔
101
                        res.push_str(joiner);
24✔
102
                    }
24✔
103
                }
104
            }
105
            _ => (),
×
106
        }
107
        if let Some(end) = &opts.end {
22✔
108
            res.push_str(&end[..]);
×
109
        }
22✔
110
        Ok(res.into())
22✔
111
    }
22✔
112
}
113

114
impl<T: ToString> From<T> for EnvKey {
115
    fn from(value: T) -> Self {
419✔
116
        EnvKey::Single(value.to_string())
419✔
117
    }
419✔
118
}
119

120
impl Env {
121
    pub fn new() -> Self {
1,018✔
122
        Self {
1,018✔
123
            inner: im::HashMap::new(),
1,018✔
124
        }
1,018✔
125
    }
1,018✔
126

127
    pub fn merge(&mut self, other: &Env) {
1,220✔
128
        for (key, value) in other.inner.iter() {
1,334✔
129
            match self.entry(key.clone()) {
1,334✔
130
                Entry::Vacant(e) => {
967✔
131
                    e.insert(value.clone());
967✔
132
                }
967✔
133
                Entry::Occupied(mut e) => {
367✔
134
                    let merged = e.get_mut().merge(value);
367✔
135
                    *e.get_mut() = merged;
367✔
136
                }
367✔
137
            }
138
        }
139
    }
1,220✔
140

141
    pub fn flatten(&self) -> Result<EnvMap<'_>, Error> {
597✔
142
        self.inner
597✔
143
            .iter()
597✔
144
            .map(|(key, value)| {
4,547✔
145
                match value.flatten() {
4,547✔
146
                    Ok(v) => Ok((key.as_str(), v)),
4,547✔
147
                    Err(e) => Err(e),
×
148
                }
149
                .with_context(|| format!("variable \"{key}\""))
4,547✔
150
            })
4,547✔
151
            .collect::<Result<EnvMap, Error>>()
597✔
152
    }
597✔
153

154
    pub fn flatten_with_opts<'a>(
27✔
155
        &'a self,
27✔
156
        merge_opts: &'a im::HashMap<String, MergeOption>,
27✔
157
    ) -> Result<EnvMap<'a>, Error> {
27✔
158
        let mut result = self
27✔
159
            .inner
27✔
160
            .iter()
27✔
161
            .map(|(key, value)| {
449✔
162
                if let Some(merge_opts) = merge_opts.get(key) {
449✔
163
                    match value.flatten_with_opts(merge_opts) {
22✔
164
                        Ok(v) => Ok((key.as_str(), v)),
22✔
165
                        Err(e) => Err(e),
×
166
                    }
167
                } else {
168
                    match value.flatten() {
427✔
169
                        Ok(v) => Ok((key.as_str(), v)),
427✔
170
                        Err(e) => Err(e),
×
171
                    }
172
                }
173
                .with_context(|| format!("variable \"{key}\""))
449✔
174
            })
449✔
175
            .collect::<Result<EnvMap, Error>>()?;
27✔
176

177
        for (key, merge_opt) in merge_opts {
54✔
178
            if let Some(other) = merge_opt.from.as_ref() {
27✔
179
                let other_value = self.get(other).with_context(|| {
×
180
                    format!("non-existing key \"{other}\" as `from` for \"{key}\"")
×
181
                })?;
×
182

183
                let flattened = other_value
×
184
                    .flatten_with_opts(merge_opt)
×
185
                    .with_context(|| format!("variable \"{key}\""))?;
×
186

187
                let previous = result.insert(key, flattened);
×
188

189
                if previous.is_some() {
×
190
                    return Err(anyhow!(
×
191
                        "variable \"{key}\" has both values and var_option `from`"
×
192
                    ));
×
193
                }
×
194
            }
27✔
195
        }
196

197
        Ok(result)
27✔
198
    }
27✔
199

200
    pub fn flatten_with_opts_option<'a>(
226✔
201
        &'a self,
226✔
202
        merge_opts: Option<&'a im::HashMap<String, MergeOption>>,
226✔
203
    ) -> Result<EnvMap<'a>, Error> {
226✔
204
        if let Some(merge_opts) = merge_opts {
226✔
205
            self.flatten_with_opts(merge_opts)
27✔
206
        } else {
207
            self.flatten()
199✔
208
        }
209
    }
226✔
210

211
    // pub fn flatten_expand<'a>(flattened: &'a HashMap<&String, String>) -> HashMap<&'a String, String> {
212
    //     flattened
213
    //         .iter()
214
    //         .map(|(key, value)| (*key, expand(value, flattened, IfMissing::Error).unwrap()))
215
    //         .collect()
216
    // }
217

218
    pub fn expand(&mut self, values: &Env) -> Result<(), Error> {
392✔
219
        let values = values.flatten()?;
392✔
220

221
        fn expand_envkey(envkey: &EnvKey, values: &EnvMap) -> EnvKey {
477✔
222
            match envkey {
477✔
223
                EnvKey::Single(key) => {
426✔
224
                    EnvKey::Single(expand(key, values, IfMissing::Ignore).unwrap())
426✔
225
                }
226
                EnvKey::List(keys) => EnvKey::List({
51✔
227
                    keys.iter()
51✔
228
                        .map(|x| expand(x, values, IfMissing::Ignore).unwrap())
60✔
229
                        .collect()
51✔
230
                }),
231
            }
232
        }
477✔
233

234
        for (_, value) in self.inner.iter_mut() {
477✔
235
            *value = expand_envkey(value, &values);
477✔
236
        }
477✔
237

238
        Ok(())
392✔
239
    }
392✔
240

241
    pub fn insert<T: Into<EnvKey>>(&mut self, key: String, value: T) -> Option<EnvKey> {
1,361✔
242
        self.inner.insert(key, value.into())
1,361✔
243
    }
1,361✔
244

245
    pub fn get(&self, key: &str) -> Option<&EnvKey> {
89✔
246
        self.inner.get(key)
89✔
247
    }
89✔
248

249
    pub fn entry(
1,709✔
250
        &mut self,
1,709✔
251
        key: String,
1,709✔
252
    ) -> im::hashmap::Entry<'_, String, EnvKey, std::collections::hash_map::RandomState> {
1,709✔
253
        self.inner.entry(key)
1,709✔
254
    }
1,709✔
255

256
    pub fn assign_from_string(&mut self, assignment: &str) -> Result<(), anyhow::Error> {
5✔
257
        if let Some((var, value)) = assignment.split_once("+=") {
5✔
258
            let mut new = Env::new();
2✔
259
            new.insert(var.to_string(), EnvKey::List(vector![value.to_owned()]));
2✔
260
            self.merge(&new);
2✔
261
        } else if let Some((var, value)) = assignment.split_once('=') {
3✔
262
            let mut new = Env::new();
3✔
263
            new.insert(var.to_string(), EnvKey::Single(value.to_string()));
3✔
264
            self.merge(&new);
3✔
265
        } else {
3✔
266
            return Err(anyhow!(format!(
×
267
                "cannot parse assignment from \"{}\"",
×
268
                assignment
×
269
            )));
×
270
        }
271

272
        Ok(())
5✔
273
    }
5✔
274
}
275

276
#[cfg(test)]
277
mod tests {
278
    use super::*;
279
    use im::vector;
280

281
    #[test]
282
    fn test_merge_nonexisting_single() {
283
        let mut upper = Env::new();
284
        let mut lower = Env::new();
285
        upper.insert("mykey".to_string(), "upper_value");
286

287
        lower.merge(&upper);
288

289
        let merged = lower;
290

291
        assert_eq!(
292
            merged.get("mykey").unwrap(),
293
            &EnvKey::Single("upper_value".to_string())
294
        );
295
    }
296

297
    #[test]
298
    fn test_merge_overwriting_single() {
299
        let mut upper = Env::new();
300
        let mut lower = Env::new();
301
        upper.insert(
302
            "mykey".to_string(),
303
            EnvKey::Single("upper_value".to_string()),
304
        );
305

306
        lower.insert(
307
            "mykey".to_string(),
308
            EnvKey::Single("lower_value".to_string()),
309
        );
310

311
        lower.merge(&upper);
312

313
        assert_eq!(
314
            lower.get("mykey").unwrap(),
315
            &EnvKey::Single("upper_value".to_string())
316
        );
317
    }
318

319
    #[test]
320
    fn test_merge_overwriting_list() {
321
        let mut upper = Env::new();
322
        let mut lower = Env::new();
323
        lower.insert(
324
            "mykey".to_string(),
325
            EnvKey::List(vector![
326
                "lower_value_1".to_string(),
327
                "lower_value_2".to_string(),
328
            ]),
329
        );
330
        upper.insert(
331
            "mykey".to_string(),
332
            EnvKey::Single("upper_value".to_string()),
333
        );
334

335
        lower.merge(&upper);
336

337
        assert_eq!(
338
            lower.get("mykey").unwrap(),
339
            &EnvKey::Single("upper_value".to_string())
340
        );
341
    }
342

343
    #[test]
344
    fn test_merge_overwriting_with_list() {
345
        let mut upper = Env::new();
346
        let mut lower = Env::new();
347
        lower.insert(
348
            "mykey".to_string(),
349
            EnvKey::Single("lower_value".to_string()),
350
        );
351

352
        upper.insert(
353
            "mykey".to_string(),
354
            EnvKey::List(vector![
355
                "upper_value_1".to_string(),
356
                "upper_value_2".to_string(),
357
            ]),
358
        );
359

360
        lower.merge(&upper);
361

362
        assert_eq!(
363
            lower.get("mykey").unwrap(),
364
            &EnvKey::List(vector![
365
                "upper_value_1".to_string(),
366
                "upper_value_2".to_string(),
367
            ]),
368
        );
369
    }
370

371
    #[test]
372
    fn test_merge_merging_list() {
373
        let mut upper = Env::new();
374
        let mut lower = Env::new();
375
        lower.insert(
376
            "mykey".to_string(),
377
            EnvKey::List(vector![
378
                "lower_value_1".to_string(),
379
                "lower_value_2".to_string(),
380
            ]),
381
        );
382

383
        upper.insert(
384
            "mykey".to_string(),
385
            EnvKey::List(vector![
386
                "upper_value_1".to_string(),
387
                "upper_value_2".to_string(),
388
            ]),
389
        );
390

391
        lower.merge(&upper);
392

393
        assert_eq!(
394
            lower.get("mykey").unwrap(),
395
            &EnvKey::List(vector![
396
                "lower_value_1".to_string(),
397
                "lower_value_2".to_string(),
398
                "upper_value_1".to_string(),
399
                "upper_value_2".to_string(),
400
            ]),
401
        );
402
    }
403

404
    #[test]
405
    fn test_basic() {
406
        let mut upper = Env::new();
407
        let mut lower = Env::new();
408
        upper.insert(
409
            "mykey".to_string(),
410
            EnvKey::Single("upper_value".to_string()),
411
        );
412
        lower.insert(
413
            "mykey".to_string(),
414
            EnvKey::Single("lower_value".to_string()),
415
        );
416

417
        lower.merge(&upper);
418
    }
419

420
    #[test]
421
    fn test_mergeopts() {
422
        let mut env = Env::new();
423
        env.insert(
424
            "mykey".to_string(),
425
            EnvKey::List(vector![
426
                "value_1".to_string(),
427
                "value_2".to_string(),
428
                "value_3".to_string(),
429
                "value_4".to_string(),
430
            ]),
431
        );
432

433
        let mut merge_opts = im::HashMap::new();
434
        merge_opts.insert(
435
            "mykey".to_string(),
436
            MergeOption {
437
                joiner: Some(",".to_string()),
438
                prefix: Some("P".to_string()),
439
                suffix: Some("S".to_string()),
440
                start: Some("(".to_string()),
441
                end: Some(")".to_string()),
442
                ..Default::default()
443
            },
444
        );
445

446
        let flattened = env.flatten_with_opts(&merge_opts).unwrap();
447

448
        assert_eq!(
449
            flattened.get("mykey").unwrap(),
450
            &"(Pvalue_1S,Pvalue_2S,Pvalue_3S,Pvalue_4S)".to_string()
451
        );
452
    }
453

454
    #[test]
455
    fn test_mergeopts_ok() {
456
        let mut env = Env::new();
457
        env.insert(
458
            "other".to_string(),
459
            EnvKey::List(vector![
460
                "value_1".to_string(),
461
                "value_2".to_string(),
462
                "value_3".to_string(),
463
                "value_4".to_string(),
464
            ]),
465
        );
466

467
        let mut merge_opts = im::HashMap::new();
468
        merge_opts.insert(
469
            "mykey".to_string(),
470
            MergeOption {
471
                from: Some("other".to_string()),
472
                joiner: Some(",".to_string()),
473
                prefix: Some("P".to_string()),
474
                suffix: Some("S".to_string()),
475
                start: Some("(".to_string()),
476
                end: Some(")".to_string()),
477
            },
478
        );
479

480
        let flattened = env.flatten_with_opts(&merge_opts).unwrap();
481

482
        assert_eq!(
483
            flattened.get("mykey").unwrap(),
484
            &"(Pvalue_1S,Pvalue_2S,Pvalue_3S,Pvalue_4S)".to_string()
485
        );
486
    }
487

488
    #[test]
489
    fn test_mergeopts_error() {
490
        let mut env = Env::new();
491
        env.insert(
492
            "other".to_string(),
493
            EnvKey::List(vector!["value_1".to_string(),]),
494
        );
495
        env.insert(
496
            "mykey".to_string(),
497
            EnvKey::Single("mykey_value".to_string()),
498
        );
499

500
        let mut merge_opts = im::HashMap::new();
501
        merge_opts.insert(
502
            "mykey".to_string(),
503
            MergeOption {
504
                from: Some("other".to_string()),
505
                joiner: Some(",".to_string()),
506
                prefix: Some("P".to_string()),
507
                suffix: Some("S".to_string()),
508
                start: Some("(".to_string()),
509
                end: Some(")".to_string()),
510
            },
511
        );
512

513
        assert!(env.flatten_with_opts(&merge_opts).is_err());
514
    }
515

516
    #[test]
517
    fn test_mergeopts_empty() {
518
        let mut env = Env::new();
519
        env.insert(
520
            "mykey".to_string(),
521
            EnvKey::List(vector![]),
522
        );
523

524
        let mut merge_opts = im::HashMap::new();
525
        merge_opts.insert(
526
            "mykey".to_string(),
527
            MergeOption {
528
                joiner: Some(",".to_string()),
529
                prefix: Some("P".to_string()),
530
                suffix: Some("S".to_string()),
531
                start: Some("(".to_string()),
532
                end: Some(")".to_string()),
533
                ..Default::default()
534
            },
535
        );
536

537
        let flattened = env.flatten_with_opts(&merge_opts).unwrap();
538
        assert_eq!(
539
            flattened.get("mykey").unwrap(),
540
            &"()".to_string()
541
        );
542
    }
543

544
    #[test]
545
    fn test_assign_from_string_override() {
546
        let mut env = Env::new();
547
        env.insert("FOO".to_string(), EnvKey::Single("whiskeyBAR".to_string()));
548

549
        env.assign_from_string("FOO=milkBAR").unwrap();
550

551
        assert_eq!(
552
            env.get("FOO").unwrap(),
553
            &EnvKey::Single("milkBAR".to_string()),
554
        );
555
    }
556

557
    #[test]
558
    fn test_assign_from_string_override_list() {
559
        let mut env = Env::new();
560
        env.insert(
561
            "FOO".to_string(),
562
            EnvKey::List(vector!["whiskeyBAR".to_string(), "beerBAR".to_string()]),
563
        );
564

565
        env.assign_from_string("FOO=milkBAR").unwrap();
566

567
        assert_eq!(
568
            env.get("FOO").unwrap(),
569
            &EnvKey::Single("milkBAR".to_string()),
570
        );
571
    }
572

573
    #[test]
574
    fn test_assign_from_string_append() {
575
        let mut env = Env::new();
576
        env.insert(
577
            "FOO".to_string(),
578
            EnvKey::List(vector!["whiskeyBAR".to_string(), "beerBAR".to_string()]),
579
        );
580

581
        env.assign_from_string("FOO+=milkBAR").unwrap();
582

583
        assert_eq!(
584
            env.get("FOO").unwrap(),
585
            &EnvKey::List(vector![
586
                "whiskeyBAR".to_string(),
587
                "beerBAR".to_string(),
588
                "milkBAR".to_string()
589
            ]),
590
        );
591
    }
592
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc