use crate::{
fs::{LazyLoader, TryLoad},
languages::{programming, spoken},
models::Error as ModelError,
Error,
};
use serde::{Deserialize, Serialize};
use std::{
fmt,
path::{Path, PathBuf},
sync::Arc,
};
use tokio::sync::RwLock;
use tracing::trace;
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub enum Status {
#[default]
NotStarted,
InProgress,
Completed,
}
impl fmt::Display for Status {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Status::NotStarted => write!(f, "Not Started"),
Status::InProgress => write!(f, "In Progress"),
Status::Completed => write!(f, "Completed"),
}
}
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct Lesson {
pub title: String,
pub description: String,
pub status: Status,
}
#[async_trait::async_trait]
impl TryLoad for Lesson {
type Error = Error;
async fn try_load(path: &Path) -> Result<Self, Error> {
let content = std::fs::read_to_string(path)?;
Ok(serde_yaml::from_str(&content)?)
}
}
pub type Metadata = Arc<RwLock<LazyLoader<Lesson>>>;
pub type LessonText = Arc<RwLock<LazyLoader<String>>>;
#[derive(Clone, Debug)]
pub struct LessonData {
name: String,
path: PathBuf,
spoken_language: spoken::Code,
programming_language: programming::Code,
lesson_text: LessonText,
metadata: Metadata,
}
impl LessonData {
pub fn get_name(&self) -> &str {
&self.name
}
pub fn get_path(&self) -> &Path {
&self.path
}
pub fn get_spoken_language(&self) -> spoken::Code {
self.spoken_language
}
pub fn get_programming_language(&self) -> programming::Code {
self.programming_language
}
pub async fn get_text(&self) -> Result<String, Error> {
let mut lesson_text = self
.lesson_text
.write() .await;
lesson_text.try_load().await.cloned()
}
pub async fn get_metadata(&self) -> Result<Lesson, Error> {
let mut metadata = self
.metadata
.write() .await;
metadata.try_load().await.cloned()
}
pub async fn update_status(&self, new_status: Status) -> Result<(), Error> {
let mut metadata = self.metadata.write().await;
let mut lesson = metadata.try_load().await.cloned()?;
lesson.status = new_status;
let lesson_yaml_path = self.path.join("lesson.yaml");
let content = serde_yaml::to_string(&lesson)?;
std::fs::write(&lesson_yaml_path, content)?;
*metadata = crate::fs::LazyLoader::Loaded(lesson);
Ok(())
}
}
#[async_trait::async_trait]
impl TryLoad for LessonData {
type Error = Error;
async fn try_load(path: &Path) -> Result<Self, Self::Error> {
trace!(
"Getting name, spoken, and programming from path: {}",
path.display()
);
let (name, spoken_language, programming_language) = {
let mut path = path.to_path_buf();
let name = path
.file_name()
.and_then(|p| p.to_str())
.ok_or::<Error>(ModelError::LessonDataDirNotFound.into())?
.to_string();
path.pop();
trace!("Lesson name: {name}, rest: {}", path.display());
let programming_language = path
.file_name()
.and_then(|p| programming::Code::try_from(p.to_string_lossy().as_ref()).ok())
.ok_or::<Error>(ModelError::NoProgrammingLanguageSpecified.into())?;
trace!("Programming language: {programming_language}");
path.pop();
let spoken_language = path
.file_name()
.and_then(|p| spoken::Code::try_from(p.to_string_lossy().as_ref()).ok())
.ok_or::<Error>(ModelError::NoSpokenLanguageSpecified.into())?;
trace!(
"Spoken language: {spoken_language}, rest: {}",
path.display()
);
(name, spoken_language, programming_language)
};
let loader = Loader::new(&name)
.path(path)
.spoken_language(spoken_language)
.programming_language(programming_language);
loader.try_load()
}
}
#[derive(Clone, Debug, Default)]
pub struct Loader {
name: String,
path: Option<PathBuf>,
spoken_language: Option<spoken::Code>,
programming_language: Option<programming::Code>,
}
impl Loader {
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
..Default::default()
}
}
pub fn path(self, path: &Path) -> Self {
Self {
path: Some(path.to_path_buf()),
..self
}
}
pub fn spoken_language(self, spoken_language: spoken::Code) -> Self {
Self {
spoken_language: Some(spoken_language),
..self
}
}
pub fn programming_language(self, programming_language: programming::Code) -> Self {
Self {
programming_language: Some(programming_language),
..self
}
}
fn try_load_lesson_text(&self, lesson_dir: &Path) -> Result<LessonText, Error> {
let lesson_text_path = lesson_dir.join("lesson.md");
if !lesson_text_path.exists() {
return Err(ModelError::LessonTextFileMissing.into());
}
Ok(Arc::new(RwLock::new(LazyLoader::NotLoaded(
lesson_text_path,
))))
}
fn try_load_metadata(&self, lesson_dir: &Path) -> Result<Metadata, Error> {
let metadata_path = lesson_dir.join("lesson.yaml");
if !metadata_path.exists() {
return Err(ModelError::LessonMetadataFileMissing.into());
}
Ok(Arc::new(RwLock::new(LazyLoader::NotLoaded(metadata_path))))
}
pub fn try_load(&self) -> Result<LessonData, Error> {
let name = self.name.clone();
let path = self
.path
.clone()
.ok_or::<Error>(ModelError::LessonDataDirNotFound.into())?;
path.exists()
.then_some(())
.ok_or::<Error>(ModelError::LessonDataDirNotFound.into())?;
let spoken_language = self
.spoken_language
.ok_or::<Error>(ModelError::NoSpokenLanguageSpecified.into())?;
let programming_language = self
.programming_language
.ok_or::<Error>(ModelError::NoProgrammingLanguageSpecified.into())?;
let lesson_text = self.try_load_lesson_text(&path)?;
let metadata = self.try_load_metadata(&path)?;
Ok(LessonData {
name,
path,
spoken_language,
programming_language,
lesson_text,
metadata,
})
}
}