lambda_appsync_proc/
lib.rs

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