Añade librería para gestionar recursos estáticos (#1)

Reviewed-on: #1
Co-authored-by: Manuel Cillero <manuel@cillero.es>
Co-committed-by: Manuel Cillero <manuel@cillero.es>
This commit is contained in:
Manuel Cillero 2025-08-08 23:58:07 +02:00 committed by Manuel Cillero
parent 8ed0e6621a
commit 47ea9d9f7d
20 changed files with 1371 additions and 41 deletions

View file

@ -0,0 +1,34 @@
//! <div align="center">
//!
//! <h1>PageTop Statics</h1>
//!
//! <p>Librería para automatizar la recopilación de recursos estáticos en <strong>PageTop</strong>.</p>
//!
//! [![Licencia](https://img.shields.io/badge/license-MIT%2FApache-blue.svg?label=Licencia&style=for-the-badge)](#-licencia)
//!
//! </div>
//!
//! ## Sobre PageTop
//!
//! [PageTop](https://docs.rs/pagetop) es un entorno de desarrollo que reivindica la esencia de la
//! web clásica para crear soluciones web SSR (*renderizadas en el servidor*) modulares, extensibles
//! y configurables, basadas en HTML, CSS y JavaScript.
#![doc(test(no_crate_inject))]
#![doc(
html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/static/favicon.ico"
)]
#![allow(clippy::needless_doctest_main)]
/// Resource definition and single module based generation.
pub mod resource;
pub use resource::Resource as StaticResource;
mod resource_dir;
pub use resource_dir::{resource_dir, ResourceDir};
mod resource_files;
pub use resource_files::{ResourceFiles, UriSegmentError};
/// Support for module based generations. Use it for large data sets (more than 128 Mb).
pub mod sets;

View file

