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