Skip to content

Commit 8cad54c

Browse files
JockeMStephen
andauthored
Add postgres chat exmaple (#2577)
Co-authored-by: Stephen <[email protected]>
1 parent b066af6 commit 8cad54c

File tree

6 files changed

+302
-0
lines changed

6 files changed

+302
-0
lines changed

.github/workflows/examples.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,15 @@ jobs:
153153
command: test
154154
args: -p sqlx-example-postgres-axum-social
155155

156+
# The Chat example has an interactive TUI which is not trivial to test automatically,
157+
# so we only check that it compiles.
158+
- name: Chat (Check)
159+
uses: actions-rs/cargo@v1
160+
env:
161+
with:
162+
command: check
163+
args: -p sqlx-example-postgres-check
164+
156165
- name: Files (Setup)
157166
working-directory: examples/postgres/files
158167
env:

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ members = [
1212
"sqlx-sqlite",
1313
"examples/mysql/todos",
1414
"examples/postgres/axum-social-with-tests",
15+
"examples/postgres/chat",
1516
"examples/postgres/files",
1617
"examples/postgres/json",
1718
"examples/postgres/listen",

examples/postgres/chat/Cargo.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[package]
2+
name = "sqlx-example-postgres-chat"
3+
version = "0.1.0"
4+
edition = "2021"
5+
workspace = "../../../"
6+
7+
[dependencies]
8+
sqlx = { path = "../../../", features = [ "postgres", "runtime-tokio-native-tls" ] }
9+
futures = "0.3.1"
10+
tokio = { version = "1.20.0", features = [ "rt-multi-thread", "macros" ] }
11+
tui = "0.19.0"
12+
crossterm = "0.25"
13+
unicode-width = "0.1"

examples/postgres/chat/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Chat Example
2+
3+
Note: this example has an interactive TUI which is not trivial to test automatically,
4+
so our CI currently only checks whether or not it compiles.
5+
6+
## Description
7+
8+
This example demonstrates how to use PostgreSQL channels to create a very simple chat application.
9+
10+
## Setup
11+
12+
1. Declare the database URL
13+
14+
```
15+
export DATABASE_URL="postgres://postgres:password@localhost/files"
16+
```
17+
18+
## Usage
19+
20+
Run the project
21+
22+
```
23+
cargo run -p sqlx-examples-postgres-chat
24+
```

examples/postgres/chat/src/main.rs

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
use crossterm::{
2+
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
3+
execute,
4+
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
5+
};
6+
use sqlx::postgres::PgListener;
7+
use sqlx::PgPool;
8+
use std::sync::Arc;
9+
use std::{error::Error, io};
10+
use tokio::{sync::Mutex, time::Duration};
11+
use tui::{
12+
backend::{Backend, CrosstermBackend},
13+
layout::{Constraint, Direction, Layout},
14+
style::{Color, Modifier, Style},
15+
text::{Span, Spans, Text},
16+
widgets::{Block, Borders, List, ListItem, Paragraph},
17+
Frame, Terminal,
18+
};
19+
use unicode_width::UnicodeWidthStr;
20+
21+
struct ChatApp {
22+
input: String,
23+
messages: Arc<Mutex<Vec<String>>>,
24+
pool: PgPool,
25+
}
26+
27+
impl ChatApp {
28+
fn new(pool: PgPool) -> Self {
29+
ChatApp {
30+
input: String::new(),
31+
messages: Arc::new(Mutex::new(Vec::new())),
32+
pool,
33+
}
34+
}
35+
36+
async fn run<B: Backend>(
37+
mut self,
38+
terminal: &mut Terminal<B>,
39+
mut listener: PgListener,
40+
) -> Result<(), Box<dyn Error>> {
41+
// setup listener task
42+
let messages = self.messages.clone();
43+
tokio::spawn(async move {
44+
while let Ok(msg) = listener.recv().await {
45+
messages.lock().await.push(msg.payload().to_string());
46+
}
47+
});
48+
49+
loop {
50+
let messages: Vec<ListItem> = self
51+
.messages
52+
.lock()
53+
.await
54+
.iter()
55+
.map(|m| {
56+
let content = vec![Spans::from(Span::raw(m.to_owned()))];
57+
ListItem::new(content)
58+
})
59+
.collect();
60+
61+
terminal.draw(|f| self.ui(f, messages))?;
62+
63+
if !event::poll(Duration::from_millis(20))? {
64+
continue;
65+
}
66+
67+
if let Event::Key(key) = event::read()? {
68+
match key.code {
69+
KeyCode::Enter => {
70+
notify(&self.pool, &self.input).await?;
71+
self.input.clear();
72+
}
73+
KeyCode::Char(c) => {
74+
self.input.push(c);
75+
}
76+
KeyCode::Backspace => {
77+
self.input.pop();
78+
}
79+
KeyCode::Esc => {
80+
return Ok(());
81+
}
82+
_ => {}
83+
}
84+
}
85+
}
86+
}
87+
88+
fn ui<B: Backend>(&mut self, frame: &mut Frame<B>, messages: Vec<ListItem>) {
89+
let chunks = Layout::default()
90+
.direction(Direction::Vertical)
91+
.margin(2)
92+
.constraints(
93+
[
94+
Constraint::Length(1),
95+
Constraint::Length(3),
96+
Constraint::Min(1),
97+
]
98+
.as_ref(),
99+
)
100+
.split(frame.size());
101+
102+
let text = Text::from(Spans::from(vec![
103+
Span::raw("Press "),
104+
Span::styled("Enter", Style::default().add_modifier(Modifier::BOLD)),
105+
Span::raw(" to send the message, "),
106+
Span::styled("Esc", Style::default().add_modifier(Modifier::BOLD)),
107+
Span::raw(" to quit"),
108+
]));
109+
let help_message = Paragraph::new(text);
110+
frame.render_widget(help_message, chunks[0]);
111+
112+
let input = Paragraph::new(self.input.as_ref())
113+
.style(Style::default().fg(Color::Yellow))
114+
.block(Block::default().borders(Borders::ALL).title("Input"));
115+
frame.render_widget(input, chunks[1]);
116+
frame.set_cursor(
117+
// Put cursor past the end of the input text
118+
chunks[1].x + self.input.width() as u16 + 1,
119+
// Move one line down, from the border to the input line
120+
chunks[1].y + 1,
121+
);
122+
123+
let messages =
124+
List::new(messages).block(Block::default().borders(Borders::ALL).title("Messages"));
125+
frame.render_widget(messages, chunks[2]);
126+
}
127+
}
128+
129+
#[tokio::main]
130+
async fn main() -> Result<(), Box<dyn Error>> {
131+
// setup postgres
132+
let conn_url =
133+
std::env::var("DATABASE_URL").expect("Env var DATABASE_URL is required for this example.");
134+
let pool = sqlx::PgPool::connect(&conn_url).await?;
135+
136+
let mut listener = PgListener::connect(&conn_url).await?;
137+
listener.listen("chan0").await?;
138+
139+
// setup terminal
140+
enable_raw_mode()?;
141+
let mut stdout = io::stdout();
142+
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
143+
let backend = CrosstermBackend::new(stdout);
144+
let mut terminal = Terminal::new(backend)?;
145+
146+
// create app and run it
147+
let app = ChatApp::new(pool);
148+
let res = app.run(&mut terminal, listener).await;
149+
150+
// restore terminal
151+
disable_raw_mode()?;
152+
execute!(
153+
terminal.backend_mut(),
154+
LeaveAlternateScreen,
155+
DisableMouseCapture,
156+
)?;
157+
terminal.show_cursor()?;
158+
159+
if let Err(err) = res {
160+
println!("{:?}", err)
161+
}
162+
163+
Ok(())
164+
}
165+
166+
async fn notify(pool: &PgPool, s: &str) -> Result<(), sqlx::Error> {
167+
sqlx::query(
168+
r#"
169+
SELECT pg_notify(chan, payload)
170+
FROM (VALUES ('chan0', $1)) v(chan, payload)
171+
"#,
172+
)
173+
.bind(s)
174+
.execute(pool)
175+
.await?;
176+
177+
Ok(())
178+
}

0 commit comments

Comments
 (0)