Applications & Application Categories endpoints

Added API endpoints for CRUD actions on applications and application categories
This commit is contained in:
Joe Bellus 2022-02-04 14:36:54 -05:00
parent 37e64cb9f2
commit 734b704fa2
11 changed files with 1946 additions and 45 deletions

1201
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -15,5 +15,8 @@ actix-cors = "0.5.4"
chrono = { version = "0.4.19", features = ["serde"] } chrono = { version = "0.4.19", features = ["serde"] }
serde = { version = "1.0.136", features= [ "derive" ] } serde = { version = "1.0.136", features= [ "derive" ] }
serde_json = "1.0.78" serde_json = "1.0.78"
actix-web = "4.0.0-rc.2" actix-web = "4.0.0-rc.1"
actix-rt = "2.6.0" actix-rt = "2.6.0"
tracing-test = "0.2.1"
tracing-actix-web = "0.5.0-rc.1"
cargo-embed = "0.12.0"

View File

@ -15,7 +15,7 @@ command = "sqlx"
args = ["migrate", "run"] args = ["migrate", "run"]
[tasks.resetdb] [tasks.resetdb]
run_task = { name = ["dropdb", "createdb", "migratedb", "entity"] } run_task = { name = ["dropdb", "createdb", "migratedb"] }
[tasks.entity] [tasks.entity]
command = "sea-orm-cli" command = "sea-orm-cli"

View File

@ -1,5 +1,27 @@
-- Add migration script here -- Add migration script here
CREATE TABLE application ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, app_name TEXT NOT NULL, url TEXT NOT NULL, description TEXT, active Boolean NOT NULL DEFAULT 1, glyph TEXT, application_category_id INTEGER); CREATE TABLE application (
CREATE TABLE application_category ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, category_name TEXT NOT NULL, active BOOLEAN NOT NULL DEFAULT 1 ); id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
CREATE TABLE bookmark ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, category_name TEXT NOT NULL, active BOOLEAN NOT NULL DEFAULT 1, glyph TEXT, bookmark_category_id INTEGER); app_name TEXT NOT NULL,
CREATE TABLE bookmark_category ( id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, category_name TEXT NOT NULL, active BOOLEAN NOT NULL DEFAULT 1, glyph TEXT ); url TEXT NOT NULL,
description TEXT,
active Boolean NOT NULL DEFAULT 1,
glyph TEXT,
application_category_id INTEGER
);
CREATE TABLE application_category (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
category_name TEXT NOT NULL,
active BOOLEAN NOT NULL DEFAULT 1
);
CREATE TABLE bookmark (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
category_name TEXT NOT NULL,
active BOOLEAN NOT NULL DEFAULT 1,
glyph TEXT, bookmark_category_id INTEGER
);
CREATE TABLE bookmark_category (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
category_name TEXT NOT NULL,
active BOOLEAN NOT NULL DEFAULT 1,
glyph TEXT
);

View File

