FEATURE: Allow map and filter on tuples.

This commit is contained in:
Jeremy Wall 2019-01-06 16:17:00 -06:00
parent ef01f166b2
commit a8c4ce1157
3 changed files with 198 additions and 2 deletions

View File

@ -223,6 +223,86 @@ should be placed. Any primitive value can be used as an argument.
"https://@:@/" % (host, port)
```
Functional processing expressions
---------------------------------
UCG has a few functional processing expressions called `map` and `filter`. Both of
them can process a list or tuple.
Their syntax starts with either map or filter followed by a symbol that
references a valid macro and the outfield for the tuple the macro produces. and
finally an expression that resolves to either a list of tuple.
### Map expressions
Map macros should produce in the result field a value or [field, value] that
will replace the element or field it is curently processing.
**For Lists**
When mapping a macro across a list the result field can be any valid value. The
macro is expected to take a single argument.
```
let list1 = [1, 2, 3, 4];
let mapper = macro(item) => { result = item + 1 };
map mapper.result list1 == [2, 3, 4, 5];
```
**For Tuples**
Macros for mapping across a tuple are expected to take two arguments. The first
argument is the name of the field. The second argument is the value in that
field. The result field should be a two item list with the first item being the
new field name and the second item being the new value.
```
let test_tpl = {
foo = "bar",
quux = "baz",
};
let tpl_mapper = macro(name, val) => {
result = select name, [name, val], {
"foo" = ["foo", "barbar"],
quux = ["cute", "pygmy"],
},
};
map tpl_mapper.result test_tpl == {foo = "barbar", cute = "pygmy"};
```
### Filter expressions
Filter expressions should return a result field with false or NULL for items to
filter out of the list or tuple. Any other value in the return field results in
the item or field staying in the resulting list or tuple.
**Lists**
```
let list2 = ["foo", "bar", "foo", "bar"];
let filtrator = macro(item) => {
result = select item, NULL, {
foo = item,
},
};
filter filtrator.result list2 == ["foo", "foo"];
```
**Tuples**
```
let test_tpl = {
foo = "bar",
quux = "baz",
};
let tpl_filter = macro(name, val) => {
result = name != "foo",
};
filter tpl_filter.result test_tpl == { quux = "baz" };
```
Include expressions
-------------------

View File

@ -13,7 +13,7 @@ let boolfiltrator = macro(item) => {
};
assert |
map mapper.result list1 == [2, 3, 4, 5];
map mapper.result list1 == [2, 3, 4, 5];
|;
assert |
(map mapper.result [1, 2, 3, 4]) == [2, 3, 4, 5];
@ -33,4 +33,46 @@ assert |
|;
assert |
filter boolfiltrator.result [1, 2, 3, 4, 5, 6, 7] == [1, 2, 3, 4];
|;
// Tuple processing
let test_tpl = {
foo = "bar",
quux = "baz",
};
let identity_tpl_mapper = macro(name, val) => {
result = [name, val],
};
assert |
map identity_tpl_mapper.result test_tpl == test_tpl;
|;
let tpl_mapper = macro(name, val) => {
result = select name, [name, val], {
"foo" = ["foo", "barbar"],
quux = ["cute", "pygmy"],
},
};
assert |
map tpl_mapper.result test_tpl == {foo = "barbar", cute = "pygmy"};
|;
let identity_tpl_filter = macro(name, val) => {
result = true,
};
// strip out foo field
let tpl_filter = macro(name, val) => {
result = name != "foo",
};
assert |
filter identity_tpl_filter.result test_tpl == test_tpl;
|;
assert |
filter tpl_filter.result test_tpl == { quux = "baz" };
|;

View File

@ -1113,7 +1113,7 @@ impl<'a> FileBuilder<'a> {
}
fn eval_functional_list_processing(
&mut self,
&self,
elems: &Vec<Rc<Val>>,
def: &MacroDef,
outfield: &PositionedItem<String>,
@ -1144,6 +1144,77 @@ impl<'a> FileBuilder<'a> {
return Ok(Rc::new(Val::List(out)));
}
fn eval_functional_tuple_processing(
&self,
fs: &Vec<(PositionedItem<String>, Rc<Val>)>,
def: &MacroDef,
outfield: &PositionedItem<String>,
typ: &ListOpType,
) -> Result<Rc<Val>, Box<dyn Error>> {
let mut out = Vec::new();
for &(ref name, ref val) in fs {
let argvals = vec![Rc::new(Val::Str(name.val.clone())), val.clone()];
let fields = def.eval(self.file.clone(), self, argvals)?;
if let Some(v) = find_in_fieldlist(&outfield.val, &fields) {
match typ {
ListOpType::Map => {
if let &Val::List(ref fs) = v.as_ref() {
if fs.len() == 2 {
// index 0 should be a string for the new field name.
// index 1 should be the val.
let new_name = if let &Val::Str(ref s) = fs[0].as_ref() {
s.clone()
} else {
return Err(Box::new(error::BuildError::new(
format!(
"map on tuple expects the first item out list to be a string but got size {}",
fs[0].type_name()
),
error::ErrorType::TypeFail,
def.pos.clone(),
)));
};
out.push((
PositionedItem::new(new_name, name.pos.clone()),
fs[1].clone(),
));
} else {
return Err(Box::new(error::BuildError::new(
format!(
"map on a tuple field expects a list of size 2 as output but got size {}",
fs.len()
),
error::ErrorType::TypeFail,
def.pos.clone(),
)));
}
} else {
return Err(Box::new(error::BuildError::new(
format!(
"map on a tuple field expects a list as output but got {:?}",
v.type_name()
),
error::ErrorType::TypeFail,
def.pos.clone(),
)));
}
}
ListOpType::Filter => {
if let &Val::Empty = v.as_ref() {
// noop
continue;
} else if let &Val::Boolean(false) = v.as_ref() {
// noop
continue;
}
out.push((name.clone(), val.clone()));
}
}
}
}
Ok(Rc::new(Val::Tuple(out)))
}
fn eval_functional_processing(
&mut self,
def: &ListOpDef,
@ -1165,6 +1236,9 @@ impl<'a> FileBuilder<'a> {
&Val::List(ref elems) => {
self.eval_functional_list_processing(elems, macdef, &def.field, &def.typ)
}
&Val::Tuple(ref fs) => {
self.eval_functional_tuple_processing(fs, macdef, &def.field, &def.typ)
}
other => Err(Box::new(error::BuildError::new(
format!("Expected List as target but got {:?}", other.type_name()),
error::ErrorType::TypeFail,