implement builtin fn map.deep-merge(..)

This commit is contained in:
Connor Skees 2023-05-11 16:38:01 +00:00
parent fa6b2933c6
commit f811b243c7
6 changed files with 240 additions and 157 deletions

View File

@ -7,6 +7,10 @@
-->
# TBD
- implement builtin map-module function `map.deep-merge(..)`
# 0.12.3
No visible changes for users of the `grass` crate

View File

@ -3,53 +3,26 @@ use crate::builtin::builtin_imports::*;
pub(crate) fn map_get(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(2)?;
let key = args.get_err(1, "key")?;
let map = match args.get_err(0, "map")? {
Value::Map(m) => m,
Value::List(v, ..) if v.is_empty() => SassMap::new(),
Value::ArgList(v) if v.is_empty() => SassMap::new(),
v => {
return Err((
format!("$map: {} is not a map.", v.inspect(args.span())?),
args.span(),
)
.into())
}
};
let map = args
.get_err(0, "map")?
.assert_map_with_name("map", args.span())?;
Ok(map.get(&key).unwrap_or(Value::Null))
}
pub(crate) fn map_has_key(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(2)?;
let key = args.get_err(1, "key")?;
let map = match args.get_err(0, "map")? {
Value::Map(m) => m,
Value::List(v, ..) if v.is_empty() => SassMap::new(),
Value::ArgList(v) if v.is_empty() => SassMap::new(),
v => {
return Err((
format!("$map: {} is not a map.", v.inspect(args.span())?),
args.span(),
)
.into())
}
};
let map = args
.get_err(0, "map")?
.assert_map_with_name("map", args.span())?;
Ok(Value::bool(map.get(&key).is_some()))
}
pub(crate) fn map_keys(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?;
let map = match args.get_err(0, "map")? {
Value::Map(m) => m,
Value::List(v, ..) if v.is_empty() => SassMap::new(),
Value::ArgList(v) if v.is_empty() => SassMap::new(),
v => {
return Err((
format!("$map: {} is not a map.", v.inspect(args.span())?),
args.span(),
)
.into())
}
};
let map = args
.get_err(0, "map")?
.assert_map_with_name("map", args.span())?;
Ok(Value::List(
map.keys(),
ListSeparator::Comma,
@ -59,18 +32,9 @@ pub(crate) fn map_keys(mut args: ArgumentResult, visitor: &mut Visitor) -> SassR
pub(crate) fn map_values(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
args.max_args(1)?;
let map = match args.get_err(0, "map")? {
Value::Map(m) => m,
Value::List(v, ..) if v.is_empty() => SassMap::new(),
Value::ArgList(v) if v.is_empty() => SassMap::new(),
v => {
return Err((
format!("$map: {} is not a map.", v.inspect(args.span())?),
args.span(),
)
.into())
}
};
let map = args
.get_err(0, "map")?
.assert_map_with_name("map", args.span())?;
Ok(Value::List(
map.values(),
ListSeparator::Comma,
@ -85,31 +49,13 @@ pub(crate) fn map_merge(mut args: ArgumentResult, visitor: &mut Visitor) -> Sass
let map2_position = args.len().saturating_sub(1);
let mut map1 = match args.get_err(0, "map1")? {
Value::Map(m) => m,
Value::List(v, ..) if v.is_empty() => SassMap::new(),
Value::ArgList(v) if v.is_empty() => SassMap::new(),
v => {
return Err((
format!("$map1: {} is not a map.", v.inspect(args.span())?),
args.span(),
)
.into())
}
};
let mut map1 = args
.get_err(0, "map1")?
.assert_map_with_name("map1", args.span())?;
let map2 = match args.get_err(map2_position, "map2")? {
Value::Map(m) => m,
Value::List(v, ..) if v.is_empty() => SassMap::new(),
Value::ArgList(v) if v.is_empty() => SassMap::new(),
v => {
return Err((
format!("$map2: {} is not a map.", v.inspect(args.span())?),
args.span(),
)
.into())
}
};
let map2 = args
.get_err(map2_position, "map2")?
.assert_map_with_name("map2", args.span())?;
let keys = args.get_variadic()?;
@ -156,18 +102,9 @@ pub(crate) fn map_merge(mut args: ArgumentResult, visitor: &mut Visitor) -> Sass
}
pub(crate) fn map_remove(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {
let mut map = match args.get_err(0, "map")? {
Value::Map(m) => m,
Value::List(v, ..) if v.is_empty() => SassMap::new(),
Value::ArgList(v) if v.is_empty() => SassMap::new(),
v => {
return Err((
format!("$map: {} is not a map.", v.inspect(args.span())?),
args.span(),
)
.into())
}
};
let mut map = args
.get_err(0, "map")?
.assert_map_with_name("map", args.span())?;
let keys = args.get_variadic()?;
for key in keys {
map.remove(&key);
@ -179,18 +116,9 @@ pub(crate) fn map_set(mut args: ArgumentResult, visitor: &mut Visitor) -> SassRe
let key_position = args.len().saturating_sub(2);
let value_position = args.len().saturating_sub(1);
let mut map = match args.get_err(0, "map")? {
Value::Map(m) => m,
Value::List(v, ..) if v.is_empty() => SassMap::new(),
Value::ArgList(v) if v.is_empty() => SassMap::new(),
v => {
return Err((
format!("$map: {} is not a map.", v.inspect(args.span())?),
args.span(),
)
.into())
}
};
let mut map = args
.get_err(0, "map")?
.assert_map_with_name("map", args.span())?;
let key = Spanned {
node: args.get_err(key_position, "key")?,

View File

@ -1,8 +1,58 @@
use crate::builtin::builtin_imports::*;
use crate::builtin::{
map::{map_get, map_has_key, map_keys, map_merge, map_remove, map_set, map_values},
modules::Module,
};
fn deep_merge_impl(map1: SassMap, map2: SassMap) -> SassMap {
if map1.is_empty() {
return map2;
}
if map2.is_empty() {
return map1;
}
let mut result = map1;
for (key, value) in map2 {
let result_map = result.get_ref(&key.node).and_then(Value::try_map);
match result_map {
Some(result_map) => match value.try_map() {
Some(value_map) => {
let merged = deep_merge_impl(result_map, value_map);
result.insert(key, Value::Map(merged));
}
None => {
result.insert(key, value);
}
},
None => {
result.insert(key, value);
}
}
}
result
}
fn deep_merge(mut args: ArgumentResult, _: &mut Visitor) -> SassResult<Value> {
args.max_args(2)?;
let span = args.span();
let map1 = args
.get_err(0, "map1")?
.assert_map_with_name("map1", span)?;
let map2 = args
.get_err(1, "map2")?
.assert_map_with_name("map2", span)?;
Ok(Value::Map(deep_merge_impl(map1, map2)))
}
pub(crate) fn declare(f: &mut Module) {
f.insert_builtin("get", map_get);
f.insert_builtin("has-key", map_has_key);
@ -11,4 +61,5 @@ pub(crate) fn declare(f: &mut Module) {
f.insert_builtin("remove", map_remove);
f.insert_builtin("values", map_values);
f.insert_builtin("set", map_set);
f.insert_builtin("deep-merge", deep_merge);
}

View File

@ -171,6 +171,23 @@ impl Value {
}
}
pub fn assert_map_with_name(self, name: &str, span: Span) -> SassResult<SassMap> {
match self {
Value::Map(m) => Ok(m),
Value::List(v, ..) if v.is_empty() => Ok(SassMap::new()),
Value::ArgList(v) if v.is_empty() => Ok(SassMap::new()),
_ => Err((
format!(
"${name}: {} is not a map.",
self.inspect(span)?,
name = name,
),
span,
)
.into()),
}
}
pub fn assert_string_with_name(
self,
name: &str,
@ -303,6 +320,15 @@ impl Value {
}
}
pub fn try_map(&self) -> Option<SassMap> {
match &self {
Value::Map(m) => Some(m.clone()),
Value::List(v, ..) if v.is_empty() => Some(SassMap::new()),
Value::ArgList(v) if v.is_empty() => Some(SassMap::new()),
_ => None,
}
}
pub fn bool(b: bool) -> Self {
if b {
Value::True

View File

@ -0,0 +1,135 @@
#[macro_use]
mod macros;
test!(
map_set_nested_empty,
"@use 'sass:map'; a {b: inspect(map.set((c: ()), c, d, e, f))}",
"a {\n b: (c: (d: (e: f)));\n}\n"
);
test!(
map_set_update_existing,
"@use 'sass:map'; a {b: inspect(map.set((c: (d: e)), c, d, f))}",
"a {\n b: (c: (d: f));\n}\n"
);
test!(
map_set_new_key,
"@use 'sass:map'; a {b: inspect(map.set((c: (d: e)), c, f, g))}",
"a {\n b: (c: (d: e, f: g));\n}\n"
);
test!(
map_set_value_is_not_map,
"@use 'sass:map'; a {b: inspect(map.set((c: 1), c, d, f))}",
"a {\n b: (c: (d: f));\n}\n"
);
test!(
map_merge_merge_into_map_with_many_keys,
r#"
@use "sass:map";
$fonts: (
"Helvetica": (
"weights": (
"regular": 400,
"medium": 500,
"bold": 700
)
)
);
a {
color: inspect(map.merge($fonts, "Helvetica", "weights", "regular", (a: 300)));
}"#,
"a {\n color: (\"Helvetica\": (\"weights\": (\"regular\": (a: 300), \"medium\": 500, \"bold\": 700)));\n}\n"
);
test!(
map_merge_nested,
r#"
@use "sass:map";
$fonts: (
"Helvetica": (
"weights": (
"regular": 400,
"medium": 500,
"bold": 700
)
)
);
a {
color: inspect(map.set($fonts, "Helvetica", "weights", "regular", 300));
}"#,
"a {\n color: (\"Helvetica\": (\"weights\": (\"regular\": 300, \"medium\": 500, \"bold\": 700)));\n}\n"
);
test!(
deep_merge_no_nesting,
r#"@use "sass:map";
a {
color: inspect(map.deep-merge($map1: (c: d), $map2: (1: 2)));
}"#,
"a {\n color: (c: d, 1: 2);\n}\n"
);
test!(
deep_merge_positional,
r#"@use "sass:map";
a {
color: inspect(map.deep-merge((a: b), (c: d)));
}"#,
"a {\n color: (a: b, c: d);\n}\n"
);
test!(
deep_merge_empty_maps,
r#"@use "sass:map";
a {
color: inspect(map.deep-merge((), ()));
}"#,
"a {\n color: ();\n}\n"
);
test!(
deep_merge_empty_maps_bracketed_list,
r#"@use "sass:map";
a {
color: inspect(map.deep-merge([], []));
}"#,
"a {\n color: ();\n}\n"
);
test!(
deep_merge_empty_first,
r#"@use "sass:map";
a {
color: inspect(map.deep-merge((a: b), ()));
}"#,
"a {\n color: (a: b);\n}\n"
);
test!(
deep_merge_empty_second,
r#"@use "sass:map";
a {
color: inspect(map.deep-merge((), (a: b)));
}"#,
"a {\n color: (a: b);\n}\n"
);
test!(
deep_merge_empty_deep,
r#"@use "sass:map";
a {
color: inspect(map.deep-merge((c: (d: e)), (c: ())));
}"#,
"a {\n color: (c: (d: e));\n}\n"
);
test!(
deep_merge_empty_bracketed_list_deep,
r#"@use "sass:map";
a {
color: inspect(map.deep-merge((c: (d: e)), (c: [])));
}"#,
"a {\n color: (c: (d: e));\n}\n"
);

View File

@ -120,66 +120,6 @@ test!(
"a {b: inspect(map-merge((c: (d: (e: (f: (g: h))))), c, d, e, f, (g: 1)))}",
"a {\n b: (c: (d: (e: (f: (g: 1)))));\n}\n"
);
test!(
map_set_nested_empty,
"@use 'sass:map'; a {b: inspect(map.set((c: ()), c, d, e, f))}",
"a {\n b: (c: (d: (e: f)));\n}\n"
);
test!(
map_set_update_existing,
"@use 'sass:map'; a {b: inspect(map.set((c: (d: e)), c, d, f))}",
"a {\n b: (c: (d: f));\n}\n"
);
test!(
map_set_new_key,
"@use 'sass:map'; a {b: inspect(map.set((c: (d: e)), c, f, g))}",
"a {\n b: (c: (d: e, f: g));\n}\n"
);
test!(
map_set_value_is_not_map,
"@use 'sass:map'; a {b: inspect(map.set((c: 1), c, d, f))}",
"a {\n b: (c: (d: f));\n}\n"
);
test!(
map_merge_merge_into_map_with_many_keys,
r#"
@use "sass:map";
$fonts: (
"Helvetica": (
"weights": (
"regular": 400,
"medium": 500,
"bold": 700
)
)
);
a {
color: inspect(map.merge($fonts, "Helvetica", "weights", "regular", (a: 300)));
}"#,
"a {\n color: (\"Helvetica\": (\"weights\": (\"regular\": (a: 300), \"medium\": 500, \"bold\": 700)));\n}\n"
);
test!(
map_merge_nested,
r#"
@use "sass:map";
$fonts: (
"Helvetica": (
"weights": (
"regular": 400,
"medium": 500,
"bold": 700
)
)
);
a {
color: inspect(map.set($fonts, "Helvetica", "weights", "regular", 300));
}"#,
"a {\n color: (\"Helvetica\": (\"weights\": (\"regular\": 300, \"medium\": 500, \"bold\": 700)));\n}\n"
);
error!(
map_merge_map1_non_map,
"a {\n color: map-merge(foo, (a: b));\n}\n", "Error: $map1: foo is not a map."
@ -241,7 +181,6 @@ test!(
"a {\n color: 1, 2;\n}\n"
);
test!(
#[ignore = "blocked on rewriting inspect"]
map_inspect_comma_separated_list_as_key,
"a {\n color: inspect(((1, 2): 3));\n}\n",
"a {\n color: ((1, 2): 3);\n}\n"