@ -0,0 +1,324 @@
use tracing::instrument;
use crate::api::api_prelude::*;
use crate::error::{Error, Result};
#[instrument]
#[get("")]
pub async fn list_application_categories(state: web::Data<AppState>) -> Result<HttpResponse> {
let cats: Vec<application_category::Model> =
ApplicationCategory::find().all(&state.db).await.unwrap();
let count = cats.len();
Ok(HttpResponse::Ok().json(ListObjects::new(cats, count)))
}
#[instrument]
#[post("")]
pub async fn new_application_category(
state: web::Data<AppState>,
data: web::Json<application_category::Model>,
) -> Result<HttpResponse> {
let model = application_category::ActiveModel {
id: NotSet,
category_name: Set(data.0.category_name),
active: Set(data.0.active),
};
let rec = model.insert(&state.db).await?;
Ok(HttpResponse::Ok().json(rec))
}
#[instrument]
#[get("{id}")]
pub async fn get_application_category(
state: web::Data<AppState>,
id: web::Path<i32>,
) -> Result<HttpResponse> {
let id = id.into_inner();
let res: Option<application_category::Model> =
ApplicationCategory::find_by_id(id).one(&state.db).await?;
match res {
Some(rec) => Ok(HttpResponse::Ok().json(rec)),
None => Err(Error::not_found()),
}
}
#[instrument]
#[put("{id}")]
pub async fn update_application_category(
state: web::Data<AppState>,
data: web::Json<application_category::Model>,
id: web::Path<i32>,
) -> Result<HttpResponse> {
let id = id.into_inner();
let res: Option<application_category::Model> =
ApplicationCategory::find_by_id(id).one(&state.db).await?;
match res {
Some(_rec) => {
let data = data.into_inner();
let ret = application_category::ActiveModel {
id: Set(id),
active: Set(data.active),
category_name: Set(data.category_name),
};
let model = ret.update(&state.db).await?;
Ok(HttpResponse::Ok().json(model))
}
None => Err(Error::not_found()),
}
}
#[instrument]
#[delete("{id}")]
pub async fn delete_application_category(
state: web::Data<AppState>,
id: web::Path<i32>,
) -> Result<HttpResponse> {
ApplicationCategory::delete_many()
.filter(application_category::Column::Id.eq(id.into_inner()))
.exec(&state.db)
.await?;
Ok(HttpResponse::Ok().body(""))
}
#[instrument]
#[get("{id}/applications")]
pub async fn application_category_applications(
state: web::Data<AppState>,
id: web::Path<i32>,
) -> Result<HttpResponse> {
let recs: Vec<application::Model> = Application::find()
.filter(application::Column::ApplicationCategoryId.eq(id.into_inner()))
.all(&state.db)
.await
.unwrap();
let count = recs.len();
Ok(HttpResponse::Ok().json(ListObjects::new(recs, count)))
}
/// Routes for the application endpoints. This binds up a scope with all endpoints for applications, to make it easier to add them to the server.
pub fn routes() -> Scope {
web::scope("/application_categories")
.service(application_category_applications)
.service(list_application_categories)
.service(update_application_category)
.service(delete_application_category)
.service(new_application_category)
.service(get_application_category)
}
#[cfg(test)]
mod tests {
use crate::api::test_prelude::*;
use actix_web::http::Method;
#[actix_rt::test]
async fn test_list_application_categories() -> Result<()> {
let state = setup_state().await?;
application_category::ActiveModel {
category_name: Set("Application 1".into()),
active: Set(true),
..Default::default()
}
.insert(&state.db)
.await?;
application_category::ActiveModel {
category_name: Set("Application 2".into()),
active: Set(true),
..Default::default()
}
.insert(&state.db)
.await?;
let req = actix_web::test::TestRequest::with_uri("/application_categories")
.method(Method::GET)
.to_request();
let resp = call_endpoint!(req, state);
assert_eq!(resp.status(), 200);
let data = get_response!(
resp,
ListObjects<crate::entity::application_category::Model>
);
assert_eq!(2_usize, data.items.len());
Ok(())
}
#[actix_rt::test]
async fn test_get_application_categories() -> Result<()> {
let state = setup_state().await?;
let model = application_category::ActiveModel {
category_name: Set("Application 1".into()),
active: Set(true),
..Default::default()
}
.insert(&state.db)
.await?;
let req = actix_web::test::TestRequest::with_uri(&format!(
"/application_categories/{}",
model.id
))
.method(Method::GET)
.to_request();
let resp = call_endpoint!(req, state);
let status = resp.status();
let mut data = get_response!(resp, crate::entity::application_category::Model);
data.id = model.id;
assert_eq!(model, data);
assert_eq!(status, 200);
Ok(())
}
#[actix_rt::test]
async fn test_new_application_category() -> Result<()> {
let model = application_category::Model {
id: 0,
category_name: "Some name".into(),
active: true,
};
let state = setup_state().await?;
let req = actix_web::test::TestRequest::with_uri("/application_categories")
.method(Method::POST)
.set_json(model.clone())
.to_request();
let resp = call_endpoint!(req, state);
assert_eq!(resp.status(), 200);
let data = get_response!(resp, crate::entity::application_category::Model);
assert_eq!(model, data);
assert_eq!(
application_category::Entity::find()
.count(&state.db)
.await?,
1
);
Ok(())
}
#[actix_rt::test]
async fn test_update_application_category() -> Result<()> {
let state = setup_state().await?;
application_category::ActiveModel {
category_name: Set("Some name".into()),
active: Set(true),
..Default::default()
}
.insert(&state.db)
.await?;
let mut model = application_category::ActiveModel {
category_name: Set("Some name".into()),
active: Set(true),
..Default::default()
}
.insert(&state.db)
.await?;
model.category_name = "Another name".into();
let req = actix_web::test::TestRequest::with_uri(&format!(
"/application_categories/{}",
model.id
))
.method(Method::PUT)
.set_json(model.clone())
.to_request();
let resp = call_endpoint!(req, state);
assert_eq!(resp.status(), 200);
let mut data = get_response!(resp, crate::entity::application_category::Model);
data.id = model.id;
assert_eq!(model, data, "Check API");
let db_model = application_category::Entity::find_by_id(model.id)
.one(&state.db)
.await?
.unwrap();
assert_eq!(db_model, model, "Check DB");
Ok(())
}
#[actix_rt::test]
async fn test_delete_application_category() -> Result<()> {
let state = setup_state().await?;
let model = application_category::ActiveModel {
category_name: Set("Some name".into()),
active: Set(true),
..Default::default()
}
.insert(&state.db)
.await?;
let req = actix_web::test::TestRequest::with_uri(&format!(
"/application_categories/{}",
model.id
))
.method(Method::DELETE)
.to_request();
let resp = call_endpoint!(req, state);
assert_eq!(resp.status(), 200);
assert_eq!(
application_category::Entity::find()
.count(&state.db)
.await?,
0
);
Ok(())
}
#[actix_rt::test]
async fn test_application_categories_applications() -> Result<()> {
let state = setup_state().await?;
let category = application_category::ActiveModel {
category_name: Set("Application 1".into()),
active: Set(true),
..Default::default()
}
.insert(&state.db)
.await?;
application::ActiveModel {
app_name: Set("Application 1".into()),
url: Set("http://somewhere/".into()),
active: Set(true),
application_category_id: Set(Some(category.id)),
..Default::default()
}
.insert(&state.db)
.await?;
application::ActiveModel {
app_name: Set("Application 2".into()),
url: Set("http://somewhere/".into()),
application_category_id: Set(Some(category.id)),
active: Set(true),
..Default::default()
}
.insert(&state.db)
.await?;
application::ActiveModel {
app_name: Set("Application 2".into()),
url: Set("http://somewhere/".into()),
active: Set(true),
..Default::default()
}
.insert(&state.db)
.await?;
let req = actix_web::test::TestRequest::with_uri(&format!(
"/application_categories/{}/applications",
category.id
))
.method(Method::GET)
.to_request();
let resp = call_endpoint!(req, state);
assert_eq!(resp.status(), 200);
let data = get_response!(resp, ListObjects<crate::entity::application::Model>);
assert_eq!(2_usize, data.items.len());
Ok(())
}
}

