Compare commits
4 Commits
a0587512eb
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ec1f3a17f | |||
| e7f20a4d82 | |||
| 8e81b99f63 | |||
| ae1084c1c3 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
|||||||
/target
|
/target
|
||||||
|
*.db
|
||||||
|
|||||||
2356
Cargo.lock
generated
Normal file
2356
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
Cargo.toml
15
Cargo.toml
@ -1,6 +1,19 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "user-server"
|
name = "hospital_server"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
axum = {version = "0.8.4", features = ["macros"]}
|
||||||
|
tokio = {version = "1.47.1", features = ["full"]}
|
||||||
|
utoipa = "5.4.0"
|
||||||
|
sqlx = {version = "0.6.0", features = ["runtime-tokio-native-tls","sqlite"]}
|
||||||
|
config = "0.15.6"
|
||||||
|
utoipa-axum = {version = "0.2.0" }
|
||||||
|
utoipa-scalar = { version = "0.3", features = ["axum"] }
|
||||||
|
tracing = "0.1.41"
|
||||||
|
serde = "1.0.228"
|
||||||
|
tracing-subscriber = "0.3.20"
|
||||||
|
dotenvy = "0.15.7"
|
||||||
|
|||||||
3
README.md
Normal file
3
README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# test_exercise
|
||||||
|
|
||||||
|
Небольшой тестовый репозиторий на раст для ликвидации skill issue
|
||||||
1
migrations/20251114144629_db.down.sql
Normal file
1
migrations/20251114144629_db.down.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
drop table "patients";
|
||||||
5
migrations/20251114144629_db.up.sql
Normal file
5
migrations/20251114144629_db.up.sql
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
create table "patients" (
|
||||||
|
"id" INTEGER PRIMARY KEY,
|
||||||
|
"full_name" TEXT NOT NULL,
|
||||||
|
"phone" TEXT NOT NULL
|
||||||
|
);
|
||||||
9
queries/create_patient.sql
Normal file
9
queries/create_patient.sql
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
insert into patients(
|
||||||
|
"full_name",
|
||||||
|
"phone"
|
||||||
|
)
|
||||||
|
values($1, $2)
|
||||||
|
returning
|
||||||
|
"id",
|
||||||
|
"full_name" as "full_name!",
|
||||||
|
"phone" as "phone!"
|
||||||
3
queries/delete_patient.sql
Normal file
3
queries/delete_patient.sql
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
delete from patients
|
||||||
|
where id = $1
|
||||||
|
returning *;
|
||||||
1
queries/get_all.sql
Normal file
1
queries/get_all.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
select * from "patients"
|
||||||
2
queries/get_patient_by_id.sql
Normal file
2
queries/get_patient_by_id.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
select * from "patients"
|
||||||
|
where id = $1;
|
||||||
3
queries/update_patient.sql
Normal file
3
queries/update_patient.sql
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
update patients
|
||||||
|
set full_name = $2, phone = $3
|
||||||
|
where id = $1;
|
||||||
6
src/app_state.rs
Normal file
6
src/app_state.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
use sqlx::SqlitePool;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AppState {
|
||||||
|
pub pool: SqlitePool,
|
||||||
|
}
|
||||||
117
src/lib.rs
Normal file
117
src/lib.rs
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
use axum::{
|
||||||
|
Router, debug_handler,
|
||||||
|
extract::{Json, Path, State},
|
||||||
|
http::StatusCode,
|
||||||
|
response::IntoResponse,
|
||||||
|
};
|
||||||
|
use models::Patient;
|
||||||
|
use sqlx::{query_file, query_file_as};
|
||||||
|
use utoipa::OpenApi;
|
||||||
|
use utoipa_axum::{router::OpenApiRouter, routes};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app_state::AppState,
|
||||||
|
models::{CreatePatient, PathParams},
|
||||||
|
openapi::route_docs,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod app_state;
|
||||||
|
pub mod models;
|
||||||
|
pub mod openapi;
|
||||||
|
pub mod schemas;
|
||||||
|
|
||||||
|
#[derive(OpenApi)]
|
||||||
|
struct Api;
|
||||||
|
|
||||||
|
pub fn router(state: AppState) -> Router {
|
||||||
|
let (router, docs) = OpenApiRouter::with_openapi(Api::openapi())
|
||||||
|
.nest(
|
||||||
|
"/api/patients/",
|
||||||
|
OpenApiRouter::new()
|
||||||
|
.routes(routes!(create_patient, get_all))
|
||||||
|
.routes(routes!(
|
||||||
|
update_patient_by_id,
|
||||||
|
delete_patient_by_id,
|
||||||
|
get_patient_by_id
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.with_state(state)
|
||||||
|
.split_for_parts();
|
||||||
|
route_docs(router, docs)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[debug_handler]
|
||||||
|
#[utoipa::path(get, path = "/", description="Get all patients", responses((status = OK, body = Patient)))]
|
||||||
|
async fn get_all(State(state): State<AppState>) -> Json<Vec<Patient>> {
|
||||||
|
let patients = query_file_as!(Patient, "queries/get_all.sql")
|
||||||
|
.fetch_all(&state.pool)
|
||||||
|
.await
|
||||||
|
.expect("Could not fetch patients");
|
||||||
|
Json(patients)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[debug_handler]
|
||||||
|
#[utoipa::path(get, path = "/{id}", description="Get all patients", responses((status = OK, body = Patient)))]
|
||||||
|
async fn get_patient_by_id(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Path(path): Path<PathParams>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let patient = query_file_as!(Patient, "queries/get_patient_by_id.sql", path.id)
|
||||||
|
.fetch_optional(&state.pool)
|
||||||
|
.await
|
||||||
|
.expect("Could not fetch patients");
|
||||||
|
match patient {
|
||||||
|
Some(p) => Json(p).into_response(),
|
||||||
|
None => StatusCode::NOT_FOUND.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[debug_handler]
|
||||||
|
#[utoipa::path(post, path = "/", description = "Create a new patient", responses((status = OK)))]
|
||||||
|
async fn create_patient(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Json(patient): Json<CreatePatient>,
|
||||||
|
) -> Json<Patient> {
|
||||||
|
let patient = query_file_as!(
|
||||||
|
Patient,
|
||||||
|
"queries/create_patient.sql",
|
||||||
|
patient.full_name,
|
||||||
|
patient.phone
|
||||||
|
)
|
||||||
|
.fetch_one(&state.pool)
|
||||||
|
.await
|
||||||
|
.expect("Could not create new patient");
|
||||||
|
|
||||||
|
Json(patient)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[debug_handler]
|
||||||
|
#[utoipa::path(post, path = "/{id}", description = "Update patient by ID", responses((status = OK)))]
|
||||||
|
async fn update_patient_by_id(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Path(path): Path<PathParams>,
|
||||||
|
Json(patient): Json<CreatePatient>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
query_file!(
|
||||||
|
"queries/update_patient.sql",
|
||||||
|
path.id,
|
||||||
|
patient.full_name,
|
||||||
|
patient.phone
|
||||||
|
)
|
||||||
|
.fetch_one(&state.pool)
|
||||||
|
.await
|
||||||
|
.expect("Could not create new patient");
|
||||||
|
StatusCode::CREATED
|
||||||
|
}
|
||||||
|
|
||||||
|
#[debug_handler]
|
||||||
|
#[utoipa::path(delete, path = "/{id}", description = "Delete patient by ID", responses((status = OK)))]
|
||||||
|
async fn delete_patient_by_id(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Path(path): Path<PathParams>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
query_file_as!(Patient, "queries/delete_patient.sql", path.id)
|
||||||
|
.fetch_one(&state.pool)
|
||||||
|
.await
|
||||||
|
.expect("Could not delete patient");
|
||||||
|
}
|
||||||
26
src/main.rs
26
src/main.rs
@ -1,3 +1,25 @@
|
|||||||
fn main() {
|
use hospital_server::app_state::AppState;
|
||||||
println!("Hello, world!");
|
use hospital_server::router;
|
||||||
|
use sqlx::SqlitePool;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let _ = dotenvy::dotenv();
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
let db_url = std::env::var("DATABASE_URL").expect("DATABASE_URL is not set");
|
||||||
|
|
||||||
|
let pool = SqlitePool::connect(&db_url)
|
||||||
|
.await
|
||||||
|
.expect("Cannot connect to db");
|
||||||
|
sqlx::migrate!().run(&pool).await.unwrap();
|
||||||
|
|
||||||
|
let state = AppState { pool };
|
||||||
|
let router = router(state);
|
||||||
|
|
||||||
|
let addr = "0.0.0.0:3000";
|
||||||
|
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||||
|
info!("Listening in {:?}", listener.local_addr());
|
||||||
|
|
||||||
|
axum::serve(listener, router).await.unwrap();
|
||||||
}
|
}
|
||||||
|
|||||||
20
src/models.rs
Normal file
20
src/models.rs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, ToSchema, Serialize, Deserialize)]
|
||||||
|
pub struct Patient {
|
||||||
|
pub id: i64,
|
||||||
|
pub full_name: String,
|
||||||
|
pub phone: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, ToSchema, Serialize, Deserialize)]
|
||||||
|
pub struct CreatePatient {
|
||||||
|
pub full_name: String,
|
||||||
|
pub phone: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct PathParams {
|
||||||
|
pub id: i64,
|
||||||
|
}
|
||||||
9
src/openapi.rs
Normal file
9
src/openapi.rs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
use axum::{Router, routing::get};
|
||||||
|
use utoipa::openapi::OpenApi;
|
||||||
|
use utoipa_scalar::{Scalar, Servable};
|
||||||
|
|
||||||
|
pub fn route_docs(router: Router, docs: OpenApi) -> Router {
|
||||||
|
router
|
||||||
|
.merge(Scalar::with_url("/", docs.clone()))
|
||||||
|
.route("/docs/openapi.json", get(docs.to_json().expect("serializing OpenAPI")))
|
||||||
|
}
|
||||||
8
src/schemas.rs
Normal file
8
src/schemas.rs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, ToSchema, Serialize, Deserialize)]
|
||||||
|
pub struct Patient {
|
||||||
|
name: String,
|
||||||
|
diagnosis: String, //todo! enum
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user