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

dcdpr / jp / 20406037953

21 Dec 2025 06:42AM UTC coverage: 51.778% (-0.2%) from 52.025%
20406037953

push

github

web-flow
feat(jp_config)!: add `prepend` string merge strategy (#323)

Refactors the string merge configuration to separate the merge strategy
(`append`/`prepend`/`replace`) from the separator
(`none`/`space`/`line`/`paragraph`), making the configuration more
flexible and composable.

Changes:

- Split `MergedStringStrategy` into base strategies (`Append`,
`Prepend`, `Replace`)
- Introduced `MergedStringSeparator` enum for controlling separators
independently
- Updated all persona configs to use the new two-field structure
- Added comprehensive test coverage for both append and prepend
strategies

New Features:

- Added `Prepend` strategy for inserting content before existing values
- Separators can now be independently controlled regardless of merge
direction

BREAKING CHANGE: Configuration format for merged strings has changed.
The `append_space`, `append_line`, and `append_paragraph` strategy
values must be split into `strategy = "append"` with `separator =
"space|line|paragraph"`.

---------

Signed-off-by: Jean Mertz <git@jeanmertz.com>

32 of 181 new or added lines in 5 files covered. (17.68%)

1 existing line in 1 file now uncovered.

8796 of 16988 relevant lines covered (51.78%)

134.05 hits per line

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

33.33
/crates/jp_config/src/types/string.rs
1
//! String types.
2

3
use std::{convert::Infallible, ops::Deref, str::FromStr};
4

5
use schematic::{Config, ConfigEnum, PartialConfig as _};
6
use serde::{Deserialize, Serialize};
7

8
use crate::{
9
    assignment::{AssignKeyValue, AssignResult, KvAssignment, missing_key},
10
    delta::PartialConfigDelta,
11
    partial::ToPartial,
12
};
13

14
/// String value, either defaulting to a merge strategy of `replace`, or
15
/// defining a specific merge strategy.
16
#[derive(Debug, Clone, PartialEq, Config)]
17
#[config(serde(untagged))]
18
pub enum MergeableString {
19
    /// A string that is merged using the [`schematic::merge::replace`]
20
    #[setting(default)]
21
    String(String),
22

23
    /// A string that is merged using the specified merge strategy.
24
    #[setting(nested)]
25
    Merged(MergedString),
26
}
27

28
impl From<&str> for PartialMergeableString {
29
    fn from(value: &str) -> Self {
12✔
30
        Self::String(value.to_string())
12✔
31
    }
12✔
32
}
33

34
impl FromStr for PartialMergeableString {
35
    type Err = Infallible;
36

37
    fn from_str(s: &str) -> Result<Self, Self::Err> {
2✔
38
        Ok(Self::String(s.to_owned()))
2✔
39
    }
2✔
40
}
41

42
impl From<MergeableString> for String {
43
    fn from(value: MergeableString) -> Self {
×
44
        match value {
×
45
            MergeableString::String(v) => v,
×
46
            MergeableString::Merged(v) => v.value,
×
47
        }
48
    }
×
49
}
50

51
impl AsRef<str> for PartialMergeableString {
52
    fn as_ref(&self) -> &str {
1✔
53
        match self {
1✔
54
            Self::String(v) => v,
1✔
55
            Self::Merged(v) => v.value.as_deref().unwrap_or_default(),
×
56
        }
57
    }
1✔
58
}
59

60
impl Deref for PartialMergeableString {
61
    type Target = str;
62

63
    fn deref(&self) -> &Self::Target {
1✔
64
        self.as_ref()
1✔
65
    }
1✔
66
}
67

68
impl AssignKeyValue for PartialMergeableString {
69
    fn assign(&mut self, kv: KvAssignment) -> AssignResult {
×
70
        match kv.key_string().as_str() {
×
71
            "" => *self = kv.try_object_or_from_str()?,
×
72
            _ => match self {
×
73
                Self::String(_) => return missing_key(&kv),
×
74
                Self::Merged(config) => config.assign(kv)?,
×
75
            },
76
        }
77

78
        Ok(())
×
79
    }
×
80
}
81

82
impl PartialConfigDelta for PartialMergeableString {
83
    fn delta(&self, next: Self) -> Self {
×
84
        if self == &next {
×
85
            return Self::empty();
×
86
        }
×
87

88
        next
×
89
    }
×
90
}
91

92
impl ToPartial for MergeableString {
93
    fn to_partial(&self) -> Self::Partial {
×
94
        match self {
×
95
            Self::String(v) => Self::Partial::String(v.clone()),
×
96
            Self::Merged(v) => Self::Partial::Merged(v.to_partial()),
×
97
        }
98
    }
×
99
}
100

101
/// Strings that are merged using the specified merge strategy.
102
#[derive(Debug, Clone, PartialEq, Config)]
103
pub struct MergedString {
104
    /// The string value.
105
    #[setting(default)]
106
    pub value: String,
107

108
    /// The merge strategy.
109
    #[setting(default)]
110
    pub strategy: MergedStringStrategy,
111

112
    /// The separator to use between the previous value and the new value.
113
    #[setting(default)]
114
    pub separator: MergedStringSeparator,
115
}
116

117
impl AssignKeyValue for PartialMergedString {
118
    fn assign(&mut self, kv: KvAssignment) -> AssignResult {
×
119
        match kv.key_string().as_str() {
×
120
            "" => *self = kv.try_object()?,
×
121
            "value" => self.value = kv.try_some_string()?,
×
122
            "strategy" => self.strategy = kv.try_some_from_str()?,
×
123
            _ => return missing_key(&kv),
×
124
        }
125

126
        Ok(())
×
127
    }
×
128
}
129

130
impl ToPartial for MergedString {
131
    fn to_partial(&self) -> Self::Partial {
×
132
        Self::Partial {
×
133
            value: Some(self.value.clone()),
×
134
            strategy: Some(self.strategy),
×
NEW
135
            separator: Some(self.separator),
×
136
        }
×
137
    }
×
138
}
139

140
/// Merge strategy for `VecWithStrategy`.
141
#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize, ConfigEnum)]
142
#[serde(rename_all = "snake_case")]
143
pub enum MergedStringStrategy {
144
    /// Append the string to the previous value.
145
    #[default]
146
    Append,
147

148
    /// Prepend the string to the previous value.
149
    Prepend,
150

151
    /// See [`schematic::merge::replace`].
152
    Replace,
153
}
154

155
/// Merge strategy for `VecWithStrategy`.
156
#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize, ConfigEnum)]
157
#[serde(rename_all = "snake_case")]
158
pub enum MergedStringSeparator {
159
    /// No separator.
160
    #[default]
161
    None,
162

163
    /// Single space separator.
164
    Space,
165

166
    /// New line separator.
167
    Line,
168

169
    /// Paragraph separator.
170
    Paragraph,
171
}
172

173
impl MergedStringSeparator {
174
    /// Returns the separator as a string.
175
    #[must_use]
176
    pub const fn as_str(&self) -> &str {
18✔
177
        match self {
18✔
178
            Self::None => "",
12✔
179
            Self::Space => " ",
2✔
180
            Self::Line => "\n",
2✔
181
            Self::Paragraph => "\n\n",
2✔
182
        }
183
    }
18✔
184
}
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