Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tutor mode continued #1420

Merged
merged 5 commits into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 101 additions & 26 deletions fastn-core/src/tutor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,15 @@ pub async fn process(
));
}

let state: TutorState =
let state =
match tokio::fs::read(dirs::home_dir().unwrap().join(".fastn").join("tutor.json")).await {
Ok(v) => serde_json::from_slice(&v)?,
Err(e) => match e.kind() {
std::io::ErrorKind::NotFound => TutorStateFS::default(),
_ => return Err(e.into()),
},
}
.try_into()?;
.to_state(std::env::current_dir()?)?;

doc.from_json(&state, &kind, &value)
}
Expand All @@ -45,93 +45,108 @@ struct TutorStateFS {
current: String,
}

#[derive(Debug, serde::Serialize)]
#[derive(Debug, serde::Serialize, PartialEq)]
struct TutorState {
workshops: Vec<Workshop>,
}

impl TryFrom<TutorStateFS> for TutorState {
type Error = ftd::interpreter::Error;
impl TutorStateFS {
fn to_state<T: AsRef<std::path::Path>>(
self: TutorStateFS,
path: T,
) -> ftd::interpreter::Result<TutorState> {
use itertools::Itertools;

fn try_from(state: TutorStateFS) -> Result<Self, Self::Error> {
// loop over all folders in current folder
let mut workshops = vec![];
static RE: once_cell::sync::Lazy<regex::Regex> =
once_cell::sync::Lazy::new(|| regex::Regex::new(r"^[a-zA-Z]-[a-zA-Z]+.*$").unwrap());

for entry in std::fs::read_dir(std::env::current_dir()?)? {
for entry in std::fs::read_dir(path)?.sorted_by(sort_path) {
let entry = entry?;
let path = entry.path();

if !path.is_dir() {
continue;
}

if !RE.is_match(&path.file_name().unwrap().to_string_lossy()) {
continue;
}

workshops.push(Workshop::load(&path, &state)?);
workshops.push(Workshop::load(&path, &self)?);
}

Ok(TutorState { workshops })
}
}

#[derive(Debug, serde::Serialize)]
fn sort_path(
a: &std::io::Result<std::fs::DirEntry>,
b: &std::io::Result<std::fs::DirEntry>,
) -> std::cmp::Ordering {
a.as_ref().unwrap().path().cmp(&b.as_ref().unwrap().path())
}

#[derive(Debug, serde::Serialize, PartialEq)]
struct Workshop {
title: String,
about: String,
url: String,
done: bool,
current: bool,
tutorials: Vec<Tutorial>,
}

impl Workshop {
fn load(path: &std::path::Path, state: &TutorStateFS) -> ftd::interpreter::Result<Self> {
let (title, about) = title_and_about_from_readme(path)?;
use itertools::Itertools;

let mut tutorials = vec![];
let id = path.file_name().unwrap().to_string_lossy();

for entry in std::fs::read_dir(path)? {
static RE: once_cell::sync::Lazy<regex::Regex> =
once_cell::sync::Lazy::new(|| regex::Regex::new(r"^[0-9][0-9]-[a-zA-Z]+.*$").unwrap());

for entry in std::fs::read_dir(path)?.sorted_by(sort_path) {
let entry = entry?;
let path = entry.path();

if !path.is_dir() {
continue;
}
if !RE.is_match(&path.file_name().unwrap().to_string_lossy()) {
continue;
}

tutorials.push(Tutorial::load(&id, &path, state)?);
}

Ok(Workshop {
title: title.to_string(),
about: about.to_string(),
title: title_from_readme(path)?,
url: format!("/{id}/"),
done: !tutorials.iter().any(|t| !t.done),
current: tutorials.iter().any(|t| t.current),
tutorials,
})
}
}

fn title_and_about_from_readme(
folder: &std::path::Path,
) -> ftd::interpreter::Result<(String, String)> {
fn title_from_readme(folder: &std::path::Path) -> ftd::interpreter::Result<String> {
let content = std::fs::read_to_string(folder.join("README.md"))?;
let (title, about) = match content.split_once("\n\n") {
let (title, _about) = match content.split_once("\n\n") {
Some(v) => v,
None => {
return Err(ftd::interpreter::Error::OtherError(
"invalid README.md".into(),
))
}
};
Ok((title.to_string(), about.to_string()))
Ok(title.replacen("# ", "", 1))
}

#[derive(Debug, serde::Serialize)]
#[derive(Debug, serde::Serialize, PartialEq)]
struct Tutorial {
id: String,
url: String,
title: String,
about: String,
done: bool,
current: bool,
}
Expand All @@ -142,14 +157,13 @@ impl Tutorial {
path: &std::path::Path,
state: &TutorStateFS,
) -> ftd::interpreter::Result<Self> {
let (title, about) = title_and_about_from_readme(path)?;
let id = format!("{parent}/{}", path.file_name().unwrap().to_string_lossy());

Ok(Tutorial {
title: title.to_string(),
about: about.to_string(),
title: title_from_readme(path)?,
done: state.done.contains(&id),
current: state.current == id,
url: format!("/{id}/"),
id,
})
}
Expand All @@ -160,3 +174,64 @@ pub fn is_tutor() -> bool {
// with either of these are passed we allow APIs like /-/shutdown/, `/-/start/` etc
std::env::args().any(|e| e == "tutor" || e == "--tutor")
}

#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;

#[test]
fn test() {
let mut ts = super::TutorState {
workshops: vec![
super::Workshop {
title: "Build Websites Using `fastn`".to_string(),
url: "/a-website/".to_string(),
done: false,
current: false,
tutorials: vec![super::Tutorial {
id: "a-website/01-hello-world".to_string(),
url: "/a-website/01-hello-world/".to_string(),
title: "Install and start using `fastn`".to_string(),
done: false,
current: false,
}],
},
super::Workshop {
title: "Build User Interfaces Using `fastn`".to_string(),
url: "/b-ui/".to_string(),
done: false,
current: false,
tutorials: vec![super::Tutorial {
id: "b-ui/01-hello-world".to_string(),
url: "/b-ui/01-hello-world/".to_string(),
title: "Install and start using `fastn`".to_string(),
done: false,
current: false,
}],
},
],
};

assert_eq!(
super::TutorStateFS::default()
.to_state("tutor-tests/one")
.unwrap(),
ts,
);

ts.workshops[0].tutorials[0].done = true;
ts.workshops[0].done = true;
ts.workshops[1].current = true;
ts.workshops[1].tutorials[0].current = true;

assert_eq!(
super::TutorStateFS {
done: vec!["a-website/01-hello-world".to_string()],
current: "b-ui/01-hello-world".to_string(),
}
.to_state("tutor-tests/one")
.unwrap(),
ts,
);
}
}
3 changes: 3 additions & 0 deletions fastn-core/tutor-tests/one/a-website/01-hello-world/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Install and start using `fastn`

In this exercise we will install fastn and create a basic hello world program.
3 changes: 3 additions & 0 deletions fastn-core/tutor-tests/one/a-website/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Build Websites Using `fastn`

This workshop teaches you how to build websites using `fastn`.
3 changes: 3 additions & 0 deletions fastn-core/tutor-tests/one/b-ui/01-hello-world/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Install and start using `fastn`

In this exercise we will install fastn and create a basic hello world program.
3 changes: 3 additions & 0 deletions fastn-core/tutor-tests/one/b-ui/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Build User Interfaces Using `fastn`

This workshop teaches you how to build user interfaces using `fastn`.
Loading