Skip to main content

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//! A type-safe framework for AWS AppSync Direct Lambda resolvers.
5//!
6//! This crate provides procedural macros and types for implementing
7//! AWS AppSync Direct Lambda resolvers. It converts GraphQL schemas into
8//! type-safe Rust code with full AWS Lambda runtime support.
9//!
10//! The recommended entry point is [`make_appsync!`], a convenience macro that generates all
11//! necessary types, the `Operation` dispatch enum, and a `Handlers` trait from a `.graphql`
12//! schema file. For finer control (e.g. shared-types libraries or multi-Lambda setups), the
13//! three composable macros [`make_types!`], [`make_operation!`], and [`make_handlers!`] can be
14//! used individually. Resolver functions are annotated with [`appsync_operation`].
15//!
16//! # Key Concepts
17//!
18//! ## Macros
19//!
20//! ### All-in-one
21//!
22//! - **[`make_appsync!`]** — reads a GraphQL schema file at compile time and generates:
23//!   - Rust types for all GraphQL objects/enums/inputs,
24//!   - an `Operation` enum covering every query/mutation/subscription field, and
25//!   - a `Handlers` trait with a `DefaultHandlers` struct for wiring up the Lambda runtime.
26//!
27//!   This is the recommended macro for single-crate projects.
28//!
29//! ### Composable
30//!
31//! - **[`make_types!`]** — generates Rust structs and enums from the schema's `type`, `input`,
32//!   and `enum` definitions.
33//! - **[`make_operation!`]** — generates the `Operation` enum, sub-enums (`QueryField`,
34//!   `MutationField`, `SubscriptionField`), argument extraction, and the `execute` dispatch
35//!   method. Requires the types from [`make_types!`] to be in scope.
36//! - **[`make_handlers!`]** — generates the `Handlers` trait and `DefaultHandlers` struct for
37//!   the Lambda runtime. Requires the `Operation` type from [`make_operation!`] to be in scope.
38//!
39//! ### Resolver attribute
40//!
41//! - **[`appsync_operation`]** — attribute macro applied to async resolver functions. It validates
42//!   that the function signature matches the corresponding GraphQL field and registers the function
43//!   as the handler for that operation.
44//!
45//! ### Deprecated
46//!
47//! - **`appsync_lambda_main!`** (`compat` feature) — the legacy monolithic macro. It combines
48//!   type generation, operation dispatch, Lambda runtime setup, and AWS SDK client initialization
49//!   into one call. Prefer [`make_appsync!`] or the composable macros for new code.
50//!
51//! ## Event and Response Types
52//!
53//! Every Lambda invocation receives an [`AppsyncEvent<O>`] where `O` is the generated `Operation`
54//! enum. The event carries:
55//!
56//! - [`AppsyncEvent::identity`] — the caller's [`AppsyncIdentity`] (Cognito, IAM, OIDC, Lambda
57//!   authorizer, or API key)
58//! - [`AppsyncEvent::info`] — an [`AppsyncEventInfo<O>`] with the specific operation, selection
59//!   set, and variables
60//! - [`AppsyncEvent::args`] — raw JSON arguments for the field; use [`arg_from_json`] to extract
61//!   individual typed arguments
62//! - [`AppsyncEvent::source`] — the parent object's resolved value for nested resolvers
63//!
64//! Resolver functions return `Result<T, `[`AppsyncError`]`>`. The framework serializes
65//! responses and wraps them in an [`AppsyncResponse`].
66//!
67//! ## Identity and Authorization
68//!
69//! [`AppsyncIdentity`] is an enum with one variant per AppSync authorization mode:
70//!
71//! | Variant | Auth mode | Detail struct |
72//! |---------|-----------|---------------|
73//! | [`AppsyncIdentity::Cognito`] | Cognito User Pools | [`AppsyncIdentityCognito`] |
74//! | [`AppsyncIdentity::Iam`] | AWS IAM / Cognito Identity Pools | [`AppsyncIdentityIam`] |
75//! | [`AppsyncIdentity::Oidc`] | OpenID Connect | [`AppsyncIdentityOidc`] |
76//! | [`AppsyncIdentity::Lambda`] | Lambda authorizer | [`AppsyncIdentityLambda`] |
77//! | [`AppsyncIdentity::ApiKey`] | API Key | *(no extra data)* |
78//!
79//! ## AWS AppSync Scalar Types
80//!
81//! The crate provides Rust types for all AppSync-specific GraphQL scalars:
82//!
83//! - [`ID`] — UUID-based GraphQL `ID` scalar
84//! - [`AWSEmail`], [`AWSPhone`], [`AWSUrl`] — validated string scalars
85//! - [`AWSDate`], [`AWSTime`], [`AWSDateTime`] — date/time scalars
86//! - [`AWSTimestamp`] — Unix epoch timestamp scalar
87//!
88//! ## Subscription Filters
89//!
90//! The [`subscription_filters`] module provides a type-safe builder for AppSync
91//! [enhanced subscription filters](https://docs.aws.amazon.com/appsync/latest/devguide/aws-appsync-real-time-enhanced-filtering.html).
92//! Filters are constructed from [`subscription_filters::FieldPath`] operator methods, combined
93//! into [`subscription_filters::Filter`] (AND logic, up to 5 conditions) and
94//! [`subscription_filters::FilterGroup`] (OR logic, up to 10 filters). AWS AppSync's size
95//! constraints are enforced at compile time.
96//!
97//! ## Error Handling
98//!
99//! [`AppsyncError`] carries an `error_type` and `error_message`. Multiple errors can be merged
100//! with the `|` operator, which concatenates both fields. Any AWS SDK error that implements
101//! `ProvideErrorMetadata` converts into an `AppsyncError` via `?`.
102//!
103//! ## Tracing Integration
104//!
105//! When the `tracing` feature is enabled, the generated `Handlers` trait automatically wraps
106//! each event dispatch in a `tracing::info_span!("AppsyncEvent", ...)` that records the
107//! operation being executed (and the batch index, when batch mode is active). This helps give you
108//! per-operation spans linked with the parent without writing any instrumentation boilerplate.
109//! The feature also re-exports `tracing` and `tracing-subscriber` for convenience.
110//!
111//! # Complete Example
112//!
113//! Given a GraphQL schema (`schema.graphql`):
114//!
115//! ```graphql
116//! type Query {
117//!   players: [Player!]!
118//!   gameStatus: GameStatus!
119//! }
120//!
121//! type Player {
122//!   id: ID!
123//!   name: String!
124//!   team: Team!
125//! }
126//!
127//! enum Team {
128//!   RUST
129//!   PYTHON
130//!   JS
131//! }
132//!
133//! enum GameStatus {
134//!   STARTED
135//!   STOPPED
136//! }
137//! ```
138//!
139//! ## Using `make_appsync!` (recommended)
140//!
141//! ```rust,no_run
142//! # use lambda_appsync::{tokio, lambda_runtime};
143//! use lambda_appsync::{make_appsync, appsync_operation, AppsyncError};
144//!
145//! // Generate types, Operation enum, and Handlers trait from schema
146//! make_appsync!("schema.graphql");
147//!
148//! // Implement resolver functions for GraphQL operations:
149//!
150//! #[appsync_operation(query(players))]
151//! async fn get_players() -> Result<Vec<Player>, AppsyncError> {
152//!     todo!()
153//! }
154//!
155//! #[appsync_operation(query(gameStatus))]
156//! async fn get_game_status() -> Result<GameStatus, AppsyncError> {
157//!     todo!()
158//! }
159//!
160//! // Wire up the Lambda runtime in main:
161//!
162//! #[tokio::main]
163//! async fn main() -> Result<(), lambda_runtime::Error> {
164//!     lambda_runtime::run(
165//!         lambda_runtime::service_fn(DefaultHandlers::service_fn)
166//!     ).await
167//! }
168//! ```
169//!
170//! ## Custom handler with authentication hook
171//!
172//! Override the `Handlers` trait to add pre-processing logic (replaces the
173//! old `hook` parameter from `appsync_lambda_main!`):
174//!
175//! ```rust,no_run
176//! # use lambda_appsync::{tokio, lambda_runtime};
177//! use lambda_appsync::{make_appsync, appsync_operation, AppsyncError};
178//! use lambda_appsync::{AppsyncEvent, AppsyncResponse, AppsyncIdentity};
179//!
180//! make_appsync!("schema.graphql");
181//!
182//! struct MyHandlers;
183//! impl Handlers for MyHandlers {
184//!     async fn appsync_handler(event: AppsyncEvent<Operation>) -> AppsyncResponse {
185//!         // Custom authentication check
186//!         if let AppsyncIdentity::ApiKey = &event.identity {
187//!             return AppsyncResponse::unauthorized();
188//!         }
189//!         // Delegate to the default operation dispatch
190//!         event.info.operation.execute(event).await
191//!     }
192//! }
193//!
194//! #[appsync_operation(query(players))]
195//! async fn get_players() -> Result<Vec<Player>, AppsyncError> {
196//!     todo!()
197//! }
198//!
199//! #[appsync_operation(query(gameStatus))]
200//! async fn get_game_status() -> Result<GameStatus, AppsyncError> {
201//!     todo!()
202//! }
203//!
204//! #[tokio::main]
205//! async fn main() -> Result<(), lambda_runtime::Error> {
206//!     lambda_runtime::run(
207//!         lambda_runtime::service_fn(MyHandlers::service_fn)
208//!     ).await
209//! }
210//! ```
211//!
212//! ## Using the composable macros
213//!
214//! For multi-crate setups (e.g. a shared types library with separate Lambda binaries),
215//! use the individual macros:
216//!
217//! ```rust,no_run
218//! # use lambda_appsync::{tokio, lambda_runtime};
219//! use lambda_appsync::{make_types, make_operation, make_handlers, appsync_operation, AppsyncError};
220//!
221//! // Step 1: Generate types (could live in a shared lib crate)
222//! make_types!("schema.graphql");
223//!
224//! // Step 2: Generate Operation enum and dispatch logic
225//! make_operation!("schema.graphql");
226//!
227//! // Step 3: Generate Handlers trait and DefaultHandlers
228//! make_handlers!();
229//!
230//! #[appsync_operation(query(players))]
231//! async fn get_players() -> Result<Vec<Player>, AppsyncError> {
232//!     todo!()
233//! }
234//!
235//! #[appsync_operation(query(gameStatus))]
236//! async fn get_game_status() -> Result<GameStatus, AppsyncError> {
237//!     todo!()
238//! }
239//!
240//! #[tokio::main]
241//! async fn main() -> Result<(), lambda_runtime::Error> {
242//!     lambda_runtime::run(
243//!         lambda_runtime::service_fn(DefaultHandlers::service_fn)
244//!     ).await
245//! }
246//! ```
247//!
248//! # Feature Flags
249//!
250//! | Feature | Description |
251//! |---------|-------------|
252//! | **`compat`** | Enables the deprecated `appsync_lambda_main!` macro and re-exports `aws_config`, `lambda_runtime`, and `tokio`. Not required when using [`make_appsync!`] or the composable macros (you depend on `lambda_runtime` and `tokio` directly). |
253//! | **`log`** | Re-exports the [`log`](https://docs.rs/log) crate so resolver code can use `log::info!` etc. without a separate dependency. |
254//! | **`env_logger`** | Initializes `env_logger` for local development. Implies `log` and `compat`. |
255//! | **`tracing`** | Re-exports `tracing` and `tracing-subscriber` for structured, async-aware logging. |
256
257mod aws_scalars;
258mod id;
259pub mod subscription_filters;
260
261use std::{collections::HashMap, ops::BitOr};
262
263use aws_smithy_types::error::metadata::ProvideErrorMetadata;
264use serde_json::Value;
265
266use serde::{de::DeserializeOwned, Deserialize, Serialize};
267use thiserror::Error;
268
269pub use aws_scalars::{
270    datetime::{AWSDate, AWSDateTime, AWSTime},
271    email::AWSEmail,
272    phone::AWSPhone,
273    timestamp::AWSTimestamp,
274    url::AWSUrl,
275};
276pub use id::ID;
277
278#[doc(inline)]
279pub use lambda_appsync_proc::appsync_operation;
280#[doc(inline)]
281pub use lambda_appsync_proc::make_appsync;
282#[doc(inline)]
283pub use lambda_appsync_proc::make_handlers;
284#[doc(inline)]
285pub use lambda_appsync_proc::make_operation;
286#[doc(inline)]
287pub use lambda_appsync_proc::make_types;
288
289// Re-export crates that are mandatory for the proc_macro to succeed
290pub use lambda_runtime;
291pub use serde;
292pub use serde_json;
293pub use tokio;
294
295/// Re-exports of `aws_config`, `lambda_runtime`, `tokio`, and `appsync_lambda_main` required by the `compat` feature.
296#[cfg(feature = "compat")]
297mod compat {
298    pub use aws_config;
299
300    #[doc(inline)]
301    pub use lambda_appsync_proc::appsync_lambda_main;
302}
303#[cfg(feature = "compat")]
304pub use compat::*;
305
306#[cfg(feature = "log")]
307pub use log;
308
309#[cfg(feature = "env_logger")]
310pub use env_logger;
311
312#[cfg(feature = "tracing")]
313pub use tracing;
314#[cfg(feature = "tracing")]
315pub use tracing_subscriber;
316
317/// Authorization strategy for AppSync operations.
318///
319/// It determines whether operations are allowed or denied based on the
320/// authentication context provided by AWS AppSync. It is typically used by AppSync
321/// itself in conjunction with AWS Cognito user pools and usually do not concern
322/// the application code.
323#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
324#[serde(rename_all = "UPPERCASE")]
325pub enum AppsyncAuthStrategy {
326    /// Allows the operation by default if no explicit authorizer is associated to the field
327    Allow,
328    /// Denies the operation by default if no explicit authorizer is associated to the field
329    Deny,
330}
331
332/// Identity information for Cognito User Pools authenticated requests.
333#[derive(Debug, Deserialize)]
334#[serde(rename_all = "camelCase")]
335pub struct AppsyncIdentityCognito {
336    /// Unique identifier of the authenticated user/client
337    pub sub: String,
338    /// Username of the authenticated user (from Cognito user pools)
339    pub username: String,
340    /// Identity provider that authenticated the request (e.g. Cognito user pool URL)
341    pub issuer: String,
342    /// Default authorization strategy for the authenticated identity
343    pub default_auth_strategy: AppsyncAuthStrategy,
344    /// Source IP addresses associated with the request
345    pub source_ip: Vec<String>,
346    /// Groups the authenticated user belongs to
347    pub groups: Option<Vec<String>>,
348    /// Additional claims/attributes associated with the identity
349    pub claims: Value,
350}
351
352/// Authentication type in a Cognito Identity Pool
353#[derive(Debug, Clone, Copy, Deserialize, PartialEq, Eq)]
354#[serde(rename_all = "lowercase")]
355pub enum CognitoIdentityAuthType {
356    /// User is authenticated with an identity provider
357    Authenticated,
358    /// User is an unauthenticated guest
359    Unauthenticated,
360}
361
362/// Cognito Identity Pool information for federated IAM authentication
363#[derive(Debug, Deserialize)]
364pub struct CognitoFederatedIdentity {
365    /// Unique identifier assigned to the authenticated/unauthenticated identity
366    /// within the Cognito Identity Pool
367    #[serde(rename = "cognitoIdentityId")]
368    pub identity_id: String,
369    /// Identifier of the Cognito Identity Pool that is being used for federation.
370    /// In the format of region:pool-id
371    #[serde(rename = "cognitoIdentityPoolId")]
372    pub identity_pool_id: String,
373    /// Indicates whether the identity is authenticated with an identity provider
374    /// or is an unauthenticated guest access
375    #[serde(rename = "cognitoIdentityAuthType")]
376    pub auth_type: CognitoIdentityAuthType,
377    /// For authenticated identities, contains information about the identity provider
378    /// used for authentication. Format varies by provider type
379    #[serde(rename = "cognitoIdentityAuthProvider")]
380    pub auth_provider: String,
381}
382
383/// Identity information for IAM-authenticated requests.
384///
385/// Contains AWS IAM-specific authentication details, including optional Cognito
386/// identity pool information when using federated identities.
387#[derive(Debug, Deserialize)]
388#[serde(rename_all = "camelCase")]
389pub struct AppsyncIdentityIam {
390    /// AWS account ID of the caller
391    pub account_id: String,
392    /// Source IP address(es) of the caller
393    pub source_ip: Vec<String>,
394    /// IAM username of the caller
395    pub username: String,
396    /// Full IAM ARN of the caller
397    pub user_arn: String,
398    /// Federated identity information when using Cognito Identity Pools
399    #[serde(flatten)]
400    pub federated_identity: Option<CognitoFederatedIdentity>,
401}
402
403/// Identity information for OIDC-authenticated requests.
404#[derive(Debug, Deserialize)]
405pub struct AppsyncIdentityOidc {
406    /// The issuer of the token
407    pub iss: String,
408    /// The subject (usually the user identifier)
409    pub sub: String,
410    /// Token audience
411    pub aud: String,
412    /// Expiration time
413    pub exp: i64,
414    /// Issued at time
415    pub iat: i64,
416    /// Additional custom claims from the OIDC provider
417    #[serde(flatten)]
418    pub additional_claims: HashMap<String, serde_json::Value>,
419}
420
421/// Identity information for Lambda-authorized requests.
422#[derive(Debug, Deserialize)]
423pub struct AppsyncIdentityLambda {
424    /// Custom resolver context returned by the Lambda authorizer
425    #[serde(rename = "resolverContext")]
426    pub resolver_context: serde_json::Value,
427}
428
429/// Identity information for an AppSync request.
430///
431/// Represents the identity context of the authenticated user/client making the request to
432/// AWS AppSync. This enum corresponds directly to AppSync's authorization types as defined
433/// in the AWS documentation.
434///
435/// Each variant maps to one of the five supported AWS AppSync authorization modes:
436///
437/// - [Cognito](AppsyncIdentity::Cognito): Uses Amazon Cognito User Pools, providing group-based
438///   access control with JWT tokens containing encoded user information like groups and custom claims.
439///
440/// - [Iam](AppsyncIdentity::Iam): Uses AWS IAM roles and policies through AWS Signature Version 4
441///   signing. Can be used either directly with IAM users/roles or through Cognito Identity Pools
442///   for federated access. Enables fine-grained access control through IAM policies.
443///
444/// - [Oidc](AppsyncIdentity::Oidc): OpenID Connect authentication integrating with any
445///   OIDC-compliant provider.
446///
447/// - [Lambda](AppsyncIdentity::Lambda): Custom authorization through an AWS Lambda function
448///   that evaluates each request.
449///
450/// - [ApiKey](AppsyncIdentity::ApiKey): Simple API key-based authentication using keys
451///   generated and managed by AppSync.
452///
453/// The variant is determined by the authorization configuration of your AppSync API and
454/// the authentication credentials provided in the request. Each variant contains structured
455/// information specific to that authentication mode, which can be used in resolvers for
456/// custom authorization logic.
457///
458/// More information can be found in the [AWS documentation](https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html).
459#[derive(Debug, Deserialize)]
460#[serde(untagged)]
461pub enum AppsyncIdentity {
462    /// Amazon Cognito User Pools authentication
463    Cognito(AppsyncIdentityCognito),
464    /// AWS IAM authentication
465    Iam(AppsyncIdentityIam),
466    /// OpenID Connect authentication
467    Oidc(AppsyncIdentityOidc),
468    /// Lambda authorizer authentication
469    Lambda(AppsyncIdentityLambda),
470    /// API Key authentication (represents null identity in JSON)
471    ApiKey,
472}
473
474/// Metadata about an AppSync GraphQL operation execution.
475///
476/// Contains detailed information about the GraphQL operation being executed,
477/// including the operation type, selected fields, and variables. The type parameter
478/// `O` represents the `Operation` enum generated by [make_appsync!] (or [make_operation!])
479/// that defines all valid operations for this Lambda resolver.
480#[derive(Debug, Deserialize)]
481#[allow(dead_code)]
482pub struct AppsyncEventInfo<O> {
483    /// The specific GraphQL operation being executed (Query/Mutation)
484    #[serde(flatten)]
485    pub operation: O,
486    /// Raw GraphQL selection set as a string
487    #[serde(rename = "selectionSetGraphQL")]
488    pub selection_set_graphql: String,
489    /// List of selected field paths in the GraphQL query
490    #[serde(rename = "selectionSetList")]
491    pub selection_set_list: Vec<String>,
492    /// Variables passed to the GraphQL operation
493    pub variables: HashMap<String, Value>,
494}
495
496/// Represents a complete AWS AppSync event sent to a Lambda resolver.
497///
498/// Contains all context and data needed to resolve a GraphQL operation, including
499/// authentication details, operation info, and arguments. The generic `O`
500/// must be the `Operation` enum generated by [make_appsync!] (or [make_operation!]).
501///
502/// # Limitations
503/// - Omits the `stash` field used for pipeline resolvers
504/// - Omits the `prev` field as it's not relevant for direct Lambda resolvers
505#[derive(Debug, Deserialize)]
506#[allow(dead_code)]
507pub struct AppsyncEvent<O> {
508    /// Authentication context
509    pub identity: AppsyncIdentity,
510    /// Raw request context from AppSync
511    pub request: Value,
512    /// Parent field's resolved value in nested resolvers
513    pub source: Value,
514    /// Metadata about the GraphQL operation
515    pub info: AppsyncEventInfo<O>,
516    /// Arguments passed to the GraphQL field
517    #[serde(rename = "arguments")]
518    pub args: Value,
519    // Should never be usefull in a Direct Lambda Invocation context
520    // pub stash: Value,
521    // pub prev: Value,
522}
523
524/// Response structure returned to AWS AppSync from a Lambda resolver.
525///
526/// Can contain either successful data or error information, but not both.
527/// Should be constructed using From implementations for either [Value] (success)
528/// or [AppsyncError] (failure).
529///
530/// # Examples
531/// ```
532/// # use serde_json::json;
533/// # use lambda_appsync::{AppsyncError, AppsyncResponse};
534/// // Success response
535/// let response: AppsyncResponse = json!({ "id": 123 }).into();
536///
537/// // Error response
538/// let error = AppsyncError::new("NotFound", "Resource not found");
539/// let response: AppsyncResponse = error.into();
540/// ```
541#[derive(Debug, Serialize)]
542pub struct AppsyncResponse {
543    data: Option<Value>,
544    #[serde(flatten, skip_serializing_if = "Option::is_none")]
545    error: Option<AppsyncError>,
546}
547
548impl AppsyncResponse {
549    /// Returns an unauthorized error response
550    ///
551    /// This creates a standard unauthorized error response for when a request
552    /// lacks proper authentication.
553    ///
554    /// # Examples
555    /// ```
556    /// # use lambda_appsync::AppsyncResponse;
557    /// let response = AppsyncResponse::unauthorized();
558    /// ```
559    pub fn unauthorized() -> Self {
560        AppsyncError::new("Unauthorized", "This operation cannot be authorized").into()
561    }
562
563    /// Returns a reference to the response data, if present
564    ///
565    /// # Examples
566    ///
567    /// ```
568    /// # use lambda_appsync::AppsyncResponse;
569    /// # use serde_json::json;
570    /// let response = AppsyncResponse::from(json!({"user": "Alice"}));
571    /// assert!(response.data().is_some());
572    /// ```
573    pub fn data(&self) -> Option<&Value> {
574        self.data.as_ref()
575    }
576
577    /// Returns a reference to the response error, if present
578    ///
579    /// # Examples
580    ///
581    /// ```
582    /// # use lambda_appsync::{AppsyncResponse, AppsyncError};
583    /// let error = AppsyncError::new("NotFound", "User not found");
584    /// let response = AppsyncResponse::from(error);
585    /// assert!(response.error().is_some());
586    /// ```
587    pub fn error(&self) -> Option<&AppsyncError> {
588        self.error.as_ref()
589    }
590}
591
592impl From<Value> for AppsyncResponse {
593    fn from(value: Value) -> Self {
594        Self {
595            data: Some(value),
596            error: None,
597        }
598    }
599}
600impl From<AppsyncError> for AppsyncResponse {
601    fn from(value: AppsyncError) -> Self {
602        Self {
603            data: None,
604            error: Some(value),
605        }
606    }
607}
608
609/// Error type for AWS AppSync operations
610///
611/// Multiple errors can be combined in one using the pipe operator
612///
613/// # Example
614/// ```
615/// # use lambda_appsync::AppsyncError;
616/// let combined_error = AppsyncError::new("ValidationError", "Email address is invalid") | AppsyncError::new("DatabaseError", "User not found in database");
617/// // error_type: "ValidationError|DatabaseError"
618/// // error_message: "Email address is invalid\nUser not found in database"
619/// ```
620///
621/// Can be created from any AWS SDK error or directly by the user.
622///
623/// # Example
624/// ```
625/// # use lambda_appsync::AppsyncError;
626/// # use aws_sdk_dynamodb::types::AttributeValue;
627/// struct Item {
628///   id: u64,
629///   data: String
630/// }
631/// async fn store_item(item: Item, client: &aws_sdk_dynamodb::Client) -> Result<(), AppsyncError> {
632///     client.put_item()
633///         .table_name("my-table")
634///         .item("id", AttributeValue::N(item.id.to_string()))
635///         .item("data", AttributeValue::S(item.data))
636///         .send()
637///         .await?;
638///     Ok(())
639/// }
640/// ```
641#[derive(Debug, Error, Serialize)]
642#[serde(rename_all = "camelCase")]
643#[error("{error_type}: {error_message}")]
644pub struct AppsyncError {
645    /// The type/category of error that occurred (e.g. "ValidationError", "NotFound", "DatabaseError")
646    pub error_type: String,
647    /// A detailed message describing the specific error condition
648    pub error_message: String,
649}
650impl AppsyncError {
651    /// Creates a new AppSync error with the specified error type and message
652    ///
653    /// # Arguments
654    /// * `error_type` - The type/category of the error (e.g. "ValidationError", "NotFound")
655    /// * `error_message` - A detailed message describing the error
656    ///
657    /// # Example
658    /// ```
659    /// # use lambda_appsync::AppsyncError;
660    /// let error = AppsyncError::new("NotFound", "User with ID 123 not found");
661    /// ```
662    pub fn new(error_type: impl Into<String>, error_message: impl Into<String>) -> Self {
663        AppsyncError {
664            error_type: error_type.into(),
665            error_message: error_message.into(),
666        }
667    }
668}
669impl<T: ProvideErrorMetadata> From<T> for AppsyncError {
670    fn from(value: T) -> Self {
671        let meta = ProvideErrorMetadata::meta(&value);
672        AppsyncError {
673            error_type: meta.code().unwrap_or("Unknown").to_owned(),
674            error_message: meta.message().unwrap_or_default().to_owned(),
675        }
676    }
677}
678
679impl BitOr for AppsyncError {
680    type Output = AppsyncError;
681    fn bitor(self, rhs: Self) -> Self::Output {
682        AppsyncError {
683            error_type: format!("{}|{}", self.error_type, rhs.error_type),
684            error_message: format!("{}\n{}", self.error_message, rhs.error_message),
685        }
686    }
687}
688
689/// Extracts and deserializes a named argument from a JSON Value into the specified type
690///
691/// # Arguments
692/// * `args` - Mutable reference to a JSON Value containing arguments
693/// * `arg_name` - Name of the argument to extract
694///
695/// # Returns
696/// * `Ok(T)` - Successfully deserialized value of type T
697/// * `Err(AppsyncError)` - Error if argument is missing or invalid format
698///
699/// # Examples
700/// ```
701/// # use serde_json::json;
702/// # use lambda_appsync::arg_from_json;
703/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
704/// let mut args = json!({
705///     "userId": "123",
706///     "count": 5
707/// });
708///
709/// // Extract userId as String
710/// let user_id: String = arg_from_json(&mut args, "userId")?;
711/// assert_eq!(user_id, "123");
712///
713/// // Extract count as i32
714/// let count: i32 = arg_from_json(&mut args, "count")?;
715/// assert_eq!(count, 5);
716///
717/// // Error case: invalid type
718/// let result: Result<String, _> = arg_from_json(&mut args, "count");
719/// assert!(result.is_err());
720///
721/// // Error case: missing argument
722/// let result: Result<String, _> = arg_from_json(&mut args, "missing");
723/// assert!(result.is_err());
724/// # Ok(())
725/// # }
726/// ```
727pub fn arg_from_json<T: DeserializeOwned>(
728    args: &mut serde_json::Value,
729    arg_name: &'static str,
730) -> Result<T, AppsyncError> {
731    serde_json::from_value(
732        args.get_mut(arg_name)
733            .unwrap_or(&mut serde_json::Value::Null)
734            .take(),
735    )
736    .map_err(|e| {
737        AppsyncError::new(
738            "InvalidArgs",
739            format!("Argument \"{arg_name}\" is not the expected format ({e})"),
740        )
741    })
742}
743
744/// Serializes a value into a JSON Value for AppSync responses
745///
746/// # Arguments
747/// * `res` - Value to serialize that implements Serialize
748///
749/// # Returns
750/// JSON Value representation of the input
751///
752/// # Panics
753/// Panics if the value cannot be serialized.
754///
755/// # Examples
756/// ```
757/// # use serde::Serialize;
758/// # use serde_json::json;
759/// # use lambda_appsync::res_to_json;
760/// #[derive(Serialize)]
761/// struct User {
762///     id: String,
763///     name: String
764/// }
765///
766/// let user = User {
767///     id: "123".to_string(),
768///     name: "John".to_string()
769/// };
770///
771/// let json = res_to_json(user);
772/// assert_eq!(json, json!({
773///     "id": "123",
774///     "name": "John"
775/// }));
776///
777/// // Simple types also work
778/// let num = res_to_json(42);
779/// assert_eq!(num, json!(42));
780/// ```
781pub fn res_to_json<T: Serialize>(res: T) -> serde_json::Value {
782    serde_json::to_value(res).expect("Appsync schema objects are JSON compatible")
783}
784
785#[cfg(test)]
786mod tests {
787    use super::*;
788    use serde_json::json;
789
790    #[test]
791    fn test_appsync_auth_strategy() {
792        let allow: AppsyncAuthStrategy = serde_json::from_str("\"ALLOW\"").unwrap();
793        let deny: AppsyncAuthStrategy = serde_json::from_str("\"DENY\"").unwrap();
794
795        match allow {
796            AppsyncAuthStrategy::Allow => (),
797            _ => panic!("Expected Allow"),
798        }
799
800        match deny {
801            AppsyncAuthStrategy::Deny => (),
802            _ => panic!("Expected Deny"),
803        }
804    }
805
806    #[test]
807    fn test_appsync_identity_cognito() {
808        let json = json!({
809            "sub": "user123",
810            "username": "testuser",
811            "issuer": "https://cognito-idp.region.amazonaws.com/pool_id",
812            "defaultAuthStrategy": "ALLOW",
813            "sourceIp": ["1.2.3.4"],
814            "groups": ["admin", "users"],
815            "claims": {
816                "email": "user@example.com",
817                "custom:role": "developer"
818            }
819        });
820
821        if let AppsyncIdentity::Cognito(cognito) = serde_json::from_value(json).unwrap() {
822            assert_eq!(cognito.sub, "user123");
823            assert_eq!(cognito.username, "testuser");
824            assert_eq!(
825                cognito.issuer,
826                "https://cognito-idp.region.amazonaws.com/pool_id"
827            );
828            assert_eq!(cognito.default_auth_strategy, AppsyncAuthStrategy::Allow);
829            assert_eq!(cognito.source_ip, vec!["1.2.3.4"]);
830            assert_eq!(
831                cognito.groups,
832                Some(vec!["admin".to_string(), "users".to_string()])
833            );
834            assert_eq!(
835                cognito.claims,
836                json!({
837                    "email": "user@example.com",
838                    "custom:role": "developer"
839                })
840            );
841        } else {
842            panic!("Expected Cognito variant");
843        }
844    }
845
846    #[test]
847    fn test_appsync_identity_iam() {
848        let json = json!({
849            "accountId": "123456789012",
850            "sourceIp": ["1.2.3.4"],
851            "username": "IAMUser",
852            "userArn": "arn:aws:iam::123456789012:user/IAMUser"
853        });
854
855        if let AppsyncIdentity::Iam(iam) = serde_json::from_value(json).unwrap() {
856            assert_eq!(iam.account_id, "123456789012");
857            assert_eq!(iam.source_ip, vec!["1.2.3.4"]);
858            assert_eq!(iam.username, "IAMUser");
859            assert_eq!(iam.user_arn, "arn:aws:iam::123456789012:user/IAMUser");
860            assert!(iam.federated_identity.is_none());
861        } else {
862            panic!("Expected IAM variant");
863        }
864    }
865
866    #[test]
867    fn test_appsync_identity_iam_with_cognito() {
868        let json = json!({
869            "accountId": "123456789012",
870            "sourceIp": ["1.2.3.4"],
871            "username": "IAMUser",
872            "userArn": "arn:aws:iam::123456789012:user/IAMUser",
873            "cognitoIdentityId": "region:id",
874            "cognitoIdentityPoolId": "region:pool_id",
875            "cognitoIdentityAuthType": "authenticated",
876            "cognitoIdentityAuthProvider": "cognito-idp.region.amazonaws.com/pool_id"
877        });
878
879        if let AppsyncIdentity::Iam(iam) = serde_json::from_value(json).unwrap() {
880            assert_eq!(iam.account_id, "123456789012");
881            assert_eq!(iam.source_ip, vec!["1.2.3.4"]);
882            assert_eq!(iam.username, "IAMUser");
883            assert_eq!(iam.user_arn, "arn:aws:iam::123456789012:user/IAMUser");
884
885            let federated = iam.federated_identity.unwrap();
886            assert_eq!(federated.identity_id, "region:id");
887            assert_eq!(federated.identity_pool_id, "region:pool_id");
888            assert!(matches!(
889                federated.auth_type,
890                CognitoIdentityAuthType::Authenticated
891            ));
892            assert_eq!(
893                federated.auth_provider,
894                "cognito-idp.region.amazonaws.com/pool_id"
895            );
896        } else {
897            panic!("Expected IAM variant");
898        }
899    }
900
901    #[test]
902    fn test_appsync_identity_oidc() {
903        let json = json!({
904            "iss": "https://auth.example.com",
905            "sub": "user123",
906            "aud": "client123",
907            "exp": 1714521210,
908            "iat": 1714517610,
909            "name": "John Doe",
910            "email": "john@example.com",
911            "roles": ["admin"],
912            "org_id": "org123",
913            "custom_claim": "value"
914        });
915
916        if let AppsyncIdentity::Oidc(oidc) = serde_json::from_value(json).unwrap() {
917            assert_eq!(oidc.iss, "https://auth.example.com");
918            assert_eq!(oidc.sub, "user123");
919            assert_eq!(oidc.aud, "client123");
920            assert_eq!(oidc.exp, 1714521210);
921            assert_eq!(oidc.iat, 1714517610);
922            assert_eq!(oidc.additional_claims.get("name").unwrap(), "John Doe");
923            assert_eq!(
924                oidc.additional_claims.get("email").unwrap(),
925                "john@example.com"
926            );
927            assert_eq!(
928                oidc.additional_claims.get("roles").unwrap(),
929                &json!(["admin"])
930            );
931            assert_eq!(oidc.additional_claims.get("org_id").unwrap(), "org123");
932            assert_eq!(oidc.additional_claims.get("custom_claim").unwrap(), "value");
933        } else {
934            panic!("Expected OIDC variant");
935        }
936    }
937
938    #[test]
939    fn test_appsync_identity_lambda() {
940        let json = json!({
941            "resolverContext": {
942                "userId": "user123",
943                "permissions": ["read", "write"],
944                "metadata": {
945                    "region": "us-west-2",
946                    "environment": "prod"
947                }
948            }
949        });
950
951        if let AppsyncIdentity::Lambda(lambda) = serde_json::from_value(json).unwrap() {
952            assert_eq!(
953                lambda.resolver_context,
954                json!({
955                    "userId": "user123",
956                    "permissions": ["read", "write"],
957                    "metadata": {
958                        "region": "us-west-2",
959                        "environment": "prod"
960                    }
961                })
962            );
963        } else {
964            panic!("Expected Lambda variant");
965        }
966    }
967
968    #[test]
969    fn test_appsync_identity_api_key() {
970        let json = serde_json::Value::Null;
971
972        if let AppsyncIdentity::ApiKey = serde_json::from_value(json).unwrap() {
973            // Test passes if we get the ApiKey variant
974        } else {
975            panic!("Expected ApiKey variant");
976        }
977    }
978
979    #[test]
980    fn test_appsync_response() {
981        let success = AppsyncResponse::from(json!({"field": "value"}));
982        assert!(success.data.is_some());
983        assert!(success.error.is_none());
984
985        let error = AppsyncResponse::from(AppsyncError::new("TestError", "message"));
986        assert!(error.data.is_none());
987        assert!(error.error.is_some());
988    }
989
990    #[test]
991    fn test_appsync_error() {
992        let error = AppsyncError::new("TestError", "message");
993        assert_eq!(error.error_type, "TestError");
994        assert_eq!(error.error_message, "message");
995
996        let error1 = AppsyncError::new("Error1", "msg1");
997        let error2 = AppsyncError::new("Error2", "msg2");
998        let combined = error1 | error2;
999
1000        assert_eq!(combined.error_type, "Error1|Error2");
1001        assert_eq!(combined.error_message, "msg1\nmsg2");
1002    }
1003
1004    #[test]
1005    fn test_arg_from_json() {
1006        let mut args = json!({
1007            "string": "test",
1008            "number": 42,
1009            "bool": true
1010        });
1011
1012        let s: String = arg_from_json(&mut args, "string").unwrap();
1013        assert_eq!(s, "test");
1014
1015        let n: i32 = arg_from_json(&mut args, "number").unwrap();
1016        assert_eq!(n, 42);
1017
1018        let b: bool = arg_from_json(&mut args, "bool").unwrap();
1019        assert!(b);
1020
1021        let err: Result<String, _> = arg_from_json(&mut args, "missing");
1022        assert!(err.is_err());
1023    }
1024
1025    #[test]
1026    fn test_res_to_json() {
1027        #[derive(Serialize)]
1028        struct Test {
1029            field: String,
1030        }
1031
1032        let test = Test {
1033            field: "value".to_string(),
1034        };
1035
1036        let json = res_to_json(test);
1037        assert_eq!(json, json!({"field": "value"}));
1038
1039        assert_eq!(res_to_json(42), json!(42));
1040        assert_eq!(res_to_json("test"), json!("test"));
1041    }
1042}