diff --git a/tavern/Cargo.toml b/tavern/Cargo.toml
index cb9521f..e2418a9 100644
--- a/tavern/Cargo.toml
+++ b/tavern/Cargo.toml
@@ -26,3 +26,4 @@ tokio = { version = "1", features = ["full"] }
default = []
database = ["sqlx"]
publisher = ["database"]
+tester = ["publisher"]
diff --git a/tavern/examples/generate_database.rs b/tavern/examples/generate_database.rs
index af19c7d..8a451e0 100644
--- a/tavern/examples/generate_database.rs
+++ b/tavern/examples/generate_database.rs
@@ -1,51 +1,66 @@
-use std::path::{Path, PathBuf};
+#![cfg(feature = "publisher")]
use chrono::NaiveDate;
-use tavern::{Adventurer, Database, Legend, Lore, Tale, Tavern};
+
+use tavern::{Adventurer, Legend, Lore, Story, Tale, Tavern};
+use tavern::Database;
-#[cfg(feature = "publisher")]
+/// This will generate a tavern that we can create a Toml file from.
fn generate_tavern() -> Tavern
{
let legend: Legend = Legend
{
- profile:
- String::from("https://cybermages.tech/about/myrddin"),
- image:
- String::from("https://cybermages.tech/about/myrddin/pic"),
- blurb: String::from("I love code!") };
+ profile: String::from("https://cybermages.tech/about/myrddin"),
+ image: String::from("https://cybermages.tech/about/myrddin/pic"),
+ blurb: String::from("I love code!")
+ };
- let author: Adventurer =
- Adventurer { name: String::from("Jason Smith"),
- handle: String::from("myrddin"),
- legend };
-
- let lore: Lore =
- Lore { title: String::from("Test post"),
- slug: String::from("test_post"),
- author: author.handle.clone(),
- summary: String::from("The Moon is made of cheese!"),
- tags: vec![String::from("Space"),
- String::from("Cheese")],
- publish_date:
- NaiveDate::from_ymd_opt(2025, 12, 25).unwrap()
- .and_hms_opt(13,
- 10, 41)
- .unwrap() };
-
- let tale: Tale = Tale { lore,
- story: PathBuf::from("posts/test_post.md") };
-
-
- // Create a dummy posts directory and file for this example to work
- if !Path::new("posts").exists()
+ let author: Adventurer = Adventurer
{
- std::fs::create_dir("posts").unwrap();
- }
- std::fs::write("posts/the-rustacean.md",
- "# Hello, Rust!\n\nThis is a **test** post.").unwrap();
+ name: String::from("Jason Smith"),
+ handle: String::from("myrddin"),
+ legend: legend
+ };
+ let lore: Lore = Lore
+ {
+ title: String::from("Test post"),
+ slug: String::from("test_post"),
+ author: author.handle.clone(),
+ summary: String::from("The Moon is made of cheese!"),
+ tags: vec![String::from("Space"), String::from("Cheese")],
+ publish_date:
+ NaiveDate::from_ymd_opt(2025, 12, 25).unwrap()
+ .and_hms_opt(13, 10, 41)
+ .unwrap()
+ };
+
+ let tale: Tale = Tale
+ {
+ lore: lore,
+ story: Story::Html("#Test Post\nThis is a test post.\n\n##Subsection\nMini post.\n* Test 1\n* Test 2\n* Test 3".to_string())
+ };
+
+ let lore2: Lore = Lore
+ {
+ title: String::from("Test Tale"),
+ slug: String::from("test_tale"),
+ author: author.handle.clone(),
+ summary: String::from("The Moon is made of rocks!"),
+ tags: vec![String::from("Space"), String::from("Rocks")],
+ publish_date:
+ NaiveDate::from_ymd_opt(2025, 12, 25).unwrap()
+ .and_hms_opt(13, 10, 41)
+ .unwrap()
+ };
+
+ let tale2: Tale = Tale
+ {
+ lore: lore2,
+ story: Story::Html("#Test Tale\nThis is a test tale.\n\n##Subsection\nMini tale.\n* Test 1\n* Test 2\n* Test 3".to_string())
+ };
Tavern { title: String::from("Runes & Ramblings"),
description: String::from("Join software engineer Jason Smith \
@@ -53,14 +68,52 @@ fn generate_tavern() -> Tavern
Explore program design, tech \
stacks, and more on this blog from \
CybeMages, LLC."),
- tales: vec![tale],
+ tales: vec![tale, tale2],
authors: vec![author] }
}
-#[cfg(feature = "publisher")]
+fn create_temp_file
(filename: P) -> std::path::PathBuf
+ where P: AsRef
+{
+ let mut path = std::env::temp_dir();
+ path.push(filename);
+
+ path
+}
+
+fn cleanup_temp_file(path: P)
+ where P: AsRef
+{
+ match path.as_ref().try_exists()
+ {
+ Ok(exists) =>
+ {
+ if exists
+ {
+ let _ = std::fs::remove_file(path);
+ }
+ }
+
+ Err(e) =>
+ {
+ eprintln!("{}", e);
+ }
+ }
+}
+
+fn write_to_file(tavern: Tavern, config_file: P) -> Result<(), Box>
+ where P: AsRef
+{
+ let toml_string = toml::to_string_pretty(&tavern)?;
+
+ std::fs::write(&config_file, &toml_string)?;
+
+ Ok(())
+}
+
fn read_from_file(config_file: P) -> Tavern
- where P: AsRef
+ where P: AsRef
{
// Read the previously written TOML file
let toml_data =
@@ -71,65 +124,36 @@ fn read_from_file(config_file: P) -> Tavern
}
-#[cfg(feature = "publisher")]
async fn create_database() -> Result<(), Box>
{
+ // First we need to generate a TOML file to work with.
+ let config_path = create_temp_file("tavern.toml");
+ cleanup_temp_file(&config_path);
+
+ write_to_file(generate_tavern(), &config_path)?;
+
// This part would be the entry point of your CI/CD script
// It would load your data and then save it to the database
// Create a Tavern object
- let _tavern = read_from_file("Tavern.toml");
- // let tavern = generate_tavern();
+ let tavern = read_from_file(&config_path);
// Open the database and save the Tavern content
- let _db = Database::open("/home/myrddin/cybermages/blog/tavern.db").await?;
- //db.insert_tavern(&tavern.title, &tavern.description)?;
- //println!("Saved site settings: Title='{}', Description='{}'",
- //tavern.title, tavern.description);
-
- //for author in &tavern.authors
- // {
- // db.insert_adventurer(author)?;
- // println!("Saved adventurer: {}", author.name);
- // }
- //
- // for tale in &tavern.tales
- // {
- // db.insert_tale(tale)?;
- // println!("Saved tale: {}", tale.title);
- // }
+ let db_path = create_temp_file("tavern.db");
+ cleanup_temp_file(&db_path);
+
+ let db = Database::open(db_path).await?;
+ db.insert_tavern(&tavern).await?;
Ok(())
}
-#[cfg(feature = "publisher")]
#[tokio::main]
pub async fn main()
{
- match std::env::set_current_dir("/home/myrddin/cybermages/blog/")
- {
- Ok(_) =>
- {
- println!("Successfully changed working directory.");
- }
- Err(e) =>
- {
- eprintln!("Failed to change directory: {}", e);
- }
- }
-
match create_database().await
{
- Ok(_) =>
- {}
- Err(e) =>
- {
- eprintln!("Error: {}", e);
- }
+ Ok(_) => {}
+ Err(e) => { eprintln!("Error: {}", e); }
}
}
-
-#[cfg(not(feature = "publisher"))]
-pub fn main()
-{
-}
diff --git a/tavern/src/adventurer.rs b/tavern/src/adventurer.rs
index 987c83d..2bc339f 100644
--- a/tavern/src/adventurer.rs
+++ b/tavern/src/adventurer.rs
@@ -1,7 +1,7 @@
use serde::{Deserialize, Serialize};
///
-#[derive(Deserialize, Serialize)]
+#[derive(Clone, PartialEq, Deserialize, Serialize)]
pub struct Legend
{
/// A link to the adventurer's profile (e.g., personal website, GitHub,
@@ -20,7 +20,7 @@ pub struct Legend
///
/// An `Adventurer` contains identifying and descriptive information
/// such as their name, handle, profile URL, image, and a short blurb.
-#[derive(Deserialize, Serialize)]
+#[derive(Clone, PartialEq, Deserialize, Serialize)]
pub struct Adventurer
{
/// The full name of the adventurer.
@@ -34,7 +34,3 @@ pub struct Adventurer
#[serde(flatten)]
pub legend: Legend
}
-
-
-
-impl Adventurer {}
diff --git a/tavern/src/database.rs b/tavern/src/database.rs
index 4613b84..662e474 100644
--- a/tavern/src/database.rs
+++ b/tavern/src/database.rs
@@ -4,20 +4,20 @@ use std::str::FromStr;
use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions};
use sqlx::{Error, Result};
-#[cfg(not(feature = "publisher"))]
+#[cfg(any(not(feature = "publisher"), feature = "tester"))]
use sqlx::Row;
use crate::adventurer::Adventurer;
-use crate::tale::Tale;
+use crate::tale::{Story, Tale};
#[cfg(feature = "publisher")]
use crate::converter::Converter;
#[cfg(feature = "publisher")]
use crate::tavern::Tavern;
-#[cfg(not(feature = "publisher"))]
+#[cfg(any(not(feature = "publisher"), feature = "tester"))]
use crate::tale::Lore;
-#[cfg(not(feature = "publisher"))]
+#[cfg(any(not(feature = "publisher"), feature = "tester"))]
use crate::adventurer::Legend;
@@ -140,13 +140,24 @@ impl Database
-> Result<()>
{
// Convert the tales content from Markdown to HTML.
- let markdown_content = std::fs::read_to_string(&tale.story)?;
- let html_content: String = Converter::markdown_to_html(&markdown_content);
+ let html_content: std::borrow::Cow<'_, str> = match &tale.story
+ {
+ Story::Html(story) => { std::borrow::Cow::Borrowed(story) }
+
+ Story::File(path) =>
+ {
+ let markdown = std::fs::read_to_string(path)?;
+ std::borrow::Cow::Owned(Converter::markdown_to_html(&markdown))
+ }
+ };
// Start a transaction.
let mut tx = self.pool.begin().await?;
// Store the tale.
+ // Pull the HTML content out first so that the str will last long enough.
+ // This get's around the macro lifetime issue.
+ let html_str = html_content.as_ref();
sqlx::query!(
"INSERT OR REPLACE INTO tales (
slug, title, author, summary, publish_date, content
@@ -156,7 +167,7 @@ impl Database
tale.lore.author,
tale.lore.summary,
tale.lore.publish_date,
- html_content
+ html_str
).execute(&mut *tx) // Pass mutable reference to the transaction
.await?;
@@ -263,7 +274,7 @@ impl Database
Ok(())
}
- #[cfg(not(feature = "publisher"))]
+ #[cfg(any(not(feature = "publisher"), feature = "tester"))]
pub async fn get_tales_summary(&self, categories: &[String])
-> Result>
{
@@ -312,7 +323,7 @@ impl Database
.unwrap_or_default();
let date_str: String = row.try_get("publish_date")?;
- let publish_date = chrono::NaiveDateTime::parse_from_str(&date_str, "%Y-%m-%dT%H:%M:%S")
+ let publish_date = chrono::NaiveDateTime::parse_from_str(&date_str, "%Y-%m-%d %H:%M:%S")
.map_err(|e| sqlx::Error::Decode(e.into()))?;
tales.push(Lore { title: row.try_get("title")?,
@@ -328,7 +339,7 @@ impl Database
Ok(tales)
}
- #[cfg(not(feature = "publisher"))]
+ #[cfg(any(not(feature = "publisher"), feature = "tester"))]
pub async fn get_tale_by_slug(&self, slug: &str) -> Result