From d2d8799dd3add5d20079ae7640f3740f64cb69b3 Mon Sep 17 00:00:00 2001 From: NoNameUser9 Date: Wed, 12 Nov 2025 13:57:40 +0300 Subject: [PATCH] /films implementation --- .gitignore | 1 + Cargo.lock | 424 +++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 6 +- compose.yaml | 9 ++ src/main.rs | 235 +++++++++++++++++++++------- 5 files changed, 620 insertions(+), 55 deletions(-) create mode 100644 compose.yaml diff --git a/.gitignore b/.gitignore index ea8c4bf..fedaa2b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +.env diff --git a/Cargo.lock b/Cargo.lock index 76ef0e4..686e3d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -273,6 +273,58 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82c79c15c05d4bf82b6f5ef163104cc81a760d8e874d38ac50ab67c8877b647b" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -320,6 +372,57 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.103" @@ -449,6 +552,240 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap", + "log", + "memchr", + "once_cell", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror", + "time", + "tokio", + "tokio-stream", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "bytes", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "time", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64", + "bitflags", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "time", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror", + "time", + "tracing", + "url", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.109" @@ -466,6 +803,93 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.48.0" diff --git a/Cargo.toml b/Cargo.toml index c2be24e..0944ce7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,5 +4,9 @@ version = "0.1.0" edition = "2024" [dependencies] -axum = { "0.8.6", features = ["full"]} +axum = "0.8.6" +chrono = "0.4.42" +dotenvy = "0.15.7" +serde = "1.0.228" +sqlx = { version = "0.8.6", features = ["postgres", "runtime-tokio", "time", "macros"] } tokio = { version = "1.48.0", features = ["full"]} diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..b1d9a5f --- /dev/null +++ b/compose.yaml @@ -0,0 +1,9 @@ +services: + database: + image: "docker.io/postgres:17" + environment: + POSTGRES_DB: "my-db" + POSTGRES_USER: "user" + POSTGRES_PASSWORD: "pass" + ports: + - 5463:5432 diff --git a/src/main.rs b/src/main.rs index 39ac3cb..b392c95 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,77 +1,204 @@ -use axum::response::IntoResponse; -use axum::{Router, routing::get}; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use axum::{ + Json, Router, + extract::{Path, Query, State}, + http::StatusCode, + response::IntoResponse, + routing::get, +}; +use serde::{Deserialize, Serialize}; +use sqlx::PgPool; use tokio::net::TcpListener; -mod router; +#[derive(Clone)] +struct AppState { + pool: PgPool, +} #[tokio::main] -async fn main() -> Result<(), Box> { - let listener = TcpListener::bind("127.0.0.1:8080").await?; +async fn main() { + let _ = dotenvy::dotenv(); - let app = Router::new() + let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL should be set"); + let pool = PgPool::connect(&database_url) + .await + .expect("Could not connect to database"); + + sqlx::raw_sql( + r#" + CREATE TABLE IF NOT EXISTS movie( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + year DATE + ); + "#, + ) + .execute(&pool) + .await + .expect("Couldn't create table"); + + let state = AppState { pool: pool }; + + let router = Router::new() .route("/", get(root)) + .route("/films", get(get_film).post(post_film).delete(delete_all)) .route( - "/films", - get(get_film) - .post(post_film) - .put(put_film) - .delete(delete_all), + "/films/{id}", + get(get_film_by_id).delete(delete_film_by_id).put(put_film), ) - .route("/films/{id}", get(get_film_by_id).delete(delete_film_by_id)); + .with_state(state); - loop { - let (mut socket, _) = listener.accept().await?; + let addr = "127.0.0.1:8000"; + let listener = TcpListener::bind(addr).await.unwrap(); + println!("Listening at {addr}"); + axum::serve(listener, router).await.unwrap(); +} - tokio::spawn(async move { - let mut buf = [0; 1024]; +#[derive(Clone, Deserialize, Serialize)] +struct Movie { + id: i32, + name: String, + year: i32, +} - // In a loop, read data from the socket and write the data back. - loop { - let n = match socket.read(&mut buf).await { - // socket closed - Ok(0) => return, - Ok(n) => n, - Err(e) => { - eprintln!("failed to read from socket; err = {:?}", e); - return; - } - }; +#[derive(Clone, Deserialize, Serialize)] +struct CreateMovie { + name: String, + year: i32, +} - // Write the data back - if let Err(e) = socket.write_all(&buf[0..n]).await { - eprintln!("failed to write to socket; err = {:?}", e); - return; - } - } - }); +#[derive(Debug, Clone, Deserialize, Serialize)] +struct GetMovieQuery { + name: Option, + year: Option, +} + +async fn root() {} + +async fn get_film( + State(state): State, + Query(query): Query, +) -> impl IntoResponse { + let movies = sqlx::query_as!(Movie, "SELECT id, name, year FROM movies") + .fetch_all(&state.pool) + .await; + + let movie = match movies { + Ok(_) => Json(movies.unwrap()), + Err(e) => { + eprintln!("error: {e:?}"); + Json(vec![]) + } + }; + + let movie_sorted: Vec = movie + .to_vec() + .into_iter() + .filter(|m| query.name.as_ref().map_or(true, |n| m.name.contains(n))) + .filter(|m| query.year.map_or(true, |y| m.year == y)) + .collect(); + + Json(movie_sorted) +} + +async fn get_film_by_id(State(state): State, Path(id): Path) -> impl IntoResponse { + let result = sqlx::query_as!( + Movie, + "SELECT id, name, year FROM movies WHERE id = ($1)", + id + ) + .fetch_optional(&state.pool) + .await; + + let movie = match result { + Ok(_) => result.unwrap(), + Err(e) => { + eprintln!("error: {e:?}"); + None + } + }; + + match movie { + Some(m) => Json(m).into_response(), + None => (StatusCode::NOT_FOUND, "Movie not found").into_response(), } } -async fn root() { - axum::http::StatusCode::NOT_IMPLEMENTED +async fn post_film(State(state): State, Json(movie): Json) -> impl IntoResponse { + let result = sqlx::query!( + "INSERT INTO movies(name, year) VALUES ($1, $2)", + movie.name, + movie.year + ) + .execute(&state.pool) + .await; + + match result { + Ok(_) => (StatusCode::CREATED, "Movie has been created").into_response(), + Err(e) => { + eprintln!("error: {e:?}"); + (StatusCode::INTERNAL_SERVER_ERROR).into_response() + } + } } -async fn get_film() -> impl IntoResponse { - axum::http::StatusCode::NOT_IMPLEMENTED +async fn put_film( + State(state): State, + Path(id): Path, + Json(movie): Json, +) -> impl IntoResponse { + let result = sqlx::query!( + "UPDATE movies + SET name = $1, year = $2 + WHERE id = $3", + movie.name, + movie.year, + id + ) + .execute(&state.pool) + .await; + + match result { + Ok(res) => match res.rows_affected() { + 0 => (StatusCode::NOT_FOUND, "Movie not found").into_response(), + _ => (StatusCode::CREATED, "Movie has been updated").into_response(), + }, + Err(e) => { + eprintln!("error: {e:?}"); + (StatusCode::INTERNAL_SERVER_ERROR).into_response() + } + } } -async fn post_film() -> impl IntoResponse { - axum::http::StatusCode::NOT_IMPLEMENTED +async fn delete_film_by_id( + State(state): State, + Path(id): Path, +) -> impl IntoResponse { + let result = sqlx::query!("DELETE FROM movies WHERE id = $1", id) + .execute(&state.pool) + .await; + + match result { + Ok(res) => match res.rows_affected() { + 0 => (StatusCode::NOT_FOUND, "Movie not found").into_response(), + _ => (StatusCode::NO_CONTENT, "Movie has been deleted").into_response(), + }, + Err(e) => { + eprintln!("error: {e:?}"); + (StatusCode::INTERNAL_SERVER_ERROR).into_response() + } + } } -async fn put_film() -> impl IntoResponse { - axum::http::StatusCode::NOT_IMPLEMENTED -} +async fn delete_all(State(state): State) -> impl IntoResponse { + let result = sqlx::query!("DELETE FROM movies") + .execute(&state.pool) + .await; -async fn delete_all() -> impl IntoResponse { - axum::http::StatusCode::NOT_IMPLEMENTED -} - -async fn get_film_by_id() -> impl IntoResponse { - axum::http::StatusCode::NOT_IMPLEMENTED -} - -async fn delete_film_by_id() -> impl IntoResponse { - axum::http::StatusCode::NOT_IMPLEMENTED + match result { + Ok(_) => StatusCode::NOT_FOUND, + Err(e) => { + eprintln!("error: {e:?}"); + StatusCode::INTERNAL_SERVER_ERROR + } + } }