Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Api #18

Merged
merged 5 commits into from
Nov 24, 2023
Merged

Api #18

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
/target
/target/
**/*.rs.bk
/target/
**/*.rs.bk


.DS_Store
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"rust-analyzer.showUnlinkedFileNotification": false
"rust-analyzer.showUnlinkedFileNotification": false,
"[rust]": {"editor.formatOnSave": true, "editor.defaultFormatter": "rust-lang.rust-analyzer"},
}
43 changes: 43 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
NAME=restful
VERSION=$(shell git rev-parse HEAD)
SEMVER_VERSION=$(shell grep version Cargo.toml | awk -F"\"" '{print $$2}' | head -n 1)
REPO=mrasheduzzaman
SHELL := /bin/bash

no_postgres:
@[ -z "$$(docker ps -q -f ancestor="postgres:latest")" ] || (echo "db running"; exit 2)
has_postgres:
@[ -n "$$(docker ps -q -f ancestor="postgres:latest")" ] || (echo "db not running"; exit 2)

db: no_postgres
@echo "Starting postgres container"
docker compose up -d db

stop:
@docker ps -aq | xargs -r docker rm -f
@pkill $(NAME) || true

setup:
cargo install cargo-watch
cargo install diesel_cli --no-default-features --features postgres

test:
./test.sh

compose:
docker compose up -d
docker compose logs $(NAME)

run: has_postgres
cargo watch -q -c -w src/ -x run

compile: has_postgres
cargo build --release

build:
@echo "Reusing built binary in current directory from make compile"
@ls -lah ./$(NAME)
docker build -t $(REPO)/$(NAME):$(VERSION) .

tag-latest: build
docker tag $(REPO)/$(NAME):$(VERSION) $(REPO)/$(NAME):latest
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,56 @@ Our CORS implementation is going to make use of Rocket's Fairings, which are lik
## How to use this template

To use this template as your project starting point, click "Use this template" at the top of this page, or click [here](https://github.com/rashed091/rustful-service/generate).

## Developing Easily
Install dependencies, setup `rustup` env, then start a dev postgres with credentials:

```sh
sudo pacman -S postgresql-libs # or distro equivalent
make setup
source env.sh
make db # runs postgres in a container
```

then run and test in parallel with:

```sh
make run
make test
```

This workflow is the fastest as it does not require waiting for docker builds, nor deal with a local installations of postgres. You just need to install `docker`, `postgresql-libs` to manage migrations in a container, and run against it with what's essentially `cargo run`.

## Using docker-compose
You can develop and test production equivalents without rust, without local postgres, without postgres libs, and without diesel-cli locally.

This is the production equivalent flow:

```sh
# Build the app with clux/muslrust
make compile
# Put the built binary into a container and compose with a db.
# Then, once the db is up, use clux/diesel-cli to run migrations:
source env.sh
make compose
# Verify
make test
```

## Caveats
**NB:** With `docker-compose` our migration would have to wait for postgres to initialize, either via a sleep or a `psql` "select 1" attempt. See `make compose` for more info.

**NB:** The compile step is required before any build step, so `docker-compose up` would fail without it. It's possible to fix this by using a multistep docker build for the app, but it makes local build caching harder.


**NB:** Sometime run command can give you address/port in use error compile. Please follow the steps below to resolve it:

```bash
#Find:
netstat -vanp tcp | grep 5001
lsof -i tcp:5001
lsof -i :5001

#Kill:
kill -9 <PID>
```
48 changes: 47 additions & 1 deletion src/controllers/user_controller.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,57 @@
use crate::services::user_service::UserService;
use salvo::prelude::*;
use uuid::Uuid;

use crate::models::user::User;
use crate::common::database::Database;

#[handler]
pub async fn all_users(_req: &mut Request, _depot: &mut Depot, res: &mut Response) {
pub async fn get_all_users(_req: &mut Request, _depot: &mut Depot, res: &mut Response) {
let mut connection = Database::new().get_connection();
let users = UserService::list(&mut connection).await;
res.render(Json(users));
}

#[handler]
pub async fn delete_all_users(_req: &mut Request, _depot: &mut Depot, res: &mut Response) {
let mut connection = Database::new().get_connection();
let users = UserService::delete_all(&mut connection).await;
res.render(Json(users));
}

#[handler]
pub async fn create_user(_req: &mut Request, _depot: &mut Depot, res: &mut Response) {
let mut connection = Database::new().get_connection();
let user = User {
id: Uuid::new_v4(),
name: "Test".to_string(),
age: 27
};
let users = UserService::create(&mut connection, user).await;
res.render(Json(users));
}

#[handler]
pub async fn get_user(req: &mut Request, _depot: &mut Depot, res: &mut Response) {
let id = Uuid::parse_str(req.param::<&str>("id").unwrap());
let mut connection = Database::new().get_connection();
let users = UserService::get(&mut connection, id.unwrap()).await;
res.render(Json(users));
}


#[handler]
pub async fn delete_user(req: &mut Request, _depot: &mut Depot, res: &mut Response) {
let id = req.param::<Uuid>("id").unwrap();
let mut connection = Database::new().get_connection();
let users = UserService::delete(&mut connection, id).await;
res.render(Json(users));
}

#[handler]
pub async fn update_user(req: &mut Request, _depot: &mut Depot, res: &mut Response) {
let id = req.param::<Uuid>("id").unwrap();
let mut connection = Database::new().get_connection();
let users = UserService::update(&mut connection, id).await;
res.render(Json(users.unwrap()));
}
33 changes: 30 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,23 @@ mod services;

use controllers::user_controller;
use dotenv::dotenv;
use salvo::cors::Cors;
use salvo::http::Method;
use salvo::logging::Logger;
use salvo::prelude::*;
use std::env;

#[handler]
async fn hello() -> &'static str {
"Hello World"
async fn health() -> &'static str {
"I'm alive"
}

#[handler]
async fn index() -> &'static str {
"Welcome to rustful-service"
}