View File

@ -1,33 +1,250 @@
use tracing::instrument; use tracing::instrument;
use crate::api::api_prelude::*; use crate::api::api_prelude::*;
use crate::error::{Error, Result};
#[instrument] #[instrument]
#[get("/applications")] #[get("")]
pub async fn list_applications(state: web::Data<AppState>) -> Result<HttpResponse, Error> { pub async fn list_applications(state: web::Data<AppState>) -> Result<HttpResponse> {
let apps: Vec<application::Model> = Application::find().all(&state.db).await.unwrap(); let apps: Vec<application::Model> = Application::find().all(&state.db).await.unwrap();
Ok(HttpResponse::Ok().json(apps)) let count = apps.len();
Ok(HttpResponse::Ok().json(ListObjects::new(apps, count)))
}
#[instrument]
#[post("")]
pub async fn new_application(
state: web::Data<AppState>,
data: web::Json<application::Model>,
) -> Result<HttpResponse> {
let model = application::ActiveModel {
id: NotSet,
app_name: Set(data.0.app_name),
description: Set(data.0.description),
url: Set(data.0.url),
active: Set(data.0.active),
glyph: Set(data.0.glyph),
application_category_id: Set(data.0.application_category_id),
};
let app = model.insert(&state.db).await?;
Ok(HttpResponse::Ok().json(app))
}
#[instrument]
#[get("{id}")]
pub async fn get_applications(
state: web::Data<AppState>,
id: web::Path<i32>,
) -> Result<HttpResponse> {
let id = id.into_inner();
let res: Option<application::Model> = Application::find_by_id(id).one(&state.db).await?;
match res {
Some(app) => Ok(HttpResponse::Ok().json(app)),
None => Err(Error::not_found()),
}
}
#[instrument]
#[put("{id}")]
pub async fn update_applications(
state: web::Data<AppState>,
data: web::Json<application::Model>,
id: web::Path<i32>,
) -> Result<HttpResponse> {
let id = id.into_inner();
let res: Option<application::Model> = Application::find_by_id(id).one(&state.db).await?;
match res {
Some(_app) => {
let data = data.into_inner();
let ret = application::ActiveModel {
id: Set(id),
active: Set(data.active),
app_name: Set(data.app_name),
description: Set(data.description),
url: Set(data.url),
application_category_id: Set(data.application_category_id),
glyph: Set(data.glyph),
};
let model = ret.update(&state.db).await?;
Ok(HttpResponse::Ok().json(model))
}
None => Err(Error::not_found()),
}
}
#[instrument]
#[delete("{id}")]
pub async fn delete_application(
state: web::Data<AppState>,
id: web::Path<i32>,
) -> Result<HttpResponse> {
Application::delete_many()
.filter(application::Column::Id.eq(id.into_inner()))
.exec(&state.db)
.await?;
Ok(HttpResponse::Ok().body(""))
}
/// Routes for the application endpoints. This binds up a scope with all endpoints for applications, to make it easier to add them to the server.
pub fn routes() -> Scope {
web::scope("/applications")
.service(list_applications)
.service(update_applications)
.service(delete_application)
.service(new_application)
.service(get_applications)
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::api::test_prelude::*; use crate::api::test_prelude::*;
use actix_web::http::Method; use actix_web::http::Method;
#[actix_rt::test] #[actix_rt::test]
async fn test_list_applications() { async fn test_list_applications() -> Result<()> {
let db = MockDatabase::new(DatabaseBackend::Sqlite) let state = setup_state().await?;
.append_query_results(vec![vec![application::Model { application::ActiveModel {
id: 1, app_name: Set("Application 1".into()),
app_name: "Application 1".into(), url: Set("http://somewhere/".into()),
..Default::default() active: Set(true),
}]]) ..Default::default()
.into_connection(); }
.insert(&state.db)
.await?;
application::ActiveModel {
app_name: Set("Application 2".into()),
url: Set("http://somewhere/".into()),
active: Set(true),
..Default::default()
}
.insert(&state.db)
.await?;
let req = actix_web::test::TestRequest::with_uri("/applications") let req = actix_web::test::TestRequest::with_uri("/applications")
.method(Method::GET) .method(Method::GET)
.to_request(); .to_request();
let resp = call_endpoint!(req, db); let resp = call_endpoint!(req, state);
assert_eq!(resp.status(), 200); assert_eq!(resp.status(), 200);
let data = get_response!(resp, ListObjects<crate::entity::application::Model>);
assert_eq!(2_usize, data.items.len());
Ok(())
}
#[actix_rt::test]
async fn test_get_applications() -> Result<()> {
let state = setup_state().await?;
let model = application::ActiveModel {
app_name: Set("Application 1".into()),
url: Set("http://somewhere/".into()),
active: Set(true),
..Default::default()
}
.insert(&state.db)
.await?;
let req = actix_web::test::TestRequest::with_uri(&format!("/applications/{}", model.id))
.method(Method::GET)
.to_request();
let resp = call_endpoint!(req, state);
let status = resp.status();
let mut data = get_response!(resp, crate::entity::application::Model);
data.id = model.id;
assert_eq!(model, data);
assert_eq!(status, 200);
Ok(())
}
#[actix_rt::test]
async fn test_new_application() -> Result<()> {
let model = application::Model {
id: 0,
app_name: "Application 1".into(),
glyph: Some("web".into()),
url: "http://example.com".into(),
description: Some("Some Application".into()),
active: true,
application_category_id: None,
};
let state = setup_state().await?;
let req = actix_web::test::TestRequest::with_uri("/applications")
.method(Method::POST)
.set_json(model.clone())
.to_request();
let resp = call_endpoint!(req, state);
assert_eq!(resp.status(), 200);
let data = get_response!(resp, crate::entity::application::Model);
assert_eq!(model, data);
assert_eq!(application::Entity::find().count(&state.db).await?, 1);
Ok(())
}
#[actix_rt::test]
async fn test_update_application() -> Result<()> {
let state = setup_state().await?;
application::ActiveModel {
app_name: Set("Application 1".into()),
url: Set("http://somewhere/".into()),
active: Set(true),
..Default::default()
}
.insert(&state.db)
.await?;
let mut model = application::ActiveModel {
app_name: Set("Application 2".into()),
url: Set("http://somewhere/".into()),
active: Set(true),
..Default::default()
}
.insert(&state.db)
.await?;
model.url = "http://updated.com".into();
let req = actix_web::test::TestRequest::with_uri(&format!("/applications/{}", model.id))
.method(Method::PUT)
.set_json(model.clone())
.to_request();
let resp = call_endpoint!(req, state);
assert_eq!(resp.status(), 200);
let mut data = get_response!(resp, crate::entity::application::Model);
data.id = model.id;
assert_eq!(model, data, "Check API");
let db_model = application::Entity::find_by_id(model.id)
.one(&state.db)
.await?
.unwrap();
assert_eq!(db_model, model, "Check DB");
Ok(())
}
#[actix_rt::test]
async fn test_delete_application() -> Result<()> {
let state = setup_state().await?;
let model = application::ActiveModel {
app_name: Set("Application 1".into()),
url: Set("http://somewhere/".into()),
active: Set(true),
..Default::default()
}
.insert(&state.db)
.await?;
let req = actix_web::test::TestRequest::with_uri(&format!("/applications/{}", model.id))
.method(Method::DELETE)
.to_request();
let resp = call_endpoint!(req, state);
assert_eq!(resp.status(), 200);
assert_eq!(application::Entity::find().count(&state.db).await?, 0);
Ok(())
} }
} }

