Initial commit with basic axum and sqlx API
This commit is contained in:
commit
c2c0f7a28d
5
.env
Normal file
5
.env
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
HOST=127.0.0.1
|
||||||
|
PORT=3000
|
||||||
|
DATABASE_URL=postgres://crawlect:nKd6kuHVGZEwCE6xqSdTQAuem3tc2a@winhost:5432/crawlect
|
||||||
|
DATABASE_MAX_CONNECTIONS=5
|
||||||
|
RUST_LOG=crawlect=debug,tower_http=debug
|
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/target
|
2164
Cargo.lock
generated
Normal file
2164
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
20
Cargo.toml
Normal file
20
Cargo.toml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
[package]
|
||||||
|
name = "crawlect"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = "1"
|
||||||
|
axum = "0.6"
|
||||||
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
|
dotenvy = "0.15"
|
||||||
|
reqwest = { version = "0.11", features = ["json"] }
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
sqlx = { version = "0.6", features = ["runtime-tokio-native-tls", "postgres", "macros", "migrate", "chrono"] }
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
tower = "0.4"
|
||||||
|
tower-http = { version = "0.4", features = ["trace"] }
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = "0.3"
|
3
build.rs
Normal file
3
build.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
println!("cargo:rerun-if-changed=migrations");
|
||||||
|
}
|
9
migrations/20230507201612_initial.sql
Normal file
9
migrations/20230507201612_initial.sql
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS "items" (
|
||||||
|
"id" SERIAL PRIMARY KEY NOT NULL,
|
||||||
|
"title" VARCHAR(255) NOT NULL,
|
||||||
|
"url" VARCHAR(255) NOT NULL,
|
||||||
|
"description" VARCHAR(255),
|
||||||
|
"created_at" timestamp(3) NOT NULL,
|
||||||
|
"updated_at" timestamp(3) NOT NULL,
|
||||||
|
"deleted_at" timestamp(3)
|
||||||
|
);
|
26
src/error.rs
Normal file
26
src/error.rs
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
use axum::{http::StatusCode, response::{IntoResponse, Response}};
|
||||||
|
|
||||||
|
// Make our own error that wraps `anyhow::Error`.
|
||||||
|
pub struct AppError(anyhow::Error);
|
||||||
|
|
||||||
|
// Tell axum how to convert `AppError` into a response.
|
||||||
|
impl IntoResponse for AppError {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
format!("Something went wrong: {}", self.0),
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This enables using `?` on functions that return `Result<_, anyhow::Error>` to turn them into
|
||||||
|
// `Result<_, AppError>`. That way you don't need to do that manually.
|
||||||
|
impl<E> From<E> for AppError
|
||||||
|
where
|
||||||
|
E: Into<anyhow::Error>,
|
||||||
|
{
|
||||||
|
fn from(err: E) -> Self {
|
||||||
|
Self(err.into())
|
||||||
|
}
|
||||||
|
}
|
19
src/handlers/item.rs
Normal file
19
src/handlers/item.rs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
use axum::{extract::{State, Path}, Json};
|
||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
use crate::error::AppError;
|
||||||
|
use crate::models::item::{create_item, get_item, CreateItem, Item};
|
||||||
|
|
||||||
|
pub async fn get(
|
||||||
|
State(pool): State<PgPool>,
|
||||||
|
Path(id): Path<i32>,
|
||||||
|
) -> Result<Json<Item>, AppError> {
|
||||||
|
Ok(Json(get_item(pool, id).await?))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn post(
|
||||||
|
State(pool): State<PgPool>,
|
||||||
|
Json(payload): Json<CreateItem>,
|
||||||
|
) -> Result<Json<Item>, AppError> {
|
||||||
|
Ok(Json(create_item(pool, payload).await?))
|
||||||
|
}
|
9
src/handlers/items.rs
Normal file
9
src/handlers/items.rs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
use axum::{extract::State, Json};
|
||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
use crate::error::AppError;
|
||||||
|
use crate::models::item::{get_items, Item};
|
||||||
|
|
||||||
|
pub async fn get(State(pool): State<PgPool>) -> Result<Json<Vec<Item>>, AppError> {
|
||||||
|
Ok(Json(get_items(pool).await?))
|
||||||
|
}
|
2
src/handlers/mod.rs
Normal file
2
src/handlers/mod.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod item;
|
||||||
|
pub mod items;
|
43
src/main.rs
Normal file
43
src/main.rs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
use axum::{
|
||||||
|
routing::{get, post},
|
||||||
|
Router,
|
||||||
|
};
|
||||||
|
use dotenvy::dotenv;
|
||||||
|
use sqlx::postgres::PgPoolOptions;
|
||||||
|
use std::env;
|
||||||
|
use tower::ServiceBuilder;
|
||||||
|
use tower_http::trace::TraceLayer;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
mod error;
|
||||||
|
mod handlers;
|
||||||
|
mod models;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
dotenv().ok();
|
||||||
|
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
|
let pool = PgPoolOptions::new()
|
||||||
|
.max_connections(env::var("DATABASE_MAX_CONNECTIONS")?.parse()?)
|
||||||
|
.connect(&env::var("DATABASE_URL")?)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
sqlx::migrate!().run(&pool).await?;
|
||||||
|
|
||||||
|
let app = Router::new()
|
||||||
|
.route("/items", get(handlers::items::get))
|
||||||
|
.route("/item", post(handlers::item::post))
|
||||||
|
.route("/item/:id", get(handlers::item::get))
|
||||||
|
.with_state(pool)
|
||||||
|
.layer(ServiceBuilder::new().layer(TraceLayer::new_for_http()));
|
||||||
|
|
||||||
|
let addr = (env::var("HOST")? + ":" + &env::var("PORT")?).parse()?;
|
||||||
|
debug!("listening on {}", addr);
|
||||||
|
axum::Server::bind(&addr)
|
||||||
|
.serve(app.into_make_service())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
53
src/models/item.rs
Normal file
53
src/models/item.rs
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
use anyhow::Context;
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Item {
|
||||||
|
id: i32,
|
||||||
|
title: String,
|
||||||
|
url: String,
|
||||||
|
description: Option<String>,
|
||||||
|
created_at: NaiveDateTime,
|
||||||
|
updated_at: NaiveDateTime,
|
||||||
|
deleted_at: Option<NaiveDateTime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct CreateItem {
|
||||||
|
title: String,
|
||||||
|
url: String,
|
||||||
|
description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_item(pool: PgPool, id: i32) -> anyhow::Result<Item> {
|
||||||
|
sqlx::query_as!(Item, "SELECT * FROM items WHERE id = $1", id)
|
||||||
|
.fetch_one(&pool)
|
||||||
|
.await
|
||||||
|
.context("Failed to fetch item")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_items(pool: PgPool) -> anyhow::Result<Vec<Item>> {
|
||||||
|
sqlx::query_as!(Item, "SELECT * FROM items")
|
||||||
|
.fetch_all(&pool)
|
||||||
|
.await
|
||||||
|
.context("Failed to fetch items")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_item(pool: PgPool, payload: CreateItem) -> anyhow::Result<Item> {
|
||||||
|
sqlx::query_as!(
|
||||||
|
Item,
|
||||||
|
"INSERT INTO items (
|
||||||
|
title, url, description, created_at, updated_at
|
||||||
|
) VALUES (
|
||||||
|
$1, $2, $3, now(), now()
|
||||||
|
) RETURNING *",
|
||||||
|
payload.title,
|
||||||
|
payload.url,
|
||||||
|
payload.description
|
||||||
|
)
|
||||||
|
.fetch_one(&pool)
|
||||||
|
.await
|
||||||
|
.context("Failed to create item")
|
||||||
|
}
|
1
src/models/mod.rs
Normal file
1
src/models/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod item;
|
Loading…
Reference in New Issue
Block a user