Skip to content

Commit cb3e230

Browse files
authored
feat: implement flatten and nest mapping operations (#49)
feat: implement flatten and nest mapping operations (closes #20)
1 parent 2a8100e commit cb3e230

File tree

4 files changed

+406
-0
lines changed

4 files changed

+406
-0
lines changed

src/mapping/ast.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,18 @@ pub enum Statement {
9494
target_type: CastType,
9595
span: Span,
9696
},
97+
/// `flatten .address` or `flatten .address -> prefix "addr"`
98+
Flatten {
99+
path: Path,
100+
prefix: Option<String>,
101+
span: Span,
102+
},
103+
/// `nest .a_x, .a_y -> .a`
104+
Nest {
105+
paths: Vec<Path>,
106+
target: Path,
107+
span: Span,
108+
},
97109
}
98110

99111
/// A parsed mapping program: a list of statements.

src/mapping/eval.rs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ fn eval_statement(stmt: &Statement, value: &Value) -> error::Result<Value> {
2424
Statement::Cast {
2525
path, target_type, ..
2626
} => eval_cast(value, path, target_type),
27+
Statement::Flatten { path, prefix, .. } => eval_flatten(value, path, prefix.as_deref()),
28+
Statement::Nest { paths, target, .. } => eval_nest(value, paths, target),
2729
}
2830
}
2931

@@ -175,6 +177,88 @@ fn cast_value(value: &Value, target_type: &CastType) -> error::Result<Value> {
175177
}
176178
}
177179

180+
// ---------------------------------------------------------------------------
181+
// flatten
182+
// ---------------------------------------------------------------------------
183+
184+
fn eval_flatten(value: &Value, path: &Path, prefix: Option<&str>) -> error::Result<Value> {
185+
let target_val = resolve_path(value, &path.segments);
186+
match target_val {
187+
Some(Value::Map(inner_map)) => {
188+
// Determine the prefix for flattened keys
189+
let key_prefix = match prefix {
190+
Some(p) => p.to_string(),
191+
None => {
192+
// Use the last segment of the path as the prefix
193+
last_field_name(&path.segments).unwrap_or_default()
194+
}
195+
};
196+
197+
// Remove the original nested field
198+
let mut result = remove_path(value, &path.segments);
199+
200+
// Insert flattened key-value pairs into the parent map
201+
if let Value::Map(ref mut parent_map) = result {
202+
for (key, val) in &inner_map {
203+
let flat_key = format!("{key_prefix}_{key}");
204+
parent_map.insert(flat_key, val.clone());
205+
}
206+
}
207+
208+
Ok(result)
209+
}
210+
Some(_) => {
211+
// Non-object: no-op
212+
Ok(value.clone())
213+
}
214+
None => {
215+
// Path doesn't exist: no-op
216+
Ok(value.clone())
217+
}
218+
}
219+
}
220+
221+
// ---------------------------------------------------------------------------
222+
// nest
223+
// ---------------------------------------------------------------------------
224+
225+
fn eval_nest(value: &Value, paths: &[Path], target: &Path) -> error::Result<Value> {
226+
// Get the target field name to strip as prefix
227+
let target_name = last_field_name(&target.segments).unwrap_or_default();
228+
229+
let mut nested_map = IndexMap::new();
230+
let mut result = value.clone();
231+
232+
for path in paths {
233+
if let Some(val) = resolve_path(value, &path.segments) {
234+
// Get the field name from the path
235+
let field_name = last_field_name(&path.segments).unwrap_or_default();
236+
237+
// Strip target prefix (e.g., "a_x" with target "a" → "x")
238+
let nested_key = if !target_name.is_empty() {
239+
let prefix_with_underscore = format!("{target_name}_");
240+
if field_name.starts_with(&prefix_with_underscore) {
241+
field_name[prefix_with_underscore.len()..].to_string()
242+
} else {
243+
field_name
244+
}
245+
} else {
246+
field_name
247+
};
248+
249+
nested_map.insert(nested_key, val);
250+
251+
// Remove the original field
252+
result = remove_path(&result, &path.segments);
253+
}
254+
}
255+
256+
// Set the nested map at the target path
257+
result = set_path(&result, &target.segments, Value::Map(nested_map));
258+
259+
Ok(result)
260+
}
261+
178262
// ---------------------------------------------------------------------------
179263
// Expression evaluation
180264
// ---------------------------------------------------------------------------

src/mapping/parser.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ impl Parser {
9797
TokenKind::Set => self.parse_set(),
9898
TokenKind::Default => self.parse_default(),
9999
TokenKind::Cast => self.parse_cast(),
100+
TokenKind::Flatten => self.parse_flatten(),
101+
TokenKind::Nest => self.parse_nest(),
100102
_ => {
101103
let suggestion = suggest_keyword(&token.kind);
102104
let msg = if let Some(s) = suggestion {
@@ -213,6 +215,65 @@ impl Parser {
213215
}
214216
}
215217

218+
fn parse_flatten(&mut self) -> error::Result<Statement> {
219+
let start = self.advance().unwrap(); // consume 'flatten'
220+
let path = self.parse_path()?;
221+
222+
// Check for optional -> prefix "..."
223+
let prefix = if matches!(self.peek_kind(), Some(TokenKind::Arrow)) {
224+
self.advance(); // consume '->'
225+
// Expect the identifier "prefix"
226+
match self.peek_kind() {
227+
Some(TokenKind::Ident(name)) if name == "prefix" => {
228+
self.advance(); // consume 'prefix'
229+
}
230+
_ => {
231+
let span = self.current_span();
232+
return Err(error::MorphError::mapping_at(
233+
"expected 'prefix' after '->' in flatten",
234+
span.line,
235+
span.column,
236+
));
237+
}
238+
}
239+
// Expect a string literal for the prefix value
240+
match self.advance() {
241+
Some(Token {
242+
kind: TokenKind::StringLit(s),
243+
..
244+
}) => Some(s),
245+
_ => {
246+
let span = self.current_span();
247+
return Err(error::MorphError::mapping_at(
248+
"expected string literal for prefix value in flatten",
249+
span.line,
250+
span.column,
251+
));
252+
}
253+
}
254+
} else {
255+
None
256+
};
257+
258+
Ok(Statement::Flatten {
259+
path,
260+
prefix,
261+
span: start.span,
262+
})
263+
}
264+
265+
fn parse_nest(&mut self) -> error::Result<Statement> {
266+
let start = self.advance().unwrap(); // consume 'nest'
267+
let paths = self.parse_path_list()?;
268+
self.expect_exact(&TokenKind::Arrow)?;
269+
let target = self.parse_path()?;
270+
Ok(Statement::Nest {
271+
paths,
272+
target,
273+
span: start.span,
274+
})
275+
}
276+
216277
fn parse_path_list(&mut self) -> error::Result<Vec<Path>> {
217278
let mut paths = vec![self.parse_path()?];
218279
while let Some(TokenKind::Comma) = self.peek_kind() {

0 commit comments

Comments
 (0)