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}