lambda_appsync_proc/
lib.rs

1#![warn(missing_docs)]
2#![warn(rustdoc::missing_crate_level_docs)]
3//! This crate provides procedural macros for implementing AWS AppSync Direct Lambda resolvers.
4//!
5//! It helps convert GraphQL schemas into type-safe Rust code with full AWS Lambda runtime support.
6//! The main functionality is provided through the `appsync_lambda_main` and `appsync_operation` macros.
7//!
8//! # Complete Example
9//!
10//! ```no_run
11//! use lambda_appsync::{appsync_lambda_main, appsync_operation, AppsyncError};
12//!
13//! // 1. First define your GraphQL schema (e.g. `schema.graphql`):
14//! //
15//! // type Query {
16//! //   players: [Player!]!
17//! //   gameStatus: GameStatus!
18//! // }
19//! //
20//! // type Player {
21//! //   id: ID!
22//! //   name: String!
23//! //   team: Team!
24//! // }
25//! //
26//! // enum Team {
27//! //   RUST
28//! //   PYTHON
29//! //   JS
30//! // }
31//! //
32//! // enum GameStatus {
33//! //   STARTED
34//! //   STOPPED
35//! // }
36//!
37//! // 2. Initialize the Lambda runtime with AWS SDK clients in main.rs:
38//!
39//! // Optional hook for custom request validation/auth
40//! async fn verify_request(
41//!     event: &lambda_appsync::AppsyncEvent<Operation>
42//! ) -> Option<lambda_appsync::AppsyncResponse> {
43//!     // Return Some(response) to short-circuit normal execution
44//!     None
45//! }
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//! #   exclude_lambda_handler = true,
57//! );
58//! # fn dynamodb() -> aws_sdk_dynamodb::Client {todo!()}
59//! # fn main() {}
60//! // 3. Implement resolver functions for GraphQL operations:
61//!
62//! #[appsync_operation(query(players))]
63//! async fn get_players() -> Result<Vec<Player>, AppsyncError> {
64//!     let client = dynamodb();
65//!     todo!()
66//! }
67//!
68//! #[appsync_operation(query(gameStatus))]
69//! async fn get_game_status() -> Result<GameStatus, AppsyncError> {
70//!     let client = dynamodb();
71//!     todo!()
72//! }
73//!
74//! // The macro ensures the function signature matches the GraphQL schema
75//! // and wires everything up to handle AWS AppSync requests automatically
76//! ```
77
78mod appsync_lambda_main;
79mod appsync_operation;
80mod common;
81
82use proc_macro::TokenStream;
83
84/// Generates the code required to handle AWS AppSync Direct Lambda resolver events based on a GraphQL schema.
85///
86/// This macro takes a path to a GraphQL schema file and generates the complete foundation
87/// for implementing an AWS AppSync Direct Lambda resolver:
88///
89/// - Rust types for all GraphQL types (enums, inputs, objects)
90/// - Query/Mutation/Subscription operation enums
91/// - AWS Lambda runtime setup with logging to handle the AWS AppSync event
92/// - Optional AWS SDK client initialization
93///
94/// # Options
95///
96/// - `batch = bool`: Enable/disable batch request handling (default: true)
97/// - `hook = fn_name`: Add a custom hook function for request validation/auth
98/// - `exclude_lambda_handler = bool`: Skip generation of Lambda handler code
99/// - `only_lambda_handler = bool`: Only generate Lambda handler code
100/// - `exclude_appsync_types = bool`: Skip generation of GraphQL type definitions
101/// - `only_appsync_types = bool`: Only generate GraphQL type definitions
102/// - `exclude_appsync_operations = bool`: Skip generation of operation enums
103/// - `only_appsync_operations = bool`: Only generate operation enums
104/// - `field_type_override = Type.field: CustomType`: Override type of a specific field
105///
106/// # Examples
107///
108/// Basic usage with authentication hook:
109/// ```no_run
110/// use lambda_appsync::{appsync_lambda_main, AppsyncEvent, AppsyncResponse, AppsyncIdentity};
111///
112///  fn is_authorized(identity: Option<&AppsyncIdentity>) -> bool {
113///     todo!()
114/// }
115///
116/// // If the function returns Some(AppsyncResponse), the Lambda function will immediatly return it
117/// // Else, the normal flow of the AppSync operation processing will continue
118/// // This is primarily intended for advanced authentication checks that AppSync cannot perform,
119/// // such as verifying that a user is requesting their own ID for example.
120/// async fn auth_hook(
121///     event: &lambda_appsync::AppsyncEvent<Operation>
122/// ) -> Option<lambda_appsync::AppsyncResponse> {
123///     // Verify JWT token, check permissions etc
124///     if !is_authorized(event.identity.as_ref()) {
125///         return Some(AppsyncResponse::unauthorized());
126///     }
127///     None
128/// }
129///
130/// appsync_lambda_main!(
131///     "schema.graphql",
132///     hook = auth_hook,
133///     dynamodb() -> aws_sdk_dynamodb::Client
134/// );
135/// ```
136///
137/// Generate only types for lib code generation:
138/// ```no_run
139/// use lambda_appsync::appsync_lambda_main;
140/// appsync_lambda_main!(
141///     "schema.graphql",
142///     only_appsync_types = true
143/// );
144/// ```
145///
146/// Override field types and use multiple AWS clients:
147/// ```no_run
148/// use lambda_appsync::appsync_lambda_main;
149/// appsync_lambda_main!(
150///     "schema.graphql",
151///     dynamodb() -> aws_sdk_dynamodb::Client,
152///     s3() -> aws_sdk_s3::Client,
153///     // Use String instead of the default lambda_appsync::ID
154///     field_type_override = Player.id: String,
155/// );
156/// ```
157///
158/// Disable batch processing:
159/// ```no_run
160/// lambda_appsync::appsync_lambda_main!(
161///     "schema.graphql",
162///     batch = false
163/// );
164/// ```
165#[proc_macro]
166pub fn appsync_lambda_main(input: TokenStream) -> TokenStream {
167    appsync_lambda_main::appsync_lambda_main_impl(input)
168}
169
170/// Marks an async function as an AWS AppSync resolver operation, binding it to a specific Query,
171/// Mutation or Subscription operation defined in the GraphQL schema.
172///
173/// The marked function must match the signature of the GraphQL operation, with parameters and return
174/// type matching what is defined in the schema. The function will be wired up to handle requests
175/// for that operation through the AWS AppSync Direct Lambda resolver.
176///
177/// # Important
178/// This macro can only be used in a crate where the [appsync_lambda_main!] macro has been used at the
179/// root level (typically in `main.rs`). The code generated by this macro depends on types and
180/// implementations that are created by [appsync_lambda_main!].
181///
182/// # Example Usage
183///
184/// ```no_run
185/// use lambda_appsync::{appsync_operation, AppsyncError};
186/// # lambda_appsync::appsync_lambda_main!(
187/// #    "schema.graphql",
188/// #     exclude_lambda_handler = true,
189/// # );
190/// # async fn dynamodb_get_players() -> Result<Vec<Player>, AppsyncError> {
191/// #    todo!()
192/// # }
193/// # async fn dynamodb_create_player(name: String) -> Result<Player, AppsyncError> {
194/// #    todo!()
195/// # }
196///
197/// // Execute when a 'players' query is received
198/// #[appsync_operation(query(players))]
199/// async fn get_players() -> Result<Vec<Player>, AppsyncError> {
200///     // Implement resolver logic
201///     Ok(dynamodb_get_players().await?)
202/// }
203///
204/// // Handle a 'createPlayer' mutation
205/// #[appsync_operation(mutation(createPlayer))]
206/// async fn create_player(name: String) -> Result<Player, AppsyncError> {
207///     Ok(dynamodb_create_player(name).await?)
208/// }
209///
210/// // (Optional) Use an enhanced subscription filter for onCreatePlayer
211/// use lambda_appsync::subscription_filters::{FilterGroup, Filter, FieldPath};
212/// #[appsync_operation(subscription(onCreatePlayer))]
213/// async fn on_create_player(name: String) -> Result<Option<FilterGroup>, AppsyncError> {
214///     Ok(Some(FilterGroup::from([
215///         Filter::from([
216///             FieldPath::new("name")?.contains(name)
217///         ])
218///     ])))
219/// }
220/// # fn main() {}
221/// ```
222///
223/// When using a single [FieldPath](lambda_appsync::subscription_filters::FieldPath) you can turn it directly into a [FilterGroup](lambda_appsync::subscription_filters::FilterGroup)
224/// ```no_run
225/// # use lambda_appsync::{appsync_operation, AppsyncError};
226/// # lambda_appsync::appsync_lambda_main!(
227/// #    "schema.graphql",
228/// #     exclude_lambda_handler = true,
229/// # );
230/// use lambda_appsync::subscription_filters::{FilterGroup, Filter, FieldPath};
231/// #[appsync_operation(subscription(onCreatePlayer))]
232/// async fn on_create_player(name: String) -> Result<Option<FilterGroup>, AppsyncError> {
233///     Ok(Some(FieldPath::new("name")?.contains(name).into()))
234/// }
235/// # fn main() {}
236/// ```
237///
238/// By default the #[appsync_operation(...)] macro will discard your function's name but
239/// you can also keep it available:
240/// ```no_run
241/// use lambda_appsync::{appsync_operation, AppsyncError};
242/// # lambda_appsync::appsync_lambda_main!(
243/// #    "schema.graphql",
244/// #     exclude_lambda_handler = true,
245/// # );
246/// # async fn dynamodb_get_players() -> Result<Vec<Player>, AppsyncError> {
247/// #    todo!()
248/// # }
249/// # async fn dynamodb_create_player(name: String) -> Result<Player, AppsyncError> {
250/// #    todo!()
251/// # }
252/// // Keep the original function name available separately
253/// #[appsync_operation(query(players), keep_original_function_name)]
254/// async fn fetch_players() -> Result<Vec<Player>, AppsyncError> {
255///     // Can still call fetch_players() directly
256///     Ok(dynamodb_get_players().await?)
257/// }
258/// # fn main() {}
259/// ```
260///
261/// The macro will ensure the function signature matches what is defined in the GraphQL schema,
262/// and wire it up to be called when AWS AppSync invokes the Lambda resolver for that operation.
263///
264///
265/// # Important Note
266///
267/// When using enhanced subscription filters (i.e., returning a [FilterGroup](lambda_appsync::subscription_filters::FilterGroup)
268/// from Subscribe operation handlers), you need to modify your ***Response*** mapping in AWS AppSync.
269/// It must contain the following:
270///
271/// ```vtl
272/// #if($context.result.data)
273/// $extensions.setSubscriptionFilter($context.result.data)
274/// #end
275/// null
276/// ```
277#[proc_macro_attribute]
278pub fn appsync_operation(args: TokenStream, input: TokenStream) -> TokenStream {
279    appsync_operation::appsync_operation_impl(args, input)
280}