Initial commit

This commit is contained in:
Franek 2025-03-17 12:16:03 +01:00
commit 66a2e1af6c
18 changed files with 1963 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
config.toml

1732
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

12
Cargo.toml Normal file
View File

@ -0,0 +1,12 @@
[package]
name = "api"
description = "API for sador.me website"
version = "1.0.0"
edition = "2024"
[dependencies]
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
diesel = { version = "2.1", features = ["postgres", "r2d2"] }
actix-web = "4"
toml = "0.8"

4
config.toml.example Normal file
View File

@ -0,0 +1,4 @@
database_url = "postgres://user:password@host/database"
api_version = "1.0.0"
host = "0.0.0.0"
port = 3000

9
diesel.toml Normal file
View File

@ -0,0 +1,9 @@
# For documentation on how to configure this file,
# see https://diesel.rs/guides/configuring-diesel-cli
[print_schema]
file = "src/schema.rs"
custom_type_derives = ["diesel::query_builder::QueryId", "Clone"]
[migrations_directory]
dir = "migrations"

0
migrations/.keep Normal file
View File

View File

@ -0,0 +1,6 @@
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.
DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);
DROP FUNCTION IF EXISTS diesel_set_updated_at();

View File

@ -0,0 +1,36 @@
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.
-- Sets up a trigger for the given table to automatically set a column called
-- `updated_at` whenever the row is modified (unless `updated_at` was included
-- in the modified columns)
--
-- # Example
--
-- ```sql
-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW());
--
-- SELECT diesel_manage_updated_at('users');
-- ```
CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$
BEGIN
EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$
BEGIN
IF (
NEW IS DISTINCT FROM OLD AND
NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
) THEN
NEW.updated_at := current_timestamp;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

View File

@ -0,0 +1 @@
DROP TABLE posts;

View File

@ -0,0 +1,8 @@
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
title VARCHAR NOT NULL,
content TEXT NOT NULL,
author_id INTEGER NOT NULL
);
CREATE INDEX idx_author_id ON posts(author_id);

19
src/config.rs Normal file
View File

@ -0,0 +1,19 @@
use serde::Deserialize;
#[derive(Deserialize, Clone)]
pub struct Config {
pub database_url: String,
pub api_version: String,
pub host: String,
pub port: u16
}
pub fn read_config() -> Config {
let config_data = std::fs
::read_to_string("config.toml")
.expect("failed to read config file, maybe it's missing?");
toml
::from_str(&config_data)
.expect("invalid TOML data, check your config")
}

13
src/database/mod.rs Normal file
View File

@ -0,0 +1,13 @@
use diesel::r2d2::{ConnectionManager, Pool};
use diesel::pg::PgConnection;
pub mod models;
pub type DbPool = Pool<ConnectionManager<PgConnection>>;
pub fn establish_connection(database_url: String) -> DbPool {
let manager = ConnectionManager::<PgConnection>::new(database_url);
Pool::builder()
.build(manager)
.expect("failed to create database pool")
}

View File

@ -0,0 +1 @@
pub mod post;

View File

@ -0,0 +1,17 @@
use serde::{Serialize, Deserialize};
use diesel::prelude::*;
#[derive(Queryable, Serialize, Deserialize)]
pub struct Post {
pub id: i32,
pub title: String,
pub content: String,
pub author_id: i32
}
#[derive(Insertable)]
#[diesel(table_name = crate::schema::posts)]
pub struct NewPost<'a> {
pub title: &'a str,
pub content: &'a str,
}

39
src/main.rs Normal file
View File

@ -0,0 +1,39 @@
use actix_web::{main, web::Data, App, HttpServer};
use actix_web::middleware::NormalizePath;
mod database;
mod config;
mod schema;
mod v1;
#[derive(Clone)]
struct State {
db: database::DbPool,
config: config::Config
}
#[main]
async fn main() -> std::io::Result<()> {
let config = config::read_config();
let conn = database::establish_connection(config.clone().database_url);
let state = State {
db: conn,
config: config.clone()
};
let server = HttpServer::new(move || {
let state = state.clone();
App::new()
.wrap(NormalizePath::trim())
.app_data(Data::new(state))
.configure(v1::init_routes)
})
.bind((config.host, config.port))?
.run();
println!("API running at on port {}", config.port);
server.await
}

10
src/schema.rs Normal file
View File

@ -0,0 +1,10 @@
// @generated automatically by Diesel CLI.
diesel::table! {
posts (id) {
id -> Int4,
title -> Varchar,
content -> Text,
author_id -> Int4,
}
}

27
src/v1/mod.rs Normal file
View File

@ -0,0 +1,27 @@
use actix_web::{Responder, HttpResponse, web::{ServiceConfig, Data, scope}, get};
use serde::Serialize;
use crate::State;
mod posts;
#[derive(Serialize)]
struct PingResponse {
version: String
}
#[get("/")]
async fn index(state: Data<State>) -> impl Responder {
let response = PingResponse {
version: state.config.api_version.clone(),
};
HttpResponse::Ok().json(response)
}
pub fn init_routes(cfg: &mut ServiceConfig) {
cfg
.service(index)
.service(scope("/api/v1")
.configure(posts::init_routes)
);
}

27
src/v1/posts.rs Normal file
View File

@ -0,0 +1,27 @@
use actix_web::{web::{ServiceConfig, Data, scope}, HttpResponse, Responder, get};
use diesel::RunQueryDsl;
use crate::database::models::post::Post;
use crate::schema::posts::dsl::*;
use crate::State;
#[get("")]
async fn get(state: Data<State>) -> impl Responder {
let mut conn = match state.db.get() {
Ok(conn) => conn,
Err(why) => return HttpResponse
::InternalServerError()
.body(format!("database connection error: {}", why)),
};
match posts.load::<Post>(&mut conn) {
Ok(data) => HttpResponse::Ok().json(data),
Err(why) => HttpResponse::InternalServerError().body(why.to_string()),
}
}
pub fn init_routes(cfg: &mut ServiceConfig) {
cfg.service(scope("/posts")
.service(get)
);
}