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}