#![warn(missing_docs)]
#![warn(rustdoc::missing_crate_level_docs)]
#![cfg_attr(docsrs, deny(rustdoc::broken_intra_doc_links))]
mod aws_scalars;
mod id;
pub mod subscription_filters;
use std::{collections::HashMap, ops::BitOr};
use aws_smithy_types::error::metadata::ProvideErrorMetadata;
use serde_json::Value;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use thiserror::Error;
pub use aws_scalars::{
datetime::{AWSDate, AWSDateTime, AWSTime},
email::AWSEmail,
phone::AWSPhone,
timestamp::AWSTimestamp,
url::AWSUrl,
};
pub use id::ID;
#[doc(inline)]
pub use lambda_appsync_proc::appsync_operation;
#[doc(inline)]
pub use lambda_appsync_proc::make_appsync;
#[doc(inline)]
pub use lambda_appsync_proc::make_handlers;
#[doc(inline)]
pub use lambda_appsync_proc::make_operation;
#[doc(inline)]
pub use lambda_appsync_proc::make_types;
pub use lambda_runtime;
pub use serde;
pub use serde_json;
pub use tokio;
#[cfg(feature = "compat")]
mod compat {
pub use aws_config;
#[doc(inline)]
pub use lambda_appsync_proc::appsync_lambda_main;
}
#[cfg(feature = "compat")]
pub use compat::*;
#[cfg(feature = "log")]
pub use log;
#[cfg(feature = "env_logger")]
pub use env_logger;
#[cfg(feature = "tracing")]
pub use tracing;
#[cfg(feature = "tracing")]
pub use tracing_subscriber;
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "UPPERCASE")]
pub enum AppsyncAuthStrategy {
Allow,
Deny,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AppsyncIdentityCognito {
pub sub: String,
pub username: String,
pub issuer: String,
pub default_auth_strategy: AppsyncAuthStrategy,
pub source_ip: Vec<String>,
pub groups: Option<Vec<String>>,
pub claims: Value,
}
#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum CognitoIdentityAuthType {
Authenticated,
Unauthenticated,
}
#[derive(Debug, Deserialize)]
pub struct CognitoFederatedIdentity {
#[serde(rename = "cognitoIdentityId")]
pub identity_id: String,
#[serde(rename = "cognitoIdentityPoolId")]
pub identity_pool_id: String,
#[serde(rename = "cognitoIdentityAuthType")]
pub auth_type: CognitoIdentityAuthType,
#[serde(rename = "cognitoIdentityAuthProvider")]
pub auth_provider: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AppsyncIdentityIam {
pub account_id: String,
pub source_ip: Vec<String>,
pub username: String,
pub user_arn: String,
#[serde(flatten)]
pub federated_identity: Option<CognitoFederatedIdentity>,
}
#[derive(Debug, Deserialize)]
pub struct AppsyncIdentityOidc {
pub iss: String,
pub sub: String,
pub aud: String,
pub exp: i64,
pub iat: i64,
#[serde(flatten)]
pub additional_claims: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Deserialize)]
pub struct AppsyncIdentityLambda {
#[serde(rename = "resolverContext")]
pub resolver_context: serde_json::Value,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum AppsyncIdentity {
Cognito(AppsyncIdentityCognito),
Iam(AppsyncIdentityIam),
Oidc(AppsyncIdentityOidc),
Lambda(AppsyncIdentityLambda),
ApiKey,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct AppsyncEventInfo<O> {
#[serde(flatten)]
pub operation: O,
#[serde(rename = "selectionSetGraphQL")]
pub selection_set_graphql: String,
#[serde(rename = "selectionSetList")]
pub selection_set_list: Vec<String>,
pub variables: HashMap<String, Value>,
}
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct AppsyncEvent<O> {
pub identity: AppsyncIdentity,
pub request: Value,
pub source: Value,
pub info: AppsyncEventInfo<O>,
#[serde(rename = "arguments")]
pub args: Value,
}
#[derive(Debug, Serialize)]
pub struct AppsyncResponse {
data: Option<Value>,
#[serde(flatten, skip_serializing_if = "Option::is_none")]
error: Option<AppsyncError>,
}
impl AppsyncResponse {
pub fn unauthorized() -> Self {
AppsyncError::new("Unauthorized", "This operation cannot be authorized").into()
}
pub fn data(&self) -> Option<&Value> {
self.data.as_ref()
}
pub fn error(&self) -> Option<&AppsyncError> {
self.error.as_ref()
}
}
impl From<Value> for AppsyncResponse {
fn from(value: Value) -> Self {
Self {
data: Some(value),
error: None,
}
}
}
impl From<AppsyncError> for AppsyncResponse {
fn from(value: AppsyncError) -> Self {
Self {
data: None,
error: Some(value),
}
}
}
#[derive(Debug, Error, Serialize)]
#[serde(rename_all = "camelCase")]
#[error("{error_type}: {error_message}")]
pub struct AppsyncError {
pub error_type: String,
pub error_message: String,
}
impl AppsyncError {
pub fn new(error_type: impl Into<String>, error_message: impl Into<String>) -> Self {
AppsyncError {
error_type: error_type.into(),
error_message: error_message.into(),
}
}
}
impl<T: ProvideErrorMetadata> From<T> for AppsyncError {
fn from(value: T) -> Self {
let meta = ProvideErrorMetadata::meta(&value);
AppsyncError {
error_type: meta.code().unwrap_or("Unknown").to_owned(),
error_message: meta.message().unwrap_or_default().to_owned(),
}
}
}
impl BitOr for AppsyncError {
type Output = AppsyncError;
fn bitor(self, rhs: Self) -> Self::Output {
AppsyncError {
error_type: format!("{}|{}", self.error_type, rhs.error_type),
error_message: format!("{}\n{}", self.error_message, rhs.error_message),
}
}
}
pub fn arg_from_json<T: DeserializeOwned>(
args: &mut serde_json::Value,
arg_name: &'static str,
) -> Result<T, AppsyncError> {
serde_json::from_value(
args.get_mut(arg_name)
.unwrap_or(&mut serde_json::Value::Null)
.take(),
)
.map_err(|e| {
AppsyncError::new(
"InvalidArgs",
format!("Argument \"{arg_name}\" is not the expected format ({e})"),
)
})
}
pub fn res_to_json<T: Serialize>(res: T) -> serde_json::Value {
serde_json::to_value(res).expect("Appsync schema objects are JSON compatible")
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_appsync_auth_strategy() {
let allow: AppsyncAuthStrategy = serde_json::from_str("\"ALLOW\"").unwrap();
let deny: AppsyncAuthStrategy = serde_json::from_str("\"DENY\"").unwrap();
match allow {
AppsyncAuthStrategy::Allow => (),
_ => panic!("Expected Allow"),
}
match deny {
AppsyncAuthStrategy::Deny => (),
_ => panic!("Expected Deny"),
}
}
#[test]
fn test_appsync_identity_cognito() {
let json = json!({
"sub": "user123",
"username": "testuser",
"issuer": "https://cognito-idp.region.amazonaws.com/pool_id",
"defaultAuthStrategy": "ALLOW",
"sourceIp": ["1.2.3.4"],
"groups": ["admin", "users"],
"claims": {
"email": "user@example.com",
"custom:role": "developer"
}
});
if let AppsyncIdentity::Cognito(cognito) = serde_json::from_value(json).unwrap() {
assert_eq!(cognito.sub, "user123");
assert_eq!(cognito.username, "testuser");
assert_eq!(
cognito.issuer,
"https://cognito-idp.region.amazonaws.com/pool_id"
);
assert_eq!(cognito.default_auth_strategy, AppsyncAuthStrategy::Allow);
assert_eq!(cognito.source_ip, vec!["1.2.3.4"]);
assert_eq!(
cognito.groups,
Some(vec!["admin".to_string(), "users".to_string()])
);
assert_eq!(
cognito.claims,
json!({
"email": "user@example.com",
"custom:role": "developer"
})
);
} else {
panic!("Expected Cognito variant");
}
}
#[test]
fn test_appsync_identity_iam() {
let json = json!({
"accountId": "123456789012",
"sourceIp": ["1.2.3.4"],
"username": "IAMUser",
"userArn": "arn:aws:iam::123456789012:user/IAMUser"
});
if let AppsyncIdentity::Iam(iam) = serde_json::from_value(json).unwrap() {
assert_eq!(iam.account_id, "123456789012");
assert_eq!(iam.source_ip, vec!["1.2.3.4"]);
assert_eq!(iam.username, "IAMUser");
assert_eq!(iam.user_arn, "arn:aws:iam::123456789012:user/IAMUser");
assert!(iam.federated_identity.is_none());
} else {
panic!("Expected IAM variant");
}
}
#[test]
fn test_appsync_identity_iam_with_cognito() {
let json = json!({
"accountId": "123456789012",
"sourceIp": ["1.2.3.4"],
"username": "IAMUser",
"userArn": "arn:aws:iam::123456789012:user/IAMUser",
"cognitoIdentityId": "region:id",
"cognitoIdentityPoolId": "region:pool_id",
"cognitoIdentityAuthType": "authenticated",
"cognitoIdentityAuthProvider": "cognito-idp.region.amazonaws.com/pool_id"
});
if let AppsyncIdentity::Iam(iam) = serde_json::from_value(json).unwrap() {
assert_eq!(iam.account_id, "123456789012");
assert_eq!(iam.source_ip, vec!["1.2.3.4"]);
assert_eq!(iam.username, "IAMUser");
assert_eq!(iam.user_arn, "arn:aws:iam::123456789012:user/IAMUser");
let federated = iam.federated_identity.unwrap();
assert_eq!(federated.identity_id, "region:id");
assert_eq!(federated.identity_pool_id, "region:pool_id");
assert!(matches!(
federated.auth_type,
CognitoIdentityAuthType::Authenticated
));
assert_eq!(
federated.auth_provider,
"cognito-idp.region.amazonaws.com/pool_id"
);
} else {
panic!("Expected IAM variant");
}
}
#[test]
fn test_appsync_identity_oidc() {
let json = json!({
"iss": "https://auth.example.com",
"sub": "user123",
"aud": "client123",
"exp": 1714521210,
"iat": 1714517610,
"name": "John Doe",
"email": "john@example.com",
"roles": ["admin"],
"org_id": "org123",
"custom_claim": "value"
});
if let AppsyncIdentity::Oidc(oidc) = serde_json::from_value(json).unwrap() {
assert_eq!(oidc.iss, "https://auth.example.com");
assert_eq!(oidc.sub, "user123");
assert_eq!(oidc.aud, "client123");
assert_eq!(oidc.exp, 1714521210);
assert_eq!(oidc.iat, 1714517610);
assert_eq!(oidc.additional_claims.get("name").unwrap(), "John Doe");
assert_eq!(
oidc.additional_claims.get("email").unwrap(),
"john@example.com"
);
assert_eq!(
oidc.additional_claims.get("roles").unwrap(),
&json!(["admin"])
);
assert_eq!(oidc.additional_claims.get("org_id").unwrap(), "org123");
assert_eq!(oidc.additional_claims.get("custom_claim").unwrap(), "value");
} else {
panic!("Expected OIDC variant");
}
}
#[test]
fn test_appsync_identity_lambda() {
let json = json!({
"resolverContext": {
"userId": "user123",
"permissions": ["read", "write"],
"metadata": {
"region": "us-west-2",
"environment": "prod"
}
}
});
if let AppsyncIdentity::Lambda(lambda) = serde_json::from_value(json).unwrap() {
assert_eq!(
lambda.resolver_context,
json!({
"userId": "user123",
"permissions": ["read", "write"],
"metadata": {
"region": "us-west-2",
"environment": "prod"
}
})
);
} else {
panic!("Expected Lambda variant");
}
}
#[test]
fn test_appsync_identity_api_key() {
let json = serde_json::Value::Null;
if let AppsyncIdentity::ApiKey = serde_json::from_value(json).unwrap() {
} else {
panic!("Expected ApiKey variant");
}
}
#[test]
fn test_appsync_response() {
let success = AppsyncResponse::from(json!({"field": "value"}));
assert!(success.data.is_some());
assert!(success.error.is_none());
let error = AppsyncResponse::from(AppsyncError::new("TestError", "message"));
assert!(error.data.is_none());
assert!(error.error.is_some());
}
#[test]
fn test_appsync_error() {
let error = AppsyncError::new("TestError", "message");
assert_eq!(error.error_type, "TestError");
assert_eq!(error.error_message, "message");
let error1 = AppsyncError::new("Error1", "msg1");
let error2 = AppsyncError::new("Error2", "msg2");
let combined = error1 | error2;
assert_eq!(combined.error_type, "Error1|Error2");
assert_eq!(combined.error_message, "msg1\nmsg2");
}
#[test]
fn test_arg_from_json() {
let mut args = json!({
"string": "test",
"number": 42,
"bool": true
});
let s: String = arg_from_json(&mut args, "string").unwrap();
assert_eq!(s, "test");
let n: i32 = arg_from_json(&mut args, "number").unwrap();
assert_eq!(n, 42);
let b: bool = arg_from_json(&mut args, "bool").unwrap();
assert!(b);
let err: Result<String, _> = arg_from_json(&mut args, "missing");
assert!(err.is_err());
}
#[test]
fn test_res_to_json() {
#[derive(Serialize)]
struct Test {
field: String,
}
let test = Test {
field: "value".to_string(),
};
let json = res_to_json(test);
assert_eq!(json, json!({"field": "value"}));
assert_eq!(res_to_json(42), json!(42));
assert_eq!(res_to_json("test"), json!("test"));
}
}