View File

@ -1,35 +1,100 @@
#[cfg(test)] use serde::{Deserialize, Serialize};
#[macro_export] #[macro_export]
#[cfg(test)]
macro_rules! call_endpoint { macro_rules! call_endpoint {
($req:ident, $db:ident) => {{ ($req:ident, $state:ident) => {{
let state = AppState { db: $db }; // let subscriber = tracing_subscriber::prelude::__tracing_subscriber_SubscriberExt::with(
// tracing_subscriber::registry(),
// tracing_subscriber::Layer::with_filter(
// tracing_subscriber::fmt::Layer::new()
// .pretty()
// .with_writer(std::io::stdout)
// .with_ansi(true),
// tracing_subscriber::filter::LevelFilter::DEBUG,
// ),
// );
// tracing::subscriber::set_global_default(subscriber)
// .expect("Unable to set a global collector");
let a = App::new() let a = App::new()
.app_data(state) .wrap(tracing_actix_web::TracingLogger::default())
.service(crate::api::applications::list_applications); .app_data($state.clone())
let mut app = actix_web::test::init_service(a).await; .service(crate::api::applications::routes())
let resp = actix_web::test::call_service(&mut app, $req).await; .service(crate::api::application_category::routes());
let app = actix_web::test::init_service(a).await;
let resp = actix_web::test::call_service(&app, $req).await;
resp resp
}}; }};
} }
#[cfg(test)]
macro_rules! get_response {
($resp: ident, $type:ty) => {{
let body = test::read_body($resp).await.to_vec();
serde_json::from_slice::<$type>(&body).unwrap()
}};
}
pub mod application_category;
pub mod applications; pub mod applications;
mod api_prelude { mod api_prelude {
pub use super::ListObjects;
pub use crate::entity::prelude::*; pub use crate::entity::prelude::*;
pub use crate::entity::*; pub use crate::entity::*;
pub use crate::AppState; pub use crate::AppState;
pub use actix_web::{get, web, Error, HttpResponse}; pub use actix_web::{delete, get, post, put, web, Error, HttpResponse, Scope};
pub use sea_orm::prelude::*; pub use sea_orm::prelude::*;
pub use sea_orm::{NotSet, Set};
} }
#[cfg(test)] #[cfg(test)]
mod test_prelude { mod test_prelude {
pub use super::ListObjects;
pub use crate::entity::*; pub use crate::entity::*;
pub use crate::AppState; pub use crate::AppState;
pub use crate::error::Result;
pub use actix_web::dev::ServiceResponse; pub use actix_web::dev::ServiceResponse;
pub use actix_web::{test, web, App}; pub use actix_web::{test, web, App};
use sea_orm::sea_query::TableCreateStatement;
use sea_orm::ConnectionTrait;
use sea_orm::Database;
use sea_orm::DbBackend;
use sea_orm::Schema;
pub use sea_orm::{ pub use sea_orm::{
entity::prelude::*, entity::*, tests_cfg::*, DatabaseBackend, MockDatabase, Transaction, entity::prelude::*, entity::*, tests_cfg::*, DatabaseBackend, MockDatabase, MockExecResult,
Transaction,
}; };
/// Sets up a testing state with an in-memory database and creates the scheme.
pub async fn setup_state() -> Result<actix_web::web::Data<AppState>> {
let db = Database::connect("sqlite::memory:").await?;
let schema = Schema::new(DbBackend::Sqlite);
let stmt: TableCreateStatement = schema.create_table_from_entity(application::Entity);
db.execute(db.get_database_backend().build(&stmt)).await?;
let stmt: TableCreateStatement =
schema.create_table_from_entity(application_category::Entity);
db.execute(db.get_database_backend().build(&stmt)).await?;
Ok(actix_web::web::Data::new(AppState { db }))
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListObjects<T>
where
T: Serialize,
{
items: Vec<T>,
total: usize,
}
impl<T: Serialize> ListObjects<T> {
pub fn new(items: Vec<T>, total: usize) -> Self {
Self { items, total }
}
} }

View File

@ -5,8 +5,10 @@ use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "application")] #[sea_orm(table_name = "application")]
#[serde(rename_all = "camelCase")]
pub struct Model { pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]
#[serde(skip_deserializing)]
pub id: i32, pub id: i32,
pub app_name: String, pub app_name: String,
pub url: String, pub url: String,
@ -16,16 +18,27 @@ pub struct Model {
pub application_category_id: Option<i32>, pub application_category_id: Option<i32>,
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation { pub enum Relation {
#[sea_orm(
belongs_to = "super::application_category::Entity",
from = "Column::ApplicationCategoryId",
to = "super::application_category::Column::Id"
)]
ApplicationCategory, ApplicationCategory,
} }
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
match self {
Self::ApplicationCategory => Entity::belongs_to(super::application_category::Entity)
.from(Column::ApplicationCategoryId)
.to(super::application_category::Column::Id)
.into(),
}
}
}
impl Related<super::application_category::Entity> for Entity {
fn to() -> RelationDef {
Relation::ApplicationCategory.def()
}
}
impl ActiveModelBehavior for ActiveModel {} impl ActiveModelBehavior for ActiveModel {}
#[cfg(test)] #[cfg(test)]

