Skip to content

Commit 2d2a54d

Browse files
authored
update README & add doc of stream::queue (#185)
1 parent 87ad61d commit 2d2a54d

File tree

3 files changed

+144
-1
lines changed

3 files changed

+144
-1
lines changed

Diff for: README.md

+33-1
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ async fn create_user(body: CreateUserRequest<'_>) -> Created<User> {
137137
}
138138

139139
/* Shorthand for Payload + Serialize */
140-
#[Payload(JSON / S)]
140+
#[Payload(JSON/S)]
141141
struct SearchResult {
142142
title: String,
143143
}
@@ -158,6 +158,35 @@ async fn search(condition: SearchQuery<'_>) -> Vec<SearchResult> {
158158

159159
<br>
160160

161+
### Payload validation
162+
163+
`where <validation expression>` in `#[Payload()]` runs the validation when responding with it or parsing request to it.
164+
165+
`<validation expression>` is an expression with `self: &Self` that returns `Result<(), impl Display>`.
166+
167+
```rust
168+
use ohkami::prelude::*;
169+
use ohkami::{typed::Payload, builtin::payload::JSON};
170+
171+
#[Payload(JSON/D where self.valid())]
172+
struct Hello<'req> {
173+
name: &'req str,
174+
repeat: usize,
175+
}
176+
177+
impl Hello<'_> {
178+
fn valid(&self) -> Result<(), String> {
179+
(self.name.len() > 0).then_some(())
180+
.ok_or_else(|| format!("`name` must not be empty"))?;
181+
(self.repeat > 0).then_some(())
182+
.ok_or_else(|| format!("`repeat` must be positive"))?;
183+
Ok(())
184+
}
185+
}
186+
```
187+
188+
<br>
189+
161190
### Use middlewares
162191

163192
Ohkami's request handling system is called "**fang**s", and middlewares are implemented on this :
@@ -167,6 +196,8 @@ use ohkami::prelude::*;
167196
168197
#[derive(Clone)]
169198
struct GreetingFang;
199+
200+
/* utility trait for auto impl `Fang` */
170201
impl FangAction for GreetingFang {
171202
async fn fore<'a>(&'a self, req: &'a mut Request) -> Result<(), Response> {
172203
println!("Welcomm request!: {req:?}");
@@ -302,6 +333,7 @@ async fn test_my_ohkami() {
302333
- [ ] HTTP/2
303334
- [ ] HTTP/3
304335
- [ ] HTTPS
336+
- [x] Server-Sent Events
305337
- [ ] WebSocket
306338

307339
## MSRV (Minimum Supported Rust Version)

Diff for: ohkami/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,7 @@ pub mod serde {
452452
pub use ::ohkami_macros::{Serialize, Deserialize};
453453
pub use ::serde::ser::{self, Serialize, Serializer};
454454
pub use ::serde::de::{self, Deserialize, Deserializer};
455+
pub use ::serde_json as json;
455456
}
456457

457458
// #[cfg(feature="websocket")]

Diff for: ohkami_lib/src/stream.rs

+110
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,116 @@
11
pub use ::futures_core::{Stream, ready};
22

33

4+
/// # Stream of an async process with a queue
5+
///
6+
/// `queue(|mut q| async move { 〜 })` makes an queue for `T` values
7+
/// and an async process that pushes items to the queue, they work as
8+
/// a stream yeilding all the items asynchronously.
9+
///
10+
/// <br>
11+
///
12+
/// _**note**_ : It's recommended to just `use ohkami::utils::stream` and
13+
/// call as **`stream::queue()`**, not direct `queue()`.
14+
///
15+
/// <br>
16+
///
17+
/// ---
18+
/// *example.rs*
19+
/// ```no_run
20+
/// use ohkami::prelude::*;
21+
/// use ohkami::typed::DataStream;
22+
/// use ohkami::utils::{StreamExt, stream};
23+
/// use tokio::time::sleep;
24+
///
25+
/// #[tokio::main]
26+
/// async fn main() {
27+
/// let qs = stream::queue(|mut q| async move {
28+
/// for i in 1..=5 {
29+
/// sleep(std::time::Duration::from_secs(1)).await;
30+
/// q.push(format!("Hello, I'm message#{i}!"))
31+
/// }
32+
///
33+
/// sleep(std::time::Duration::from_secs(1)).await;
34+
///
35+
/// q.push("done".to_string())
36+
/// });
37+
/// }
38+
/// ```
39+
///
40+
/// <br>
41+
///
42+
/// ---
43+
/// *openai.rs*
44+
/// ```ignore
45+
/// use ohkami::prelude::*;
46+
/// use ohkami::Memory;
47+
/// use ohkami::typed::DataStream;
48+
/// use ohkami::utils::{StreamExt, stream};
49+
///
50+
/// pub async fn relay_chat_completion(
51+
/// api_key: Memory<'_, &'static str>,
52+
/// UserMessage(message): UserMessage,
53+
/// ) -> Result<DataStream<String, Error>, Error> {
54+
/// let mut gpt_response = reqwest::Client::new()
55+
/// .post("https://api.openai.com/v1/chat/completions")
56+
/// .bearer_auth(*api_key)
57+
/// .json(&ChatCompletions {
58+
/// model: "gpt-4o",
59+
/// stream: true,
60+
/// messages: vec![
61+
/// ChatMessage {
62+
/// role: Role::user,
63+
/// content: message,
64+
/// }
65+
/// ],
66+
/// })
67+
/// .send().await?
68+
/// .bytes_stream();
69+
///
70+
/// Ok(DataStream::from_stream(stream::queue(|mut q| async move {
71+
/// let mut push_line = |mut line: String| {
72+
/// line.strip_suffix("\n\n").ok();
73+
///
74+
/// #[cfg(debug_assertions)] {
75+
/// if line != "[DONE]" {
76+
/// let chunk: models::ChatCompletionChunk
77+
/// = serde_json::from_str(&line).unwrap();
78+
/// let content = chunk
79+
/// .choices[0]
80+
/// .delta
81+
/// .content.as_deref().unwrap_or(""));
82+
/// print!("{content}");
83+
/// std::io::Write::flush(&mut std::io::stdout()).ok();
84+
/// } else {
85+
/// println!()
86+
/// }
87+
/// }
88+
///
89+
/// q.push(Ok(line));
90+
/// };
91+
///
92+
/// let mut remaining = String::new();
93+
/// while let Some(Ok(raw_chunk)) = gpt_response.next().await {
94+
/// for line in std::str::from_utf8(&raw_chunk).unwrap()
95+
/// .split_inclusive("\n\n")
96+
/// {
97+
/// if let Some(data) = line.strip_prefix("data: ") {
98+
/// if data.ends_with("\n\n") {
99+
/// push_line(data.to_string())
100+
/// } else {
101+
/// remaining = data.into()
102+
/// }
103+
/// } else {
104+
/// #[cfg(debug_assertions)] {
105+
/// assert!(line.ends_with("\n\n"))
106+
/// }
107+
/// push_line(std::mem::take(&mut remaining) + line)
108+
/// }
109+
/// }
110+
/// }
111+
/// })))
112+
/// }
113+
/// ```
4114
pub fn queue<T, F, Fut>(f: F) -> stream::QueueStream<F, T, Fut>
5115
where
6116
F: FnOnce(stream::Queue<T>) -> Fut,

0 commit comments

Comments
 (0)