Skip to content

Commit

Permalink
Merge pull request #199 from KisaragiEffective/feat/add-basic-access-…
Browse files Browse the repository at this point in the history
…level
  • Loading branch information
KisaragiEffective authored Jul 2, 2023
2 parents 45cb570 + 7ed5e3c commit 5007020
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 32 deletions.
10 changes: 10 additions & 0 deletions packages/toy-blog-endpoint-model/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,16 @@ pub struct Article {
pub created_at: DateTime<Local>,
pub updated_at: DateTime<Local>,
pub content: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub visibility: Option<Visibility>
}

#[derive(Deserialize, Serialize, Copy, Clone, Eq, PartialEq, Debug)]
#[serde(rename_all = "snake_case")]
pub enum Visibility {
Public,
Restricted,
Private,
}

#[derive(Serialize, Clone, Eq, PartialEq, Debug)]
Expand Down
24 changes: 21 additions & 3 deletions packages/toy-blog/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

mod extension;
mod service;
mod migration;

use std::fs::File;
use std::io::{BufReader, Read, stdin};
Expand All @@ -20,11 +21,12 @@ use actix_web_httpauth::extractors::bearer::Config as BearerAuthConfig;
use clap::{Parser, Subcommand};
use fern::colors::ColoredLevelConfig;
use log::{debug, info};
use serde_json::Value;
use service::http::auth::WRITE_TOKEN;

use crate::service::http::api::{article, meta};
use crate::service::http::cors::middleware_factory as cors_middleware_factory;
use toy_blog_endpoint_model::ArticleId;
use toy_blog_endpoint_model::{ArticleId, Visibility};
use crate::service::http::api::list::{article_id_list, article_id_list_by_year, article_id_list_by_year_and_month};
use crate::service::http::repository::GLOBAL_FILE;
use crate::service::persistence::ArticleRepository;
Expand Down Expand Up @@ -95,7 +97,22 @@ async fn main() -> Result<()> {
stdin().read_line(&mut buf).expect("failed to read from stdin");
buf.trim_end().to_string()
};
GLOBAL_FILE.set(ArticleRepository::new("data/article.json").await).expect("unreachable!");

const PATH: &str = "data/article.json";

// migration
{
#[allow(unused_qualifications)]
let migrated_data = crate::migration::migrate_article_repr(
serde_json::from_reader::<_, Value>(File::open(PATH).expect("ow, failed!")).expect("ow, failed!")
);

info!("migrated");

serde_json::to_writer(File::options().write(true).truncate(true).open(PATH).expect("ow, failed!"), &migrated_data)
.expect("ow, failed!");
}
GLOBAL_FILE.set(ArticleRepository::new(PATH).await).expect("unreachable!");

WRITE_TOKEN.set(bearer_token).unwrap();

Expand Down Expand Up @@ -137,6 +154,7 @@ async fn main() -> Result<()> {
.wrap(cors_middleware_factory())
});

