use crate::{
runtime::{
coercion::CoerceArguments, value::Value, Context, Prelude, RuntimeError, RuntimeErrorKind,
ValueHandler,
},
Location,
};
use base64::Engine;
use std::iter::zip;
use std::rc::Rc;
use super::coercion::Coerce;
use crate::runtime::value::{Array, InternalValue, Object};
use base64::engine::general_purpose;
fn concat(location: Location, _: &dyn Context, arguments: &[Value]) -> Result<Value, RuntimeError> {
let (a, b): (String, String) = arguments.coerce_arguments(location)?;
let b = b.as_str();
Ok(Value::string(a + b))
}
fn subtract(
location: Location,
context: &dyn Context,
arguments: &[Value],
) -> Result<Value, RuntimeError> {
let (container, content) = match arguments {
[container, content] => Ok((container, content)),
[] => Err(RuntimeError {
location,
kind: RuntimeErrorKind::NotEnoughArguments,
}),
_ => Err(RuntimeError {
location,
kind: RuntimeErrorKind::TooManyArguments,
}),
}?;
let mut container = container.clone();
if matches!(container.internal, InternalValue::Reference(_)) {
if let Some(detached) = container.to_value_handler(context).and_then(|h| h.detach()) {
container = detached;
}
}
let content = content.clone();
match (&container.internal, &content.internal) {
(InternalValue::Array(first), InternalValue::Array(second)) => {
subtract_array_array(first, second)
}
(InternalValue::Object(first), InternalValue::Array(second)) => {
subtract_object_array(location, first, second)
}
(InternalValue::Object(first), InternalValue::Object(second)) => {
subtract_object_object(first, second)
}
(_, _) => Err(RuntimeError {
location,
kind: RuntimeErrorKind::TypeMismatch,
}),
}
}
fn subtract_array_array(first: &Rc<Array>, second: &Rc<Array>) -> Result<Value, RuntimeError> {
let result = first
.iter()
.filter(|x| second.iter().all(|y| !compare_as_strs(x, y)))
.cloned()
.collect();
Ok(Value::array(result))
}
fn cast_string_array(location: Location, array: &Rc<Array>) -> Result<Vec<String>, RuntimeError> {
let mut result = Vec::new();
for value in array.iter() {
if let Some(value) = value.cast() {
result.push(value);
} else {
return Err(RuntimeError {
location,
kind: RuntimeErrorKind::TypeMismatch,
});
}
}
Ok(result)
}
fn subtract_object_array(
location: Location,
first: &Rc<Object>,
second: &Rc<Array>,
) -> Result<Value, RuntimeError> {
let second = cast_string_array(location, second)?;
let result = first
.iter()
.filter(|(key, _)| !second.contains(key))
.map(|(key, value)| (key.clone(), value.clone()))
.collect();
Ok(Value::object(result))
}
fn subtract_object_object(first: &Rc<Object>, second: &Rc<Object>) -> Result<Value, RuntimeError> {
let result = first
.iter()
.filter(|(key, first)| {
second
.get(*key)
.map(|second| !compare_as_strs(first, second))
.unwrap_or(true)
})
.map(|(key, value)| (key.clone(), value.clone()))
.collect();
Ok(Value::object(result))
}
fn compare_as_strs(first: &Value, second: &Value) -> bool {
match (&first.internal, &second.internal) {
(InternalValue::Object(first), InternalValue::Object(second)) => {
first.len() == second.len()
&& first.iter().all(|(key, first)| {
second
.get(key)
.map(|second| compare_as_strs(first, second))
.unwrap_or(false)
})
}
(InternalValue::Array(first), InternalValue::Array(second)) => {
first.len() == second.len()
&& zip(first.iter(), second.iter())
.all(|(first, second)| compare_as_strs(first, second))
}
_ => {
let first: Option<String> = first.cast();
let second: Option<String> = second.cast();
first == second
}
}
}
fn contains(
location: Location,
_: &dyn Context,
arguments: &[Value],
) -> Result<Value, RuntimeError> {
match arguments {
[container, content] => {
let result = if let Some(container) = container.as_str() {
let pattern: String = content.coerce(location)?;
container.contains(&pattern)
} else if let Some(array) = container.as_slice() {
array.contains(content)
} else {
let container: String = container.coerce(location)?;
let pattern: String = content.coerce(location)?;
container.contains(&pattern)
};
Ok(Value::bool(result))
}
[] => Err(RuntimeError {
location,
kind: RuntimeErrorKind::NotEnoughArguments,
}),
_ => Err(RuntimeError {
location,
kind: RuntimeErrorKind::TooManyArguments,
}),
}
}
fn lower(location: Location, _: &dyn Context, arguments: &[Value]) -> Result<Value, RuntimeError> {
match arguments {
[text] => {
if text.is_null() {
Ok(Value::null())
} else {
let text: String = text.coerce(location)?;
Ok(Value::string(text.to_lowercase()))
}
}
[] => Err(RuntimeError {
location,
kind: RuntimeErrorKind::NotEnoughArguments,
}),
_ => Err(RuntimeError {
location,
kind: RuntimeErrorKind::TooManyArguments,
}),
}
}
fn trim(location: Location, _: &dyn Context, arguments: &[Value]) -> Result<Value, RuntimeError> {
let (s,): (String,) = arguments.coerce_arguments(location)?;
Ok(Value::string(s.trim().to_string()))
}
fn uuid_v4(
location: Location,
_: &dyn Context,
arguments: &[Value],
) -> Result<Value, RuntimeError> {
if !arguments.is_empty() {
return Err(RuntimeError {
location,
kind: super::RuntimeErrorKind::TooManyArguments,
});
}
let mut bytes = [0u8; 16];
getrandom::getrandom(&mut bytes).map_err(|_| RuntimeError {
location,
kind: RuntimeErrorKind::UnavailableRandomGenerator,
})?;
let mut builder = uuid::Builder::from_bytes(bytes);
let uuid = builder
.set_variant(uuid::Variant::RFC4122)
.set_version(uuid::Version::Random)
.as_uuid();
Ok(Value::string(uuid.to_string()))
}
fn size_of(
location: Location,
context: &dyn Context,
arguments: &[Value],
) -> Result<Value, RuntimeError> {
let object = arguments.first().ok_or(RuntimeError {
location,
kind: RuntimeErrorKind::TooManyArguments,
})?;
let size = object
.to_value_handler(context)
.and_then(|vh| vh.size())
.ok_or(RuntimeError {
location,
kind: RuntimeErrorKind::UnavailableSize,
})?;
Ok(Value::number(size as f64))
}
fn is_empty(
location: Location,
_: &dyn Context,
arguments: &[Value],
) -> Result<Value, RuntimeError> {
let object = arguments.first().ok_or(RuntimeError {
location,
kind: RuntimeErrorKind::NotEnoughArguments,
})?;
Ok(Value::bool(object.is_empty()))
}
fn split_by(
location: Location,
_: &dyn Context,
arguments: &[Value],
) -> Result<Value, RuntimeError> {
match arguments {
[text, separator] => {
if text.is_null() {
Ok(Value::null())
} else {
let text: String = text.coerce(location)?;
let separator: String = separator.coerce(location)?;
if separator.is_empty() {
let s = text.chars().map(|s| Value::string(s.to_string())).collect();
Ok(Value::array(s))
} else {
let s = text
.split(&separator)
.map(|s| Value::string(s.to_string()))
.collect();
Ok(Value::array(s))
}
}
}
[] => Err(RuntimeError {
location,
kind: RuntimeErrorKind::NotEnoughArguments,
}),
_ => Err(RuntimeError {
location,
kind: RuntimeErrorKind::TooManyArguments,
}),
}
}
fn substring_after(
location: Location,
_: &dyn Context,
arguments: &[Value],
) -> Result<Value, RuntimeError> {
match arguments {
[text, separator] => {
if text.is_null() {
Ok(Value::null())
} else {
let text: String = text.coerce(location)?;
let separator: String = separator.coerce(location)?;
if let Some((_, result)) = text.split_once(&separator) {
Ok(Value::string(result.to_string()))
} else {
Ok(Value::string("".to_string()))
}
}
}
[] => Err(RuntimeError {
location,
kind: RuntimeErrorKind::NotEnoughArguments,
}),
_ => Err(RuntimeError {
location,
kind: RuntimeErrorKind::TooManyArguments,
}),
}
}
fn substring_after_last(
location: Location,
_: &dyn Context,
arguments: &[Value],
) -> Result<Value, RuntimeError> {
match arguments {
[text, separator] => {
if text.is_null() {
Ok(Value::null())
} else {
let text: String = text.coerce(location)?;
let separator: String = separator.coerce(location)?;
if let Some((_, result)) = text.rsplit_once(&separator) {
Ok(Value::string(result.to_string()))
} else {
Ok(Value::string("".to_string()))
}
}
}
[] => Err(RuntimeError {
location,
kind: RuntimeErrorKind::NotEnoughArguments,
}),
_ => Err(RuntimeError {
location,
kind: RuntimeErrorKind::TooManyArguments,
}),
}
}
fn substring_before(
location: Location,
_: &dyn Context,
arguments: &[Value],
) -> Result<Value, RuntimeError> {
match arguments {
[text, separator] => {
if text.is_null() {
Ok(Value::null())
} else {
let text: String = text.coerce(location)?;
let separator: String = separator.coerce(location)?;
if let Some((result, _)) = text.split_once(&separator) {
Ok(Value::string(result.to_string()))
} else {
Ok(Value::string("".to_string()))
}
}
}
[] => Err(RuntimeError {
location,
kind: RuntimeErrorKind::NotEnoughArguments,
}),
_ => Err(RuntimeError {
location,
kind: RuntimeErrorKind::TooManyArguments,
}),
}
}
fn substring_before_last(
location: Location,
_: &dyn Context,
arguments: &[Value],
) -> Result<Value, RuntimeError> {
match arguments {
[text, separator] => {
if text.is_null() {
Ok(Value::null())
} else {
let text: String = text.coerce(location)?;
let separator: String = separator.coerce(location)?;
if separator.is_empty() && !text.is_empty() {
Ok(Value::string(text[0..text.len() - 1].to_string()))
} else if let Some((result, _)) = text.rsplit_once(&separator) {
Ok(Value::string(result.to_string()))
} else {
Ok(Value::string("".to_string()))
}
}
}
[] => Err(RuntimeError {
location,
kind: RuntimeErrorKind::NotEnoughArguments,
}),
_ => Err(RuntimeError {
location,
kind: RuntimeErrorKind::TooManyArguments,
}),
}
}
fn upper(location: Location, _: &dyn Context, arguments: &[Value]) -> Result<Value, RuntimeError> {
match arguments {
[text] => {
if text.is_null() {
Ok(Value::null())
} else {
let text: String = text.coerce(location)?;
Ok(Value::string(text.to_uppercase()))
}
}
[] => Err(RuntimeError {
location,
kind: RuntimeErrorKind::NotEnoughArguments,
}),
_ => Err(RuntimeError {
location,
kind: RuntimeErrorKind::TooManyArguments,
}),
}
}
fn to_base64(
location: Location,
_: &dyn Context,
arguments: &[Value],
) -> Result<Value, RuntimeError> {
match arguments {
[binary] => {
let bytes: Vec<u8> = binary.coerce(location)?;
Ok(Value::string(general_purpose::STANDARD.encode(bytes)))
}
[] => Err(RuntimeError {
location,
kind: RuntimeErrorKind::NotEnoughArguments,
}),
_ => Err(RuntimeError {
location,
kind: RuntimeErrorKind::TooManyArguments,
}),
}
}
fn from_base64(
location: Location,
_: &dyn Context,
arguments: &[Value],
) -> Result<Value, RuntimeError> {
match arguments {
[text] => {
let text: String = text.coerce(location)?;
let unpadded_text: String = text.trim_end_matches('=').to_string();
Ok(Value::binary(
general_purpose::STANDARD_NO_PAD
.decode(&unpadded_text)
.map_err(|_| RuntimeError {
location,
kind: RuntimeErrorKind::TypeMismatch,
})?,
))
}
[] => Err(RuntimeError {
location,
kind: RuntimeErrorKind::NotEnoughArguments,
}),
_ => Err(RuntimeError {
location,
kind: RuntimeErrorKind::TooManyArguments,
}),
}
}
fn to_string(
location: Location,
_: &dyn Context,
arguments: &[Value],
) -> Result<Value, RuntimeError> {
match arguments {
[binary, encoding] => {
let encoding: String = encoding.coerce(location)?;
if !encoding.eq_ignore_ascii_case("UTF-8") {
return Err(RuntimeError {
location,
kind: RuntimeErrorKind::TypeMismatch,
});
};
let coerced: String = binary.coerce(location)?;
Ok(Value::string(coerced))
}
[] | [_] => Err(RuntimeError {
location,
kind: RuntimeErrorKind::NotEnoughArguments,
}),
_ => Err(RuntimeError {
location,
kind: RuntimeErrorKind::TooManyArguments,
}),
}
}
fn to_binary(
location: Location,
_: &dyn Context,
arguments: &[Value],
) -> Result<Value, RuntimeError> {
match arguments {
[text, encoding] => {
let text: String = text.coerce(location)?;
let encoding: String = encoding.coerce(location)?;
if !encoding.eq_ignore_ascii_case("UTF-8") {
return Err(RuntimeError {
location,
kind: RuntimeErrorKind::TypeMismatch,
});
};
Ok(Value::binary(text.into_bytes()))
}
[] | [_] => Err(RuntimeError {
location,
kind: RuntimeErrorKind::NotEnoughArguments,
}),
_ => Err(RuntimeError {
location,
kind: RuntimeErrorKind::TooManyArguments,
}),
}
}
type PreludeFunction = fn(Location, &dyn Context, &[Value]) -> Result<Value, RuntimeError>;
static PRELUDE: &[(&str, PreludeFunction)] = &[
("++", concat),
("--", subtract),
("contains", contains),
("lower", lower),
("sizeOf", size_of),
("splitBy", split_by),
("substringAfter", substring_after),
("substringAfterLast", substring_after_last),
("substringBefore", substring_before),
("substringBeforeLast", substring_before_last),
("toBase64", to_base64),
("fromBase64", from_base64),
("toString", to_string),
("toBinary", to_binary),
("trim", trim),
("upper", upper),
("uuid", uuid_v4),
("isEmpty", is_empty),
];
pub fn prelude() -> Prelude {
PRELUDE
.iter()
.map(|(key, function)| (*key, Value::function_from_fn(function)))
.collect()
}
#[cfg(test)]
mod tests {
use crate::runtime::{Binding, ValueHandler};
use super::{concat, split_by, subtract, trim, Context, Location, Value};
struct TestContext;
impl Context for TestContext {
fn resolve(&self, _: &crate::expression::Symbol) -> Binding {
unreachable!()
}
fn value_handler(&self, _reference: crate::Reference) -> Option<&dyn ValueHandler> {
unreachable!()
}
}
const LOCATION: Location = Location {
start: 100,
end: 200,
};
const CONTEXT: &dyn Context = &TestContext;
#[test]
fn trim_string() {
let result = trim(
LOCATION,
CONTEXT,
&[Value::string(" hello world ".to_string())],
)
.unwrap();
assert_eq!("hello world", result.as_str().unwrap());
}
#[test]
fn concat_strings() {
let result = concat(
LOCATION,
CONTEXT,
&[
Value::string("hello ".to_string()),
Value::string("world".to_string()),
],
)
.unwrap();
assert_eq!("hello world", result.as_str().unwrap());
}
#[test]
fn concat_numbers() {
let result = concat(
LOCATION,
CONTEXT,
&[Value::number(15.04), Value::number(12.01)],
)
.unwrap();
assert_eq!("15.0412.01", result.as_str().unwrap());
}
#[test]
fn concat_booleans() {
let result = concat(LOCATION, CONTEXT, &[Value::bool(true), Value::bool(false)]).unwrap();
assert_eq!("truefalse", result.as_str().unwrap());
}
#[test]
fn fail_when_concat_null() {
let result = concat(LOCATION, CONTEXT, &[Value::null(), Value::bool(false)]);
assert!(result.is_err());
}
#[test]
fn split_by_strings() {
let result = split_by(
LOCATION,
CONTEXT,
&[
Value::string("Peregrine Expression Language".to_string()),
Value::string("re".to_string()),
],
)
.unwrap();
let expected = &[
Value::string("Pe".to_string()),
Value::string("grine Exp".to_string()),
Value::string("ssion Language".to_string()),
];
assert_eq!(expected, result.as_slice().unwrap());
}
#[test]
fn subtract_object_minus_array_removes_keys() {
use std::collections::HashMap;
let mut map = HashMap::new();
map.insert("client_id".to_string(), Value::string("secret".to_string()));
map.insert("accept".to_string(), Value::string("*/*".to_string()));
let obj = Value::object(map);
let keys = Value::array(vec![Value::string("client_id".to_string())]);
let result = subtract(LOCATION, CONTEXT, &[obj, keys]).unwrap();
let o = result.as_object().unwrap();
assert!(o.get("client_id").is_none());
assert_eq!(Some(&Value::string("*/*".to_string())), o.get("accept"));
}
}