support $keys... argument for map.get (#83)

per the sass-lang docs [1], the user should be able to invoke
`map.get($my-map, "key1", "key2")` to perform a nested lookup
of the two keys.  the current implementation fails if provided
more than two arguments to `map.get`.  this change implements
the nested get.  fixes #80.

[1] https://sass-lang.com/documentation/modules/map/
This commit is contained in:
greenwoodcm 2023-07-09 10:55:45 -07:00 committed by GitHub
parent 346b8c127b
commit beb64abac4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 76 additions and 6 deletions

View File

@ -233,10 +233,14 @@ impl ArgumentResult {
pub(crate) fn min_args(&self, min: usize) -> SassResult<()> {
let len = self.len();
if len < min {
if min == 1 {
return Err(("At least one argument must be passed.", self.span()).into());
}
todo!("min args greater than one")
let phrase = match min {
1 => "one argument",
2 => "two arguments",
3 => "three arguments",
_ => todo!("min args greater than three"),
};
return Err((format!("At least {phrase} must be passed."), self.span()).into());
}
Ok(())
}

View File

@ -1,12 +1,43 @@
use crate::builtin::builtin_imports::*;
/// map.get($map, $key, $keys...)
/// map-get($map, $key, $keys...)
///
/// If $keys is empty, returns the value in $map associated with $key.
/// If $map doesnt have a value associated with $key, returns null.
/// If $keys is not empty, follows the set of keys including $key and
/// excluding the last key in $keys, from left to right, to find the
/// nested map targeted for searching.
/// Returns the value in the targeted map associated with the last key
/// in $keys.
/// Returns null if the map does not have a value associated with the
/// key, or if any key in $keys is missing from a map or references a
/// value that is not a map.
///
/// https://sass-lang.com/documentation/modules/map/
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 = args
.get_err(0, "map")?
.assert_map_with_name("map", args.span())?;
Ok(map.get(&key).unwrap_or(Value::Null))
// since we already extracted the map and first key,
// neither will be returned in the variadic args list
let keys = args.get_variadic()?;
let mut val = map.get(&key).unwrap_or(Value::Null);
for key in keys {
// if at any point we find a value that's not a map,
// we return null
let val_map = match val.try_map() {
Some(val_map) => val_map,
None => return Ok(Value::Null),
};
val = val_map.get(&key).unwrap_or(Value::Null);
}
Ok(val)
}
pub(crate) fn map_has_key(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<Value> {

View File

@ -5,6 +5,11 @@ test!(
"a {\n color: map-get((a: b), a);\n}\n",
"a {\n color: b;\n}\n"
);
test!(
map_get_key_exists_named,
"a {\n color: map-get($map: (a: b), $key: a);\n}\n",
"a {\n color: b;\n}\n"
);
test!(
map_get_key_does_not_exist,
"a {\n color: map-get((a: b), foo);\n}\n",
@ -19,6 +24,36 @@ error!(
map_get_non_map,
"a {\n color: map-get(foo, foo);\n}\n", "Error: $map: foo is not a map."
);
test!(
map_get_nested,
"a {\n color: map-get((a: (b: (c: d))), a, b, c);\n}\n",
"a {\n color: d;\n}\n"
);
// it's an odd thing to do, but the spec suggests that the user
// can call the function like:
// map.get("key2", "key3", $map: $my-map, $key: "key1")
// in this case we are to use the named argument $key as the
// first key and use the positional arguments at the front as
// $keys. this test verifies this behavior.
test!(
map_get_nested_named_and_positional,
"a {\n color: map-get(b, c, $map: (a: (b: (c: d))), $key: a);\n}\n",
"a {\n color: d;\n}\n"
);
test!(
map_get_nested_key_does_not_exist,
"a {\n color: map-get((a: (b: (c: d))), a, d, e, f);\n}\n",
""
);
test!(
map_get_nested_non_map,
"a {\n color: map-get((a: (b: c)), a, b, c, d);\n}\n",
""
);
error!(
map_get_no_args,
"a {\n color: map-get();\n}\n", "Error: Missing argument $key."
);
error!(
map_get_one_arg,
"a {\n color: map-get(1);\n}\n", "Error: Missing argument $key."