pub mod api;
#[cfg(feature = "axum")]
mod axum_ui;
mod pages;
#[cfg(feature = "warp")]
mod warp_ui;
use crate::pages::{PageError, Pages};
use feattle_core::{BoxError, Feattles, HistoryError, UpdateError};
use serde_json::Value;
use std::sync::Arc;
use crate::api::v1;
#[cfg(feature = "axum")]
pub use axum_ui::axum_router;
#[cfg(feature = "warp")]
pub use warp_ui::run_warp_server;
pub struct AdminPanel<F> {
feattles: Arc<F>,
pages: Pages,
}
#[derive(Debug, Clone)]
pub struct RenderedPage {
pub content_type: String,
pub content: Vec<u8>,
}
#[derive(Debug, thiserror::Error)]
pub enum RenderError {
#[error("the requested page does not exist")]
NotFound,
#[error("the template failed to render")]
Template(#[from] handlebars::RenderError),
#[error("failed to serialize or deserialize JSON")]
Serialization(#[from] serde_json::Error),
#[error("failed to recover history information")]
History(#[from] HistoryError),
#[error("failed to update value")]
Update(#[from] UpdateError),
#[error("failed to reload new version")]
Reload(#[source] BoxError),
}
impl From<PageError> for RenderError {
fn from(error: PageError) -> Self {
match error {
PageError::NotFound => RenderError::NotFound,
PageError::Template(error) => RenderError::Template(error),
PageError::Serialization(error) => RenderError::Serialization(error),
}
}
}
impl<F: Feattles + Sync> AdminPanel<F> {
pub fn new(feattles: Arc<F>, label: String) -> Self {
AdminPanel {
feattles,
pages: Pages::new(label),
}
}
pub async fn list_feattles(&self) -> Result<RenderedPage, RenderError> {
let data = self.list_feattles_api_v1().await?;
Ok(self
.pages
.render_feattles(&data.definitions, data.last_reload, data.reload_failed)?)
}
pub async fn list_feattles_api_v1(&self) -> Result<v1::ListFeattlesResponse, RenderError> {
let reload_failed = self.feattles.reload().await.is_err();
Ok(v1::ListFeattlesResponse {
definitions: self.feattles.definitions(),
last_reload: self.feattles.last_reload(),
reload_failed,
})
}
pub async fn show_feattle(&self, key: &str) -> Result<RenderedPage, RenderError> {
let data = self.show_feattle_api_v1(key).await?;
Ok(self.pages.render_feattle(
&data.definition,
&data.history,
data.last_reload,
data.reload_failed,
)?)
}
pub async fn show_feattle_api_v1(
&self,
key: &str,
) -> Result<v1::ShowFeattleResponse, RenderError> {
let reload_failed = self.feattles.reload().await.is_err();
let definition = self.feattles.definition(key).ok_or(RenderError::NotFound)?;
let history = self.feattles.history(key).await?;
Ok(v1::ShowFeattleResponse {
definition,
history,
last_reload: self.feattles.last_reload(),
reload_failed,
})
}
pub async fn edit_feattle(
&self,
key: &str,
value_json: &str,
modified_by: String,
) -> Result<(), RenderError> {
let value: Value = serde_json::from_str(value_json)?;
self.edit_feattle_api_v1(key, v1::EditFeattleRequest { value, modified_by })
.await?;
Ok(())
}
pub async fn edit_feattle_api_v1(
&self,
key: &str,
request: v1::EditFeattleRequest,
) -> Result<v1::EditFeattleResponse, RenderError> {
log::info!(
"Received edit request for key {} with value {}",
key,
request.value
);
self.feattles.reload().await.map_err(RenderError::Reload)?;
self.feattles
.update(key, request.value, request.modified_by)
.await?;
Ok(v1::EditFeattleResponse {})
}
pub fn render_public_file(&self, path: &str) -> Result<RenderedPage, RenderError> {
Ok(self.pages.render_public_file(path)?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use feattle_core::{feattles, Feattles};
feattles! {
struct MyToggles { a: bool, b: i32 }
}
#[tokio::test]
async fn test() {
use feattle_core::persist::NoPersistence;
let my_toggles = Arc::new(MyToggles::new(Arc::new(NoPersistence)));
my_toggles.reload().await.unwrap();
let admin_panel = Arc::new(AdminPanel::new(
my_toggles,
"Project Panda - DEV".to_owned(),
));
admin_panel.list_feattles().await.unwrap();
admin_panel.show_feattle("a").await.unwrap();
admin_panel.show_feattle("non-existent").await.unwrap_err();
admin_panel.render_public_file("script.js").unwrap();
admin_panel.render_public_file("non-existent").unwrap_err();
admin_panel
.edit_feattle("a", "true", "user".to_owned())
.await
.unwrap();
admin_panel
.edit_feattle("a", "17", "user".to_owned())
.await
.unwrap_err();
}
}