@ -0,0 +1,249 @@
use path_slash::PathExt;
use std::{
fs::{self, File, Metadata},
io::{self, Write},
path::{Path, PathBuf},
time::SystemTime,
};
/// Static files resource.
pub struct Resource {
pub data: &'static [u8],
pub modified: u64,
pub mime_type: &'static str,
}
/// Used internally in generated functions.
#[inline]
pub fn new_resource(data: &'static [u8], modified: u64, mime_type: &'static str) -> Resource {
Resource {
data,
modified,
mime_type,
}
}
pub(crate) const DEFAULT_VARIABLE_NAME: &str = "r";
/// Generate resources for `project_dir` using `filter`.
/// Result saved in `generated_filename` and function named as `fn_name`.
///
/// in `build.rs`:
/// ```rust
/// use std::{env, path::Path};
/// use pagetop_statics::resource::generate_resources;
///
/// fn main() {
/// let out_dir = env::var("OUT_DIR").unwrap();
/// let generated_filename = Path::new(&out_dir).join("generated.rs");
/// generate_resources("./tests", None, generated_filename, "generate", "pagetop_statics").unwrap();
/// }
/// ```
///
/// in `main.rs`:
/// ```rust
/// include!(concat!(env!("OUT_DIR"), "/generated.rs"));
///
/// fn main() {
/// let generated_file = generate();
///
/// assert_eq!(generated_file.len(), 4);
/// }
/// ```
pub fn generate_resources<P: AsRef<Path>, G: AsRef<Path>>(
project_dir: P,
filter: Option<fn(p: &Path) -> bool>,
generated_filename: G,
fn_name: &str,
crate_name: &str,
) -> io::Result<()> {
let resources = collect_resources(&project_dir, filter)?;
let mut f = File::create(&generated_filename)?;
generate_function_header(&mut f, fn_name, crate_name)?;
generate_uses(&mut f, crate_name)?;
generate_variable_header(&mut f, DEFAULT_VARIABLE_NAME)?;
generate_resource_inserts(&mut f, &project_dir, DEFAULT_VARIABLE_NAME, resources)?;
generate_variable_return(&mut f, DEFAULT_VARIABLE_NAME)?;
generate_function_end(&mut f)?;
Ok(())
}
/// Generate resource mapping for `project_dir` using `filter`.
/// Result saved in `generated_filename` as anonymous block which returns HashMap<&'static str, Resource>.
///
/// in `build.rs`:
/// ```rust
///
/// use std::{env, path::Path};
/// use pagetop_statics::resource::generate_resources_mapping;
///
/// fn main() {
/// let out_dir = env::var("OUT_DIR").unwrap();
/// let generated_filename = Path::new(&out_dir).join("generated_mapping.rs");
/// generate_resources_mapping("./tests", None, generated_filename, "pagetop_statics").unwrap();
/// }
/// ```
///
/// in `main.rs`:
/// ```rust
/// use std::collections::HashMap;
///
/// use pagetop_statics::StaticResource;
///
/// fn generate_mapping() -> HashMap<&'static str, StaticResource> {
/// include!(concat!(env!("OUT_DIR"), "/generated_mapping.rs"))
/// }
///
/// fn main() {
/// let generated_file = generate_mapping();
///
/// assert_eq!(generated_file.len(), 4);
///
/// }
/// ```
pub fn generate_resources_mapping<P: AsRef<Path>, G: AsRef<Path>>(
project_dir: P,
filter: Option<fn(p: &Path) -> bool>,
generated_filename: G,
crate_name: &str,
) -> io::Result<()> {
let resources = collect_resources(&project_dir, filter)?;
let mut f = File::create(&generated_filename)?;
writeln!(f, "{{")?;
generate_uses(&mut f, crate_name)?;
generate_variable_header(&mut f, DEFAULT_VARIABLE_NAME)?;
generate_resource_inserts(&mut f, &project_dir, DEFAULT_VARIABLE_NAME, resources)?;
generate_variable_return(&mut f, DEFAULT_VARIABLE_NAME)?;
writeln!(f, "}}")?;
Ok(())
}
#[cfg(not(feature = "sort"))]
pub(crate) fn collect_resources<P: AsRef<Path>>(
path: P,
filter: Option<fn(p: &Path) -> bool>,
) -> io::Result<Vec<(PathBuf, Metadata)>> {
collect_resources_nested(path, filter)
}
#[cfg(feature = "sort")]
pub(crate) fn collect_resources<P: AsRef<Path>>(
path: P,
filter: Option<fn(p: &Path) -> bool>,
) -> io::Result<Vec<(PathBuf, Metadata)>> {
let mut resources = collect_resources_nested(path, filter)?;
resources.sort_by(|a, b| a.0.cmp(&b.0));
Ok(resources)
}
#[inline]
fn collect_resources_nested<P: AsRef<Path>>(
path: P,
filter: Option<fn(p: &Path) -> bool>,
) -> io::Result<Vec<(PathBuf, Metadata)>> {
let mut result = vec![];
for entry in fs::read_dir(&path)? {
let entry = entry?;
let path = entry.path();
if let Some(ref filter) = filter {
if !filter(path.as_ref()) {
continue;
}
}
if path.is_dir() {
let nested = collect_resources(path, filter)?;
result.extend(nested);
} else {
result.push((path, entry.metadata()?));
}
}
Ok(result)
}
pub(crate) fn generate_resource_inserts<P: AsRef<Path>, W: Write>(
f: &mut W,
project_dir: &P,
variable_name: &str,
resources: Vec<(PathBuf, Metadata)>,
) -> io::Result<()> {
for resource in &resources {
generate_resource_insert(f, project_dir, variable_name, resource)?;
}
Ok(())
}
#[allow(clippy::unnecessary_debug_formatting)]
pub(crate) fn generate_resource_insert<P: AsRef<Path>, W: Write>(
f: &mut W,
project_dir: &P,
variable_name: &str,
resource: &(PathBuf, Metadata),
) -> io::Result<()> {
let (path, metadata) = resource;
let abs_path = path.canonicalize()?;
let key_path = path.strip_prefix(project_dir).unwrap().to_slash().unwrap();
let modified = if let Ok(Ok(modified)) = metadata
.modified()
.map(|x| x.duration_since(SystemTime::UNIX_EPOCH))
{
modified.as_secs()
} else {
0
};
let mime_type = mime_guess::MimeGuess::from_path(path).first_or_octet_stream();
writeln!(
f,
"{}.insert({:?},n(i!({:?}),{:?},{:?}));",
variable_name, &key_path, &abs_path, modified, &mime_type,
)
}
pub(crate) fn generate_function_header<F: Write>(
f: &mut F,
fn_name: &str,
crate_name: &str,
) -> io::Result<()> {
writeln!(
f,
"#[allow(clippy::unreadable_literal)] pub fn {fn_name}() -> ::std::collections::HashMap<&'static str, ::{crate_name}::StaticResource> {{",
)
}
pub(crate) fn generate_function_end<F: Write>(f: &mut F) -> io::Result<()> {
writeln!(f, "}}")
}
pub(crate) fn generate_uses<F: Write>(f: &mut F, crate_name: &str) -> io::Result<()> {
writeln!(
f,
"use ::{crate_name}::resource::new_resource as n;
use ::std::include_bytes as i;",
)
}
pub(crate) fn generate_variable_header<F: Write>(f: &mut F, variable_name: &str) -> io::Result<()> {
writeln!(
f,
"let mut {variable_name} = ::std::collections::HashMap::new();",
)
}
pub(crate) fn generate_variable_return<F: Write>(f: &mut F, variable_name: &str) -> io::Result<()> {
writeln!(f, "{variable_name}")
}

