feat: with biteme cli

This commit is contained in:
2023-03-07 15:44:30 +01:00
parent 762c792c05
commit f56f8c6818
11 changed files with 773 additions and 120 deletions

23
crates/biteme/Cargo.toml Normal file
View File

@@ -0,0 +1,23 @@
[package]
name = "biteme"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
domain = { workspace = true }
serde = { workspace = true }
uuid = { workspace = true }
chrono = { workspace = true }
clap = "4.1.8"
color-eyre = "0.6.2"
eyre = "0.6.8"
inquire = { version = "0.6.0", features = ["editor", "chrono", "date"] }
tokio = { version = "1.26.0", features = ["full"] }
tracing = { version = "0.1.37", features = ["log"] }
serde_yaml = "0.9.19"
tracing-subscriber = "0.3.16"
regex = "1.7.1"

95
crates/biteme/src/main.rs Normal file
View File

@@ -0,0 +1,95 @@
use domain::{Event, Image};
use inquire::validator::ValueRequiredValidator;
use regex::Regex;
#[tokio::main]
async fn main() -> eyre::Result<()> {
tracing_subscriber::fmt::init();
color_eyre::install()?;
let cli = clap::Command::new("biteme")
.subcommand(clap::Command::new("generate").subcommand(clap::Command::new("article")));
let args = std::env::args();
let matches = cli.get_matches_from(args);
match matches.subcommand() {
Some(("generate", subm)) => match subm.subcommand() {
Some(("article", _subm)) => {
generate_article().await?;
}
_ => panic!("command not valid"),
},
_ => panic!("command not valid"),
}
Ok(())
}
async fn generate_article() -> eyre::Result<()> {
let name = inquire::Text::new("What are you going to eat?")
.with_validator(ValueRequiredValidator::default())
.prompt()?;
let description = inquire::Editor::new("Do you want to provide a description?")
.prompt_skippable()?
.and_then(|ci| if ci == "" { None } else { Some(ci) });
let time = inquire::DateSelect::new("When is the event?")
.with_min_date(chrono::Local::now().date_naive())
.prompt()?;
let cover_image = inquire::Text::new("Do you have a picture for it?")
.prompt_skippable()?
.and_then(|ci| if ci == "" { None } else { Some(ci) });
let cover_alt = if let Some(_) = cover_image {
Some(
inquire::Text::new("Do you have a description for the image?")
.with_validator(ValueRequiredValidator::default())
.prompt()?,
)
} else {
None
};
let prepared_name = name.replace(" ", "-");
let prepared_name = prepared_name.replace("--", "-");
let prepared_name = prepared_name.trim_matches('-');
let re = Regex::new(r"[a-zA-Z-_0-9]*")?;
let name_slug = re
.find_iter(&prepared_name)
.map(|n| n.as_str())
.collect::<Vec<_>>();
let name_slug = name_slug.join("");
let slug = format!("{}-{}", time.format("%Y-%m-%d"), name_slug.to_lowercase());
let event = Event {
id: uuid::Uuid::new_v4(),
cover_image: cover_image.zip(cover_alt).map(|(image, alt)| Image {
id: uuid::Uuid::new_v4(),
url: image,
alt,
metadata: None,
}),
name,
description: description.clone(),
time,
recipe_id: None,
images: Vec::new(),
metadata: None,
};
let contents = serde_yaml::to_string(&event)?;
let contents = format!(
"---
{}---
{}",
contents,
description.unwrap_or("".into())
);
tokio::fs::write(format!("articles/events/{}.md", slug), contents).await?;
Ok(())
}

View File

@@ -6,12 +6,14 @@ use serde::{Deserialize, Serialize};
pub struct Metadata(HashMap<String, String>);
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Recipe {
pub id: uuid::Uuid,
pub metadata: Option<Metadata>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Image {
pub id: uuid::Uuid,
pub url: String,
@@ -20,6 +22,7 @@ pub struct Image {
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Event {
pub id: uuid::Uuid,
pub cover_image: Option<Image>,
@@ -32,6 +35,7 @@ pub struct Event {
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EventOverview {
pub id: uuid::Uuid,
pub cover_image: Option<Image>,

View File

@@ -9,6 +9,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
cached = "0.42.0"
chrono = { version = "0.4.23", features = ["serde"] }
domain = { path = "../domain" }
eyre = "0.6.8"

View File

@@ -1,10 +1,13 @@
use std::path::PathBuf;
use std::sync::Arc;
use cached::proc_macro::{cached, once};
use domain::{Event, Image, Metadata};
use serde::{Deserialize, Serialize};
pub struct EventStore {
pub path: PathBuf,
events: Arc<tokio::sync::RwLock<Vec<Event>>>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
@@ -16,6 +19,7 @@ pub struct RawImage {
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RawEvent {
#[serde(alias = "coverImage")]
pub cover_image: Option<RawImage>,
pub name: String,
pub description: Option<String>,
@@ -29,7 +33,7 @@ pub struct RawEvent {
}
mod short_time_stamp {
use chrono::{DateTime, NaiveDate, TimeZone, Utc};
use chrono::NaiveDate;
use serde::{self, Deserialize, Deserializer, Serializer};
const FORMAT: &'static str = "%Y-%m-%d";
@@ -79,42 +83,65 @@ impl From<RawImage> for Image {
impl EventStore {
pub fn new(path: PathBuf) -> Self {
Self { path }
Self {
path,
events: Default::default(),
}
}
pub async fn get_upcoming_events(&self) -> eyre::Result<Vec<Event>> {
let mut event_path = self.path.clone();
event_path.push("events");
let mut dir = tokio::fs::read_dir(event_path).await?;
let mut events = vec![];
let events = fetch_events(event_path).await?;
while let Ok(Some(entry)) = dir.next_entry().await {
let metadata = entry.metadata().await?;
if metadata.is_file() {
let file = tokio::fs::read(entry.path()).await?;
let content = std::str::from_utf8(&file)?;
if content.starts_with("---\n") {
let after_marker = &content[4..];
if let Some(marker_end) = after_marker.find("---\n") {
let raw_front_matter = &content[4..marker_end + 4];
let mut raw_event: RawEvent = serde_yaml::from_str(raw_front_matter)?;
raw_event.content = content[marker_end + 4..].to_string();
events.push(raw_event.into())
}
}
}
}
let mut e = self.events.write().await;
*e = events.clone();
Ok(events)
}
pub async fn get_event(&self, event_id: uuid::Uuid) -> eyre::Result<Option<Event>> {
let events = self.events.read().await;
let event = events.iter().find(|e| e.id == event_id);
Ok(event.map(|e| e.clone()))
}
}
#[once(time = 60, result = true, sync_writes = true)]
pub async fn fetch_events(event_path: PathBuf) -> eyre::Result<Vec<Event>> {
let mut dir = tokio::fs::read_dir(event_path).await?;
let mut events = vec![];
while let Ok(Some(entry)) = dir.next_entry().await {
let metadata = entry.metadata().await?;
if metadata.is_file() {
let file = tokio::fs::read(entry.path()).await?;
let content = std::str::from_utf8(&file)?;
if content.starts_with("---\n") {
let after_marker = &content[4..];
if let Some(marker_end) = after_marker.find("---\n") {
let raw_front_matter = &content[4..marker_end + 4];
let mut raw_event: RawEvent = serde_yaml::from_str(raw_front_matter)?;
raw_event.content = content[marker_end + 4..].to_string();
events.push(raw_event.into())
}
}
}
}
Ok(events)
}
impl Default for EventStore {
fn default() -> Self {
Self {
path: PathBuf::from("articles"),
events: Default::default(),
}
}
}