#[tokio::main]
async fn main() {
env::set_var("RUST_LOG", "debug");
Expand All @@ -25,7 +34,25 @@ async fn main() {
let port = env::var("PORT").expect("PORT is not set in .env file");
let server_url = format!("{host}:{port}");

let router = Router::new().get(user_controller::all_users);
let cors = Cors::new()
.allow_methods(vec![Method::GET, Method::POST, Method::DELETE])
.into_handler();


let router = Router::with_hoop(cors).hoop(Logger::new())
.get(index)
.push(Router::with_path("/healthz").get(health))
.push(Router::with_path("/users")
.get(user_controller::get_all_users)
.post(user_controller::create_user)
.delete(user_controller::delete_all_users)
.push(Router::with_path("<id>")
.get(user_controller::get_user)
.patch(user_controller::update_user)
.delete(user_controller::delete_user)
)
);

let acceptor = TcpListener::new(server_url).bind().await;

Server::new(acceptor).serve(router).await;
Expand Down
2 changes: 1 addition & 1 deletion src/models/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use uuid::Uuid;

use crate::schema::users;

#[derive(Serialize, Deserialize, Queryable, Insertable)]
#[derive(Serialize, Deserialize, Queryable, Insertable, Selectable)]
#[diesel(table_name = users)]
pub struct User {
pub id: Uuid,
Expand Down
50 changes: 28 additions & 22 deletions src/services/user_service.rs
Original file line number Diff line number Diff line change
@@ -1,40 +1,46 @@
use diesel;
use diesel::prelude::*;
// use uuid::Uuid;
use uuid::Uuid;

use crate::common::database::Connection;
use crate::models::user::User;

use crate::schema::users;
// use crate::schema::users::dsl::users as all_users;
use crate::schema::users::dsl::users as all_users;

pub struct UserService;

impl UserService {
pub async fn list(connection: &mut Connection) -> Vec<User> {
users::table.load::<User>(connection).unwrap()
pub async fn create(connection: &mut Connection, new_user: User) -> User {
let _result = diesel::insert_into(users::table).values(new_user).execute(connection);

users::table
.order(users::id.desc())
.first(connection)
.unwrap()
}

// pub async fn create(connection: &mut Connection, new_user: &User) -> User {
// let data = User {
// id: Uuid::new_v4(),
// name: new_user.name.clone(),
// age: new_user.age.clone()
// };
pub async fn get(connection: &mut Connection, user_id: Uuid) -> User {
users::table.find(user_id).select(User::as_select()).first(connection).unwrap()
}

// let _result = diesel::insert_into(users::table).values(&data).execute(connection);
pub async fn update(connection: &mut Connection, user_id: Uuid) -> QueryResult<User> {
use crate::schema::users::dsl::{name, age};

// users::table
// .order(users::id.desc())
// .first(connection)
// .unwrap()
// }
diesel::update(users::table.find(user_id))
.set((name.eq("Rooh Rafan"), age.eq(30),))
.get_result::<User>(connection)
}

// pub async fn delete(connection: &mut Connection, user_id: Uuid) -> bool {
// diesel::delete(users::table.find(user_id)).execute(connection).is_ok()
// }
pub async fn delete(connection: &mut Connection, user_id: Uuid) -> bool {
diesel::delete(users::table.find(user_id)).execute(connection).is_ok()
}

// pub async fn delete_all(connection: &mut Connection) -> bool {
// diesel::delete(all_users).execute(connection).is_ok()
// }
pub async fn list(connection: &mut Connection) -> Vec<User> {
users::table.load::<User>(connection).unwrap()
}

pub async fn delete_all(connection: &mut Connection) -> bool {
diesel::delete(all_users).execute(connection).is_ok()
}
}
24 changes: 24 additions & 0 deletions test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/bin/bash
set -ex

res=$(curl -s -X POST http://127.0.0.1:5001/users -H "Content-Type: application/json" \
-d '{"name": "Rashed", "age": "27"}')

echo "${res}"

# lastid="$(echo "${res}" | jq ".id")"

# can get it individually
# curl -s -X GET "http://127.0.0.1:5001/users/${lastid}"

# post not published yet
# curl -s -X GET http://127.0.0.1:5001/users | grep -v "${lastid}"

# publish
# curl -s -X PUT "http://127.0.0.1:5001/users/${lastid}"

# post now published
# curl -s -X GET http://127.0.0.1:5001/users | grep "${lastid}"

# delete post
# curl -s -X DELETE "http://127.0.0.1:5001/users/${lastid}"
Loading