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}