use serde::Serialize;
use crate::{
AWSDate, AWSDateTime, AWSEmail, AWSPhone, AWSTime, AWSTimestamp, AWSUrl, AppsyncError, ID,
};
pub trait IFSValueMarker: private::Sealed + Serialize {
fn to_value(&self) -> serde_json::Value {
serde_json::to_value(self).expect("cannot fail for IFSValueMarker types")
}
}
pub trait IFSBValueMarker: private::Sealed + Serialize {
fn to_value(&self) -> serde_json::Value {
serde_json::to_value(self).expect("cannot fail for IFSBValueMarker types")
}
}
mod private {
pub trait Sealed {}
}
macro_rules! impl_markers {
(nested $tr:ty, ($($t:ty),+)) => {
$(impl $tr for $t {})+
};
($($tr:ty),+| $t:tt) => {
$(impl_markers!(nested $tr, $t);)+
}
}
impl_markers!(
IFSBValueMarker,
private::Sealed
| (
u8,
i8,
u16,
i16,
u32,
i32,
u64,
i64,
u128,
i128,
f32,
f64,
bool,
String,
&str,
ID,
AWSEmail,
AWSUrl,
AWSDate,
AWSTime,
AWSPhone,
AWSDateTime,
AWSTimestamp
)
);
impl_markers!(
IFSValueMarker
| (
u8,
i8,
u16,
i16,
u32,
i32,
u64,
i64,
u128,
i128,
f32,
f64,
String,
&str,
ID,
AWSEmail,
AWSUrl,
AWSDate,
AWSTime,
AWSPhone,
AWSDateTime,
AWSTimestamp
)
);
#[derive(Debug, Clone, PartialEq)]
pub struct FixedVec<T, const N: usize>([Option<T>; N]);
impl<T: Serialize, const N: usize> Serialize for FixedVec<T, N> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.collect_seq(self.0.iter().flatten())
}
}
impl<T: IFSValueMarker, const N: usize> FixedVec<T, N> {
fn to_value(&self) -> serde_json::Value {
serde_json::to_value(self).expect("cannot fail for IFSValueMarker types")
}
}
type InVec<T> = FixedVec<T, 5>;
type ContainsAnyVec<T> = FixedVec<T, 20>;
macro_rules! impl_from_array {
(none 5) => {
[None, None, None, None, None]
};
(none 10) => {
[None, None, None, None, None, None, None, None, None, None]
};
(none 20) => {
[
None, None, None, None, None, None, None, None, None, None, None, None, None, None,
None, None, None, None, None, None,
]
};
($m:tt; $n:literal; $(($idx:tt, $v:ident)),*) => {
impl<T> From<[T; $n]> for FixedVec<T, $m> {
fn from([$($v),*]: [T; $n]) -> Self {
let mut slice = impl_from_array!(none $m);
$((slice[$idx]).replace($v);)*
Self(slice)
}
}
};
}
impl_from_array!(5; 1; (0, v1));
impl_from_array!(5; 2; (0, v1), (1, v2));
impl_from_array!(5; 3; (0, v1), (1, v2), (2, v3));
impl_from_array!(5; 4; (0, v1), (1, v2), (2, v3), (3, v4));
impl_from_array!(5; 5; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5));
impl_from_array!(10; 1; (0, v1));
impl_from_array!(10; 2; (0, v1), (1, v2));
impl_from_array!(10; 3; (0, v1), (1, v2), (2, v3));
impl_from_array!(10; 4; (0, v1), (1, v2), (2, v3), (3, v4));
impl_from_array!(10; 5; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5));
impl_from_array!(10; 6; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6));
impl_from_array!(10; 7; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6), (6, v7));
impl_from_array!(10; 8; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6), (6, v7), (7, v8));
impl_from_array!(10; 9; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6), (6, v7), (7, v8), (8, v9));
impl_from_array!(10; 10; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6), (6, v7), (7, v8), (8, v9), (9, v10));
impl_from_array!(20; 1; (0, v1));
impl_from_array!(20; 2; (0, v1), (1, v2));
impl_from_array!(20; 3; (0, v1), (1, v2), (2, v3));
impl_from_array!(20; 4; (0, v1), (1, v2), (2, v3), (3, v4));
impl_from_array!(20; 5; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5));
impl_from_array!(20; 6; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6));
impl_from_array!(20; 7; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6), (6, v7));
impl_from_array!(20; 8; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6), (6, v7), (7, v8));
impl_from_array!(20; 9; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6), (6, v7), (7, v8), (8, v9));
impl_from_array!(20; 10; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6), (6, v7), (7, v8), (8, v9), (9, v10));
impl_from_array!(20; 11; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6), (6, v7), (7, v8), (8, v9), (9, v10), (10, v11));
impl_from_array!(20; 12; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6), (6, v7), (7, v8), (8, v9), (9, v10), (10, v11), (11, v12));
impl_from_array!(20; 13; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6), (6, v7), (7, v8), (8, v9), (9, v10), (10, v11), (11, v12), (12, v13));
impl_from_array!(20; 14; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6), (6, v7), (7, v8), (8, v9), (9, v10), (10, v11), (11, v12), (12, v13), (13, v14));
impl_from_array!(20; 15; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6), (6, v7), (7, v8), (8, v9), (9, v10), (10, v11), (11, v12), (12, v13), (13, v14), (14, v15));
impl_from_array!(20; 16; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6), (6, v7), (7, v8), (8, v9), (9, v10), (10, v11), (11, v12), (12, v13), (13, v14), (14, v15), (15, v16));
impl_from_array!(20; 17; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6), (6, v7), (7, v8), (8, v9), (9, v10), (10, v11), (11, v12), (12, v13), (13, v14), (14, v15), (15, v16), (16, v17));
impl_from_array!(20; 18; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6), (6, v7), (7, v8), (8, v9), (9, v10), (10, v11), (11, v12), (12, v13), (13, v14), (14, v15), (15, v16), (16, v17), (17, v18));
impl_from_array!(20; 19; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6), (6, v7), (7, v8), (8, v9), (9, v10), (10, v11), (11, v12), (12, v13), (13, v14), (14, v15), (15, v16), (16, v17), (17, v18), (18, v19));
impl_from_array!(20; 20; (0, v1), (1, v2), (2, v3), (3, v4), (4, v5), (5, v6), (6, v7), (7, v8), (8, v9), (9, v10), (10, v11), (11, v12), (12, v13), (13, v14), (14, v15), (15, v16), (16, v17), (17, v18), (18, v19), (19, v20));
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(transparent)]
pub struct FieldPath(String);
impl std::fmt::Display for FieldPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl FieldPath {
pub fn new(path: impl Into<String>) -> Result<Self, AppsyncError> {
let path = path.into();
if path.len() > 256 {
return Err(AppsyncError::new(
"ValidationError",
"Field path exceeds 256 characters",
));
}
Ok(Self(path))
}
pub unsafe fn new_unchecked(path: impl Into<String>) -> Self {
Self(path.into())
}
pub fn eq<IFSB: IFSBValueMarker>(self, ifsb: IFSB) -> FieldFilter {
FieldFilter::new(self, ifsb.to_value(), FilterOp::Eq)
}
pub unsafe fn eq_unchecked<T: Serialize>(self, value: T) -> FieldFilter {
FieldFilter::new(self, serde_json::to_value(value).unwrap(), FilterOp::Eq)
}
pub fn ne<IFSB: IFSBValueMarker>(self, ifsb: IFSB) -> FieldFilter {
FieldFilter::new(self, ifsb.to_value(), FilterOp::Ne)
}
pub unsafe fn ne_unchecked<T: Serialize>(self, value: T) -> FieldFilter {
FieldFilter::new(self, serde_json::to_value(value).unwrap(), FilterOp::Ne)
}
pub fn le<IFS: IFSValueMarker>(self, ifs: IFS) -> FieldFilter {
FieldFilter::new(self, ifs.to_value(), FilterOp::Le)
}
pub unsafe fn le_unchecked<T: Serialize>(self, value: T) -> FieldFilter {
FieldFilter::new(self, serde_json::to_value(value).unwrap(), FilterOp::Le)
}
pub fn lt<IFS: IFSValueMarker>(self, ifs: IFS) -> FieldFilter {
FieldFilter::new(self, ifs.to_value(), FilterOp::Lt)
}
pub unsafe fn lt_unchecked<T: Serialize>(self, value: T) -> FieldFilter {
FieldFilter::new(self, serde_json::to_value(value).unwrap(), FilterOp::Lt)
}
pub fn ge<IFS: IFSValueMarker>(self, ifs: IFS) -> FieldFilter {
FieldFilter::new(self, ifs.to_value(), FilterOp::Ge)
}
pub unsafe fn ge_unchecked<T: Serialize>(self, value: T) -> FieldFilter {
FieldFilter::new(self, serde_json::to_value(value).unwrap(), FilterOp::Ge)
}
pub fn gt<IFS: IFSValueMarker>(self, ifs: IFS) -> FieldFilter {
FieldFilter::new(self, ifs.to_value(), FilterOp::Gt)
}
pub unsafe fn gt_unchecked<T: Serialize>(self, value: T) -> FieldFilter {
FieldFilter::new(self, serde_json::to_value(value).unwrap(), FilterOp::Gt)
}
pub fn contains<IFS: IFSValueMarker>(self, ifs: IFS) -> FieldFilter {
FieldFilter::new(self, ifs.to_value(), FilterOp::Contains)
}
pub unsafe fn contains_unchecked<T: Serialize>(self, value: T) -> FieldFilter {
FieldFilter::new(
self,
serde_json::to_value(value).unwrap(),
FilterOp::Contains,
)
}
pub fn not_contains<IFS: IFSValueMarker>(self, ifs: IFS) -> FieldFilter {
FieldFilter::new(self, ifs.to_value(), FilterOp::NotContains)
}
pub unsafe fn not_contains_unchecked<T: Serialize>(self, value: T) -> FieldFilter {
FieldFilter::new(
self,
serde_json::to_value(value).unwrap(),
FilterOp::NotContains,
)
}
pub fn begins_with(self, value: impl Into<String>) -> FieldFilter {
FieldFilter::new(
self,
serde_json::Value::String(value.into()),
FilterOp::BeginsWith,
)
}
pub fn in_values<IFS: IFSValueMarker>(self, values: impl Into<InVec<IFS>>) -> FieldFilter {
let in_vec = values.into();
FieldFilter::new(self, in_vec.to_value(), FilterOp::In)
}
pub unsafe fn in_values_unchecked<T: Serialize>(
self,
values: impl Into<InVec<T>>,
) -> FieldFilter {
let in_vec = values.into();
FieldFilter::new(self, serde_json::to_value(in_vec).unwrap(), FilterOp::In)
}
pub fn not_in<IFS: IFSValueMarker>(self, values: impl Into<InVec<IFS>>) -> FieldFilter {
let in_vec = values.into();
FieldFilter::new(self, in_vec.to_value(), FilterOp::NotIn)
}
pub unsafe fn not_in_unchecked<T: Serialize>(self, values: impl Into<InVec<T>>) -> FieldFilter {
let in_vec = values.into();
FieldFilter::new(self, serde_json::to_value(in_vec).unwrap(), FilterOp::NotIn)
}
pub fn between<IFS: IFSValueMarker>(self, start: IFS, end: IFS) -> FieldFilter {
FieldFilter::new(
self,
FixedVec([Some(start), Some(end)]).to_value(),
FilterOp::Between,
)
}
pub unsafe fn between_unchecked<T: Serialize>(self, start: T, end: T) -> FieldFilter {
FieldFilter::new(
self,
serde_json::to_value(FixedVec([Some(start), Some(end)])).unwrap(),
FilterOp::Between,
)
}
pub fn contains_any<IFS: IFSValueMarker>(
self,
values: impl Into<ContainsAnyVec<IFS>>,
) -> FieldFilter {
let contains_vec = values.into();
FieldFilter::new(self, contains_vec.to_value(), FilterOp::ContainsAny)
}
pub unsafe fn contains_any_unchecked<T: Serialize>(
self,
values: impl Into<ContainsAnyVec<T>>,
) -> FieldFilter {
let contains_vec = values.into();
FieldFilter::new(
self,
serde_json::to_value(contains_vec).unwrap(),
FilterOp::ContainsAny,
)
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
enum FilterOp {
Eq,
Ne,
Le,
Lt,
Ge,
Gt,
Contains,
NotContains,
BeginsWith,
In,
NotIn,
Between,
ContainsAny,
}
#[derive(Debug, Clone, Serialize)]
pub struct FieldFilter {
#[serde(rename = "fieldName")]
path: FieldPath,
operator: FilterOp,
value: serde_json::Value,
}
impl FieldFilter {
fn new(path: FieldPath, value: serde_json::Value, operator: FilterOp) -> Self {
Self {
path,
value,
operator,
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct Filter {
filters: FixedVec<FieldFilter, 5>,
}
impl<T> From<T> for Filter
where
T: Into<FixedVec<FieldFilter, 5>>,
{
fn from(filters: T) -> Self {
Self {
filters: filters.into(),
}
}
}
impl From<FieldFilter> for Filter {
fn from(value: FieldFilter) -> Self {
Filter::from([value])
}
}
#[derive(Debug, Clone, Serialize)]
pub struct FilterGroup {
#[serde(rename = "filterGroup")]
filters: FixedVec<Filter, 10>,
}
impl<T> From<T> for FilterGroup
where
T: Into<FixedVec<Filter, 10>>,
{
fn from(filters: T) -> Self {
Self {
filters: filters.into(),
}
}
}
impl From<FieldFilter> for FilterGroup {
fn from(value: FieldFilter) -> Self {
FilterGroup::from(Filter::from([value]))
}
}
impl From<Filter> for FilterGroup {
fn from(value: Filter) -> Self {
FilterGroup::from([value])
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn filter_value(f: &FieldFilter) -> serde_json::Value {
serde_json::to_value(f).unwrap()
}
#[test]
fn test_create_paths() {
let path = FieldPath::new("user.name").unwrap();
assert_eq!(path.to_string(), "user.name");
let path = FieldPath::new("nested.one.two.three.four.five");
assert!(path.is_ok());
let long_path = "a".repeat(257);
assert!(FieldPath::new(long_path).is_err());
}
#[test]
fn test_eq_operator() {
let filter = FieldPath::new("service").unwrap().eq("AWS AppSync");
assert_eq!(
filter_value(&filter),
json!({
"fieldName": "service",
"operator": "eq",
"value": "AWS AppSync"
})
);
let filter = FieldPath::new("severity").unwrap().eq(5);
assert_eq!(
filter_value(&filter),
json!({
"fieldName": "severity",
"operator": "eq",
"value": 5
})
);
let filter = FieldPath::new("enabled").unwrap().eq(true);
assert_eq!(
filter_value(&filter),
json!({
"fieldName": "enabled",
"operator": "eq",
"value": true
})
);
}
#[test]
fn test_ne_operator() {
let filter = FieldPath::new("service").unwrap().ne("AWS AppSync");
assert_eq!(
filter_value(&filter),
json!({
"fieldName": "service",
"operator": "ne",
"value": "AWS AppSync"
})
);
let filter = FieldPath::new("severity").unwrap().ne(5);
assert_eq!(
filter_value(&filter),
json!({
"fieldName": "severity",
"operator": "ne",
"value": 5
})
);
let filter = FieldPath::new("enabled").unwrap().ne(true);
assert_eq!(
filter_value(&filter),
json!({
"fieldName": "enabled",
"operator": "ne",
"value": true
})
);
}
#[test]
fn test_comparison_operators() {
let path = FieldPath::new("size").unwrap();
let filter = path.clone().le(5);
assert_eq!(
filter_value(&filter),
json!({
"fieldName": "size",
"operator": "le",
"value": 5
})
);
let filter = path.clone().lt(5);
assert_eq!(
filter_value(&filter),
json!({
"fieldName": "size",
"operator": "lt",
"value": 5
})
);
let filter = path.clone().ge(5);
assert_eq!(
filter_value(&filter),
json!({
"fieldName": "size",
"operator": "ge",
"value": 5
})
);
let filter = path.gt(5);
assert_eq!(
filter_value(&filter),
json!({
"fieldName": "size",
"operator": "gt",
"value": 5
})
);
}
#[test]
fn test_contains_operators() {
let path = FieldPath::new("seats").unwrap();
let filter = path.clone().contains(10);
assert_eq!(
filter_value(&filter),
json!({
"fieldName": "seats",
"operator": "contains",
"value": 10
})
);
let event_path = FieldPath::new("event").unwrap();
let filter = event_path.clone().contains("launch");
assert_eq!(
filter_value(&filter),
json!({
"fieldName": "event",
"operator": "contains",
"value": "launch"
})
);
let filter = path.not_contains(10);
assert_eq!(
filter_value(&filter),
json!({
"fieldName": "seats",
"operator": "notContains",
"value": 10
})
);
}
#[test]
fn test_begins_with() {
let filter = FieldPath::new("service").unwrap().begins_with("AWS");
assert_eq!(
filter_value(&filter),
json!({
"fieldName": "service",
"operator": "beginsWith",
"value": "AWS"
})
);
}
#[test]
fn test_in_operators() {
let path = FieldPath::new("severity").unwrap();
let filter = path.clone().in_values([1, 2, 3]);
assert_eq!(
filter_value(&filter),
json!({
"fieldName": "severity",
"operator": "in",
"value": [1, 2, 3]
})
);
let filter = path.not_in([1, 2, 3]);
assert_eq!(
filter_value(&filter),
json!({
"fieldName": "severity",
"operator": "notIn",
"value": [1, 2, 3]
})
);
}
#[test]
fn test_between() {
let filter = FieldPath::new("severity").unwrap().between(1, 5);
assert_eq!(
filter_value(&filter),
json!({
"fieldName": "severity",
"operator": "between",
"value": [1, 5]
})
);
}
#[test]
fn test_contains_any() {
let filter = FieldPath::new("seats").unwrap().contains_any([10, 15, 20]);
assert_eq!(
filter_value(&filter),
json!({
"fieldName": "seats",
"operator": "containsAny",
"value": [10, 15, 20]
})
);
}
#[test]
fn test_filter_group() {
let group = FilterGroup::from([Filter::from([
FieldPath::new("userId").unwrap().eq(1),
FieldPath::new("group")
.unwrap()
.in_values(["Admin", "Developer"]),
])]);
assert_eq!(
serde_json::to_value(group).unwrap(),
json!({
"filterGroup": [
{
"filters": [
{
"fieldName": "userId",
"operator": "eq",
"value": 1
},
{
"fieldName": "group",
"operator": "in",
"value": ["Admin", "Developer"]
}
]
}
]
})
);
}
#[test]
fn test_complex_filter() {
let group = FilterGroup::from([
Filter::from([
FieldPath::new("severity").unwrap().le(3),
FieldPath::new("type").unwrap().eq("error"),
]),
Filter::from([
FieldPath::new("service").unwrap().begins_with("AWS"),
FieldPath::new("region")
.unwrap()
.in_values(["us-east-1", "eu-west-1"]),
]),
]);
assert_eq!(
serde_json::to_value(group).unwrap(),
json!({
"filterGroup": [
{
"filters": [
{
"fieldName": "severity",
"operator": "le",
"value": 3
},
{
"fieldName": "type",
"operator": "eq",
"value": "error"
}
]
},
{
"filters": [
{
"fieldName": "service",
"operator": "beginsWith",
"value": "AWS"
},
{
"fieldName": "region",
"operator": "in",
"value": ["us-east-1", "eu-west-1"]
}
]
}
]
})
);
}
}