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