ohkami_macros/lib.rs
1mod util;
2
3mod from_request;
4mod serde;
5
6#[cfg(feature = "openapi")]
7mod openapi;
8
9#[cfg(feature = "worker")]
10mod worker;
11
12#[cfg(feature = "openapi")]
13/// # Deriving `openapi::Schema`
14///
15/// register the struct as a `schema` of OpenAPI document
16///
17/// <br>
18///
19/// ## Helper attributes
20///
21/// ### Container attributes
22///
23/// #### `#[openapi(component)]`
24/// Define the schema in `components`
25///
26/// ### Field attributes
27///
28/// #### `#[openapi(schema_with = "schema_fn")]`
29/// Use `schema_fn()` instead for the field. `schema_fn`:
30///
31/// - must be callable as `fn() -> impl Into<ohkami::openapi::SchemaRef>`
32/// - can be a path like `schema_fns::a_schema`
33///
34/// ### Variant attributes
35///
36/// #### `#[openapi(schema_with = "schema_fn")]`
37/// Use `schema_fn()` instead for the variant. `schema_fn`:
38///
39/// - must be callable as `fn() -> impl Into<ohkami::openapi::SchemaRef>`
40/// - can be a path like `schema_fns::a_schema`
41///
42/// <br>
43///
44/// ## Example
45///
46/// ```ignore
47/// use ohkami::prelude::*;
48/// use ohkami::openapi;
49///
50/// #[derive(Deserialize, openapi::Schema)]
51/// struct HelloRequest<'req> {
52/// name: Option<&'req str>,
53/// repeat: Option<usize>,
54/// }
55///
56/// async fn hello(
57/// Json(req): Json<HelloRequest<'_>>,
58/// ) -> String {
59/// let name = req.name.unwrap_or("world");
60/// let repeat = req.name.repeat.unwrap_or(1);
61/// vec![format!("Hello, {name}!"); repeat].join(" ")
62/// }
63///
64/// #[tokio::main]
65/// async fn main() {
66/// Ohkami::new((
67/// "/hello".GET(hello),
68/// )).howl("localhost:3000").await
69/// }
70/// ```
71#[proc_macro_derive(Schema, attributes(openapi))]
72pub fn derive_schema(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
73 openapi::derive_schema(input.into())
74 .unwrap_or_else(syn::Error::into_compile_error)
75 .into()
76}
77
78#[cfg(feature = "openapi")]
79/// ```ignore
80/// /* custom operationId (default: name of the fn) */
81/// #[operation(get_hello)]
82/// /// description for `get_hello` operation
83/// async fn hello() -> Result<String, MyError> {
84/// //...
85/// }
86///
87/// /* custom operationId and summary */
88/// #[operation(get_hello2 { summary: "HELLO greeting" })]
89/// /// description for `get_hello2` operation
90/// async fn hello2() -> Result<String, MyError> {
91/// //...
92/// }
93///
94/// /* custom summary */
95/// #[operation({ summary: "HELLO greeting" })]
96/// /// description for `hello3` operation
97/// async fn hello3() -> Result<String, MyError> {
98/// //...
99/// }
100///
101/// /* custom operationId and some descriptions */
102/// #[operation(get_hello4 {
103/// requestBody: "User name (text/plain).",
104/// 200: "Successfully returning a HELLO greeting for the user",
105/// })]
106/// /// description for `get_hello4` operation
107/// async fn hello4(
108/// Text(name): Text,
109/// ) -> Result<String, MyError> {
110/// //...
111/// }
112/// ```
113#[proc_macro_attribute]
114pub fn operation(
115 args: proc_macro::TokenStream,
116 handler: proc_macro::TokenStream,
117) -> proc_macro::TokenStream {
118 openapi::operation(args.into(), handler.into())
119 .unwrap_or_else(syn::Error::into_compile_error)
120 .into()
121}
122
123#[cfg(feature = "worker")]
124/// Create an worker Ohkami, running on Cloudflare Workers !
125///
126/// - This only handle `fetch` event.
127/// - Expected signature: `() -> Ohkami` or `(bindings) -> Ohkami` ( both sync/async are available )
128///
129/// `(bindings) -> Ohkami` pattern is called **global bindings**.
130///
131/// ---
132/// ```ignore
133/// use ohkami::prelude::*;
134///
135/// #[ohkami::worker]
136/// fn my_ohkami() -> Ohkami {
137/// Ohkami::new((
138/// "/".GET(|| async {"Hello, world!"})
139/// ))
140/// }
141/// ```
142/// ---
143/// ```ignore
144/// use ohkami::{prelude::*, worker, bindings};
145///
146/// #[bindings]
147/// struct Bindings {
148/// MY_KV: bindings::KV,
149/// }
150///
151/// async fn get_from_kv(
152/// Path(key): Path<String>,
153/// Context(kv): Context<'_, bindings::KV>,
154/// ) -> Result<String, worker::Error> {
155/// kv.get(&key).text().await?.ok_or_else(|| worker::Error::RustError(
156/// format!("Key '{}' not found in KV", key)
157/// ))
158/// }
159///
160/// #[worker]
161/// // global bindings
162/// fn my_ohkami(b: Bindings) -> Ohkami {
163/// Ohkami::new((
164/// Context::new(b.MY_KV),
165/// "/".GET(|| async {"Hello, world!"}),
166/// "/kv/:key".GET(get_from_kv),
167/// ))
168/// }
169/// ```
170/// ---
171///
172/// `#[worker]` accepts an argument in following format for *document purpose*:
173///
174/// ```ts
175/// {
176/// title: string,
177/// version: string | number,
178/// servers: [
179/// {
180/// url: string,
181/// description: string,
182/// variables: {
183/// [string]: {
184/// default: string,
185/// enum: [string],
186/// }
187/// }
188/// }
189/// ]
190/// }
191/// ```
192///
193/// Every field is optional.
194///
195/// example:
196///
197/// ---
198/// *lib.rs*
199/// ```ignore
200/// use ohkami::prelude::*;
201///
202/// #[ohkami::worker({
203/// title: "My Ohkami Worker",
204/// version: "1.0.0",
205/// servers: [
206/// {
207/// url: "https://my-worker.example.com",
208/// description: "My Ohkami Worker server",
209/// },
210/// {
211/// url: "http://localhost:8787",
212/// description: "My Ohkami Worker server for local development",
213/// }
214/// ]
215/// })]
216/// fn my_ohkami() -> Ohkami {
217/// Ohkami::new((
218/// "/".GET(|| async {"Hello, world!"})
219/// ))
220/// }
221/// ```
222/// ---
223///
224/// **Every field is optional** and **any other fields are acceptable**,
225/// but when `openapi` feature is activated, these fields are used for the
226/// document generation ( if missing, some default values will be used ).
227#[proc_macro_attribute]
228pub fn worker(
229 args: proc_macro::TokenStream,
230 ohkami_fn: proc_macro::TokenStream,
231) -> proc_macro::TokenStream {
232 worker::worker(args.into(), ohkami_fn.into())
233 .unwrap_or_else(syn::Error::into_compile_error)
234 .into()
235}
236
237#[cfg(feature = "worker")]
238/// Integrate the struct with Workers runtime as a Durable Object.\
239/// This requires to impl `DurableObject` trait and the trait requires this attribute.
240///
241/// ### Example
242///
243/// ```
244/// use worker::{State, Env};
245/// use ohkami::DurableObject;
246///
247/// # struct User;
248/// # struct Message;
249///
250/// #[DurableObject]
251/// pub struct Chatroom {
252/// users: Vec<User>,
253/// messages: Vec<Message>,
254/// state: State,
255/// env: Env, // access `Env` across requests, use inside `fetch`
256/// }
257///
258/// impl DurableObject for Chatroom {
259/// fn new(state: State, env: Env) -> Self {
260/// Self {
261/// users: vec![],
262/// messages: vec![],
263/// state,
264/// env,
265/// }
266/// }
267///
268/// async fn fetch(&mut self, _req: Request) -> worker::Result<worker::Response> {
269/// // do some work when a worker makes a request to this DO
270/// worker::Response::ok(&format!("{} active users.", self.users.len()))
271/// }
272/// }
273/// ```
274///
275/// ### Note
276///
277/// You can specify the usage of the Durable Object via an argument in order to control WASM/JS outout:
278///
279/// * `fetch`: simple `fetch` target
280/// * `alarm`: with [Alarms API](https://developers.cloudflare.com/durable-objects/examples/alarms-api/)
281/// * `websocket`: [WebSocket server](https://developers.cloudflare.com/durable-objects/examples/websocket-hibernation-server/)
282///
283/// ```ignore
284/// #[DurableObject(fetch)]
285/// pub struct Chatroom {
286/// users: Vec<User>,
287/// messages: Vec<Message>,
288/// state: State,
289/// env: Env, // access `Env` across requests, use inside `fetch`
290/// }
291/// ```
292#[proc_macro_attribute]
293#[allow(non_snake_case)]
294pub fn DurableObject(
295 args: proc_macro::TokenStream,
296 input: proc_macro::TokenStream,
297) -> proc_macro::TokenStream {
298 worker::DurableObject(args.into(), input.into())
299 .unwrap_or_else(syn::Error::into_compile_error)
300 .into()
301}
302
303#[cfg(feature = "worker")]
304/// Automatically bind bindings in wrangler.toml to Rust struct.
305///
306/// - This uses the default (top-level) env by default. You can configure it
307/// by argument: `#[bindings(dev)]`
308/// - Binded struct implements `FromRequest` and it can be used as an
309/// handler argument
310///
311/// <br>
312///
313/// ## 2 ways of binding
314///
315/// following wrangler.toml for example :
316///
317/// ```ignore
318/// [[kv_namespaces]]
319/// binding = "MY_KV"
320/// id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
321/// ```
322///
323/// ### Auto binding mode
324///
325/// For **unit struct**, `#[bindings]` automatically collects **all** bindings from
326/// your *wrangler.toml* and generates fields for them.
327///
328/// ```ignore
329/// #[ohkami::bindings]
330/// struct Bindings;
331///
332/// async fn handler(b: Bindings) -> String {
333/// let data = b.MY_KV.get("data").text().await
334/// .expect("Failed to get data");
335///
336/// //...
337/// }
338/// ```
339///
340/// ### Manual binding mode
341///
342/// For **struct with named fields**, `#[bindings]` just collects bindings
343/// that have the **same name as its fields** from your *wrangler.toml*,
344///
345/// In this way, types in `ohkami::bindings` module are useful to avoid
346/// inconsistency and unclear namings of `worker` crate's binding types.
347///
348/// ```ignore
349/// use ohkami::bindings;
350///
351/// #[bindings]
352/// struct Bindings {
353/// MY_KV: bindings::KV,
354/// }
355///
356/// async fn handler(b: Bindings) -> String {
357/// let data = b.MY_KV.get("data").text().await
358/// .expect("Failed to get data");
359///
360/// //...
361/// }
362/// ```
363///
364/// <br>
365///
366/// ## Note
367///
368/// - `#[bindings]` currently supports:
369/// - AI
370/// - KV
371/// - R2
372/// - D1
373/// - Queue (producer)
374/// - Service
375/// - Variables
376/// - Durable Objects
377/// - Hyperdrive
378/// - `Queue` may cause a lot of *WARNING*s on `npm run dev`, but
379/// it's not an actual problem and `Queue` binding does work.
380///
381/// <br>
382///
383/// ## Tips
384///
385/// - You can switch between multiple `env`s by feature flags
386/// like `#[cfg_attr(feature = "...", bindings(env_name))]`.
387/// - For `rust-analyzer` user : When you edit wrangler.toml around bindings in **auto binding mode**,
388/// you'll need to notify the change of `#[bindings]` if you're using auto binding mode.
389/// For that, all you have to do is just **deleting `;` and immediate restoring it**.
390#[proc_macro_attribute]
391pub fn bindings(
392 env_name: proc_macro::TokenStream,
393 bindings_struct: proc_macro::TokenStream,
394) -> proc_macro::TokenStream {
395 worker::bindings(env_name.into(), bindings_struct.into())
396 .unwrap_or_else(syn::Error::into_compile_error)
397 .into()
398}
399
400/// The *perfect* reexport of [serde](https://crates.io/crates/serde)'s `Serialize`.
401///
402/// <br>
403///
404/// *example.rs*
405/// ```ignore
406/// use ohkami::serde::Serialize;
407///
408/// #[derive(Serialize)]
409/// struct User {
410/// #[serde(rename = "username")]
411/// name: String,
412/// bio: Option<String>,
413/// }
414/// ```
415#[proc_macro_derive(Serialize, attributes(serde))]
416#[allow(non_snake_case)]
417pub fn Serialize(data: proc_macro::TokenStream) -> proc_macro::TokenStream {
418 serde::Serialize(data.into())
419 .unwrap_or_else(|e| e.into_compile_error())
420 .into()
421}
422/// The *perfect* reexport of [serde](https://crates.io/crates/serde)'s `Deserialize`.
423///
424/// <br>
425///
426/// *example.rs*
427/// ```ignore
428/// use ohkami::serde::Deserialize;
429///
430/// #[derive(Deserialize)]
431/// struct CreateUser<'req> {
432/// #[serde(rename = "username")]
433/// name: &'req str,
434/// bio: Option<&'req str>,
435/// }
436/// ```
437#[proc_macro_derive(Deserialize, attributes(serde))]
438#[allow(non_snake_case)]
439pub fn Deserialize(data: proc_macro::TokenStream) -> proc_macro::TokenStream {
440 serde::Deserialize(data.into())
441 .unwrap_or_else(|e| e.into_compile_error())
442 .into()
443}
444
445/// Deriving `FromRequest` impl for a struct composed of
446/// `FromRequest` types
447///
448/// <br>
449///
450/// *example.rs*
451/// ```ignore
452/// use ohkami::fang::Context;
453/// use sqlx::PgPool;
454///
455/// #[derive(ohkami::FromRequest)]
456/// struct MyItems1<'req> {
457/// db: Context<'req, PgPool>,
458/// }
459///
460/// #[derive(FromRequest)]
461/// struct MyItems2<'req>(
462/// MyItems1<'req>,
463/// );
464/// ```
465#[proc_macro_derive(FromRequest)]
466pub fn derive_from_request(target: proc_macro::TokenStream) -> proc_macro::TokenStream {
467 from_request::derive_from_request(target.into())
468 .unwrap_or_else(|e| e.into_compile_error())
469 .into()
470}
471
472#[doc(hidden)]
473#[proc_macro_attribute]
474pub fn consume_struct(
475 _: proc_macro::TokenStream,
476 _: proc_macro::TokenStream,
477) -> proc_macro::TokenStream {
478 proc_macro::TokenStream::new()
479}