println!("running!");
http_server
.bind((http_host, http_port))?
.run()
Expand Down Expand Up @@ -167,7 +185,7 @@ async fn main() -> Result<()> {

match content {
Ok(content) => {
GLOBAL_FILE.get().expect("must be fully-initialized").create_entry(&article_id, content).await?;
GLOBAL_FILE.get().expect("must be fully-initialized").create_entry(&article_id, content, Visibility::Private).await?;
info!("Successfully imported as {article_id}.");
Ok(())
}
Expand Down
77 changes: 77 additions & 0 deletions packages/toy-blog/src/migration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
//! data migration
use serde_json::{Map, Value};

trait SerdeJsonValueMoveExtension {
///
/// # Example
///
/// ```rust
/// use serde_json::{Map, Value};
///
/// # fn main() {
/// let mut m = Map::new();
/// m.insert("a".to_string(), Value::Null);
/// m.insert("b".to_string(), Value::String("hi".to_string()));
///
/// assert_eq!(Value::Object(m.clone()).into_object(), Ok(m));
/// assert_eq!(Value::Number(42).into_object(), Err(Value::Number(42)));
/// # }
/// ```
fn into_object(self) -> Result<Map<String, Value>, Value>;
}

impl SerdeJsonValueMoveExtension for Value {
fn into_object(self) -> Result<Map<String, Value>, Value> {
match self {
Value::Object(map) => Ok(map),
other => Err(other),
}
}
}

trait ArticleMigration {
fn migrate(&self, raw_config: Value) -> Value;
}

struct AddTagVersion;

impl ArticleMigration for AddTagVersion {
fn migrate(&self, raw_config: Value) -> Value {
let mut m = raw_config.into_object().expect("top level must be an object");
m.insert("version".to_string(), Value::from("1"));

Value::from(m)
}
}

struct AddAccessLevel;

impl ArticleMigration for AddAccessLevel {
fn migrate(&self, raw_config: Value) -> Value {
let mut top = raw_config
.into_object().expect("top level must be an object");

if top["version"].as_str().expect("version must be string").parse::<i32>().expect("schema version must be fit in i32") >= 2 {
return Value::from(top)
}

top.insert("version".to_string(), Value::from(2));

let mut article_table = top
.get_mut("data").expect("article must exist")
.as_object_mut().expect("article table must be object");

article_table.values_mut().for_each(|article| {
article.as_object_mut().expect("article").insert("access".to_string(), Value::from("public"));
});

Value::from(top)
}
}

pub(crate) fn migrate_article_repr(raw_article_table: Value) -> Value {
let raw_article_table = AddTagVersion.migrate(raw_article_table);

AddAccessLevel.migrate(raw_article_table)
}
Empty file.
54 changes: 33 additions & 21 deletions packages/toy-blog/src/service/http/api/article.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,14 @@

use actix_web::{HttpRequest, Responder};
use actix_web::{HttpRequest, HttpResponse, Responder};
use actix_web::{delete, get, post, put};

use actix_web::http::header::USER_AGENT;
use actix_web::http::StatusCode;
use actix_web::web::{Bytes, Path};
use actix_web_httpauth::extractors::bearer::BearerAuth;
use log::info;
use log::{error, info};
use once_cell::unsync::Lazy;
use toy_blog_endpoint_model::{
ArticleContent,
ArticleCreatedNotice,
ArticleCreateWarning,
ArticleId,
ArticleSnapshot,
ArticleSnapshotMetadata,
CreateArticleError,
DeleteArticleError,
GetArticleError,
OwnedMetadata,
UpdateArticleError
};
use toy_blog_endpoint_model::{ArticleContent, ArticleCreatedNotice, ArticleCreateWarning, ArticleId, ArticleSnapshot, ArticleSnapshotMetadata, CreateArticleError, DeleteArticleError, GetArticleError, OwnedMetadata, UpdateArticleError, Visibility};
use crate::service::http::auth::is_wrong_token;
use crate::service::http::inner_no_leak::{UnhandledError};
use crate::service::http::repository::GLOBAL_FILE;
Expand Down Expand Up @@ -49,7 +38,7 @@ pub async fn create(path: Path<String>, data: Bytes, bearer: BearerAuth, request
let Ok(text) = plain_text else { return Ok(Err(CreateArticleError::InvalidUtf8)) };

info!("valid utf8");
let res = x_get().create_entry(&path, text.clone()).await;
let res = x_get().create_entry(&path, text.clone(), Visibility::Private).await;
match res {
Ok(_) => {}
Err(err) => return Err(UnhandledError::new(err))
Expand Down Expand Up @@ -80,28 +69,42 @@ pub async fn create(path: Path<String>, data: Bytes, bearer: BearerAuth, request

#[get("/{article_id}")]
pub async fn fetch(path: Path<String>) -> impl Responder {

enum Res {
Internal(UnhandledError),
General(GetArticleError),
Ok(OwnedMetadata<ArticleSnapshotMetadata, ArticleSnapshot>),
}

let res = || async {
let article_id = ArticleId::new(path.into_inner());

let exists = match x_get().exists(&article_id).await {
Ok(exists) => exists,
Err(e) => return Err(UnhandledError::new(e))
Err(e) => return Res::Internal(UnhandledError::new(e))
};

if !exists {
return Ok(Err(GetArticleError::NoSuchArticleFoundById))
return Res::General(GetArticleError::NoSuchArticleFoundById)
}

let content = match x_get().read_snapshot(&article_id).await {
Ok(content) => content,
Err(e) => return Err(UnhandledError::new(e))
Err(e) => return Res::Internal(UnhandledError::new(e))
};

match content.visibility {
Some(x) if x != Visibility::Public => {
return Res::General(GetArticleError::NoSuchArticleFoundById)
}
_ => {}
}

let u = content.updated_at;
let uo = u.offset();
let uu = u.with_timezone(uo);

Ok(Ok(OwnedMetadata {
(Res::Ok(OwnedMetadata {
metadata: ArticleSnapshotMetadata {
updated_at: uu
},
Expand All @@ -111,7 +114,16 @@ pub async fn fetch(path: Path<String>) -> impl Responder {
}))
};

EndpointRepresentationCompiler::from_value(res().await).into_plain_text()
let x = match res().await {
Res::Internal(sre) => {
error!("{sre:?}");
return HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR)
}
Res::General(e) => Err(e),
Res::Ok(v) => Ok(v)
};

EndpointRepresentationCompiler::from_value(x).into_plain_text().map_into_boxed_body()
}

#[put("/{article_id}")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mod tests;

use std::fmt::{Display, Formatter};
use std::iter::{Empty, empty, once, Once};
use actix_web::body::BoxBody;
use actix_web::HttpResponse;
use actix_web::http::header::{CONTENT_TYPE, HeaderName, HeaderValue, LAST_MODIFIED, WARNING};
use actix_web::http::StatusCode;
Expand Down Expand Up @@ -146,7 +147,6 @@ impl<T: Serialize + HttpStatusCode + ContainsHeaderMap> EndpointRepresentationCo
}
}


pub struct HttpFormattedDate(chrono::DateTime<FixedOffset>);

impl HttpFormattedDate {
Expand Down
4 changes: 4 additions & 0 deletions packages/toy-blog/src/service/http/repository.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
use std::fs::File;
use log::info;
use once_cell::sync::OnceCell;
use serde_json::Value;
use crate::service::persistence::ArticleRepository;

// FIXME: OnceCell
pub static GLOBAL_FILE: OnceCell<ArticleRepository> = OnceCell::new();
28 changes: 21 additions & 7 deletions packages/toy-blog/src/service/persistence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use fs2::FileExt;
use log::{debug, error, info, trace};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use toy_blog_endpoint_model::{ArticleId, FlatId, ListArticleResponse};
use toy_blog_endpoint_model::{ArticleId, FlatId, ListArticleResponse, Visibility};

#[derive(Debug)]
pub struct ArticleRepository {
Expand Down Expand Up @@ -71,16 +71,18 @@ impl ArticleRepository {
Ok(())
}

pub async fn create_entry(&self, article_id: &ArticleId, article_content: String) -> Result<(), PersistenceError> {
pub async fn create_entry(&self, article_id: &ArticleId, article_content: String, visibility: Visibility) -> Result<(), PersistenceError> {
self.invalidate();

let current_date = Local::now();
self.cache.write().expect("lock is poisoned").data.insert(article_id.clone(), Article {
created_at: current_date,
updated_at: current_date,
// visible: false,
content: article_content,
});
created_at: current_date,
updated_at: current_date,
// visible: false,
content: article_content,
visibility: Some(visibility),
});


self.save()?;
Ok(())
Expand Down Expand Up @@ -114,6 +116,18 @@ impl ArticleRepository {
Ok(())
}

pub async fn change_visibility(&self, article_id: &ArticleId, new_visibility: Visibility) -> Result<(), PersistenceError> {
info!("calling change_visibility");
self.invalidate();

self.cache.write().expect("poisoned").deref_mut().data.get_mut(article_id)
.ok_or(PersistenceError::AbsentValue)?.visibility = Some(new_visibility);

self.save()?;

Ok(())
}

pub async fn read_snapshot(&self, article_id: &ArticleId) -> Result<Article, PersistenceError> {
self.reconstruct_cache();

Expand Down

0 comments on commit 5007020

Please sign in to comment.