use std::path::{ Path, PathBuf };
use serde::{ Serialize, Deserialize };
use crate::{ FileContent, Error };
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "source_type")]
pub enum ContentSource
{
Inline
{
content: FileContent,
},
File
{
path: PathBuf,
},
Url
{
url: String,
},
}
pub trait IntoContentSource
{
fn into_content_source( self ) -> ContentSource;
}
impl IntoContentSource for ContentSource
{
fn into_content_source( self ) -> ContentSource
{
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FileRef( pub PathBuf );
impl FileRef
{
pub fn new( path: impl Into< PathBuf > ) -> Self
{
Self( path.into() )
}
}
impl IntoContentSource for FileRef
{
fn into_content_source( self ) -> ContentSource
{
ContentSource::File { path: self.0 }
}
}
impl From< PathBuf > for FileRef
{
fn from( path: PathBuf ) -> Self
{
Self( path )
}
}
impl From< &str > for FileRef
{
fn from( path: &str ) -> Self
{
Self( PathBuf::from( path ) )
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UrlRef( pub String );
impl UrlRef
{
pub fn new( url: impl Into< String > ) -> Self
{
Self( url.into() )
}
}
impl IntoContentSource for UrlRef
{
fn into_content_source( self ) -> ContentSource
{
ContentSource::Url { url: self.0 }
}
}
impl From< String > for UrlRef
{
fn from( url: String ) -> Self
{
Self( url )
}
}
impl From< &str > for UrlRef
{
fn from( url: &str ) -> Self
{
Self( url.to_string() )
}
}
#[derive(Debug, Clone)]
pub struct InlineContent( pub FileContent );
impl InlineContent
{
#[must_use]
pub fn new( content: FileContent ) -> Self
{
Self( content )
}
pub fn text( text: impl Into< String > ) -> Self
{
Self( FileContent::Text( text.into() ) )
}
#[must_use]
pub fn binary( bytes: Vec< u8 > ) -> Self
{
Self( FileContent::Binary( bytes ) )
}
}
impl IntoContentSource for InlineContent
{
fn into_content_source( self ) -> ContentSource
{
ContentSource::Inline { content: self.0 }
}
}
impl From< FileContent > for InlineContent
{
fn from( content: FileContent ) -> Self
{
Self( content )
}
}
impl ContentSource
{
#[must_use]
pub fn is_inline( &self ) -> bool
{
matches!( self, Self::Inline { .. } )
}
#[must_use]
pub fn is_file( &self ) -> bool
{
matches!( self, Self::File { .. } )
}
#[must_use]
pub fn is_url( &self ) -> bool
{
matches!( self, Self::Url { .. } )
}
#[must_use]
pub fn as_inline( &self ) -> Option< &FileContent >
{
if let Self::Inline { content } = self
{
Some( content )
}
else
{
None
}
}
#[must_use]
pub fn as_file_path( &self ) -> Option< &Path >
{
if let Self::File { path } = self
{
Some( path )
}
else
{
None
}
}
#[must_use]
pub fn as_url( &self ) -> Option< &str >
{
if let Self::Url { url } = self
{
Some( url )
}
else
{
None
}
}
}
pub trait ContentResolver
{
fn resolve( &self, source: &ContentSource ) -> Result< FileContent, Error >;
}
pub trait ContentStorage
{
fn store( &mut self, path: &Path, content: &FileContent ) -> Result< (), Error >;
}
#[derive(Debug)]
pub struct DefaultContentResolver;
impl DefaultContentResolver
{
#[must_use]
pub fn new() -> Self
{
Self
}
}
impl Default for DefaultContentResolver
{
fn default() -> Self
{
Self::new()
}
}
impl ContentResolver for DefaultContentResolver
{
fn resolve( &self, source: &ContentSource ) -> Result< FileContent, Error >
{
match source
{
ContentSource::Inline { content } =>
{
Ok( content.clone() )
}
ContentSource::File { path } =>
{
let data = std::fs::read( path )?;
match String::from_utf8( data.clone() )
{
Ok( text ) => Ok( FileContent::Text( text ) ),
Err( _ ) => Ok( FileContent::Binary( data ) ),
}
}
ContentSource::Url { url } =>
{
Err( Error::Render( format!(
"URL fetching not supported in default resolver. \
Implement custom ContentResolver to fetch from: {url}"
) ) )
}
}
}
}
#[derive(Debug)]
pub struct DefaultContentStorage;
impl DefaultContentStorage
{
#[must_use]
pub fn new() -> Self
{
Self
}
}
impl Default for DefaultContentStorage
{
fn default() -> Self
{
Self::new()
}
}
impl ContentStorage for DefaultContentStorage
{
fn store( &mut self, path: &Path, content: &FileContent ) -> Result< (), Error >
{
if let Some( parent ) = path.parent()
{
std::fs::create_dir_all( parent )?;
}
match content
{
FileContent::Text( text ) =>
{
std::fs::write( path, text )?;
}
FileContent::Binary( bytes ) =>
{
std::fs::write( path, bytes )?;
}
}
Ok( () )
}
}