lambda_appsync/lib.rs
1#![warn(missing_docs)]
2#![warn(rustdoc::missing_crate_level_docs)]
3#![cfg_attr(docsrs, deny(rustdoc::broken_intra_doc_links))]
4//! This crate provides procedural macros and types for implementing
5//! AWS AppSync Direct Lambda resolvers.
6//!
7//! It helps convert GraphQL schemas into type-safe Rust code with full AWS Lambda runtime support.
8//! The main functionality is provided through the [appsync_lambda_main] and [appsync_operation] macros.
9//!
10//! # Complete Example
11//!
12//! ```no_run
13//! use lambda_appsync::{appsync_lambda_main, appsync_operation, AppsyncError};
14//!
15//! // 1. First define your GraphQL schema (e.g. `schema.graphql`):
16//! //
17//! // type Query {
18//! // players: [Player!]!
19//! // gameStatus: GameStatus!
20//! // }
21//! //
22//! // type Player {
23//! // id: ID!
24//! // name: String!
25//! // team: Team!
26//! // }
27//! //
28//! // enum Team {
29//! // RUST
30//! // PYTHON
31//! // JS
32//! // }
33//! //
34//! // enum GameStatus {
35//! // STARTED
36//! // STOPPED
37//! // }
38//!
39//! // 2. Initialize the Lambda runtime with AWS SDK clients in main.rs:
40//!
41//! // Optional hook for custom request validation/auth
42//! async fn verify_request(
43//! event: &lambda_appsync::AppsyncEvent<Operation>
44//! ) -> Option<lambda_appsync::AppsyncResponse> {
45//! // Return Some(response) to short-circuit normal execution
46//! None
47//! }
48//! // Generate types and runtime setup from schema
49//! appsync_lambda_main!(
50//! "schema.graphql",
51//! // Initialize DynamoDB client if needed
52//! dynamodb() -> aws_sdk_dynamodb::Client,
53//! // Enable validation hook
54//! hook = verify_request,
55//! // Enable batch processing
56//! batch = true
57//! );
58//!
59//! // 3. Implement resolver functions for GraphQL operations:
60//!
61//! #[appsync_operation(query(players))]
62//! async fn get_players() -> Result<Vec<Player>, AppsyncError> {
63//! let client = dynamodb();
64//! todo!()
65//! }
66//!
67//! #[appsync_operation(query(gameStatus))]
68//! async fn get_game_status() -> Result<GameStatus, AppsyncError> {
69//! let client = dynamodb();
70//! todo!()
71//! }
72//! // The macro ensures the function signature matches the GraphQL schema
73//! // and wires everything up to handle AWS AppSync requests automatically
74//! # mod child {fn main() {}}
75//! ```
76
77mod aws_scalars;
78mod id;
79pub mod subscription_filters;
80
81use std::{collections::HashMap, ops::BitOr};
82
83use aws_smithy_types::error::metadata::ProvideErrorMetadata;
84use serde_json::Value;
85
86use serde::{de::DeserializeOwned, Deserialize, Serialize};
87use thiserror::Error;
88
89pub use aws_scalars::{
90 datetime::{AWSDate, AWSDateTime, AWSTime},
91 email::AWSEmail,
92 phone::AWSPhone,
93 timestamp::AWSTimestamp,
94 url::AWSUrl,
95};
96pub use id::ID;
97
98#[doc(inline)]
99pub use lambda_appsync_proc::appsync_lambda_main;
100
101#[doc(inline)]
102pub use lambda_appsync_proc::appsync_operation;
103
104// Re-export crates that are mandatory for the proc_macro to succeed
105pub use aws_config;
106pub use env_logger;
107pub use lambda_runtime;
108pub use log;
109pub use serde;
110pub use serde_json;
111pub use tokio;
112
113/// Authorization strategy for AppSync operations.
114///
115/// It determines whether operations are allowed or denied based on the
116/// authentication context provided by AWS AppSync. It is typically used by AppSync
117/// itself in conjunction with AWS Cognito user pools and usually do not concern
118/// the application code.
119#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
120#[serde(rename_all = "UPPERCASE")]
121pub enum AppsyncAuthStrategy {
122 /// Allows the operation by default if no explicit authorizer is associated to the field
123 Allow,
124 /// Denies the operation by default if no explicit authorizer is associated to the field
125 Deny,
126}
127
128/// Identity information for Cognito User Pools authenticated requests.
129#[derive(Debug, Deserialize)]
130#[serde(rename_all = "camelCase")]
131pub struct AppsyncIdentityCognito {
132 /// Unique identifier of the authenticated user/client
133 pub sub: String,
134 /// Username of the authenticated user (from Cognito user pools)
135 pub username: String,
136 /// Identity provider that authenticated the request (e.g. Cognito user pool URL)
137 pub issuer: String,
138 /// Default authorization strategy for the authenticated identity
139 pub default_auth_strategy: AppsyncAuthStrategy,
140 /// Source IP addresses associated with the request
141 pub source_ip: Vec<String>,
142 /// Groups the authenticated user belongs to
143 pub groups: Option<Vec<String>>,
144 /// Additional claims/attributes associated with the identity
145 pub claims: Value,
146}
147
148/// Authentication type in a Cognito Identity Pool
149#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
150#[serde(rename_all = "lowercase")]
151pub enum CognitoIdentityAuthType {
152 /// User is authenticated with an identity provider
153 Authenticated,
154 /// User is an unauthenticated guest
155 Unauthenticated,
156}
157
158/// Cognito Identity Pool information for federated IAM authentication
159#[derive(Debug, Deserialize)]
160pub struct CognitoFederatedIdentity {
161 /// Unique identifier assigned to the authenticated/unauthenticated identity
162 /// within the Cognito Identity Pool
163 #[serde(rename = "cognitoIdentityId")]
164 pub identity_id: String,
165 /// Identifier of the Cognito Identity Pool that is being used for federation.
166 /// In the format of region:pool-id
167 #[serde(rename = "cognitoIdentityPoolId")]
168 pub identity_pool_id: String,
169 /// Indicates whether the identity is authenticated with an identity provider
170 /// or is an unauthenticated guest access
171 #[serde(rename = "cognitoIdentityAuthType")]
172 pub auth_type: CognitoIdentityAuthType,
173 /// For authenticated identities, contains information about the identity provider
174 /// used for authentication. Format varies by provider type
175 #[serde(rename = "cognitoIdentityAuthProvider")]
176 pub auth_provider: String,
177}
178
179/// Identity information for IAM-authenticated requests.
180///
181/// Contains AWS IAM-specific authentication details, including optional Cognito
182/// identity pool information when using federated identities.
183#[derive(Debug, Deserialize)]
184#[serde(rename_all = "camelCase")]
185pub struct AppsyncIdentityIam {
186 /// AWS account ID of the caller
187 pub account_id: String,
188 /// Source IP address(es) of the caller
189 pub source_ip: Vec<String>,
190 /// IAM username of the caller
191 pub username: String,
192 /// Full IAM ARN of the caller
193 pub user_arn: String,
194 /// Federated identity information when using Cognito Identity Pools
195 #[serde(flatten)]
196 pub federated_identity: Option<CognitoFederatedIdentity>,
197}
198
199/// Identity information for OIDC-authenticated requests.
200#[derive(Debug, Deserialize)]
201pub struct AppsyncIdentityOidc {
202 /// The issuer of the token
203 pub iss: String,
204 /// The subject (usually the user identifier)
205 pub sub: String,
206 /// Token audience
207 pub aud: String,
208 /// Expiration time
209 pub exp: i64,
210 /// Issued at time
211 pub iat: i64,
212 /// Additional custom claims from the OIDC provider
213 #[serde(flatten)]
214 pub additional_claims: HashMap<String, serde_json::Value>,
215}
216
217/// Identity information for Lambda-authorized requests.
218#[derive(Debug, Deserialize)]
219pub struct AppsyncIdentityLambda {
220 /// Custom resolver context returned by the Lambda authorizer
221 #[serde(rename = "resolverContext")]
222 pub resolver_context: serde_json::Value,
223}
224
225/// Identity information for an AppSync request.
226///
227/// Represents the identity context of the authenticated user/client making the request to
228/// AWS AppSync. This enum corresponds directly to AppSync's authorization types as defined
229/// in the AWS documentation.
230///
231/// Each variant maps to one of the five supported AWS AppSync authorization modes:
232///
233/// - [Cognito](AppsyncIdentity::Cognito): Uses Amazon Cognito User Pools, providing group-based
234/// access control with JWT tokens containing encoded user information like groups and custom claims.
235///
236/// - [Iam](AppsyncIdentity::Iam): Uses AWS IAM roles and policies through AWS Signature Version 4
237/// signing. Can be used either directly with IAM users/roles or through Cognito Identity Pools
238/// for federated access. Enables fine-grained access control through IAM policies.
239///
240/// - [Oidc](AppsyncIdentity::Oidc): OpenID Connect authentication integrating with any
241/// OIDC-compliant provider.
242///
243/// - [Lambda](AppsyncIdentity::Lambda): Custom authorization through an AWS Lambda function
244/// that evaluates each request.
245///
246/// - [ApiKey](AppsyncIdentity::ApiKey): Simple API key-based authentication using keys
247/// generated and managed by AppSync.
248///
249/// The variant is determined by the authorization configuration of your AppSync API and
250/// the authentication credentials provided in the request. Each variant contains structured
251/// information specific to that authentication mode, which can be used in resolvers for
252/// custom authorization logic.
253///
254/// More information can be found in the [AWS documentation](https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html).
255#[derive(Debug, Deserialize)]
256#[serde(untagged)]
257pub enum AppsyncIdentity {
258 /// Amazon Cognito User Pools authentication
259 Cognito(AppsyncIdentityCognito),
260 /// AWS IAM authentication
261 Iam(AppsyncIdentityIam),
262 /// OpenID Connect authentication
263 Oidc(AppsyncIdentityOidc),
264 /// Lambda authorizer authentication
265 Lambda(AppsyncIdentityLambda),
266 /// API Key authentication (represents null identity in JSON)
267 ApiKey,
268}
269
270/// Metadata about an AppSync GraphQL operation execution.
271///
272/// Contains detailed information about the GraphQL operation being executed,
273/// including the operation type, selected fields, and variables. The type parameter
274/// `O` represents the enum generated by [appsync_lambda_main] that defines all valid
275/// operations for this Lambda resolver.
276#[derive(Debug, Deserialize)]
277#[allow(dead_code)]
278pub struct AppsyncEventInfo<O> {
279 /// The specific GraphQL operation being executed (Query/Mutation)
280 #[serde(flatten)]
281 pub operation: O,
282 /// Raw GraphQL selection set as a string
283 #[serde(rename = "selectionSetGraphQL")]
284 pub selection_set_graphql: String,
285 /// List of selected field paths in the GraphQL query
286 #[serde(rename = "selectionSetList")]
287 pub selection_set_list: Vec<String>,
288 /// Variables passed to the GraphQL operation
289 pub variables: HashMap<String, Value>,
290}
291
292/// Represents a complete AWS AppSync event sent to a Lambda resolver.
293///
294/// Contains all context and data needed to resolve a GraphQL operation, including
295/// authentication details, operation info, and arguments. The generics `O`
296/// must be the Operation enum generated by the [appsync_lambda_main] macro.
297///
298/// # Limitations
299/// - Omits the `stash` field used for pipeline resolvers
300/// - Omits the `prev` field as it's not relevant for direct Lambda resolvers
301#[derive(Debug, Deserialize)]
302#[allow(dead_code)]
303pub struct AppsyncEvent<O> {
304 /// Authentication context
305 pub identity: AppsyncIdentity,
306 /// Raw request context from AppSync
307 pub request: Value,
308 /// Parent field's resolved value in nested resolvers
309 pub source: Value,
310 /// Metadata about the GraphQL operation
311 pub info: AppsyncEventInfo<O>,
312 /// Arguments passed to the GraphQL field
313 #[serde(rename = "arguments")]
314 pub args: Value,
315 // Should never be usefull in a Direct Lambda Invocation context
316 // pub stash: Value,
317 // pub prev: Value,
318}
319
320/// Response structure returned to AWS AppSync from a Lambda resolver.
321///
322/// Can contain either successful data or error information, but not both.
323/// Should be constructed using From implementations for either [Value] (success)
324/// or [AppsyncError] (failure).
325///
326/// # Examples
327/// ```
328/// # use serde_json::json;
329/// # use lambda_appsync::{AppsyncError, AppsyncResponse};
330/// // Success response
331/// let response: AppsyncResponse = json!({ "id": 123 }).into();
332///
333/// // Error response
334/// let error = AppsyncError::new("NotFound", "Resource not found");
335/// let response: AppsyncResponse = error.into();
336/// ```
337#[derive(Debug, Serialize)]
338pub struct AppsyncResponse {
339 data: Option<Value>,
340 #[serde(flatten, skip_serializing_if = "Option::is_none")]
341 error: Option<AppsyncError>,
342}
343
344impl AppsyncResponse {
345 /// Returns an unauthorized error response
346 ///
347 /// This creates a standard unauthorized error response for when a request
348 /// lacks proper authentication.
349 ///
350 /// # Examples
351 /// ```
352 /// # use lambda_appsync::AppsyncResponse;
353 /// let response = AppsyncResponse::unauthorized();
354 /// ```
355 pub fn unauthorized() -> Self {
356 AppsyncError::new("Unauthorized", "This operation cannot be authorized").into()
357 }
358}
359
360impl From<Value> for AppsyncResponse {
361 fn from(value: Value) -> Self {
362 Self {
363 data: Some(value),
364 error: None,
365 }
366 }
367}
368impl From<AppsyncError> for AppsyncResponse {
369 fn from(value: AppsyncError) -> Self {
370 Self {
371 data: None,
372 error: Some(value),
373 }
374 }
375}
376
377/// Error type for AWS AppSync operations
378///
379/// Multiple errors can be combined in one using the pipe operator
380///
381/// # Example
382/// ```
383/// # use lambda_appsync::AppsyncError;
384/// let combined_error = AppsyncError::new("ValidationError", "Email address is invalid") | AppsyncError::new("DatabaseError", "User not found in database");
385/// // error_type: "ValidationError|DatabaseError"
386/// // error_message: "Email address is invalid\nUser not found in database"
387/// ```
388///
389/// Can be created from any AWS SDK error or directly by the user.
390///
391/// # Example
392/// ```
393/// # use lambda_appsync::AppsyncError;
394/// # use aws_sdk_dynamodb::types::AttributeValue;
395/// struct Item {
396/// id: u64,
397/// data: String
398/// }
399/// async fn store_item(item: Item, client: &aws_sdk_dynamodb::Client) -> Result<(), AppsyncError> {
400/// client.put_item()
401/// .table_name("my-table")
402/// .item("id", AttributeValue::N(item.id.to_string()))
403/// .item("data", AttributeValue::S(item.data))
404/// .send()
405/// .await?;
406/// Ok(())
407/// }
408/// ```
409#[derive(Debug, Error, Serialize)]
410#[serde(rename_all = "camelCase")]
411#[error("{error_type}: {error_message}")]
412pub struct AppsyncError {
413 /// The type/category of error that occurred (e.g. "ValidationError", "NotFound", "DatabaseError")
414 pub error_type: String,
415 /// A detailed message describing the specific error condition
416 pub error_message: String,
417}
418impl AppsyncError {
419 /// Creates a new AppSync error with the specified error type and message
420 ///
421 /// # Arguments
422 /// * `error_type` - The type/category of the error (e.g. "ValidationError", "NotFound")
423 /// * `error_message` - A detailed message describing the error
424 ///
425 /// # Example
426 /// ```
427 /// # use lambda_appsync::AppsyncError;
428 /// let error = AppsyncError::new("NotFound", "User with ID 123 not found");
429 /// ```
430 pub fn new(error_type: impl Into<String>, error_message: impl Into<String>) -> Self {
431 AppsyncError {
432 error_type: error_type.into(),
433 error_message: error_message.into(),
434 }
435 }
436}
437impl<T: ProvideErrorMetadata> From<T> for AppsyncError {
438 fn from(value: T) -> Self {
439 let meta = ProvideErrorMetadata::meta(&value);
440 AppsyncError {
441 error_type: meta.code().unwrap_or("Unknown").to_owned(),
442 error_message: meta.message().unwrap_or_default().to_owned(),
443 }
444 }
445}
446
447impl BitOr for AppsyncError {
448 type Output = AppsyncError;
449 fn bitor(self, rhs: Self) -> Self::Output {
450 AppsyncError {
451 error_type: format!("{}|{}", self.error_type, rhs.error_type),
452 error_message: format!("{}\n{}", self.error_message, rhs.error_message),
453 }
454 }
455}
456
457/// Extracts and deserializes a named argument from a JSON Value into the specified type
458///
459/// # Arguments
460/// * `args` - Mutable reference to a JSON Value containing arguments
461/// * `arg_name` - Name of the argument to extract
462///
463/// # Returns
464/// * `Ok(T)` - Successfully deserialized value of type T
465/// * `Err(AppsyncError)` - Error if argument is missing or invalid format
466///
467/// # Examples
468/// ```
469/// # use serde_json::json;
470/// # use lambda_appsync::arg_from_json;
471/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
472/// let mut args = json!({
473/// "userId": "123",
474/// "count": 5
475/// });
476///
477/// // Extract userId as String
478/// let user_id: String = arg_from_json(&mut args, "userId")?;
479/// assert_eq!(user_id, "123");
480///
481/// // Extract count as i32
482/// let count: i32 = arg_from_json(&mut args, "count")?;
483/// assert_eq!(count, 5);
484///
485/// // Error case: invalid type
486/// let result: Result<String, _> = arg_from_json(&mut args, "count");
487/// assert!(result.is_err());
488///
489/// // Error case: missing argument
490/// let result: Result<String, _> = arg_from_json(&mut args, "missing");
491/// assert!(result.is_err());
492/// # Ok(())
493/// # }
494/// ```
495pub fn arg_from_json<T: DeserializeOwned>(
496 args: &mut serde_json::Value,
497 arg_name: &'static str,
498) -> Result<T, AppsyncError> {
499 serde_json::from_value(
500 args.get_mut(arg_name)
501 .unwrap_or(&mut serde_json::Value::Null)
502 .take(),
503 )
504 .map_err(|e| {
505 AppsyncError::new(
506 "InvalidArgs",
507 format!("Argument \"{arg_name}\" is not the expected format ({e})"),
508 )
509 })
510}
511
512/// Serializes a value into a JSON Value for AppSync responses
513///
514/// # Arguments
515/// * `res` - Value to serialize that implements Serialize
516///
517/// # Returns
518/// JSON Value representation of the input
519///
520/// # Panics
521/// Panics if the value cannot be serialized to JSON. This should never happen
522/// for valid AppSync schema objects as generated by the `appsync_lambda_main` proc macro.
523///
524/// # Examples
525/// ```
526/// # use serde::Serialize;
527/// # use serde_json::json;
528/// # use lambda_appsync::res_to_json;
529/// #[derive(Serialize)]
530/// struct User {
531/// id: String,
532/// name: String
533/// }
534///
535/// let user = User {
536/// id: "123".to_string(),
537/// name: "John".to_string()
538/// };
539///
540/// let json = res_to_json(user);
541/// assert_eq!(json, json!({
542/// "id": "123",
543/// "name": "John"
544/// }));
545///
546/// // Simple types also work
547/// let num = res_to_json(42);
548/// assert_eq!(num, json!(42));
549/// ```
550pub fn res_to_json<T: Serialize>(res: T) -> serde_json::Value {
551 serde_json::to_value(res).expect("Appsync schema objects are JSON compatible")
552}
553
554#[cfg(test)]
555mod tests {
556 use super::*;
557 use serde_json::json;
558
559 #[test]
560 fn test_appsync_auth_strategy() {
561 let allow: AppsyncAuthStrategy = serde_json::from_str("\"ALLOW\"").unwrap();
562 let deny: AppsyncAuthStrategy = serde_json::from_str("\"DENY\"").unwrap();
563
564 match allow {
565 AppsyncAuthStrategy::Allow => (),
566 _ => panic!("Expected Allow"),
567 }
568
569 match deny {
570 AppsyncAuthStrategy::Deny => (),
571 _ => panic!("Expected Deny"),
572 }
573 }
574
575 #[test]
576 fn test_appsync_identity_cognito() {
577 let json = json!({
578 "sub": "user123",
579 "username": "testuser",
580 "issuer": "https://cognito-idp.region.amazonaws.com/pool_id",
581 "defaultAuthStrategy": "ALLOW",
582 "sourceIp": ["1.2.3.4"],
583 "groups": ["admin", "users"],
584 "claims": {
585 "email": "user@example.com",
586 "custom:role": "developer"
587 }
588 });
589
590 if let AppsyncIdentity::Cognito(cognito) = serde_json::from_value(json).unwrap() {
591 assert_eq!(cognito.sub, "user123");
592 assert_eq!(cognito.username, "testuser");
593 assert_eq!(
594 cognito.issuer,
595 "https://cognito-idp.region.amazonaws.com/pool_id"
596 );
597 assert_eq!(cognito.default_auth_strategy, AppsyncAuthStrategy::Allow);
598 assert_eq!(cognito.source_ip, vec!["1.2.3.4"]);
599 assert_eq!(
600 cognito.groups,
601 Some(vec!["admin".to_string(), "users".to_string()])
602 );
603 assert_eq!(
604 cognito.claims,
605 json!({
606 "email": "user@example.com",
607 "custom:role": "developer"
608 })
609 );
610 } else {
611 panic!("Expected Cognito variant");
612 }
613 }
614
615 #[test]
616 fn test_appsync_identity_iam() {
617 let json = json!({
618 "accountId": "123456789012",
619 "sourceIp": ["1.2.3.4"],
620 "username": "IAMUser",
621 "userArn": "arn:aws:iam::123456789012:user/IAMUser"
622 });
623
624 if let AppsyncIdentity::Iam(iam) = serde_json::from_value(json).unwrap() {
625 assert_eq!(iam.account_id, "123456789012");
626 assert_eq!(iam.source_ip, vec!["1.2.3.4"]);
627 assert_eq!(iam.username, "IAMUser");
628 assert_eq!(iam.user_arn, "arn:aws:iam::123456789012:user/IAMUser");
629 assert!(iam.federated_identity.is_none());
630 } else {
631 panic!("Expected IAM variant");
632 }
633 }
634
635 #[test]
636 fn test_appsync_identity_iam_with_cognito() {
637 let json = json!({
638 "accountId": "123456789012",
639 "sourceIp": ["1.2.3.4"],
640 "username": "IAMUser",
641 "userArn": "arn:aws:iam::123456789012:user/IAMUser",
642 "cognitoIdentityId": "region:id",
643 "cognitoIdentityPoolId": "region:pool_id",
644 "cognitoIdentityAuthType": "authenticated",
645 "cognitoIdentityAuthProvider": "cognito-idp.region.amazonaws.com/pool_id"
646 });
647
648 if let AppsyncIdentity::Iam(iam) = serde_json::from_value(json).unwrap() {
649 assert_eq!(iam.account_id, "123456789012");
650 assert_eq!(iam.source_ip, vec!["1.2.3.4"]);
651 assert_eq!(iam.username, "IAMUser");
652 assert_eq!(iam.user_arn, "arn:aws:iam::123456789012:user/IAMUser");
653
654 let federated = iam.federated_identity.unwrap();
655 assert_eq!(federated.identity_id, "region:id");
656 assert_eq!(federated.identity_pool_id, "region:pool_id");
657 assert!(matches!(
658 federated.auth_type,
659 CognitoIdentityAuthType::Authenticated
660 ));
661 assert_eq!(
662 federated.auth_provider,
663 "cognito-idp.region.amazonaws.com/pool_id"
664 );
665 } else {
666 panic!("Expected IAM variant");
667 }
668 }
669
670 #[test]
671 fn test_appsync_identity_oidc() {
672 let json = json!({
673 "iss": "https://auth.example.com",
674 "sub": "user123",
675 "aud": "client123",
676 "exp": 1714521210,
677 "iat": 1714517610,
678 "name": "John Doe",
679 "email": "john@example.com",
680 "roles": ["admin"],
681 "org_id": "org123",
682 "custom_claim": "value"
683 });
684
685 if let AppsyncIdentity::Oidc(oidc) = serde_json::from_value(json).unwrap() {
686 assert_eq!(oidc.iss, "https://auth.example.com");
687 assert_eq!(oidc.sub, "user123");
688 assert_eq!(oidc.aud, "client123");
689 assert_eq!(oidc.exp, 1714521210);
690 assert_eq!(oidc.iat, 1714517610);
691 assert_eq!(oidc.additional_claims.get("name").unwrap(), "John Doe");
692 assert_eq!(
693 oidc.additional_claims.get("email").unwrap(),
694 "john@example.com"
695 );
696 assert_eq!(
697 oidc.additional_claims.get("roles").unwrap(),
698 &json!(["admin"])
699 );
700 assert_eq!(oidc.additional_claims.get("org_id").unwrap(), "org123");
701 assert_eq!(oidc.additional_claims.get("custom_claim").unwrap(), "value");
702 } else {
703 panic!("Expected OIDC variant");
704 }
705 }
706
707 #[test]
708 fn test_appsync_identity_lambda() {
709 let json = json!({
710 "resolverContext": {
711 "userId": "user123",
712 "permissions": ["read", "write"],
713 "metadata": {
714 "region": "us-west-2",
715 "environment": "prod"
716 }
717 }
718 });
719
720 if let AppsyncIdentity::Lambda(lambda) = serde_json::from_value(json).unwrap() {
721 assert_eq!(
722 lambda.resolver_context,
723 json!({
724 "userId": "user123",
725 "permissions": ["read", "write"],
726 "metadata": {
727 "region": "us-west-2",
728 "environment": "prod"
729 }
730 })
731 );
732 } else {
733 panic!("Expected Lambda variant");
734 }
735 }
736
737 #[test]
738 fn test_appsync_identity_api_key() {
739 let json = serde_json::Value::Null;
740
741 if let AppsyncIdentity::ApiKey = serde_json::from_value(json).unwrap() {
742 // Test passes if we get the ApiKey variant
743 } else {
744 panic!("Expected ApiKey variant");
745 }
746 }
747
748 #[test]
749 fn test_appsync_response() {
750 let success = AppsyncResponse::from(json!({"field": "value"}));
751 assert!(success.data.is_some());
752 assert!(success.error.is_none());
753
754 let error = AppsyncResponse::from(AppsyncError::new("TestError", "message"));
755 assert!(error.data.is_none());
756 assert!(error.error.is_some());
757 }
758
759 #[test]
760 fn test_appsync_error() {
761 let error = AppsyncError::new("TestError", "message");
762 assert_eq!(error.error_type, "TestError");
763 assert_eq!(error.error_message, "message");
764
765 let error1 = AppsyncError::new("Error1", "msg1");
766 let error2 = AppsyncError::new("Error2", "msg2");
767 let combined = error1 | error2;
768
769 assert_eq!(combined.error_type, "Error1|Error2");
770 assert_eq!(combined.error_message, "msg1\nmsg2");
771 }
772
773 #[test]
774 fn test_arg_from_json() {
775 let mut args = json!({
776 "string": "test",
777 "number": 42,
778 "bool": true
779 });
780
781 let s: String = arg_from_json(&mut args, "string").unwrap();
782 assert_eq!(s, "test");
783
784 let n: i32 = arg_from_json(&mut args, "number").unwrap();
785 assert_eq!(n, 42);
786
787 let b: bool = arg_from_json(&mut args, "bool").unwrap();
788 assert!(b);
789
790 let err: Result<String, _> = arg_from_json(&mut args, "missing");
791 assert!(err.is_err());
792 }
793
794 #[test]
795 fn test_res_to_json() {
796 #[derive(Serialize)]
797 struct Test {
798 field: String,
799 }
800
801 let test = Test {
802 field: "value".to_string(),
803 };
804
805 let json = res_to_json(test);
806 assert_eq!(json, json!({"field": "value"}));
807
808 assert_eq!(res_to_json(42), json!(42));
809 assert_eq!(res_to_json("test"), json!("test"));
810 }
811}