View File

@ -5,20 +5,31 @@ use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Deserialize)]
#[sea_orm(table_name = "application_category")] #[sea_orm(table_name = "application_category")]
#[serde(rename_all = "camelCase")]
pub struct Model { pub struct Model {
#[sea_orm(primary_key)] #[sea_orm(primary_key)]
#[serde(skip_deserializing)]
pub id: i32, pub id: i32,
pub category_name: String, pub category_name: String,
pub active: bool, pub active: bool,
} }
#[derive(Copy, Clone, Debug, EnumIter)] #[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {} pub enum Relation {
Application,
}
impl RelationTrait for Relation { impl RelationTrait for Relation {
fn def(&self) -> RelationDef { fn def(&self) -> RelationDef {
panic!("No RelationDef") match self {
Self::Application => Entity::has_many(super::application::Entity).into(),
}
} }
} }
impl Related<super::application::Entity> for Entity {
fn to() -> RelationDef {
Relation::Application.def()
}
}
impl ActiveModelBehavior for ActiveModel {} impl ActiveModelBehavior for ActiveModel {}

52
src/error.rs Normal file
View File

@ -0,0 +1,52 @@
use serde::{Deserialize, Serialize};
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub enum ErrorCode {
NotFound,
DatabaseError,
Internal,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Error {
code: ErrorCode,
message: String,
}
impl Error {
pub fn not_found() -> Self {
Self {
code: ErrorCode::NotFound,
message: "Resource not found".to_string(),
}
}
}
impl From<&str> for Error {
fn from(s: &str) -> Self {
Error {
code: ErrorCode::Internal,
message: s.into(),
}
}
}
impl actix_web::error::ResponseError for Error {}
impl From<sea_orm::DbErr> for Error {
fn from(e: sea_orm::DbErr) -> Self {
Self {
code: ErrorCode::DatabaseError,
message: e.to_string(),
}
}
}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}