View file

@ -0,0 +1,118 @@
use super::sets::{generate_resources_sets, SplitByCount};
use std::{
env, io,
path::{Path, PathBuf},
};
/// Generate resources for `resource_dir`.
///
/// ```rust,no_run
/// // Generate resources for ./tests dir with file name generated.rs
/// // stored in path defined by OUT_DIR environment variable.
/// // Function name is 'generate'
/// use pagetop_statics::resource_dir;
///
/// resource_dir("./tests").build().unwrap();
/// ```
pub fn resource_dir<P: AsRef<Path>>(resource_dir: P) -> ResourceDir {
ResourceDir {
resource_dir: resource_dir.as_ref().into(),
..Default::default()
}
}
/// Resource dir.
///
/// A builder structure allows to change default settings for:
/// - file filter
/// - generated file name
/// - generated function name
#[derive(Default)]
pub struct ResourceDir {
pub(crate) resource_dir: PathBuf,
pub(crate) filter: Option<fn(p: &Path) -> bool>,
pub(crate) generated_filename: Option<PathBuf>,
pub(crate) generated_fn: Option<String>,
pub(crate) module_name: Option<String>,
pub(crate) count_per_module: Option<usize>,
}
pub const DEFAULT_MODULE_NAME: &str = "sets";
pub const DEFAULT_COUNT_PER_MODULE: usize = 256;
impl ResourceDir {
/// Generates resources for current configuration.
pub fn build(self) -> io::Result<()> {
self.internal_build("pagetop")
}
/// Generates resources for testing current configuration.
#[allow(dead_code)]
pub(crate) fn build_test(self) -> io::Result<()> {
self.internal_build("pagetop_statics")
}
fn internal_build(self, crate_name: &str) -> io::Result<()> {
let generated_filename = self.generated_filename.unwrap_or_else(|| {
let out_dir = env::var("OUT_DIR").unwrap();
Path::new(&out_dir).join("generated.rs")
});
let generated_fn = self.generated_fn.unwrap_or_else(|| "generate".into());
let module_name = self
.module_name
.unwrap_or_else(|| format!("{}_{}", &generated_fn, DEFAULT_MODULE_NAME));
let count_per_module = self.count_per_module.unwrap_or(DEFAULT_COUNT_PER_MODULE);
generate_resources_sets(
&self.resource_dir,
self.filter,
&generated_filename,
module_name.as_str(),
&generated_fn,
&mut SplitByCount::new(count_per_module),
crate_name,
)
}
/// Sets the file filter.
pub fn with_filter(&mut self, filter: fn(p: &Path) -> bool) -> &mut Self {
self.filter = Some(filter);
self
}
/// Sets the generated filename.
pub fn with_generated_filename<P: AsRef<Path>>(&mut self, generated_filename: P) -> &mut Self {
self.generated_filename = Some(generated_filename.as_ref().into());
self
}
/// Sets the generated function name.
pub fn with_generated_fn<S>(&mut self, generated_fn: S) -> &mut Self
where
S: Into<String>,
{
self.generated_fn = Some(generated_fn.into());
self
}
/// Sets the generated module name.
///
/// Default value is based on generated function name and the suffix "sets".
/// Generated module would be overriden by each call.
pub fn with_module_name<S>(&mut self, module_name: S) -> &mut Self
where
S: Into<String>,
{
self.module_name = Some(module_name.into());
self
}
/// Sets maximal count of files per module.
pub fn with_count_per_module(&mut self, count_per_module: usize) -> &mut Self {
self.count_per_module = Some(count_per_module);
self
}
}

