feat: strategy! modularized it, added getter route

This commit is contained in:
əlemi 2023-12-22 23:37:23 +01:00
parent dbcf022019
commit b28e8fc394
Signed by: alemi
GPG key ID: A4895B84D311642C
4 changed files with 157 additions and 0 deletions

21
src/model.rs Normal file
View file

@ -0,0 +1,21 @@
use serde::{Serialize, Deserialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Suggestion {
pub author: Option<String>,
pub contact: Option<String>,
pub body: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Acknowledgement {
Sent(String),
Refused(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PageOptions {
pub offset: Option<usize>,
pub limit: Option<usize>,
}

View file

@ -0,0 +1,22 @@
use crate::model::Suggestion;
use super::NotificationProcessor;
pub struct ConsoleTracingNotifier {}
#[async_trait::async_trait]
impl NotificationProcessor<Suggestion> for ConsoleTracingNotifier {
async fn process(&self, suggestion: &Suggestion) {
tracing::info!(" >> {:?}", suggestion);
}
}
pub struct ConsolePrettyNotifier {}
#[async_trait::async_trait]
impl NotificationProcessor<Suggestion> for ConsolePrettyNotifier {
async fn process(&self, suggestion: &Suggestion) {
println!("{} -- {} <{}>", suggestion.body, suggestion.author.as_deref().unwrap_or("anon"), suggestion.contact.as_deref().unwrap_or(""));
}
}

61
src/routes.rs Normal file
View file

@ -0,0 +1,61 @@
use std::sync::Arc;
use axum::{http::StatusCode, Json, Form, Router, routing::{put, post, get}, extract::{State, Query}, response::IntoResponse};
use tokio::sync::RwLock;
use crate::{notifications::NotificationProcessor, model::{Suggestion, Acknowledgement, PageOptions}, storage::StorageStrategy};
pub fn create_router_with_app_routes(state: Context) -> Router {
Router::new()
.route("/send", get(get_suggestion))
.route("/send", post(send_suggestion_form))
.route("/send", put(send_suggestion_json))
.with_state(Arc::new(RwLock::new(state)))
}
type SafeContext = Arc<RwLock<Context>>;
pub struct Context {
providers: Vec<Box<dyn NotificationProcessor<Suggestion>>>,
storage: Box<dyn StorageStrategy<Suggestion>>,
}
impl Context {
pub fn new(storage: Box<dyn StorageStrategy<Suggestion>>) -> Self {
Context { providers: Vec::new(), storage }
}
pub fn register(mut self, notifier: Box<dyn NotificationProcessor<Suggestion>>) -> Self {
self.providers.push(notifier);
self
}
async fn process(&self, x: &Suggestion) {
for p in self.providers.iter() {
p.process(x).await;
}
}
}
async fn send_suggestion(payload: Suggestion, state: SafeContext) -> impl IntoResponse {
let mut lock = state.write().await;
lock.process(&payload).await;
match lock.storage.archive(payload).await {
Ok(()) => (StatusCode::OK, Json(Acknowledgement::Sent("".into()))),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(Acknowledgement::Refused(e.to_string()))),
}
}
async fn send_suggestion_json(State(state): State<SafeContext>, Json(payload): Json<Suggestion>) -> impl IntoResponse { send_suggestion(payload, state).await }
async fn send_suggestion_form(State(state): State<SafeContext>, Form(payload): Form<Suggestion>) -> impl IntoResponse { send_suggestion(payload, state).await }
async fn get_suggestion(State(state): State<SafeContext>, Query(page): Query<PageOptions>) -> Result<Json<Vec<Suggestion>>, String> {
let offset = page.offset.unwrap_or(0);
let limit = std::cmp::min(page.limit.unwrap_or(20), 20);
match state.read().await.storage.extract(offset, limit).await {
Ok(x) => Ok(Json(x)),
Err(e) => Err(e.to_string()),
}
}

53
src/storage.rs Normal file
View file

@ -0,0 +1,53 @@
use crate::model::Suggestion;
#[derive(Debug, thiserror::Error)]
pub enum StorageStrategyError {
#[error("could not interact with filesystem: {0}")]
IOError(#[from] std::io::Error),
#[error("could not serialize/deserialize data: {0}")]
JsonSerializeError(#[from] serde_json::Error),
}
#[async_trait::async_trait]
pub trait StorageStrategy<T> : Send + Sync {
async fn archive(&mut self, payload: T) -> Result<(), StorageStrategyError>;
async fn extract(&self, offset: usize, window: usize) -> Result<Vec<T>, StorageStrategyError>;
}
/// this strategy is rather inefficient since it has to iterate the whole file every time, but it
/// requires literally zero effort
pub struct JsonFileStorageStrategy {
path: String,
}
impl JsonFileStorageStrategy {
pub fn new(path: &str) -> Self {
JsonFileStorageStrategy { path: path.to_string() }
}
}
#[async_trait::async_trait]
impl StorageStrategy<Suggestion> for JsonFileStorageStrategy {
async fn archive(&mut self, payload: Suggestion) -> Result<(), StorageStrategyError> {
let file_content = std::fs::read_to_string(&self.path)?;
let mut current_content : Vec<Suggestion> = serde_json::from_str(&file_content)?;
current_content.push(payload);
let updated_content = serde_json::to_string(&current_content)?;
std::fs::write(&self.path, updated_content)?;
Ok(())
}
async fn extract(&self, offset: usize, window: usize) -> Result<Vec<Suggestion>, StorageStrategyError> {
let file_content = std::fs::read_to_string(&self.path)?;
let current_content : Vec<Suggestion> = serde_json::from_str(&file_content)?;
let mut out = Vec::new();
for sugg in current_content.iter().rev().skip(offset) {
out.push(sugg.clone());
if out.len() >= window { break };
}
Ok(out)
}
}