View File

@ -1,13 +1,11 @@
use std::sync::Arc; use actix_web::{web, App, HttpServer};
use actix_web::{App, HttpServer};
use sea_orm::{Database, DatabaseConnection}; use sea_orm::{Database, DatabaseConnection};
use tracing::{instrument, info}; use tracing::{info, instrument};
use tracing_subscriber::prelude::*; use tracing_subscriber::prelude::*;
mod api; mod api;
mod entity; mod entity;
mod error;
#[derive(Debug)] #[derive(Debug)]
pub struct AppState { pub struct AppState {
@ -26,13 +24,14 @@ async fn main() {
tracing::subscriber::set_global_default(subscriber).expect("Unable to set a global collector"); tracing::subscriber::set_global_default(subscriber).expect("Unable to set a global collector");
let db = setup_database().await.unwrap(); let db = setup_database().await.unwrap();
let state = Arc::new(AppState { db }); let state = web::Data::new(AppState { db });
info!("Starting http server on 8080"); info!("Starting http server on 8080");
HttpServer::new(move || { HttpServer::new(move || {
App::new() App::new()
.app_data(state.clone()) .app_data(state.clone())
.service(api::applications::list_applications) .service(api::applications::routes())
.service(api::application_category::routes())
}) })
.bind("127.0.0.1:8080") .bind("127.0.0.1:8080")
.unwrap() .unwrap()