View file

@ -0,0 +1,396 @@
use super::resource::Resource;
use actix_web::{
dev::{
always_ready, AppService, HttpServiceFactory, ResourceDef, Service, ServiceFactory,
ServiceRequest, ServiceResponse,
},
error::Error,
guard::{Guard, GuardContext},
http::{
header::{self, ContentType},
Method, StatusCode,
},
HttpMessage, HttpRequest, HttpResponse, ResponseError,
};
use derive_more::{Deref, Display, Error};
use futures_util::future::{ok, FutureExt, LocalBoxFuture, Ready};
use std::{collections::HashMap, ops::Deref, rc::Rc};
/// Static resource files handling
///
/// `ResourceFiles` service must be registered with `App::service` method.
///
/// ```rust
/// use std::collections::HashMap;
///
/// use actix_web::App;
///
/// fn main() {
/// // serve root directory with default options:
/// // - resolve index.html
/// let files: HashMap<&'static str, pagetop_statics::StaticResource> = HashMap::new();
/// let app = App::new()
/// .service(pagetop_statics::ResourceFiles::new("/", files));
/// // or subpath with additional option to not resolve index.html
/// let files: HashMap<&'static str, pagetop_statics::StaticResource> = HashMap::new();
/// let app = App::new()
/// .service(pagetop_statics::ResourceFiles::new("/imgs", files)
/// .do_not_resolve_defaults());
/// }
/// ```
#[allow(clippy::needless_doctest_main)]
pub struct ResourceFiles {
not_resolve_defaults: bool,
use_guard: bool,
not_found_resolves_to: Option<String>,
inner: Rc<ResourceFilesInner>,
}
pub struct ResourceFilesInner {
path: String,
files: HashMap<&'static str, Resource>,
}
const INDEX_HTML: &str = "index.html";
impl ResourceFiles {
pub fn new(path: &str, files: HashMap<&'static str, Resource>) -> Self {
let inner = ResourceFilesInner {
path: path.into(),
files,
};
Self {
inner: Rc::new(inner),
not_resolve_defaults: false,
not_found_resolves_to: None,
use_guard: false,
}
}
/// By default trying to resolve '.../' to '.../index.html' if it exists.
/// Turn off this resolution by calling this function.
pub fn do_not_resolve_defaults(mut self) -> Self {
self.not_resolve_defaults = true;
self
}
/// Resolves not found references to this path.
///
/// This can be useful for angular-like applications.
pub fn resolve_not_found_to<S: ToString>(mut self, path: S) -> Self {
self.not_found_resolves_to = Some(path.to_string());
self
}
/// Resolves not found references to root path.
///
/// This can be useful for angular-like applications.
pub fn resolve_not_found_to_root(self) -> Self {
self.resolve_not_found_to(INDEX_HTML)
}
/// If this is called, we will use an [actix_web::guard::Guard] to check if this request should be handled.
/// If set to true, we skip using the handler for files that haven't been found, instead of sending 404s.
/// Would be ignored, if `resolve_not_found_to` or `resolve_not_found_to_root` is used.
///
/// Can be useful if you want to share files on a (sub)path that's also used by a different route handler.
pub fn skip_handler_when_not_found(mut self) -> Self {
self.use_guard = true;
self
}
fn select_guard(&self) -> Box<dyn Guard> {
if self.not_resolve_defaults {
Box::new(NotResolveDefaultsGuard::from(self))
} else {
Box::new(ResolveDefaultsGuard::from(self))
}
}
}
impl Deref for ResourceFiles {
type Target = ResourceFilesInner;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
struct NotResolveDefaultsGuard {
inner: Rc<ResourceFilesInner>,
}
impl Guard for NotResolveDefaultsGuard {
fn check(&self, ctx: &GuardContext<'_>) -> bool {
self.inner
.files
.contains_key(ctx.head().uri.path().trim_start_matches('/'))
}
}
impl From<&ResourceFiles> for NotResolveDefaultsGuard {
fn from(files: &ResourceFiles) -> Self {
Self {
inner: files.inner.clone(),
}
}
}
struct ResolveDefaultsGuard {
inner: Rc<ResourceFilesInner>,
}
impl Guard for ResolveDefaultsGuard {
fn check(&self, ctx: &GuardContext<'_>) -> bool {
let path = ctx.head().uri.path().trim_start_matches('/');
self.inner.files.contains_key(path)
|| ((path.is_empty() || path.ends_with('/'))
&& self
.inner
.files
.contains_key((path.to_string() + INDEX_HTML).as_str()))
}
}
impl From<&ResourceFiles> for ResolveDefaultsGuard {
fn from(files: &ResourceFiles) -> Self {
Self {
inner: files.inner.clone(),
}
}
}
impl HttpServiceFactory for ResourceFiles {
fn register(self, config: &mut AppService) {
let prefix = self.path.trim_start_matches('/');
let rdef = if config.is_root() {
ResourceDef::root_prefix(prefix)
} else {
ResourceDef::prefix(prefix)
};
let guards = if self.use_guard && self.not_found_resolves_to.is_none() {
Some(vec![self.select_guard()])
} else {
None
};
config.register_service(rdef, guards, self, None);
}
}
impl ServiceFactory<ServiceRequest> for ResourceFiles {
type Config = ();
type Response = ServiceResponse;
type Error = Error;
type Service = ResourceFilesService;
type InitError = ();
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
fn new_service(&self, _: ()) -> Self::Future {
ok(ResourceFilesService {
resolve_defaults: !self.not_resolve_defaults,
not_found_resolves_to: self.not_found_resolves_to.clone(),
inner: self.inner.clone(),
})
.boxed_local()
}
}
#[derive(Deref)]
pub struct ResourceFilesService {
resolve_defaults: bool,
not_found_resolves_to: Option<String>,
#[deref]
inner: Rc<ResourceFilesInner>,
}
impl Service<ServiceRequest> for ResourceFilesService {
type Response = ServiceResponse;
type Error = Error;
type Future = Ready<Result<Self::Response, Self::Error>>;
always_ready!();
fn call(&self, req: ServiceRequest) -> Self::Future {
match *req.method() {
Method::HEAD | Method::GET => (),
_ => {
return ok(ServiceResponse::new(
req.into_parts().0,
HttpResponse::MethodNotAllowed()
.insert_header(ContentType::plaintext())
.insert_header((header::ALLOW, "GET, HEAD"))
.body("This resource only supports GET and HEAD."),
));
}
}
let req_path = req.match_info().unprocessed();
let mut item = self.files.get(req_path);
if item.is_none()
&& self.resolve_defaults
&& (req_path.is_empty() || req_path.ends_with('/'))
{
let index_req_path = req_path.to_string() + INDEX_HTML;
item = self.files.get(index_req_path.trim_start_matches('/'));
}
let (req, response) = if item.is_some() {
let (req, _) = req.into_parts();
let response = respond_to(&req, item);
(req, response)
} else {
let real_path = match get_pathbuf(req_path) {
Ok(item) => item,
Err(e) => return ok(req.error_response(e)),
};
let (req, _) = req.into_parts();
let mut item = self.files.get(real_path.as_str());
if item.is_none() && self.not_found_resolves_to.is_some() {
let not_found_path = self.not_found_resolves_to.as_ref().unwrap();
item = self.files.get(not_found_path.as_str());
}
let response = respond_to(&req, item);
(req, response)
};
ok(ServiceResponse::new(req, response))
}
}
fn respond_to(req: &HttpRequest, item: Option<&Resource>) -> HttpResponse {
if let Some(file) = item {
let etag = Some(header::EntityTag::new_strong(format!(
"{:x}:{:x}",
file.data.len(),
file.modified
)));
let precondition_failed = !any_match(etag.as_ref(), req);
let not_modified = !none_match(etag.as_ref(), req);
let mut resp = HttpResponse::build(StatusCode::OK);
resp.insert_header((header::CONTENT_TYPE, file.mime_type));
if let Some(etag) = etag {
resp.insert_header(header::ETag(etag));
}
if precondition_failed {
return resp.status(StatusCode::PRECONDITION_FAILED).finish();
} else if not_modified {
return resp.status(StatusCode::NOT_MODIFIED).finish();
}
resp.body(file.data)
} else {
HttpResponse::NotFound().body("Not found")
}
}
/// Returns true if `req` has no `If-Match` header or one which matches `etag`.
fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
match req.get_header::<header::IfMatch>() {
None | Some(header::IfMatch::Any) => true,
Some(header::IfMatch::Items(ref items)) => {
if let Some(some_etag) = etag {
for item in items {
if item.strong_eq(some_etag) {
return true;
}
}
}
false
}
}
}
/// Returns true if `req` doesn't have an `If-None-Match` header matching `req`.
fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
match req.get_header::<header::IfNoneMatch>() {
Some(header::IfNoneMatch::Any) => false,
Some(header::IfNoneMatch::Items(ref items)) => {
if let Some(some_etag) = etag {
for item in items {
if item.weak_eq(some_etag) {
return false;
}
}
}
true
}
None => true,
}
}
/// Error type representing invalid characters in a URI path segment.
///
/// This enum is used to report specific formatting errors in individual segments of a URI path,
/// such as starting, ending, or containing disallowed characters. Each variant wraps the offending
/// character that caused the error.
#[derive(Debug, PartialEq, Display, Error)]
pub enum UriSegmentError {
/// The segment started with the wrapped invalid character.
#[display(fmt = "The segment started with the wrapped invalid character")]
BadStart(#[error(not(source))] char),
/// The segment contained the wrapped invalid character.
#[display(fmt = "The segment contained the wrapped invalid character")]
BadChar(#[error(not(source))] char),
/// The segment ended with the wrapped invalid character.
#[display(fmt = "The segment ended with the wrapped invalid character")]
BadEnd(#[error(not(source))] char),
}
#[cfg(test)]
mod tests_error_impl {
use super::*;
fn assert_send_and_sync<T: Send + Sync + 'static>() {}
#[test]
fn test_error_impl() {
// ensure backwards compatibility when migrating away from failure
assert_send_and_sync::<UriSegmentError>();
}
}
/// Return `BadRequest` for `UriSegmentError`
impl ResponseError for UriSegmentError {
fn error_response(&self) -> HttpResponse {
HttpResponse::new(StatusCode::BAD_REQUEST)
}
}
fn get_pathbuf(path: &str) -> Result<String, UriSegmentError> {
let mut buf = Vec::new();
for segment in path.split('/') {
if segment == ".." {
buf.pop();
} else if segment.starts_with('.') {
return Err(UriSegmentError::BadStart('.'));
} else if segment.starts_with('*') {
return Err(UriSegmentError::BadStart('*'));
} else if segment.ends_with(':') {
return Err(UriSegmentError::BadEnd(':'));
} else if segment.ends_with('>') {
return Err(UriSegmentError::BadEnd('>'));
} else if segment.ends_with('<') {
return Err(UriSegmentError::BadEnd('<'));
} else if segment.is_empty() {
continue;
} else if cfg!(windows) && segment.contains('\\') {
return Err(UriSegmentError::BadChar('\\'));
} else {
buf.push(segment)
}
}
Ok(buf.join("/"))
}

View file

@ -0,0 +1,184 @@
use std::{
fs::{self, File, Metadata},
io::{self, Write},
path::{Path, PathBuf},
};
use super::resource::{
collect_resources, generate_function_end, generate_function_header, generate_resource_insert,
generate_uses, generate_variable_header, generate_variable_return, DEFAULT_VARIABLE_NAME,
};
/// Defines the split strategie.
pub trait SetSplitStrategie {
/// Register next file from resources.
fn register(&mut self, path: &Path, metadata: &Metadata);
/// Determine, should we split modules now.
fn should_split(&self) -> bool;
/// Resets internal counters after split.
fn reset(&mut self);
}
/// Split modules by files count.
pub struct SplitByCount {
current: usize,
max: usize,
}
impl SplitByCount {
pub fn new(max: usize) -> Self {
Self { current: 0, max }
}
}
impl SetSplitStrategie for SplitByCount {
fn register(&mut self, _path: &Path, _metadata: &Metadata) {
self.current += 1;
}
fn should_split(&self) -> bool {
self.current >= self.max
}
fn reset(&mut self) {
self.current = 0;
}
}
/// Generate resources for `project_dir` using `filter`
/// breaking them into separate modules using `set_split_strategie` (recommended for large > 128 Mb setups).
///
/// Result saved in module named `module_name`. It exports
/// only one function named `fn_name`. It is then exported from
/// `generated_filename`. `generated_filename` is also used to determine
/// the parent directory for the module.
///
/// in `build.rs`:
/// ```rust
///
/// use std::{env, path::Path};
/// use pagetop_statics::sets::{generate_resources_sets, SplitByCount};
///
/// fn main() {
/// let out_dir = env::var("OUT_DIR").unwrap();
/// let generated_filename = Path::new(&out_dir).join("generated_sets.rs");
/// generate_resources_sets(
/// "./tests",
/// None,
/// generated_filename,
/// "sets",
/// "generate",
/// &mut SplitByCount::new(2),
/// "pagetop_statics",
/// )
/// .unwrap();
/// }
/// ```
///
/// in `main.rs`:
/// ```rust
/// include!(concat!(env!("OUT_DIR"), "/generated_sets.rs"));
///
/// fn main() {
/// let generated_file = generate();
///
/// assert_eq!(generated_file.len(), 4);
///
/// }
/// ```
pub fn generate_resources_sets<P, G, S>(
project_dir: P,
filter: Option<fn(p: &Path) -> bool>,
generated_filename: G,
module_name: &str,
fn_name: &str,
set_split_strategie: &mut S,
crate_name: &str,
) -> io::Result<()>
where
P: AsRef<Path>,
G: AsRef<Path>,
S: SetSplitStrategie,
{
let resources = collect_resources(&project_dir, filter)?;
let mut generated_file = File::create(&generated_filename)?;
let module_dir = generated_filename.as_ref().parent().map_or_else(
|| PathBuf::from(module_name),
|parent| parent.join(module_name),
);
fs::create_dir_all(&module_dir)?;
let mut module_file = File::create(module_dir.join("mod.rs"))?;
generate_uses(&mut module_file, crate_name)?;
writeln!(
module_file,
"
use ::{crate_name}::StaticResource;
use ::std::collections::HashMap;"
)?;
let mut modules_count = 1;
let mut set_file = create_set_module_file(&module_dir, modules_count)?;
let mut should_split = set_split_strategie.should_split();
for resource in &resources {
let (path, metadata) = &resource;
if should_split {
set_split_strategie.reset();
modules_count += 1;
generate_function_end(&mut set_file)?;
set_file = create_set_module_file(&module_dir, modules_count)?;
}
set_split_strategie.register(path, metadata);
should_split = set_split_strategie.should_split();
generate_resource_insert(&mut set_file, &project_dir, DEFAULT_VARIABLE_NAME, resource)?;
}
generate_function_end(&mut set_file)?;
for module_index in 1..=modules_count {
writeln!(module_file, "mod set_{module_index};")?;
}
generate_function_header(&mut module_file, fn_name, crate_name)?;
generate_variable_header(&mut module_file, DEFAULT_VARIABLE_NAME)?;
for module_index in 1..=modules_count {
writeln!(
module_file,
"set_{module_index}::generate(&mut {DEFAULT_VARIABLE_NAME});",
)?;
}
generate_variable_return(&mut module_file, DEFAULT_VARIABLE_NAME)?;
generate_function_end(&mut module_file)?;
writeln!(
generated_file,
"mod {module_name};
pub use {module_name}::{fn_name};",
)?;
Ok(())
}
fn create_set_module_file(module_dir: &Path, module_index: usize) -> io::Result<File> {
let mut set_module = File::create(module_dir.join(format!("set_{module_index}.rs")))?;
writeln!(
set_module,
"#[allow(clippy::wildcard_imports)]
use super::*;
#[allow(clippy::unreadable_literal)]
pub(crate) fn generate({DEFAULT_VARIABLE_NAME}: &mut HashMap<&'static str, StaticResource>) {{",
)?;
Ok(set_module)
}