mirror of
https://github.com/kjuulh/bitebuds.git
synced 2025-08-09 15:43:27 +02:00
feat: with biteme cli
This commit is contained in:
23
crates/biteme/Cargo.toml
Normal file
23
crates/biteme/Cargo.toml
Normal 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
95
crates/biteme/src/main.rs
Normal 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(())
|
||||
}
|
@@ -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>,
|
||||
|
@@ -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"
|
||||
|
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user