Skip to content

Commit

Permalink
build(ssg): 🔧 Add caching mechanism
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastienrousseau committed Sep 19, 2024
1 parent b41dc80 commit bd24aec
Show file tree
Hide file tree
Showing 8 changed files with 283 additions and 58 deletions.
10 changes: 7 additions & 3 deletions ssg-core/src/compiler/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use ssg_rss::{
generate_rss, macro_set_rss_data_fields, models::data::RssData,
};
use ssg_sitemap::create_site_map_data;
use std::time::Duration;

use crate::{
macro_cleanup_directories, macro_create_directories,
Expand Down Expand Up @@ -70,15 +71,18 @@ pub fn compile(
HashMap::new();

// Initialize the templating engine
let engine = Engine::new(template_path.to_str().unwrap());
let mut engine = Engine::new(
template_path.to_str().unwrap(),
Duration::from_secs(60),
);

// Process source files and store results in 'compiled_files' vector
let compiled_files: Result<Vec<FileData>> = source_files
.into_iter()
.map(|file| {
process_file(
&file,
&engine,
&mut engine,
template_path,
&navigation,
&mut global_tags_data,
Expand Down Expand Up @@ -127,7 +131,7 @@ pub fn compile(
/// Processes a single file, generating all necessary data and content.
fn process_file(
file: &FileData,
engine: &Engine,
engine: &mut Engine,
_template_path: &Path,
navigation: &str,
global_tags_data: &mut HashMap<String, Vec<PageData>>,
Expand Down
2 changes: 1 addition & 1 deletion ssg-template/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ regex = "1.5"

# reqwest is a popular HTTP client for Rust, used for handling remote template fetching.
# The `blocking` feature allows synchronous HTTP requests.
reqwest = { version = "0.12", features = ["blocking"] }
reqwest = { version = "0.12", features = ["json", "blocking"] }

# serde is used for serializing and deserializing data structures, including JSON.
# The `derive` feature simplifies the process of creating serializable and deserializable structs.
Expand Down
101 changes: 81 additions & 20 deletions ssg-template/examples/template_example.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#![allow(missing_docs)]

use ssg_template::{Context, Engine, TemplateError};
use std::time::Duration;
use std::{
collections::HashMap,
fs::{self, File},
Expand Down Expand Up @@ -106,7 +107,10 @@ fn demonstrates_template_rendering_example() -> Result<(), TemplateError>
);

// Step 4: Initialize the Engine with the base directory where the template is located
let engine = Engine::new(base_template_path.to_str().unwrap()); // Provide the dynamic base directory
let mut engine = Engine::new(
base_template_path.to_str().unwrap(),
Duration::from_secs(60),
); // Provide the dynamic base directory

// Step 5: Render the template by specifying the filename without the extension (just "index")
let rendered_template = engine.render_page(&context, "index")?;
Expand Down Expand Up @@ -136,7 +140,10 @@ fn demonstrates_error_handling_example() -> Result<(), TemplateError> {
"#;
template_file.write_all(template_content.as_bytes())?;

let engine = Engine::new(template_dir_path.to_str().unwrap());
let mut engine = Engine::new(
template_dir_path.to_str().unwrap(),
Duration::from_secs(60),
);

match engine.render_page(&context, "invalid") {
// Pass only "invalid", not "invalid.html"
Expand Down Expand Up @@ -181,7 +188,10 @@ fn demonstrates_template_with_loop_example() -> Result<(), TemplateError>
"#;
template_file.write_all(template_content.as_bytes())?;

let engine = Engine::new(template_dir_path.to_str().unwrap());
let mut engine = Engine::new(
template_dir_path.to_str().unwrap(),
Duration::from_secs(60),
);

let rendered_template = engine.render_page(&context, "items")?;
println!("Rendered Template with Loop: ✅ \n{}", rendered_template);
Expand Down Expand Up @@ -222,7 +232,10 @@ fn demonstrates_template_with_conditionals_example(
"#;
template_file.write_all(template_content.as_bytes())?;

let engine = Engine::new(template_dir_path.to_str().unwrap());
let mut engine = Engine::new(
template_dir_path.to_str().unwrap(),
Duration::from_secs(60),
);

let rendered_template =
engine.render_page(&context, "conditional")?;
Expand Down Expand Up @@ -262,7 +275,10 @@ fn demonstrates_nested_context_example() -> Result<(), TemplateError> {
"#;
template_file.write_all(template_content.as_bytes())?;

let engine = Engine::new(template_dir_path.to_str().unwrap());
let mut engine = Engine::new(
template_dir_path.to_str().unwrap(),
Duration::from_secs(60),
);

let rendered_template =
engine.render_page(&context, "user_profile")?; // Pass only "user_profile", not "user_profile.html"
Expand Down Expand Up @@ -313,7 +329,10 @@ fn demonstrates_template_with_partials_example(
"#;
main_template_file.write_all(main_template_content.as_bytes())?;

let engine = Engine::new(template_dir_path.to_str().unwrap());
let mut engine = Engine::new(
template_dir_path.to_str().unwrap(),
Duration::from_secs(60),
);

// Render the header and footer templates separately and inject the results into the main template
let rendered_header = engine.render_page(&context, "header")?;
Expand Down Expand Up @@ -375,7 +394,10 @@ fn demonstrates_template_with_inheritance_example(
context.set("footer".to_string(), "Footer Content".to_string());

// Initialize the Engine with the template directory
let engine = Engine::new(template_dir_path.to_str().unwrap());
let mut engine = Engine::new(
template_dir_path.to_str().unwrap(),
Duration::from_secs(60),
);

// Render the 'about' content separately
let about_content = fs::read_to_string(about_path)?;
Expand Down Expand Up @@ -439,7 +461,10 @@ fn demonstrates_multiple_templates_example() -> Result<(), TemplateError>
.set("email".to_string(), "[email protected]".to_string());

// Initialize the Engine with the template directory
let engine = Engine::new(template_dir_path.to_str().unwrap());
let mut engine = Engine::new(
template_dir_path.to_str().unwrap(),
Duration::from_secs(60),
);

// Render the home and contact pages independently
let rendered_home = engine.render_page(&context_home, "home")?;
Expand Down Expand Up @@ -480,7 +505,10 @@ fn demonstrates_large_context_example() -> Result<(), TemplateError> {
template_file.write_all(template_content.as_bytes())?;

// Initialize the Engine with the template directory
let engine = Engine::new(template_dir_path.to_str().unwrap());
let mut engine = Engine::new(
template_dir_path.to_str().unwrap(),
Duration::from_secs(60),
);

// Render the template with the large context
let rendered_template =
Expand Down Expand Up @@ -524,7 +552,10 @@ fn demonstrates_dynamic_content_example() -> Result<(), TemplateError> {
"#;
template_file.write_all(template_content.as_bytes())?;

let engine = Engine::new(template_dir_path.to_str().unwrap());
let mut engine = Engine::new(
template_dir_path.to_str().unwrap(),
Duration::from_secs(60),
);

// Render the template with the dynamic content
let rendered_template =
Expand Down Expand Up @@ -558,7 +589,10 @@ fn demonstrates_template_caching_example() -> Result<(), TemplateError>
template_file.write_all(template_content.as_bytes())?;

// Initialize the Engine with the template directory
let engine = Engine::new(template_dir_path.to_str().unwrap());
let mut engine = Engine::new(
template_dir_path.to_str().unwrap(),
Duration::from_secs(60),
);

// Create a context for rendering
let mut context = Context::new();
Expand Down Expand Up @@ -611,7 +645,10 @@ fn demonstrates_template_debugging_example() -> Result<(), TemplateError>
"#;
template_file.write_all(template_content.as_bytes())?;

let engine = Engine::new(template_dir_path.to_str().unwrap());
let mut engine = Engine::new(
template_dir_path.to_str().unwrap(),
Duration::from_secs(60),
);

// Try to render the template and catch any missing context variables
match engine.render_page(&context, "debug_template") {
Expand Down Expand Up @@ -660,7 +697,10 @@ fn demonstrates_form_template_example() -> Result<(), TemplateError> {
context.set("email".to_string(), "[email protected]".to_string());

// Initialize the Engine with the template directory
let engine = Engine::new(template_dir_path.to_str().unwrap());
let mut engine = Engine::new(
template_dir_path.to_str().unwrap(),
Duration::from_secs(60),
);

// Render the form template
let rendered_template =
Expand All @@ -680,7 +720,10 @@ fn demonstrates_custom_error_handling_example(
.set("title".to_string(), "Custom Error Example".to_string());

// Try to render a non-existent template to trigger a custom error
let engine = Engine::new("path/to/nonexistent/templates");
let mut engine = Engine::new(
"path/to/nonexistent/templates",
Duration::from_secs(60),
);

match engine.render_page(&context, "nonexistent_template") {
Ok(rendered) => println!("Unexpected success: \n{}", rendered),
Expand Down Expand Up @@ -736,7 +779,10 @@ fn demonstrates_pagination_example() -> Result<(), TemplateError> {
context.set("total_pages".to_string(), "3".to_string());

// Initialize the Engine with the template directory
let engine = Engine::new(template_dir_path.to_str().unwrap());
let mut engine = Engine::new(
template_dir_path.to_str().unwrap(),
Duration::from_secs(60),
);

// Render the paginated template with the pre-processed posts
let rendered_template =
Expand Down Expand Up @@ -781,7 +827,10 @@ fn demonstrates_internationalization_example(
"#;
template_file.write_all(template_content.as_bytes())?;

let engine = Engine::new(template_dir_path.to_str().unwrap());
let mut engine = Engine::new(
template_dir_path.to_str().unwrap(),
Duration::from_secs(60),
);

// Render the template with the language-based greeting
let rendered_template =
Expand Down Expand Up @@ -821,7 +870,10 @@ fn demonstrates_template_fragments_example() -> Result<(), TemplateError>
"#;
main_template_file.write_all(main_template_content.as_bytes())?;

let engine = Engine::new(template_dir_path.to_str().unwrap());
let mut engine = Engine::new(
template_dir_path.to_str().unwrap(),
Duration::from_secs(60),
);

// Load and render fragments
let rendered_header =
Expand Down Expand Up @@ -883,7 +935,10 @@ fn demonstrates_markdown_to_html_example() -> Result<(), TemplateError>
template_file.write_all(template_content.as_bytes())?;

// Step 6: Initialize the Engine with the template directory
let engine = Engine::new(template_dir_path.to_str().unwrap());
let mut engine = Engine::new(
template_dir_path.to_str().unwrap(),
Duration::from_secs(60),
);

// Step 7: Render the template with the manually converted markdown content
let rendered_template =
Expand Down Expand Up @@ -923,7 +978,10 @@ fn demonstrates_template_versioning_example(
if client_version == 1 { "v1" } else { "v2" };

// Initialize the Engine with the template directory
let engine = Engine::new(template_dir_path.to_str().unwrap());
let mut engine = Engine::new(
template_dir_path.to_str().unwrap(),
Duration::from_secs(60),
);

// Render the appropriate version of the template
let rendered_template =
Expand Down Expand Up @@ -964,7 +1022,10 @@ fn demonstrates_ajax_template_example() -> Result<(), TemplateError> {
"#;
template_file.write_all(template_content.as_bytes())?;

let engine = Engine::new(template_dir_path.to_str().unwrap());
let mut engine = Engine::new(
template_dir_path.to_str().unwrap(),
Duration::from_secs(60),
);

// Render the template meant for AJAX
let rendered_template =
Expand Down
98 changes: 98 additions & 0 deletions ssg-template/src/cache.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
use std::collections::HashMap;
use std::time::{Duration, Instant};

/// Represents a cached item with its value and expiration time.
struct CachedItem<T> {
value: T,
expiration: Instant,
}

/// A simple cache implementation with expiration.
pub struct Cache<K, V> {
items: HashMap<K, CachedItem<V>>,
ttl: Duration,
}

impl<K: std::hash::Hash + Eq, V: Clone> Cache<K, V> {
/// Creates a new Cache with the specified time-to-live (TTL) for items.
///
/// # Arguments
///
/// * `ttl` - The time-to-live for cached items.
///
/// # Example
///
/// ```
/// use ssg_template::cache::Cache;
/// use std::time::Duration;
///
/// let cache: Cache<String, String> = Cache::new(Duration::from_secs(60));
/// ```
pub fn new(ttl: Duration) -> Self {
Cache {
items: HashMap::new(),
ttl,
}
}

/// Inserts a key-value pair into the cache.
///
/// # Arguments
///
/// * `key` - The key to insert.
/// * `value` - The value to insert.
///
/// # Example
///
/// ```
/// use ssg_template::cache::Cache;
/// use std::time::Duration;
///
/// let mut cache = Cache::new(Duration::from_secs(60));
/// cache.insert("key".to_string(), "value".to_string());
/// ```
pub fn insert(&mut self, key: K, value: V) {
let expiration = Instant::now() + self.ttl;
self.items.insert(key, CachedItem { value, expiration });
}

/// Retrieves a value from the cache if it exists and hasn't expired.
///
/// # Arguments
///
/// * `key` - The key to look up.
///
/// # Returns
///
/// An `Option` containing the value if it exists and hasn't expired, or `None` otherwise.
///
/// # Example
///
/// ```
/// use ssg_template::cache::Cache;
/// use std::time::Duration;
///
/// let mut cache = Cache::new(Duration::from_secs(60));
/// cache.insert("key".to_string(), "value".to_string());
///
/// assert_eq!(cache.get(&"key".to_string()), Some(&"value".to_string()));
/// ```
pub fn get(&self, key: &K) -> Option<&V> {
self.items.get(key).and_then(|item| {
if item.expiration > Instant::now() {
Some(&item.value)
} else {
None
}
})
}

/// Removes expired items from the cache.
///
/// This method should be called periodically to clean up the cache.
///
pub fn remove_expired(&mut self) {
self.items
.retain(|_, item| item.expiration > Instant::now());
}
}
Loading

0 comments on commit bd24aec

Please sign in to comment.