Skip to content

Commit 84ca8a0

Browse files
committed
Alpha working(?) version of the new macros
1 parent 6bb9362 commit 84ca8a0

22 files changed

Lines changed: 1556 additions & 932 deletions

Cargo.lock

Lines changed: 194 additions & 203 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lambda-appsync-proc/Cargo.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,16 @@ proc-macro2 = { workspace = true }
2121
graphql-parser = { workspace = true }
2222

2323
[dev-dependencies]
24-
lambda-appsync = { path = "../lambda-appsync", default-features = false, features = ["log", "env_logger", "tracing"] }
24+
lambda-appsync = { path = "../lambda-appsync", default-features = false, features = ["log", "env_logger", "tracing", "compat"] }
2525
tracing = { workspace = true }
2626
aws-sdk-s3 = { workspace = true }
2727
aws-sdk-dynamodb = { workspace = true }
2828
serde = { workspace = true }
2929
trybuild = { workspace = true }
3030

3131
[features]
32-
default = ["log", "env_logger", "tracing"]
32+
default = ["log", "env_logger", "tracing", "compat"]
33+
compat = []
3334
log = []
34-
env_logger = []
35+
env_logger = ["compat"]
3536
tracing = []
Lines changed: 275 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
1+
Generates the code required to handle AWS AppSync Direct Lambda resolver events based on a GraphQL schema.
2+
3+
This macro takes a path to a GraphQL schema file and generates the complete foundation
4+
for implementing an AWS AppSync Direct Lambda resolver:
5+
6+
- Rust types for all GraphQL types (enums, inputs, objects)
7+
- Query/Mutation/Subscription operation enums
8+
- AWS Lambda runtime setup with logging to handle the AWS AppSync event
9+
- Optional AWS SDK client initialization
10+
11+
# Schema Path Argument
12+
13+
The first argument to this macro must be a string literal containing the path to your GraphQL schema file.
14+
The schema path can be:
15+
16+
- An absolute filesystem path (e.g. "/home/user/project/schema.graphql")
17+
- A relative path, that will be relative to your crate's root directory (e.g. "schema.graphql", "graphql/schema.gql")
18+
- When in a workspace context, the relative path will be relative to the workspace root directory
19+
20+
# Options
21+
22+
- `batch = bool`: Enable/disable batch request handling (default: true)
23+
- `hook = fn_name`: Add a custom hook function for request validation/auth
24+
- `log_init = fn_name`: Use a custom log initialization function instead of the default one
25+
- (feature: `log`) `event_logging = bool`: If true, the macro will generate code to dump the
26+
lambda payload JSON as well as parsed `AppsyncEvent<Operation>`s in the logs at debug level (default: `false`)
27+
- `exclude_lambda_handler = bool`: Skip generation of Lambda handler code
28+
- `only_lambda_handler = bool`: Only generate Lambda handler code
29+
- `exclude_appsync_types = bool`: Skip generation of GraphQL type definitions
30+
- `only_appsync_types = bool`: Only generate GraphQL type definitions
31+
- `exclude_appsync_operations = bool`: Skip generation of operation enums
32+
- `only_appsync_operations = bool`: Only generate operation enums
33+
- `type_override` - see section below for details
34+
- `name_override` - see section below for details
35+
- `field_type_override` (Deprecated): Same as `type_override`
36+
37+
## Type Overrides
38+
39+
The `type_override` option allows overriding Rust types affected to various schema elements:
40+
41+
- GraphQL `type` and `input` Field types: `type_override = Type.field: CustomType`
42+
- Operation return types (Query/Mutation): `type_override = OpType.operation: CustomType`
43+
- Operation arguments (Query/Mutation/Subscription): `type_override = OpType.operation.arg: CustomType`
44+
45+
These overrides are only for the Rust code and must be compatible for serialization/deserialization purposes,
46+
i.e. you can use `String` for a GraphQL `ID` but you cannot use a `u32` for a GraphQL `Float`.
47+
48+
## Name Overrides
49+
50+
The `name_override` option supports renaming various schema elements:
51+
52+
- Type/input/enum names: `name_override = TypeName: NewTypeName`
53+
- Field names: `name_override = Type.field: new_field_name`
54+
- Enum variants: `name_override = Enum.VARIANT: NewVariant`
55+
56+
These overrides are only for the Rust code and will not change serialization/deserialization,
57+
i.e. `serde` will rename to the original GraphQL schema name.
58+
59+
# AWS SDK Clients
60+
61+
AWS SDK clients can be initialized by providing function definitions that return a cached SDK client type.
62+
Each client is initialized only once and stored in a static [OnceLock](std::sync::OnceLock), making subsequent function calls
63+
essentially free:
64+
65+
- Function name: Any valid Rust identifier that will be used to access the client
66+
- Return type: Must be a valid AWS SDK client like `aws_sdk_dynamodb::Client`
67+
68+
```no_run
69+
# mod sub {
70+
use lambda_appsync::appsync_lambda_main;
71+
72+
// Single client
73+
appsync_lambda_main!(
74+
"schema.graphql",
75+
dynamodb() -> aws_sdk_dynamodb::Client,
76+
);
77+
# }
78+
# fn main() {}
79+
```
80+
```no_run
81+
# mod sub {
82+
# use lambda_appsync::appsync_lambda_main;
83+
// Multiple clients
84+
appsync_lambda_main!(
85+
"schema.graphql",
86+
dynamodb() -> aws_sdk_dynamodb::Client,
87+
s3() -> aws_sdk_s3::Client,
88+
);
89+
# }
90+
# fn main() {}
91+
```
92+
93+
These client functions can then be called from anywhere in the Lambda crate:
94+
```no_run
95+
# fn dynamodb() -> aws_sdk_dynamodb::Client {
96+
# todo!()
97+
# }
98+
# fn s3() -> aws_sdk_s3::Client {
99+
# todo!()
100+
# }
101+
# mod sub {
102+
use crate::{dynamodb, s3};
103+
async fn do_something() {
104+
let dynamodb_client = dynamodb();
105+
let s3_client = s3();
106+
// Use clients...
107+
}
108+
# }
109+
# fn main() {}
110+
```
111+
112+
# Examples
113+
114+
## Basic usage with authentication hook:
115+
```no_run
116+
# mod sub {
117+
use lambda_appsync::{appsync_lambda_main, AppsyncEvent, AppsyncResponse, AppsyncIdentity};
118+
119+
fn is_authorized(identity: &AppsyncIdentity) -> bool {
120+
todo!()
121+
}
122+
123+
// If the function returns Some(AppsyncResponse), the Lambda function will immediately return it.
124+
// Otherwise, the normal flow of the AppSync operation processing will continue.
125+
// This is primarily intended for advanced authentication checks that AppSync cannot perform, such as verifying that a user is requesting their own ID.
126+
async fn auth_hook(
127+
event: &lambda_appsync::AppsyncEvent<Operation>
128+
) -> Option<lambda_appsync::AppsyncResponse> {
129+
// Verify JWT token, check permissions etc
130+
if !is_authorized(&event.identity) {
131+
return Some(AppsyncResponse::unauthorized());
132+
}
133+
None
134+
}
135+
136+
appsync_lambda_main!(
137+
"schema.graphql",
138+
hook = auth_hook,
139+
dynamodb() -> aws_sdk_dynamodb::Client
140+
);
141+
# }
142+
# fn main() {}
143+
```
144+
145+
## Generate only types for lib code generation:
146+
```no_run
147+
# mod sub {
148+
use lambda_appsync::appsync_lambda_main;
149+
appsync_lambda_main!(
150+
"schema.graphql",
151+
only_appsync_types = true
152+
);
153+
# }
154+
# fn main() {}
155+
```
156+
157+
## Override field types, operation return type or argument types:
158+
```no_run
159+
# mod sub {
160+
use lambda_appsync::appsync_lambda_main;
161+
appsync_lambda_main!(
162+
"schema.graphql",
163+
// Use String instead of the default lambda_appsync::ID
164+
// Override Player.id to use String instead of ID
165+
type_override = Player.id: String,
166+
// Multiple overrides, here changing another `Player` field type
167+
type_override = Player.team: String,
168+
// Return value override
169+
type_override = Query.gameStatus: String,
170+
type_override = Mutation.setGameStatus: String,
171+
// Argument override
172+
type_override = Query.player.id: String,
173+
type_override = Mutation.deletePlayer.id: String,
174+
type_override = Subscription.onDeletePlayer.id: String,
175+
);
176+
# }
177+
# fn main() {}
178+
```
179+
180+
## Override type, input, enum, fields or variants names:
181+
```no_run
182+
# mod sub {
183+
use lambda_appsync::appsync_lambda_main;
184+
appsync_lambda_main!(
185+
"schema.graphql",
186+
// Override Player struct name
187+
name_override = Player: NewPlayer,
188+
// Override Player struct field name
189+
name_override = Player.name: email,
190+
// Override team `PYTHON` to be `Snake` (instead of `Python`)
191+
name_override = Team.PYTHON: Snake,
192+
// MUST also override ALL the operations return type !!!
193+
type_override = Query.players: NewPlayer,
194+
type_override = Query.player: NewPlayer,
195+
type_override = Mutation.createPlayer: NewPlayer,
196+
type_override = Mutation.deletePlayer: NewPlayer,
197+
);
198+
# }
199+
# fn main() {}
200+
```
201+
Note that when using `name_override`, the macro does not automatically change the case:
202+
you are responsible to provide the appropriate casing or Clippy will complain.
203+
204+
## Use a custom log initialization function:
205+
206+
### Feature `env_logger` (default)
207+
208+
By default, `lambda_appsync` exposes and uses `log` and `env_logger`. You can override the
209+
initialization code if you wish:
210+
211+
```no_run
212+
# mod sub {
213+
// This is in fact equivalent to the default initialization code
214+
fn log_init_fct() {
215+
use lambda_appsync::env_logger;
216+
env_logger::Builder::from_env(
217+
env_logger::Env::default()
218+
// Default log level is info, expect tracing::span is warn
219+
.default_filter_or("info,tracing::span=warn")
220+
.default_write_style_or("never"),
221+
)
222+
// Format timestamps with microseconds
223+
.format_timestamp_micros()
224+
.init();
225+
}
226+
lambda_appsync::appsync_lambda_main!(
227+
"schema.graphql",
228+
log_init = log_init_fct
229+
);
230+
# }
231+
# fn main() {}
232+
```
233+
234+
### Feature `tracing`
235+
236+
Alternatively, you can use the `tracing` feature so `lambda_appsync` exposes and uses `log`, `tracing` and `tracing-subscriber`
237+
238+
```no_run
239+
# mod sub {
240+
// This is in fact equivalent to the default initialization code
241+
fn tracing_init_fct() {
242+
use lambda_appsync::{tracing, tracing_subscriber};
243+
tracing_subscriber::fmt()
244+
.json()
245+
.with_env_filter(
246+
tracing_subscriber::EnvFilter::from_default_env()
247+
.add_directive(tracing::Level::INFO.into()),
248+
)
249+
// this needs to be set to remove duplicated information in the log.
250+
.with_current_span(false)
251+
// this needs to be set to false, otherwise ANSI color codes will
252+
// show up in a confusing manner in CloudWatch logs.
253+
.with_ansi(false)
254+
// remove the name of the function from every log entry
255+
.with_target(false)
256+
.init();
257+
}
258+
lambda_appsync::appsync_lambda_main!(
259+
"schema.graphql",
260+
log_init = tracing_init_fct
261+
);
262+
# }
263+
# fn main() {}
264+
```
265+
266+
## Disable batch processing:
267+
```no_run
268+
# mod sub {
269+
lambda_appsync::appsync_lambda_main!(
270+
"schema.graphql",
271+
batch = false
272+
);
273+
# }
274+
# fn main() {}
275+
```

0 commit comments

Comments
 (0)