split project into fe/be crates
This commit is contained in:
parent
2468d3f0c3
commit
9153ec6c43
@ -1,4 +1,7 @@
|
||||
[frontend.build]
|
||||
tagret = "wasm32-unknown-unknown"
|
||||
|
||||
[env]
|
||||
POSTGRES_USER = "postgres"
|
||||
POSTGRES_PASSWORD = "DB_PASS_HERE"
|
||||
POSTGRES_HOST = "localhost"
|
||||
POSTGRES_PASSWORD = "12345"
|
||||
POSTGRES_HOST = "localhost"
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -1 +1,2 @@
|
||||
/target
|
||||
*target
|
||||
*dist
|
1279
Cargo.lock
generated
1279
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
18
Cargo.toml
18
Cargo.toml
@ -1,13 +1,7 @@
|
||||
[package]
|
||||
name = "rust-web"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
[workspace]
|
||||
members = [
|
||||
"backend",
|
||||
"frontend"
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
axum = "0.7.5"
|
||||
serde = { version = "1.0.198", features = ["derive"] }
|
||||
serde_json = "1.0.116"
|
||||
sqlx = { version = "0.7.4", features = ["postgres", "migrate", "runtime-tokio-rustls"] }
|
||||
tokio = { version = "1.2", features = ["full"] }
|
||||
|
@ -1,5 +1,5 @@
|
||||
###### David Westgate
|
||||
# Rust Axum Question and Answer Server
|
||||
# Rust Question and Answer Web app
|
||||
|
||||
## Background
|
||||
This is a simple [Rust](https://www.rust-lang.org/) Web [Axum](https://docs.rs/axum/latest/axum/) server, so far serving a handful of [REST](https://en.wikipedia.org/wiki/REST) [API](https://en.wikipedia.org/wiki/API) endpoints as the basis of a "Q&A" or question and answer application. The APIs supported offer basic [CRUD](https://www.codecademy.com/article/what-is-crud) functionality. Specifically, this example mirrors that which is found in the early chapters of the [Rust Web Development textbook by Bastian Gruber](https://rustwebdevelopment.com/) (except using Axum and not Warp)
|
||||
|
@ -1 +0,0 @@
|
||||
[]
|
14
backend/Cargo.toml
Normal file
14
backend/Cargo.toml
Normal file
@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "backend"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
axum = "0.7.5"
|
||||
serde = { version = "1.0.198", features = ["derive"] }
|
||||
serde_json = "1.0.116"
|
||||
sqlx = { version = "0.7.4", features = ["postgres", "migrate", "runtime-tokio-rustls"] }
|
||||
tokio = { version = "1.38.0", features = ["full"] }
|
96
backend/README.md
Normal file
96
backend/README.md
Normal file
@ -0,0 +1,96 @@
|
||||
###### David Westgate
|
||||
# Rust Axum Question and Answer Server
|
||||
|
||||
## Background
|
||||
This is a simple [Rust](https://www.rust-lang.org/) Web [Axum](https://docs.rs/axum/latest/axum/) server, so far serving a handful of [REST](https://en.wikipedia.org/wiki/REST) [API](https://en.wikipedia.org/wiki/API) endpoints as the basis of a "Q&A" or question and answer application. The APIs supported offer basic [CRUD](https://www.codecademy.com/article/what-is-crud) functionality. Specifically, this example mirrors that which is found in the early chapters of the [Rust Web Development textbook by Bastian Gruber](https://rustwebdevelopment.com/) (except using Axum and not Warp)
|
||||
|
||||
## Contents
|
||||
|
||||
I'll do my best to keep these contents up-to-date as changes are made to the source code
|
||||
|
||||
* Setup
|
||||
* Dependency overview
|
||||
* Source overview
|
||||
* Looking ahead
|
||||
|
||||
### Setup
|
||||
|
||||
#### Rustup
|
||||
First install Rust. I suggest using the [rustup toolchain](https://rustup.rs/)
|
||||
|
||||
#### Database
|
||||
A postgres database is required. At this time, the database must be setup and run manually.
|
||||
|
||||
It is convient to run postgres in docker with the postgres:latest docker image. This can be done in a few easy clicks on docker desktop, or as follows with the docker CLI (be sure to pick a POSTGRES_PASSWORD and update [.cargo/config.toml](.cargo/config.toml))
|
||||
```
|
||||
docker pull postgres:latest
|
||||
docker run --name my-postgres-container -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=mypassword -e -p 5432:5432 -d postgres:latest
|
||||
```
|
||||
|
||||
#### Build and Run
|
||||
```
|
||||
cargo run
|
||||
```
|
||||
|
||||
### Dependency overview
|
||||
|
||||
#### std
|
||||
We utilize std library crates for file input/output, hash maps, and network sockets
|
||||
#### [Axum](https://crates.io/crates/axum)
|
||||
Axum is the rust web framework doing much of the heavy lifting of this application. It is used here for its core functionality of routing, and abstracting http concepts like methods, request/response handling, and status codes.
|
||||
#### [Tokio](https://crates.io/crates/tokio)
|
||||
The tokio framework allows rust to operate asynchoronously. By axums asychonous web nature, it is required by axum.
|
||||
#### [Serde/serde_json](https://crates.io/crates/serde)
|
||||
A useful package for serializing and deserializing data.
|
||||
#### [sqlx](https://crates.io/crates/sqlx)
|
||||
Manages connections and queries to our postgres database
|
||||
|
||||
### Source overview
|
||||
|
||||
#### `src/main.rs`
|
||||
Our applications main entry point, here we declare the use of the applications external packages for easier management.
|
||||
`main` is then responsible for creating the new question `store` (behind the necessary tokio `RwLock`). `main` creates the axum router, and specifies all of the routes our application supports by pointing them to handlers in the `src/api.rs` file via the appropriate http methods, before serving the axum service
|
||||
#### `src/api.rs`
|
||||
Five handlers are defined here for our five API endpoints, supporting the basic CRUD operations regarding questions. Care was taken to handle most common possible error cases, such as attempting to get a question by an id which does not exist, or attempting to create a question with the same ID as one that already exists.
|
||||
|
||||
Some minor derivations were taken here from the exact routes specified in the text
|
||||
* GET /question/:id is included to return a specific question by id
|
||||
* GET /questions is programmed to use pagination
|
||||
* PUT /question does not include the question ID in its path param, but rather just the body
|
||||
* DELETE /question/:id returns in its body the deleted content
|
||||
#### `src/question.rs`
|
||||
This is the definition of our `struct Question`, `NewQuestion` and `QuestionDTO`.
|
||||
|
||||
#### `src/pg_store.rs`
|
||||
The store is responsible for the management of the questions, tags, question_tags and answers. It does this by
|
||||
* Providing public functions to create, retrieve, update or delete questions, create/delete tags, update question_tag associations, and add answers
|
||||
* Handling possible database interactions errors with some grace
|
||||
|
||||
### `question_tag.rs`
|
||||
Definition of `QuestionTag` struct
|
||||
### `tag.rs`
|
||||
Definition of `Tag` struct
|
||||
|
||||
### `answer.rs`
|
||||
Definition of `Answer` and `NewAnswer` structs
|
||||
|
||||
|
||||
### Looking ahead
|
||||
These are a few things still to be added
|
||||
#### Code cleanup
|
||||
There is some dept here related to code cleanup I want to address very soon
|
||||
* Stop using major nested match statements in functions that return options or results. question mark operator should work better for these.
|
||||
* Deal with some exceptions where I use rust string formatting on query inputs; In some places, this was difficult to avoid.
|
||||
* Change URL params of ids from u8 to i32 to avoid casting later on.
|
||||
|
||||
#### Higher priority
|
||||
* Add some simple API curl examples to this README
|
||||
* API documentation tooling (`utoipa`)
|
||||
* API testing tooling (`swagger`)
|
||||
* Coded API tests with mock data
|
||||
* Specific defined Error types for common errors
|
||||
#### Lesser priority
|
||||
* Serve basic web page(s) to utilize all APIs
|
||||
* Optimize the put/update handler to support path or body identification, and to update only individual fields passed.
|
||||
* Host application on my raspberry pi on the internet
|
||||
|
@ -31,7 +31,7 @@ impl Store {
|
||||
Ok(pool) => pool,
|
||||
Err(_) => panic!("Could not establish database connection"),
|
||||
};
|
||||
sqlx::migrate!().run(&db_pool).await.unwrap();
|
||||
sqlx::migrate!("./migrations").run(&db_pool).await.unwrap();
|
||||
Store {
|
||||
connection: db_pool,
|
||||
}
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
1334
frontend/Cargo.lock
generated
Normal file
1334
frontend/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
frontend/Cargo.toml
Normal file
16
frontend/Cargo.toml
Normal file
@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "frontend"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
|
||||
[dependencies]
|
||||
|
||||
gloo-console = "0.3.0"
|
||||
gloo-net = "0.2"
|
||||
serde = { version = "1.0.198", features = ["derive"] }
|
||||
wasm-bindgen-futures = "0.4"
|
||||
wasm-cookies = "0.2.1"
|
||||
web-sys = { version = "0.3.69", features = ["HtmlTextAreaElement"] }
|
||||
yew = { git = "https://github.com/yewstack/yew/", features = ["csr"] }
|
||||
|
36
frontend/README.md
Normal file
36
frontend/README.md
Normal file
@ -0,0 +1,36 @@
|
||||
# Questions - A yew frontend WASM application
|
||||
This is a frontend web application intended to provide an interface to the APIs provided by my [questions API rust server](https://gitlab.cecs.pdx.edu/djw2/rust-web). This application is a fork of the course [knock-knock-yew](https://github.com/pdx-cs-rust-web/knock-knock-yew) frontend, primarily authored by Bart Massey
|
||||
|
||||
## Contents
|
||||
|
||||
I'll do my best to keep these contents up-to-date as changes are made to the source code
|
||||
|
||||
* Setup
|
||||
* Dependency overview
|
||||
* Source overview
|
||||
* Looking ahead
|
||||
|
||||
## Setup
|
||||
First, the [questions API rust server](https://gitlab.cecs.pdx.edu/djw2/rust-web) should be running on a known address and port
|
||||
```
|
||||
rustup update
|
||||
rustup target add wasm32-unknown-unknown
|
||||
cargo install trunk --locked
|
||||
cargo build
|
||||
```
|
||||
|
||||
## Run
|
||||
```
|
||||
trunk serve
|
||||
```
|
||||
|
||||
### Dependency overview
|
||||
|
||||
#### std
|
||||
#### [Yew](https://crates.io/crates/yew)
|
||||
|
||||
### Source overview
|
||||
|
||||
#### `src/main.rs`
|
||||
|
||||
### Looking ahead
|
25
frontend/index.css
Normal file
25
frontend/index.css
Normal file
@ -0,0 +1,25 @@
|
||||
.teller {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tellee {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.annotation {
|
||||
font-style: italic;
|
||||
font-size: 60%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-weight: bold;
|
||||
font-size: 150%;
|
||||
}
|
||||
|
||||
.joke {
|
||||
border: 1px solid black;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #881111;
|
||||
}
|
8
frontend/index.html
Normal file
8
frontend/index.html
Normal file
@ -0,0 +1,8 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Questions and Answers</title>
|
||||
<link data-trunk rel="css" href="index.css" />
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
36
frontend/src/finder.rs
Normal file
36
frontend/src/finder.rs
Normal file
@ -0,0 +1,36 @@
|
||||
use crate::*;
|
||||
|
||||
#[derive(Properties, Clone, PartialEq)]
|
||||
pub struct FinderProps {
|
||||
pub on_find: Callback<Option<String>>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn Finder(props: &FinderProps) -> Html {
|
||||
let key = use_state(|| <Option<String>>::None);
|
||||
let change_key = {
|
||||
let key = key.clone();
|
||||
Callback::from(move |e: InputEvent| {
|
||||
let input: HtmlTextAreaElement = e.target_unchecked_into();
|
||||
let value = input.value();
|
||||
// log!(format!("key change: {:?}", value));
|
||||
let value = value.trim();
|
||||
let value = if value.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(value.to_string())
|
||||
};
|
||||
// log!(format!("key change final: {:?}", value));
|
||||
key.set(value);
|
||||
})
|
||||
};
|
||||
let props = props.clone();
|
||||
html! { <>
|
||||
<div>
|
||||
<input type="text" placeholder="joke id" oninput={change_key}/>
|
||||
<button onclick={move |_| props.on_find.emit((*key).clone())}>
|
||||
{"Find this joke"}
|
||||
</button>
|
||||
</div>
|
||||
</> }
|
||||
}
|
57
frontend/src/joke.rs
Normal file
57
frontend/src/joke.rs
Normal file
@ -0,0 +1,57 @@
|
||||
use crate::*;
|
||||
|
||||
#[derive(Properties, Clone, PartialEq, serde::Deserialize)]
|
||||
pub struct JokeStruct {
|
||||
pub id: String,
|
||||
pub whos_there: String,
|
||||
pub answer_who: String,
|
||||
pub tags: Option<HashSet<String>>,
|
||||
pub source: Option<String>,
|
||||
}
|
||||
|
||||
impl JokeStruct {
|
||||
pub async fn get_joke(key: Option<String>) -> Msg {
|
||||
let request = match &key {
|
||||
None => "http://localhost:3000/api/v1/joke".to_string(),
|
||||
Some(ref key) => format!("http://localhost:3000/api/v1/joke/{}", key,),
|
||||
};
|
||||
let response = http::Request::get(&request).send().await;
|
||||
match response {
|
||||
Err(e) => Msg::GotJoke(Err(e)),
|
||||
Ok(data) => Msg::GotJoke(data.json().await),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn format_tags(tags: &HashSet<String>) -> String {
|
||||
let taglist: Vec<&str> = tags.iter().map(String::as_ref).collect();
|
||||
taglist.join(", ")
|
||||
}
|
||||
|
||||
#[derive(Properties, Clone, PartialEq, serde::Deserialize)]
|
||||
pub struct JokeProps {
|
||||
pub joke: JokeStruct,
|
||||
}
|
||||
|
||||
#[function_component(Joke)]
|
||||
pub fn joke(joke: &JokeProps) -> Html {
|
||||
let joke = &joke.joke;
|
||||
html! { <>
|
||||
<div class="joke">
|
||||
<span class="teller">{"Knock-Knock!"}</span><br/>
|
||||
<span class="tellee">{"Who's there?"}</span><br/>
|
||||
<span class="teller">{joke.whos_there.clone()}</span><br/>
|
||||
<span class="tellee">{format!("{} who?", &joke.whos_there)}</span><br/>
|
||||
<span class="teller">{joke.answer_who.clone()}</span>
|
||||
</div>
|
||||
<span class="annotation">
|
||||
{format!("[id: {}", &joke.id)}
|
||||
if let Some(ref tags) = joke.tags {
|
||||
{format!("; tags: {}", &format_tags(tags))}
|
||||
}
|
||||
if let Some(ref source) = joke.source {
|
||||
{format!("; source: {}", source)}
|
||||
}
|
||||
{"]"}
|
||||
</span>
|
||||
</> }
|
||||
}
|
82
frontend/src/main.rs
Normal file
82
frontend/src/main.rs
Normal file
@ -0,0 +1,82 @@
|
||||
mod finder;
|
||||
mod joke;
|
||||
|
||||
use finder::*;
|
||||
use joke::*;
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
extern crate serde;
|
||||
// use gloo_console::log;
|
||||
use gloo_net::http;
|
||||
extern crate wasm_bindgen_futures;
|
||||
use web_sys::HtmlTextAreaElement;
|
||||
use yew::prelude::*;
|
||||
|
||||
pub type JokeResult = Result<JokeStruct, gloo_net::Error>;
|
||||
|
||||
struct App {
|
||||
joke: JokeResult,
|
||||
}
|
||||
|
||||
pub enum Msg {
|
||||
GotJoke(JokeResult),
|
||||
GetJoke(Option<String>),
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn refresh_joke(ctx: &Context<Self>, key: Option<String>) {
|
||||
let got_joke = JokeStruct::get_joke(key);
|
||||
ctx.link().send_future(got_joke);
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for App {
|
||||
type Message = Msg;
|
||||
type Properties = ();
|
||||
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
App::refresh_joke(ctx, None);
|
||||
let joke = Err(gloo_net::Error::GlooError("Loading Joke…".to_string()));
|
||||
Self { joke }
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
Msg::GotJoke(joke) => {
|
||||
self.joke = joke;
|
||||
true
|
||||
}
|
||||
Msg::GetJoke(key) => {
|
||||
// log!(format!("GetJoke: {:?}", key));
|
||||
App::refresh_joke(ctx, key);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let joke = &self.joke;
|
||||
html! {
|
||||
<>
|
||||
<h1>{ "Knock-Knock" }</h1>
|
||||
if let Ok(ref joke) = joke {
|
||||
<Joke joke={joke.clone()}/>
|
||||
}
|
||||
if let Err(ref error) = joke {
|
||||
<div>
|
||||
<span class="error">{format!("Server Error: {error}")}</span>
|
||||
</div>
|
||||
}
|
||||
<div>
|
||||
<button onclick={ctx.link().callback(|_| Msg::GetJoke(None))}>{"Tell me another!"}</button>
|
||||
</div>
|
||||
<Finder on_find={ctx.link().callback(Msg::GetJoke)}/>
|
||||
</>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
yew::Renderer::<App>::new().render();
|
||||
}
|
122
questions.json
122
questions.json
@ -1,122 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Have a question?",
|
||||
"content": "Just ask me",
|
||||
"tags": ["intro"]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"title": "How?",
|
||||
"content": "Please help!",
|
||||
"tags": ["general"]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"title": "What is the capital of France?",
|
||||
"content": "Need to know for a quiz!",
|
||||
"tags": ["geography", "quiz"]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"title": "Can you explain relativity?",
|
||||
"content": "I'm confused about Einstein's theory.",
|
||||
"tags": ["science", "physics"]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"title": "What's a good vegan recipe?",
|
||||
"content": "Looking for dinner ideas.",
|
||||
"tags": ["food", "recipes"]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"title": "How do I change a tire?",
|
||||
"content": "My car tire is flat, what should I do?",
|
||||
"tags": ["automotive", "DIY"]
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"title": "Best programming language for beginners?",
|
||||
"content": "I'm new to coding.",
|
||||
"tags": ["technology", "programming"]
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"title": "Is coffee good for health?",
|
||||
"content": "Can drinking coffee have health benefits?",
|
||||
"tags": ["health", "nutrition"]
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"title": "What's the current time in Tokyo?",
|
||||
"content": "Need to make a call there.",
|
||||
"tags": ["time", "world_clock"]
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"title": "How to meditate?",
|
||||
"content": "I want to start meditation.",
|
||||
"tags": ["wellness", "mindfulness"]
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"title": "Why do cats purr?",
|
||||
"content": "Curious about cat behaviors.",
|
||||
"tags": ["animals", "cats"]
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"title": "History of the Roman Empire?",
|
||||
"content": "Looking for a brief overview.",
|
||||
"tags": ["history", "education"]
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"title": "Effective study techniques?",
|
||||
"content": "I need to improve my study habits.",
|
||||
"tags": ["education", "learning"]
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"title": "Best way to save for retirement?",
|
||||
"content": "Planning my financial future.",
|
||||
"tags": ["finance", "retirement"]
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"title": "Symptoms of dehydration?",
|
||||
"content": "What should I look out for?",
|
||||
"tags": ["health", "hydration"]
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"title": "How to write a cover letter?",
|
||||
"content": "Applying for a new job.",
|
||||
"tags": ["career", "advice"]
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"title": "What is quantum computing?",
|
||||
"content": "I've heard it's the future of tech.",
|
||||
"tags": ["technology", "computing"]
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"title": "How to improve my credit score?",
|
||||
"content": "Need advice on managing credit.",
|
||||
"tags": ["finance", "credit"]
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"title": "What causes earthquakes?",
|
||||
"content": "Wondering how earthquakes happen.",
|
||||
"tags": ["science", "geology"]
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"title": "How to make homemade bread?",
|
||||
"content": "Interested in baking.",
|
||||
"tags": ["cooking", "baking"]
|
||||
}
|
||||
]
|
Reference in New Issue
Block a user