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/// - `log_init = fn_name`: Use a custom log initialization function instead of the default one
33/// - (feature: `log`) `event_logging = bool`: If true, the macro will generate code to dump the
34///   lambda payload JSON as well as parsed `AppsyncEvent<Operation>`s in the logs at debug level (default: `false`)
35/// - `exclude_lambda_handler = bool`: Skip generation of Lambda handler code
36/// - `only_lambda_handler = bool`: Only generate Lambda handler code
37/// - `exclude_appsync_types = bool`: Skip generation of GraphQL type definitions
38/// - `only_appsync_types = bool`: Only generate GraphQL type definitions
39/// - `exclude_appsync_operations = bool`: Skip generation of operation enums
40/// - `only_appsync_operations = bool`: Only generate operation enums
41/// - `type_override` - see section below for details
42/// - `name_override` - see section below for details
43/// - `field_type_override` (Deprecated): Same as `type_override`
44///
45/// ## Type Overrides
46///
47/// The `type_override` option allows overriding Rust types affected to various schema elements:
48///
49/// - GraphQL `type` and `input` Field types: `type_override = Type.field: CustomType`
50/// - Operation return types (Query/Mutation): `type_override = OpType.operation: CustomType`
51/// - Operation arguments (Query/Mutation/Subscription): `type_override = OpType.operation.arg: CustomType`
52///
53/// These overrides are only for the Rust code and must be compatible for serialization/deserialization purposes,
54/// i.e. you can use `String` for a GraphQL `ID` but you cannot use a `u32` for a GraphQL `Float`.
55///
56/// ## Name Overrides
57///
58/// The `name_override` option supports renaming various schema elements:
59///
60/// - Type/input/enum names: `name_override = TypeName: NewTypeName`
61/// - Field names: `name_override = Type.field: new_field_name`
62/// - Enum variants: `name_override = Enum.VARIANT: NewVariant`
63///
64/// These overrides are only for the Rust code and will not change serialization/deserialization,
65/// i.e. `serde` will rename to the original GraphQL schema name.
66///
67/// # AWS SDK Clients
68///
69/// AWS SDK clients can be initialized by providing function definitions that return a cached SDK client type.
70/// Each client is initialized only once and stored in a static [OnceLock](std::sync::OnceLock), making subsequent function calls
71/// essentially free:
72///
73/// - Function name: Any valid Rust identifier that will be used to access the client
74/// - Return type: Must be a valid AWS SDK client like `aws_sdk_dynamodb::Client`
75///
76/// ```no_run
77/// # mod sub {
78/// use lambda_appsync::appsync_lambda_main;
79///
80/// // Single client
81/// appsync_lambda_main!(
82///     "schema.graphql",
83///     dynamodb() -> aws_sdk_dynamodb::Client,
84/// );
85/// # }
86/// # fn main() {}
87/// ```
88/// ```no_run
89/// # mod sub {
90/// # use lambda_appsync::appsync_lambda_main;
91/// // Multiple clients
92/// appsync_lambda_main!(
93///     "schema.graphql",
94///     dynamodb() -> aws_sdk_dynamodb::Client,
95///     s3() -> aws_sdk_s3::Client,
96/// );
97/// # }
98/// # fn main() {}
99/// ```
100///
101/// These client functions can then be called from anywhere in the Lambda crate:
102/// ```no_run
103/// # fn dynamodb() -> aws_sdk_dynamodb::Client {
104/// #   todo!()
105/// # }
106/// # fn s3() -> aws_sdk_s3::Client {
107/// #   todo!()
108/// # }
109/// # mod sub {
110/// use crate::{dynamodb, s3};
111/// async fn do_something() {
112///     let dynamodb_client = dynamodb();
113///     let s3_client = s3();
114///     // Use clients...
115/// }
116/// # }
117/// # fn main() {}
118/// ```
119///
120/// # Examples
121///
122/// ## Basic usage with authentication hook:
123/// ```no_run
124/// # mod sub {
125/// use lambda_appsync::{appsync_lambda_main, AppsyncEvent, AppsyncResponse, AppsyncIdentity};
126///
127/// fn is_authorized(identity: &AppsyncIdentity) -> bool {
128///     todo!()
129/// }
130///
131/// // If the function returns Some(AppsyncResponse), the Lambda function will immediately return it.
132/// // Otherwise, the normal flow of the AppSync operation processing will continue.
133/// // This is primarily intended for advanced authentication checks that AppSync cannot perform, such as verifying that a user is requesting their own ID.
134/// async fn auth_hook(
135///     event: &lambda_appsync::AppsyncEvent<Operation>
136/// ) -> Option<lambda_appsync::AppsyncResponse> {
137///     // Verify JWT token, check permissions etc
138///     if !is_authorized(&event.identity) {
139///         return Some(AppsyncResponse::unauthorized());
140///     }
141///     None
142/// }
143///
144/// appsync_lambda_main!(
145///     "schema.graphql",
146///     hook = auth_hook,
147///     dynamodb() -> aws_sdk_dynamodb::Client
148/// );
149/// # }
150/// # fn main() {}
151/// ```
152///
153/// ## Generate only types for lib code generation:
154/// ```no_run
155/// # mod sub {
156/// use lambda_appsync::appsync_lambda_main;
157/// appsync_lambda_main!(
158///     "schema.graphql",
159///     only_appsync_types = true
160/// );
161/// # }
162/// # fn main() {}
163/// ```
164///
165/// ## Override field types, operation return type or argument types:
166/// ```no_run
167/// # mod sub {
168/// use lambda_appsync::appsync_lambda_main;
169/// appsync_lambda_main!(
170///     "schema.graphql",
171///     // Use String instead of the default lambda_appsync::ID
172///     // Override Player.id to use String instead of ID
173///     type_override = Player.id: String,
174///     // Multiple overrides, here changing another `Player` field type
175///     type_override = Player.team: String,
176///     // Return value override
177///     type_override = Query.gameStatus: String,
178///     type_override = Mutation.setGameStatus: String,
179///     // Argument override
180///     type_override = Query.player.id: String,
181///     type_override = Mutation.deletePlayer.id: String,
182///     type_override = Subscription.onDeletePlayer.id: String,
183/// );
184/// # }
185/// # fn main() {}
186/// ```
187///
188/// ## Override type, input, enum, fields or variants names:
189/// ```no_run
190/// # mod sub {
191/// use lambda_appsync::appsync_lambda_main;
192/// appsync_lambda_main!(
193///     "schema.graphql",
194///     // Override Player struct name
195///     name_override = Player: NewPlayer,
196///     // Override Player struct field name
197///     name_override = Player.name: email,
198///     // Override team `PYTHON` to be `Snake` (instead of `Python`)
199///     name_override = Team.PYTHON: Snake,
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/// ## Use a custom log initialization function:
213///
214/// ### Feature `env_logger` (default)
215///
216/// By default, `lambda_appsync` exposes and uses `log` and `env_logger`. You can override the
217/// initialization code if you wish:
218///
219/// ```no_run
220/// # mod sub {
221/// // This is in fact equivalent to the default initialization code
222/// fn log_init_fct() {
223///     use lambda_appsync::env_logger;
224///     env_logger::Builder::from_env(
225///         env_logger::Env::default()
226///         // Default log level is info, expect tracing::span is warn
227///         .default_filter_or("info,tracing::span=warn")
228///         .default_write_style_or("never"),
229///     )
230///     // Format timestamps with microseconds
231///     .format_timestamp_micros()
232///     .init();
233/// }
234/// lambda_appsync::appsync_lambda_main!(
235///     "schema.graphql",
236///     log_init = log_init_fct
237/// );
238/// # }
239/// # fn main() {}
240/// ```
241///
242/// ### Feature `tracing`
243///
244/// Alternatively, you can use the `tracing` feature so `lambda_appsync` exposes and uses `log`, `tracing` and `tracing-subscriber`
245///
246/// ```no_run
247/// # mod sub {
248/// // This is in fact equivalent to the default initialization code
249/// fn tracing_init_fct() {
250///     use lambda_appsync::{tracing, tracing_subscriber};
251///     tracing_subscriber::fmt()
252///         .json()
253///         .with_env_filter(
254///             tracing_subscriber::EnvFilter::from_default_env()
255///                 .add_directive(tracing::Level::INFO.into()),
256///         )
257///         // this needs to be set to remove duplicated information in the log.
258///         .with_current_span(false)
259///         // this needs to be set to false, otherwise ANSI color codes will
260///         // show up in a confusing manner in CloudWatch logs.
261///         .with_ansi(false)
262///         // remove the name of the function from every log entry
263///         .with_target(false)
264///         .init();
265/// }
266/// lambda_appsync::appsync_lambda_main!(
267///     "schema.graphql",
268///     log_init = tracing_init_fct
269/// );
270/// # }
271/// # fn main() {}
272/// ```
273///
274/// ## Disable batch processing:
275/// ```no_run
276/// # mod sub {
277/// lambda_appsync::appsync_lambda_main!(
278///     "schema.graphql",
279///     batch = false
280/// );
281/// # }
282/// # fn main() {}
283/// ```
284#[proc_macro]
285pub fn appsync_lambda_main(input: TokenStream) -> TokenStream {
286    appsync_lambda_main::appsync_lambda_main_impl(input)
287}
288
289/// Marks an async function as an AWS AppSync resolver operation, binding it to a specific Query,
290/// Mutation or Subscription operation defined in the GraphQL schema.
291///
292/// The marked function must match the signature of the GraphQL operation, with parameters and return
293/// type matching what is defined in the schema. The function will be wired up to handle requests
294/// for that operation through the AWS AppSync Direct Lambda resolver.
295///
296/// # Important
297/// This macro can only be used in a crate where the [appsync_lambda_main!] macro has been used at the
298/// root level (typically in `main.rs`). The code generated by this macro depends on types and
299/// implementations that are created by [appsync_lambda_main!].
300///
301/// # Example Usage
302///
303/// ```no_run
304/// # lambda_appsync::appsync_lambda_main!(
305/// #    "schema.graphql",
306/// #     exclude_lambda_handler = true,
307/// # );
308/// # mod sub {
309/// # async fn dynamodb_get_players() -> Result<Vec<Player>, AppsyncError> {
310/// #    todo!()
311/// # }
312/// # async fn dynamodb_create_player(name: String) -> Result<Player, AppsyncError> {
313/// #    todo!()
314/// # }
315/// use lambda_appsync::{appsync_operation, AppsyncError};
316///
317/// // Your types are declared at the crate level by the appsync_lambda_main! macro
318/// use crate::Player;
319///
320/// // Execute when a 'players' query is received
321/// #[appsync_operation(query(players))]
322/// async fn get_players() -> Result<Vec<Player>, AppsyncError> {
323///     // Implement resolver logic
324///     Ok(dynamodb_get_players().await?)
325/// }
326///
327/// // Handle a 'createPlayer' mutation
328/// #[appsync_operation(mutation(createPlayer))]
329/// async fn create_player(name: String) -> Result<Player, AppsyncError> {
330///     Ok(dynamodb_create_player(name).await?)
331/// }
332/// # }
333/// # fn main() {}
334/// ```
335///
336/// ## Using the AppSync event
337///
338/// You may need to explore the [AppsyncEvent](struct.AppsyncEvent.html) received by the lambda
339/// in your operation handler. You can make it available by adding the `with_appsync_event` flag and
340/// adding a reference to it in your operation handler signature (must be the last argument), like so:
341/// ```no_run
342/// # lambda_appsync::appsync_lambda_main!(
343/// #    "schema.graphql",
344/// #     exclude_lambda_handler = true,
345/// # );
346/// # mod sub {
347/// # async fn dynamodb_create_player(name: String) -> Result<Player, AppsyncError> {
348/// #    todo!()
349/// # }
350/// use lambda_appsync::{appsync_operation, AppsyncError, AppsyncEvent, AppsyncIdentity};
351///
352/// // Your types are declared at the crate level by the appsync_lambda_main! macro
353/// use crate::{Operation, Player};
354///
355/// // Use the AppsyncEvent
356/// #[appsync_operation(mutation(createPlayer), with_appsync_event)]
357/// async fn create_player(name: String, event: &AppsyncEvent<Operation>) -> Result<Player, AppsyncError> {
358///     // Example: extract the cognito user ID
359///     let user_id = if let AppsyncIdentity::Cognito(cognito_id) = &event.identity {
360///         cognito_id.sub.clone()
361///     } else {
362///         return Err(AppsyncError::new("Unauthorized", "Must be Cognito authenticated"))
363///     };
364///     Ok(dynamodb_create_player(name).await?)
365/// }
366/// # }
367/// # fn main() {}
368/// ```
369///
370/// Note that the `args` field of the [AppsyncEvent](struct.AppsyncEvent.html) will always contain
371/// [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
372/// the argument values for the operation.
373///
374/// ## Preserve original function name
375///
376/// By default the [macro@appsync_operation] macro will discard your function's name but
377/// you can also keep it available by adding the `keep_original_function_name` flag:
378/// ```no_run
379/// # lambda_appsync::appsync_lambda_main!(
380/// #    "schema.graphql",
381/// #     exclude_lambda_handler = true,
382/// # );
383/// # mod sub {
384/// use lambda_appsync::{appsync_operation, AppsyncError};
385///
386/// // Your types are declared at the crate level by the appsync_lambda_main! macro
387/// use crate::Player;
388///
389/// # async fn dynamodb_get_players() -> Result<Vec<Player>, AppsyncError> {
390/// #    todo!()
391/// # }
392/// // Keep the original function name available separately
393/// #[appsync_operation(query(players), keep_original_function_name)]
394/// async fn fetch_players() -> Result<Vec<Player>, AppsyncError> {
395///     Ok(dynamodb_get_players().await?)
396/// }
397/// async fn other_stuff() {
398///     // Can still call fetch_players() directly
399///     fetch_players().await;
400/// }
401/// # }
402/// # fn main() {}
403/// ```
404///
405/// ## Using enhanced subscription filters
406///
407/// ```no_run
408/// # lambda_appsync::appsync_lambda_main!(
409/// #    "schema.graphql",
410/// #     exclude_lambda_handler = true,
411/// # );
412/// // (Optional) Use an enhanced subscription filter for onCreatePlayer
413/// use lambda_appsync::{appsync_operation, AppsyncError};
414/// use lambda_appsync::subscription_filters::{FilterGroup, Filter, FieldPath};
415///
416/// #[appsync_operation(subscription(onCreatePlayer))]
417/// async fn on_create_player(name: String) -> Result<Option<FilterGroup>, AppsyncError> {
418///     Ok(Some(FilterGroup::from([
419///         Filter::from([
420///             FieldPath::new("name")?.contains(name)
421///         ])
422///     ])))
423/// }
424/// # fn main() {}
425/// ```
426///
427/// When using a single [FieldPath](subscription_filters/struct.FieldPath.html) you can turn it directly into a [FilterGroup](subscription_filters/struct.FilterGroup.html).
428/// The following code is equivalent to the one above:
429/// ```no_run
430/// # lambda_appsync::appsync_lambda_main!(
431/// #    "schema.graphql",
432/// #     exclude_lambda_handler = true,
433/// # );
434/// # use lambda_appsync::{appsync_operation, AppsyncError};
435/// # use lambda_appsync::subscription_filters::{FilterGroup, Filter, FieldPath};
436/// #[appsync_operation(subscription(onCreatePlayer))]
437/// async fn on_create_player(name: String) -> Result<Option<FilterGroup>, AppsyncError> {
438///     Ok(Some(FieldPath::new("name")?.contains(name).into()))
439/// }
440/// # fn main() {}
441/// ```
442///
443/// ### Important Note
444///
445/// When using enhanced subscription filters (i.e., returning a [FilterGroup](subscription_filters/struct.FilterGroup.html)
446/// from Subscribe operation handlers), you need to modify your ***Response*** mapping in AWS AppSync.
447/// It must contain the following:
448///
449/// ```vtl
450/// #if($context.result.data)
451/// $extensions.setSubscriptionFilter($context.result.data)
452/// #end
453/// null
454/// ```
455#[proc_macro_attribute]
456pub fn appsync_operation(args: TokenStream, input: TokenStream) -> TokenStream {
457    appsync_operation::appsync_operation_impl(args, input)
458}