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 in a file (path can be relative to crate root
14//! // or workspace root depending on the Cargo invocation context, 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//!
48//! // Generate types and runtime setup from schema
49//! appsync_lambda_main!(
50//!     "schema.graphql",
51//!     // Initialize DynamoDB client if needed
52//!     dynamodb() -> aws_sdk_dynamodb::Client,
53//!     // Enable validation hook
54//!     hook = verify_request,
55//!     // Enable batch processing
56//!     batch = true,
57//! #   exclude_lambda_handler = true,
58//! );
59//! # fn dynamodb() -> aws_sdk_dynamodb::Client {todo!()}
60//! # fn main() {}
61//! // 3. Implement resolver functions for GraphQL operations:
62//!
63//! #[appsync_operation(query(players))]
64//! async fn get_players() -> Result<Vec<Player>, AppsyncError> {
65//!     let client = dynamodb();
66//!     todo!()
67//! }
68//!
69//! #[appsync_operation(query(gameStatus))]
70//! async fn get_game_status() -> Result<GameStatus, AppsyncError> {
71//!     let client = dynamodb();
72//!     todo!()
73//! }
74//!
75//! // The macro ensures the function signature matches the GraphQL schema
76//! // and wires everything up to handle AWS AppSync requests automatically
77//! ```
78
79mod appsync_lambda_main;
80mod appsync_operation;
81mod common;
82
83use proc_macro::TokenStream;
84
85/// Generates the code required to handle AWS AppSync Direct Lambda resolver events based on a GraphQL schema.
86///
87/// This macro takes a path to a GraphQL schema file and generates the complete foundation
88/// for implementing an AWS AppSync Direct Lambda resolver:
89///
90/// - Rust types for all GraphQL types (enums, inputs, objects)
91/// - Query/Mutation/Subscription operation enums
92/// - AWS Lambda runtime setup with logging to handle the AWS AppSync event
93/// - Optional AWS SDK client initialization
94///
95/// # Schema Path Argument
96///
97/// The first argument to this macro must be a string literal containing the path to your GraphQL schema file.
98/// The schema path can be:
99///
100/// - An absolute filesystem path (e.g. "/home/user/project/schema.graphql")
101/// - A relative path, that will be relative to your crate's root directory (e.g. "schema.graphql", "graphql/schema.gql")
102/// - When in a workspace context, the relative path will be relative to the workspace root directory
103///
104/// # Options
105///
106/// - `batch = bool`: Enable/disable batch request handling (default: true)
107/// - `hook = fn_name`: Add a custom hook function for request validation/auth
108/// - `exclude_lambda_handler = bool`: Skip generation of Lambda handler code
109/// - `only_lambda_handler = bool`: Only generate Lambda handler code
110/// - `exclude_appsync_types = bool`: Skip generation of GraphQL type definitions
111/// - `only_appsync_types = bool`: Only generate GraphQL type definitions
112/// - `exclude_appsync_operations = bool`: Skip generation of operation enums
113/// - `only_appsync_operations = bool`: Only generate operation enums
114/// - `field_type_override = Type.field: CustomType`: Override type of a specific field
115///
116/// # AWS SDK Clients
117///
118/// AWS SDK clients can be initialized by providing function definitions that return a cached SDK client type.
119/// Each client is initialized only once and stored in a static [OnceCell], making subsequent function calls
120/// essentially free:
121///
122/// - Function name: Any valid Rust identifier that will be used to access the client
123/// - Return type: Must be a valid AWS SDK client like `aws_sdk_dynamodb::Client`
124///
125/// ```no_run
126/// use lambda_appsync::appsync_lambda_main;
127///
128/// // Single client
129/// appsync_lambda_main!(
130///     "schema.graphql",
131///     dynamodb() -> aws_sdk_dynamodb::Client,
132/// );
133/// ```
134/// ```no_run
135/// # use lambda_appsync::appsync_lambda_main;
136/// // Multiple clients
137/// appsync_lambda_main!(
138///     "schema.graphql",
139///     dynamodb() -> aws_sdk_dynamodb::Client,
140///     s3() -> aws_sdk_s3::Client,
141/// );
142/// ```
143///
144/// These client functions can then be called from anywhere in the Lambda crate:
145/// ```no_run
146/// # fn dynamodb() -> aws_sdk_dynamodb::Client {
147/// #  todo!()
148/// # }
149/// # fn s3() -> aws_sdk_s3::Client {
150/// #   todo!()
151/// # }
152/// # mod sub {
153/// use crate::{dynamodb, s3};
154/// async fn do_something() {
155///     let dynamodb_client = dynamodb();
156///     let s3_client = s3();
157///     // Use clients...
158/// }
159/// # }
160/// # fn main() {}
161/// ```
162///
163/// # Examples
164///
165/// Basic usage with authentication hook:
166/// ```no_run
167/// use lambda_appsync::{appsync_lambda_main, AppsyncEvent, AppsyncResponse, AppsyncIdentity};
168///
169/// fn is_authorized(identity: &AppsyncIdentity) -> bool {
170///     todo!()
171/// }
172///
173/// // If the function returns Some(AppsyncResponse), the Lambda function will immediately return it.
174/// // Otherwise, the normal flow of the AppSync operation processing will continue.
175/// // This is primarily intended for advanced authentication checks that AppSync cannot perform, such as verifying that a user is requesting their own ID.
176/// async fn auth_hook(
177///     event: &lambda_appsync::AppsyncEvent<Operation>
178/// ) -> Option<lambda_appsync::AppsyncResponse> {
179///     // Verify JWT token, check permissions etc
180///     if !is_authorized(&event.identity) {
181///         return Some(AppsyncResponse::unauthorized());
182///     }
183///     None
184/// }
185///
186/// appsync_lambda_main!(
187///     "schema.graphql",
188///     hook = auth_hook,
189///     dynamodb() -> aws_sdk_dynamodb::Client
190/// );
191/// ```
192///
193/// Generate only types for lib code generation:
194/// ```no_run
195/// use lambda_appsync::appsync_lambda_main;
196/// appsync_lambda_main!(
197///     "schema.graphql",
198///     only_appsync_types = true
199/// );
200/// ```
201///
202/// Override field types (you can use this option multiple times):
203/// ```no_run
204/// use lambda_appsync::appsync_lambda_main;
205/// appsync_lambda_main!(
206///     "schema.graphql",
207///     // Use String instead of the default lambda_appsync::ID
208///     field_type_override = Player.id: String,
209/// );
210/// ```
211///
212/// Disable batch processing:
213/// ```no_run
214/// lambda_appsync::appsync_lambda_main!(
215///     "schema.graphql",
216///     batch = false
217/// );
218/// ```
219#[proc_macro]
220pub fn appsync_lambda_main(input: TokenStream) -> TokenStream {
221    appsync_lambda_main::appsync_lambda_main_impl(input)
222}
223
224/// Marks an async function as an AWS AppSync resolver operation, binding it to a specific Query,
225/// Mutation or Subscription operation defined in the GraphQL schema.
226///
227/// The marked function must match the signature of the GraphQL operation, with parameters and return
228/// type matching what is defined in the schema. The function will be wired up to handle requests
229/// for that operation through the AWS AppSync Direct Lambda resolver.
230///
231/// # Important
232/// This macro can only be used in a crate where the [appsync_lambda_main!] macro has been used at the
233/// root level (typically in `main.rs`). The code generated by this macro depends on types and
234/// implementations that are created by [appsync_lambda_main!].
235///
236/// # Example Usage
237///
238/// ```no_run
239/// # lambda_appsync::appsync_lambda_main!(
240/// #    "schema.graphql",
241/// #     exclude_lambda_handler = true,
242/// # );
243/// # mod sub {
244/// # async fn dynamodb_get_players() -> Result<Vec<Player>, AppsyncError> {
245/// #    todo!()
246/// # }
247/// # async fn dynamodb_create_player(name: String) -> Result<Player, AppsyncError> {
248/// #    todo!()
249/// # }
250/// use lambda_appsync::{appsync_operation, AppsyncError};
251///
252/// // Your types are declared at the crate level by the appsync_lambda_main! macro
253/// use crate::Player;
254///
255/// // Execute when a 'players' query is received
256/// #[appsync_operation(query(players))]
257/// async fn get_players() -> Result<Vec<Player>, AppsyncError> {
258///     // Implement resolver logic
259///     Ok(dynamodb_get_players().await?)
260/// }
261///
262/// // Handle a 'createPlayer' mutation
263/// #[appsync_operation(mutation(createPlayer))]
264/// async fn create_player(name: String) -> Result<Player, AppsyncError> {
265///     Ok(dynamodb_create_player(name).await?)
266/// }
267/// # }
268/// # fn main() {}
269/// ```
270///
271/// ## Using the AppSync event
272///
273/// You may need to explore the [AppsyncEvent](lambda_appsync::AppsyncEvent) received by the lambda
274/// in your operation handler. You can make it available by adding the `with_appsync_event` flag and
275/// adding a reference to it in your operation handler signature (must be the last argument), like so:
276/// ```no_run
277/// # lambda_appsync::appsync_lambda_main!(
278/// #    "schema.graphql",
279/// #     exclude_lambda_handler = true,
280/// # );
281/// # mod sub {
282/// # async fn dynamodb_create_player(name: String) -> Result<Player, AppsyncError> {
283/// #    todo!()
284/// # }
285/// use lambda_appsync::{appsync_operation, AppsyncError, AppsyncEvent, AppsyncIdentity};
286///
287/// // Your types are declared at the crate level by the appsync_lambda_main! macro
288/// use crate::{Operation, Player};
289///
290/// // Use the AppsyncEvent
291/// #[appsync_operation(mutation(createPlayer), with_appsync_event)]
292/// async fn create_player(name: String, event: &AppsyncEvent<Operation>) -> Result<Player, AppsyncError> {
293///     // Example: extract the cognito user ID
294///     let user_id = if let AppsyncIdentity::Cognito(cognito_id) = &event.identity {
295///         cognito_id.sub.clone()
296///     } else {
297///         return Err(AppsyncError::new("Unauthorized", "Must be Cognito authenticated"))
298///     };
299///     Ok(dynamodb_create_player(name).await?)
300/// }
301/// # }
302/// # fn main() {}
303/// ```
304///
305/// Note that the `args` field of the [AppsyncEvent](lambda_appsync::AppsyncEvent) will always contain
306/// [Null](serde_json::Value::Null) at this stage because its initial content is taken to extract
307/// the argument values for the operation.
308///
309/// ## Preserve original function name
310///
311/// By default the #[[appsync_operation(...)]] macro will discard your function's name but
312/// you can also keep it available by adding the `keep_original_function_name` flag:
313/// ```no_run
314/// # lambda_appsync::appsync_lambda_main!(
315/// #    "schema.graphql",
316/// #     exclude_lambda_handler = true,
317/// # );
318/// # mod sub {
319/// use lambda_appsync::{appsync_operation, AppsyncError};
320///
321/// // Your types are declared at the crate level by the appsync_lambda_main! macro
322/// use crate::Player;
323///
324/// # async fn dynamodb_get_players() -> Result<Vec<Player>, AppsyncError> {
325/// #    todo!()
326/// # }
327/// // Keep the original function name available separately
328/// #[appsync_operation(query(players), keep_original_function_name)]
329/// async fn fetch_players() -> Result<Vec<Player>, AppsyncError> {
330///     Ok(dynamodb_get_players().await?)
331/// }
332/// async fn other_stuff() {
333///     // Can still call fetch_players() directly
334///     fetch_players().await;
335/// }
336/// # }
337/// # fn main() {}
338/// ```
339///
340/// ## Using enhanced subscription filters
341///
342/// ```no_run
343/// # lambda_appsync::appsync_lambda_main!(
344/// #    "schema.graphql",
345/// #     exclude_lambda_handler = true,
346/// # );
347/// // (Optional) Use an enhanced subscription filter for onCreatePlayer
348/// use lambda_appsync::{appsync_operation, AppsyncError};
349/// use lambda_appsync::subscription_filters::{FilterGroup, Filter, FieldPath};
350///
351/// #[appsync_operation(subscription(onCreatePlayer))]
352/// async fn on_create_player(name: String) -> Result<Option<FilterGroup>, AppsyncError> {
353///     Ok(Some(FilterGroup::from([
354///         Filter::from([
355///             FieldPath::new("name")?.contains(name)
356///         ])
357///     ])))
358/// }
359/// # fn main() {}
360/// ```
361///
362/// When using a single [FieldPath](lambda_appsync::subscription_filters::FieldPath) you can turn it directly into a [FilterGroup](lambda_appsync::subscription_filters::FilterGroup).
363/// The following code is equivalent to the one above:
364/// ```no_run
365/// # lambda_appsync::appsync_lambda_main!(
366/// #    "schema.graphql",
367/// #     exclude_lambda_handler = true,
368/// # );
369/// # use lambda_appsync::{appsync_operation, AppsyncError};
370/// # use lambda_appsync::subscription_filters::{FilterGroup, Filter, FieldPath};
371/// #[appsync_operation(subscription(onCreatePlayer))]
372/// async fn on_create_player(name: String) -> Result<Option<FilterGroup>, AppsyncError> {
373///     Ok(Some(FieldPath::new("name")?.contains(name).into()))
374/// }
375/// # fn main() {}
376/// ```
377///
378/// ### Important Note
379///
380/// When using enhanced subscription filters (i.e., returning a [FilterGroup](lambda_appsync::subscription_filters::FilterGroup)
381/// from Subscribe operation handlers), you need to modify your ***Response*** mapping in AWS AppSync.
382/// It must contain the following:
383///
384/// ```vtl
385/// #if($context.result.data)
386/// $extensions.setSubscriptionFilter($context.result.data)
387/// #end
388/// null
389/// ```
390#[proc_macro_attribute]
391pub fn appsync_operation(args: TokenStream, input: TokenStream) -> TokenStream {
392    appsync_operation::appsync_operation_impl(args, input)
393}