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

feat(tools): implement move command #1

Merged
merged 47 commits into from
Oct 2, 2024
Merged
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
c9e8ac0
some scaffolding around arguments, slug paths, ignore .DS_Store
argl Aug 28, 2024
a0f64fb
fix some testing issues rot rari-tools
argl Aug 28, 2024
0da34e2
serializer for frontmatter, PageWriter for Doc scaffolding, factor ou…
argl Aug 29, 2024
8f2e0b5
more file system stuff
argl Aug 30, 2024
c00a6e0
Merge remote-tracking branch 'origin/main' into MP-1459-tools
argl Sep 17, 2024
00eaf87
fix merge issues
argl Sep 17, 2024
58b7b80
some more refactoring to keep track of main
argl Sep 17, 2024
5ebaeaa
some refactoring
argl Sep 17, 2024
6c4ac81
some path woes, but we're getting there
argl Sep 18, 2024
e8a1648
some cleanup, wikihistory
argl Sep 18, 2024
4d7f91b
cargo
argl Sep 19, 2024
f95e2fd
Merge branch 'main' into MP-1459-tools
argl Sep 19, 2024
9b413ac
resolve merge conflicts, make sure target directory is created before…
argl Sep 19, 2024
3eaa205
minimal cloning implementation of parts of the add_redirects function
argl Sep 20, 2024
c8e513e
more tests
argl Sep 22, 2024
3fdd0f9
fixed test
argl Sep 22, 2024
b2d451d
write out redirects file
argl Sep 22, 2024
987b522
more validations for redirects, fixed translated content move, git co…
argl Sep 22, 2024
9fb5e31
add a dry-run test for move, added a translated content root for test…
argl Sep 23, 2024
afd1502
placeholder for `short_cuts` call
argl Sep 23, 2024
1e0d99f
Merge branch 'main' into MP-1459-tools
argl Sep 23, 2024
580e099
fix merge issues: Locale::all -> Locale::for_generic_and_spas
argl Sep 23, 2024
0f1eb9d
simplified forbidden symbol check
argl Sep 24, 2024
1304743
Merge branch 'main' into MP-1459-tools
argl Sep 24, 2024
82e905b
moved redirect manipulation from rari_docs to rari_tools
argl Sep 24, 2024
b287a66
refactored validation functions, fixed processing logic
argl Sep 25, 2024
585e6ae
some cleanup, disable checking for existing file/folders on the `from…
argl Sep 25, 2024
9a7b82d
remove static fixtures
argl Sep 26, 2024
ac4c1c5
gitignore dynamic fixture paths
argl Sep 26, 2024
eaa1995
intermediate fixture stuff
argl Sep 26, 2024
b300832
adjust gitignore
argl Sep 26, 2024
6b385dc
remove unused fixtures
argl Sep 26, 2024
40b1707
dynamic docs fixtures with automatic removal, make test code conditio…
argl Sep 26, 2024
cda6789
more fixtures, make fixture-dependent tests run serially
argl Sep 27, 2024
f2ffe3e
do_move tests for default and translated languages
argl Sep 27, 2024
6404a96
Merge branch 'main' into MP-1459-tools
argl Sep 27, 2024
0acc456
fixed merge errors
argl Sep 27, 2024
9a88fed
fix(lint): clippy
fiji-flo Oct 1, 2024
8b6c85d
keep .keep
argl Oct 1, 2024
71bab97
chore(doc): buffed writer
fiji-flo Oct 1, 2024
e545453
chore(tool): optimize move
fiji-flo Oct 1, 2024
273e84a
chore(fmt): fmt with nightly
fiji-flo Oct 1, 2024
a6ee131
chore(various): enhance wikihistory and yaml
fiji-flo Oct 2, 2024
7f9b2ff
chote(fmt): fmt
fiji-flo Oct 2, 2024
5223f68
fix(fm): fix frontmatter formatting
fiji-flo Oct 2, 2024
159acab
Merge remote-tracking branch 'upstream/main' into MP-1459-tools
fiji-flo Oct 2, 2024
27f0d5e
more locale derefs
fiji-flo Oct 2, 2024
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
1 change: 1 addition & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
[env]
TESTING_CONTENT_ROOT = { value = "tests/data/content/files", relative = true }
TESTING_CONTENT_TRANSLATED_ROOT = { value = "tests/data/translated_content/files", relative = true }
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
/target
.config.toml
.DS_Store
tests/data/content/files/*
!tests/data/content/files/.keep
tests/data/translated_content/files/*
!tests/data/translated_content/files/.keep
195 changes: 189 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -79,6 +79,7 @@ reqwest = { version = "0.12", default-features = false, features = [
"rustls-tls",
"gzip",
] }
indoc = "2"


[dependencies]
30 changes: 29 additions & 1 deletion crates/rari-cli/main.rs
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@ use rari_doc::search_index::build_search_index;
use rari_doc::utils::TEMPL_RECORDER_SENDER;
use rari_tools::history::gather_history;
use rari_tools::popularities::update_popularities;
use rari_tools::r#move::r#move;
use rari_types::globals::{build_out_root, content_root, content_translated_root, SETTINGS};
use rari_types::settings::Settings;
use self_update::cargo_crate_version;
@@ -43,6 +44,8 @@ struct Cli {
no_cache: bool,
#[arg(long)]
skip_updates: bool,
#[arg(short = 'y', long, help = "Assume yes to all prompts")]
assume_yes: bool,
#[command(subcommand)]
command: Commands,
}
@@ -55,6 +58,21 @@ enum Commands {
GitHistory,
Popularities,
Update(UpdateArgs),
#[command(subcommand)]
Content(ContentSubcommand),
}

#[derive(Subcommand)]
enum ContentSubcommand {
/// Moves content from one slug to another
Move(MoveArgs),
}

#[derive(Args)]
struct MoveArgs {
old_slug: String,
new_slug: String,
locale: Option<String>,
}

#[derive(Args)]
@@ -296,7 +314,7 @@ fn main() -> Result<(), Error> {
serve::serve()?
}
Commands::GitHistory => {
println!("Gathering histroy 📜");
println!("Gathering history 📜");
let start = std::time::Instant::now();
gather_history();
println!("Took: {:?}", start.elapsed());
@@ -307,6 +325,16 @@ fn main() -> Result<(), Error> {
update_popularities(20000);
println!("Took: {:?}", start.elapsed());
}
Commands::Content(content_subcommand) => match content_subcommand {
ContentSubcommand::Move(args) => {
r#move(
&args.old_slug,
&args.new_slug,
args.locale.as_deref(),
cli.assume_yes,
)?;
}
},
Commands::Update(args) => update(args.version)?,
}
Ok(())
5 changes: 4 additions & 1 deletion crates/rari-doc/Cargo.toml
Original file line number Diff line number Diff line change
@@ -17,8 +17,10 @@ itertools.workspace = true
constcat.workspace = true
indexmap.workspace = true

serde_yaml = "0.9"
serde_yaml_ng = "0.10"
yaml-rust = "0.4"
pretty_yaml = "0.5"
yaml_parser = "0.2"
percent-encoding = "2"
pest = "2"
pest_derive = "2"
@@ -54,3 +56,4 @@ css-syntax = { path = "../css-syntax", features = ["rari"] }

[dev-dependencies]
rari-types = { path = "../rari-types", features = ["testing"] }
indoc.workspace = true
5 changes: 3 additions & 2 deletions crates/rari-doc/src/cached_readers.rs
Original file line number Diff line number Diff line change
@@ -68,7 +68,7 @@ pub fn read_sidebar(name: &str, locale: Locale, slug: &str) -> Result<Arc<MetaSi
file.push(name);
file.set_extension("yaml");
let raw = read_to_string(&file)?;
let sidebar: Sidebar = serde_yaml::from_str(&raw)?;
let sidebar: Sidebar = serde_yaml_ng::from_str(&raw)?;
let sidebar = Arc::new(MetaSidebar::from(sidebar));
if cache_content() {
CACHED_SIDEBAR_FILES.write()?.insert(key, sidebar.clone());
@@ -249,7 +249,8 @@ pub fn gather_blog_authors() -> Result<HashMap<String, Arc<Author>>, DocError> {
let path = f.into_path();
let raw = read_to_string(&path)?;
let (fm, _) = split_fm(&raw);
let frontmatter: AuthorFrontmatter = serde_yaml::from_str(fm.unwrap_or_default())?;
let frontmatter: AuthorFrontmatter =
serde_yaml_ng::from_str(fm.unwrap_or_default())?;
let name = path
.parent()
.and_then(|p| p.file_name())
4 changes: 3 additions & 1 deletion crates/rari-doc/src/error.rs
Original file line number Diff line number Diff line change
@@ -44,7 +44,9 @@ pub enum DocError {
#[error("Missing frontmatter")]
NoFrontmatter,
#[error("Invalid frontmatter: {0}")]
InvalidFrontmatter(#[from] serde_yaml::Error),
InvalidFrontmatter(#[from] serde_yaml_ng::Error),
#[error("Invalid frontmatter to format: {0}")]
InvalidFrontmatterToFmt(#[from] yaml_parser::SyntaxError),
#[error(transparent)]
EnvError(#[from] EnvError),
#[error(transparent)]
2 changes: 1 addition & 1 deletion crates/rari-doc/src/html/sidebar.rs
Original file line number Diff line number Diff line change
@@ -492,7 +492,7 @@ mod test {
#[test]
fn test_details_ser() {
let yaml_str = r#"details: closed"#;
let entry: BasicEntry = serde_yaml::from_str(yaml_str).unwrap();
let entry: BasicEntry = serde_yaml_ng::from_str(yaml_str).unwrap();
assert_eq!(entry.details, Details::Closed);
}
}
4 changes: 4 additions & 0 deletions crates/rari-doc/src/pages/page.rs
Original file line number Diff line number Diff line change
@@ -233,6 +233,10 @@ pub trait PageReader {
fn read(path: impl Into<PathBuf>, locale: Option<Locale>) -> Result<Page, DocError>;
}

pub trait PageWriter {
fn write(&self) -> Result<(), DocError>;
}

pub trait PageBuilder {
fn build(&self) -> Result<BuiltDocy, DocError>;
}
4 changes: 2 additions & 2 deletions crates/rari-doc/src/pages/types/blog.rs
Original file line number Diff line number Diff line change
@@ -160,7 +160,7 @@ impl BlogPostBuildMeta {
} = fm;
let (locale, _) = locale_and_typ_from_path(&full_path)
.unwrap_or((Default::default(), PageCategory::BlogPost));
let url = build_url(&slug, &locale, PageCategory::BlogPost)?;
let url = build_url(&slug, locale, PageCategory::BlogPost)?;
let path = full_path
.strip_prefix(blog_root().ok_or(DocError::NoBlogRoot)?)?
.to_path_buf();
@@ -296,7 +296,7 @@ fn read_blog_post(path: impl Into<PathBuf>) -> Result<BlogPost, DocError> {
let raw = read_to_string(&full_path)?;
let (fm, content_start) = split_fm(&raw);
let fm = fm.ok_or(DocError::NoFrontmatter)?;
let fm: BlogPostFrontmatter = serde_yaml::from_str(fm)?;
let fm: BlogPostFrontmatter = serde_yaml_ng::from_str(fm)?;

let read_time = readtime(&raw[content_start..]);
Ok(BlogPost {
2 changes: 1 addition & 1 deletion crates/rari-doc/src/pages/types/contributors.rs
Original file line number Diff line number Diff line change
@@ -246,7 +246,7 @@ fn read_contributor_spotlight(
let raw = read_to_string(&full_path)?;
let (fm, content_start) = split_fm(&raw);
let fm = fm.ok_or(DocError::NoFrontmatter)?;
let fm: ContributorFrontMatter = serde_yaml::from_str(fm)?;
let fm: ContributorFrontMatter = serde_yaml_ng::from_str(fm)?;

Ok(ContributorSpotlight {
meta: ContributorBuildMeta::from_fm(fm, full_path, locale)?,
2 changes: 1 addition & 1 deletion crates/rari-doc/src/pages/types/curriculum.rs
Original file line number Diff line number Diff line change
@@ -165,7 +165,7 @@ impl PageReader for CurriculumPage {
summary,
template,
topic,
} = serde_yaml::from_str(fm)?;
} = serde_yaml_ng::from_str(fm)?;
let path = full_path
.strip_prefix(curriculum_root().ok_or(DocError::NoCurriculumRoot)?)?
.to_path_buf();
142 changes: 122 additions & 20 deletions crates/rari-doc/src/pages/types/doc.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
use std::collections::HashMap;
use std::fs;
use std::io::{BufWriter, Write};
use std::path::{Path, PathBuf};
use std::sync::Arc;

use pretty_yaml::config::{FormatOptions, LanguageOptions};
use rari_md::m2h;
use rari_types::fm_types::{FeatureStatus, PageType};
use rari_types::locale::Locale;
use rari_types::RariEnv;
use rari_utils::io::read_to_string;
use serde::{Deserialize, Serialize};
use serde_yaml::Value;
use serde_yaml_ng::Value;
use tracing::debug;
use validator::Validate;

use crate::cached_readers::{doc_page_from_static_files, CACHED_DOC_PAGE_FILES};
use crate::error::DocError;
use crate::pages::page::{Page, PageCategory, PageLike, PageReader};
use crate::pages::page::{Page, PageCategory, PageLike, PageReader, PageWriter};
use crate::resolve::{build_url, url_to_folder_path};
use crate::utils::{locale_and_typ_from_path, root_for_locale, split_fm, t_or_vec};
use crate::utils::{
locale_and_typ_from_path, root_for_locale, serialize_t_or_vec, split_fm, t_or_vec,
};

/*
"attribute-order": [
@@ -35,22 +40,45 @@ use crate::utils::{locale_and_typ_from_path, root_for_locale, split_fm, t_or_vec
pub struct FrontMatter {
#[validate(length(max = 120))]
pub title: String,
#[serde(rename = "short-title")]
#[serde(rename = "short-title", skip_serializing_if = "Option::is_none")]
#[validate(length(max = 60))]
pub short_title: Option<String>,
#[serde(default)]
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
pub slug: String,
#[serde(rename = "page-type")]
pub page_type: PageType,
#[serde(deserialize_with = "t_or_vec", default)]
#[serde(
deserialize_with = "t_or_vec",
serialize_with = "serialize_t_or_vec",
default,
skip_serializing_if = "Vec::is_empty"
)]
pub status: Vec<FeatureStatus>,
#[serde(rename = "browser-compat", deserialize_with = "t_or_vec", default)]
#[serde(
rename = "browser-compat",
deserialize_with = "t_or_vec",
serialize_with = "serialize_t_or_vec",
default,
skip_serializing_if = "Vec::is_empty"
)]
pub browser_compat: Vec<String>,
#[serde(rename = "spec-urls", deserialize_with = "t_or_vec", default)]
#[serde(
rename = "spec-urls",
deserialize_with = "t_or_vec",
serialize_with = "serialize_t_or_vec",
default,
skip_serializing_if = "Vec::is_empty"
)]
pub spec_urls: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub original_slug: Option<String>,
#[serde(deserialize_with = "t_or_vec", default)]
#[serde(
deserialize_with = "t_or_vec",
serialize_with = "serialize_t_or_vec",
default,
skip_serializing_if = "Vec::is_empty"
)]
pub sidebar: Vec<String>,
#[serde(flatten)]
pub other: HashMap<String, Value>,
@@ -158,6 +186,12 @@ impl PageReader for Doc {
}
}

impl PageWriter for Doc {
fn write(&self) -> Result<(), DocError> {
write_doc(self)
}
}

impl PageLike for Doc {
fn url(&self) -> &str {
&self.meta.url
@@ -251,8 +285,8 @@ fn read_doc(path: impl Into<PathBuf>) -> Result<Doc, DocError> {
original_slug,
sidebar,
..
} = serde_yaml::from_str(fm)?;
let url = build_url(&slug, &locale, PageCategory::Doc)?;
} = serde_yaml_ng::from_str(fm)?;
let url = build_url(&slug, locale, PageCategory::Doc)?;
let path = full_path
.strip_prefix(root_for_locale(locale)?)?
.to_path_buf();
@@ -279,13 +313,72 @@ fn read_doc(path: impl Into<PathBuf>) -> Result<Doc, DocError> {
})
}

fn write_doc(doc: &Doc) -> Result<(), DocError> {
let path = doc.path();
let locale = doc.meta.locale;

let mut file_path = root_for_locale(locale)?.to_path_buf();
file_path.push(path);

let (fm, content_start) = split_fm(&doc.raw);
let fm = fm.ok_or(DocError::NoFrontmatter)?;
// Read original frontmatter to pass additional fields along,
// overwrite fields from meta
let mut frontmatter: FrontMatter = serde_yaml_ng::from_str(fm)?;
frontmatter = FrontMatter {
title: doc.meta.title.clone(),
short_title: doc.meta.short_title.clone(),
tags: doc.meta.tags.clone(),
slug: doc.meta.slug.clone(),
page_type: doc.meta.page_type,
status: doc.meta.status.clone(),
browser_compat: doc.meta.browser_compat.clone(),
spec_urls: doc.meta.spec_urls.clone(),
original_slug: doc.meta.original_slug.clone(),
sidebar: doc.meta.sidebar.clone(),
..frontmatter
};

if let Some(parent) = file_path.parent() {
std::fs::create_dir_all(parent)?;
}
let fm_str = fm_to_string(&frontmatter)?;

let file = fs::File::create(&file_path)?;
let mut buffer = BufWriter::new(file);
buffer.write_all(b"---\n")?;
buffer.write_all(fm_str.as_bytes())?;
buffer.write_all(b"---\n")?;

buffer.write_all(doc.raw[content_start..].as_bytes())?;

Ok(())
}

fn fm_to_string(fm: &FrontMatter) -> Result<String, DocError> {
let fm_str = serde_yaml_ng::to_string(fm)?;
Ok(pretty_yaml::format_text(
&fm_str,
&FormatOptions {
language: LanguageOptions {
quotes: pretty_yaml::config::Quotes::PreferDouble,
indent_block_sequence_in_map: true,
..Default::default()
},
..Default::default()
},
)?)
}

pub fn render_md_to_html(input: &str, locale: Locale) -> Result<String, DocError> {
let html = m2h(input, locale)?;
Ok(html)
}

#[cfg(test)]
mod tests {
use indoc::indoc;

use super::*;

#[test]
@@ -295,30 +388,39 @@ mod tests {
- non-standard
- experimental
"#;
let meta = serde_yaml::from_str::<FrontMatter>(fm).unwrap();
let meta = serde_yaml_ng::from_str::<FrontMatter>(fm).unwrap();
assert_eq!(meta.status.len(), 2);

let fm = r#"
status: experimental
"#;
let meta = serde_yaml::from_str::<FrontMatter>(fm).unwrap();
let meta = serde_yaml_ng::from_str::<FrontMatter>(fm).unwrap();
assert_eq!(meta.status.len(), 1);
}

#[test]
fn browser_compat_test() {
let fm = r#"
browser-compat:
- foo
- ba
"#;
let meta = serde_yaml::from_str::<FrontMatter>(fm).unwrap();
let fm = indoc!(
r#"
title: "007"
slug: foo
page-type: none
browser-compat:
- foo
- bar
foo:
- bar
"#
);
let meta = serde_yaml_ng::from_str::<FrontMatter>(fm).unwrap();
assert_eq!(meta.browser_compat.len(), 2);

assert_eq!(fm, fm_to_string(&meta).unwrap());

let fm = r#"
browser-compat: foo
"#;
let meta = serde_yaml::from_str::<FrontMatter>(fm).unwrap();
let meta = serde_yaml_ng::from_str::<FrontMatter>(fm).unwrap();
assert_eq!(meta.browser_compat.len(), 1);
}
}
2 changes: 1 addition & 1 deletion crates/rari-doc/src/pages/types/generic.rs
Original file line number Diff line number Diff line change
@@ -211,7 +211,7 @@ fn read_generic_page(
let raw = read_to_string(&full_path)?;
let (fm, content_start) = split_fm(&raw);
let fm = fm.ok_or(DocError::NoFrontmatter)?;
let fm: GenericPageFrontmatter = serde_yaml::from_str(fm)?;
let fm: GenericPageFrontmatter = serde_yaml_ng::from_str(fm)?;
let path = full_path.strip_prefix(root)?.to_path_buf();
let page = path.with_extension("");
let page = page.to_string_lossy();
4 changes: 2 additions & 2 deletions crates/rari-doc/src/resolve.rs
Original file line number Diff line number Diff line change
@@ -71,11 +71,11 @@ pub fn url_meta_from(url: &str) -> Result<UrlMeta<'_>, UrlError> {
})
}

pub fn build_url(slug: &str, locale: &Locale, typ: PageCategory) -> Result<String, DocError> {
pub fn build_url(slug: &str, locale: Locale, typ: PageCategory) -> Result<String, DocError> {
Ok(match typ {
PageCategory::Doc => concat_strs!("/", locale.as_url_str(), "/docs/", slug),
PageCategory::BlogPost => concat_strs!("/", locale.as_url_str(), "/blog/", slug, "/"),
PageCategory::SPA => SPA::from_slug(slug, *locale)
PageCategory::SPA => SPA::from_slug(slug, locale)
.ok_or(DocError::PageNotFound(slug.to_string(), PageCategory::SPA))?
.url()
.to_owned(),
21 changes: 20 additions & 1 deletion crates/rari-doc/src/utils.rs
Original file line number Diff line number Diff line change
@@ -14,7 +14,8 @@ use rari_types::error::EnvError;
use rari_types::globals::{blog_root, content_root, content_translated_root};
use rari_types::locale::{Locale, LocaleError};
use serde::de::{self, value, SeqAccess, Visitor};
use serde::{Deserialize, Deserializer, Serializer};
use serde::ser::SerializeSeq;
use serde::{Deserialize, Deserializer, Serialize, Serializer};

use crate::error::DocError;
use crate::pages::page::PageCategory;
@@ -87,6 +88,24 @@ where
deserializer.deserialize_any(TOrVec::<T>(PhantomData))
}

pub fn serialize_t_or_vec<T, S>(value: &Vec<T>, serializer: S) -> Result<S::Ok, S::Error>
where
T: Serialize,
S: Serializer,
{
if value.len() == 1 {
// Serialize as a single element
value[0].serialize(serializer)
} else {
// Serialize as a sequence
let mut seq = serializer.serialize_seq(Some(value.len()))?;
for item in value {
seq.serialize_element(item)?;
}
seq.end()
}
}

pub fn root_for_locale(locale: Locale) -> Result<&'static Path, EnvError> {
match locale {
Locale::EnUs => Ok(content_root()),
9 changes: 8 additions & 1 deletion crates/rari-tools/Cargo.toml
Original file line number Diff line number Diff line change
@@ -9,15 +9,22 @@ rust-version.workspace = true
[dependencies]
rari-types.workspace = true
rari-utils.workspace = true
rari-doc.workspace = true
thiserror.workspace = true
serde.workspace = true
serde_json.workspace = true
chrono.workspace = true
tracing.workspace = true
reqwest.workspace = true
url.workspace = true
indoc.workspace = true

console = "0"
dialoguer = "0"
csv = "1"


[dev-dependencies]
serial_test = { version = "3", features = ["file_locks"] }
rari-types = { workspace = true, features = ["testing"] }
fake = { version = "2", features = ["chrono", "serde_json"] }
rand = "0"
54 changes: 54 additions & 0 deletions crates/rari-tools/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use std::borrow::Cow;

use rari_doc::error::{DocError, UrlError};
use rari_types::error::EnvError;
use rari_types::locale::LocaleError;
use rari_utils::error::RariIoError;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum ToolError {
#[error("Invalid slug: {0}")]
InvalidSlug(Cow<'static, str>),
#[error("Git error: {0}")]
GitError(String),

#[error(transparent)]
LocaleError(#[from] LocaleError),
#[error(transparent)]
DocError(#[from] DocError),
#[error(transparent)]
EnvError(#[from] EnvError),
#[error(transparent)]
UrlError(#[from] UrlError),
#[error(transparent)]
IoError(#[from] std::io::Error),
#[error(transparent)]
RariIoError(#[from] RariIoError),
#[error(transparent)]
JsonError(#[from] serde_json::Error),

#[error("Invalid Redirection: {0}")]
InvalidRedirectionEntry(String),
#[error("Error reading redirects file: {0}")]
ReadRedirectsError(String),
#[error("Error writing redirects file: {0}")]
WriteRedirectsError(String),
#[error("Invalid 'from' URL for redirect: {0}")]
InvalidRedirectFromURL(String),
#[error("Invalid 'to' URL for redirect: {0}")]
InvalidRedirectToURL(String),
#[error(transparent)]
RedirectError(#[from] RedirectError),

#[error("Unknonwn error")]
Unknown(&'static str),
}

#[derive(Debug, Clone, Error)]
pub enum RedirectError {
#[error("RedirectError: {0}")]
Cycle(String),
#[error("No cased version {0}")]
NoCased(String),
}
5 changes: 5 additions & 0 deletions crates/rari-tools/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
pub mod error;
pub mod history;
pub mod r#move;
pub mod popularities;
pub mod redirects;
#[cfg(test)]
pub mod tests;
pub mod wikihistory;
588 changes: 588 additions & 0 deletions crates/rari-tools/src/move.rs

Large diffs are not rendered by default.

1,016 changes: 945 additions & 71 deletions crates/rari-tools/src/redirects.rs

Large diffs are not rendered by default.

141 changes: 141 additions & 0 deletions crates/rari-tools/src/tests/fixtures/docs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
use std::fs;
use std::path::PathBuf;

use fake::faker::lorem::en::Paragraph;
use fake::Fake;
use indoc::formatdoc;
use rari_doc::pages::page::PageCategory;
use rari_doc::resolve::{build_url, url_meta_from, UrlMeta};
use rari_doc::utils::root_for_locale;
use rari_types::locale::Locale;

pub(crate) struct DocFixtures {
// files: Vec<String>,
locale: Locale,
do_not_remove: bool,
}

impl DocFixtures {
pub fn new(slugs: &[String], locale: Locale) -> Self {
Self::new_internal(slugs, locale, false)
}

#[allow(dead_code)]
pub fn debug_new(slugs: &[String], locale: Locale) -> Self {
Self::new_internal(slugs, locale, true)
}

fn new_internal(slugs: &[String], locale: Locale, do_not_remove: bool) -> Self {
// create doc file for each slug in the vector, in the configured root directory for the locale

// Iterate over each slug and create a file in the root directory
let _files: Vec<String> = slugs
.iter()
.map(|slug| Self::create_doc_file(slug, locale))
.collect();

DocFixtures {
// files,
locale,
do_not_remove,
}
}

fn capitalize(s: &str) -> String {
if s.is_empty() {
return String::new();
}
let mut chars = s.chars();
let first = chars.next().unwrap().to_uppercase().to_string();
let rest: String = chars.collect::<String>();
first + &rest
}

fn path_from_slug(slug: &str, locale: Locale) -> PathBuf {
let mut folder_path = PathBuf::new();
folder_path.push(locale.as_folder_str());
let url = build_url(slug, locale, PageCategory::Doc).unwrap();
let UrlMeta {
folder_path: path, ..
} = url_meta_from(&url).unwrap();
folder_path.push(path);
folder_path
}

fn create_doc_file(slug: &str, locale: Locale) -> String {
let slug_components = slug.split('/').collect::<Vec<&str>>();

let mut current_slug = String::new();
let locale_root = root_for_locale(locale).unwrap();

for slug_component in slug_components {
current_slug.push_str(slug_component);

let folder_path = Self::path_from_slug(current_slug.as_str(), locale);
let abs_folder_path = locale_root.join(&folder_path);

let title = Self::capitalize(current_slug.split("/").last().unwrap());
let content = formatdoc! {
r#"---
title: {}
slug: {}
---
{}
"#,
title,
current_slug,
Paragraph(1..3).fake::<String>()
};
// first create the parent path
fs::create_dir_all(&abs_folder_path).unwrap();
let path = abs_folder_path.join("index.md");
// overwrite file if it exists
if fs::exists(&path).unwrap() {
if path.is_dir() {
println!(
"File path is a directory - replacing with file: {}",
path.to_string_lossy()
);
fs::remove_dir_all(&path).unwrap();
fs::write(&path, content).unwrap();
}
} else {
fs::write(&path, content).unwrap();
}
current_slug.push('/');
}

let path = locale_root
.join(Self::path_from_slug(current_slug.as_str(), locale))
.join("index.md");
path.to_string_lossy().to_string()
}
}

impl Drop for DocFixtures {
fn drop(&mut self) {
if self.do_not_remove {
println!("Leaving doc fixtures in place for debugging");
return;
}
// Perform cleanup actions, recursively remove all files
// in the locale folder
let path = root_for_locale(self.locale)
.unwrap()
.join(self.locale.as_folder_str());
let entries = fs::read_dir(&path).unwrap();

for entry in entries {
let entry = entry.unwrap();
let path = entry.path();

if path.is_dir() {
fs::remove_dir_all(&path).unwrap();
} else {
fs::remove_file(&path).unwrap();
}
}
fs::remove_dir_all(&path).unwrap();
}
}
3 changes: 3 additions & 0 deletions crates/rari-tools/src/tests/fixtures/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod docs;
pub mod redirects;
pub mod wikihistory;
65 changes: 65 additions & 0 deletions crates/rari-tools/src/tests/fixtures/redirects.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use std::fs;
use std::path::PathBuf;

use rari_doc::utils::root_for_locale;
use rari_types::locale::Locale;

pub(crate) struct RedirectFixtures {
path: PathBuf,
do_not_remove: bool,
}

impl RedirectFixtures {
pub fn new(entries: &Vec<(String, String)>, locale: Locale) -> Self {
Self::new_internal(entries, locale, false)
}
#[allow(dead_code)]
pub fn debug_new(entries: &Vec<(String, String)>, locale: Locale) -> Self {
Self::new_internal(entries, locale, true)
}

fn new_internal(entries: &Vec<(String, String)>, locale: Locale, do_not_remove: bool) -> Self {
// create wiki history file for each slug in the vector, in the configured root directory for the locale
let mut folder_path = PathBuf::new();
folder_path.push(root_for_locale(locale).unwrap());
folder_path.push(locale.as_folder_str());
fs::create_dir_all(&folder_path).unwrap();
folder_path.push("_redirects.txt");

let mut content = String::new();
for (from, to) in entries {
content.push_str(
format!(
"/{}/{}\t/{}/{}\n",
locale.as_url_str(),
from,
locale.as_url_str(),
to
)
.as_str(),
);
}
content.push('\n');

fs::write(&folder_path, content).unwrap();

RedirectFixtures {
path: folder_path,
do_not_remove,
}
}
}

impl Drop for RedirectFixtures {
fn drop(&mut self) {
if self.do_not_remove {
println!(
"Leaving redirects fixture {} in place for debugging",
self.path.display()
);
return;
}

fs::remove_file(&self.path).unwrap();
}
}
92 changes: 92 additions & 0 deletions crates/rari-tools/src/tests/fixtures/wikihistory.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
use std::collections::BTreeMap;
use std::fs;
use std::path::PathBuf;

use chrono::{DateTime, SecondsFormat};
use fake::faker::chrono::en::DateTimeBetween;
use fake::faker::internet::en::Username;
use fake::Fake;
use rari_doc::utils::root_for_locale;
use rari_types::locale::Locale;
use serde_json::Value;

#[allow(dead_code)]
pub(crate) struct WikihistoryFixtures {
path: PathBuf,
do_not_remove: bool,
}

impl WikihistoryFixtures {
pub fn new(slugs: &Vec<String>, locale: Locale) -> Self {
Self::new_internal(slugs, locale, false)
}
#[allow(dead_code)]
pub fn debug_new(slugs: &Vec<String>, locale: Locale) -> Self {
Self::new_internal(slugs, locale, true)
}
fn new_internal(slugs: &Vec<String>, locale: Locale, do_not_remove: bool) -> Self {
// create wiki history file for each slug in the vector, in the configured root directory for the locale
let mut folder_path = PathBuf::new();
folder_path.push(root_for_locale(locale).unwrap());
folder_path.push(locale.as_folder_str());
fs::create_dir_all(&folder_path).unwrap();
folder_path.push("_wikihistory.json");

let mut entries: BTreeMap<String, Value> = BTreeMap::new();
for slug in slugs {
let value: BTreeMap<String, Value> = BTreeMap::from([
(
"modified".to_string(),
Value::String(random_date_rfc3339_string()),
),
("contributors".to_string(), Value::Array(random_names())),
]);
let map: serde_json::Map<String, Value> = value.into_iter().collect();
entries.insert(slug.to_string(), Value::Object(map));
}

let mut json_string = serde_json::to_string_pretty(&entries).unwrap();
json_string.push('\n');
fs::write(&folder_path, json_string).unwrap();

WikihistoryFixtures {
path: folder_path,
do_not_remove,
}
}
}

impl Drop for WikihistoryFixtures {
fn drop(&mut self) {
if self.do_not_remove {
println!(
"Leaving wikihistory fixture {} in place for debugging",
self.path.display()
);
return;
}

fs::remove_file(&self.path).unwrap();
}
}

fn random_names() -> Vec<Value> {
let num_entries = rand::random::<u8>() % 10 + 1;
let names: Vec<Value> = (0..num_entries)
.map(|_| Value::String(Username().fake()))
.collect();
names
}

fn random_date_rfc3339_string() -> String {
DateTimeBetween(
DateTime::parse_from_rfc3339("2015-01-01T00:00:00Z")
.unwrap()
.to_utc(),
DateTime::parse_from_rfc3339("2020-12-31T23:59:59Z")
.unwrap()
.to_utc(),
)
.fake::<DateTime<chrono::Utc>>()
.to_rfc3339_opts(SecondsFormat::Secs, true)
}
1 change: 1 addition & 0 deletions crates/rari-tools/src/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod fixtures;
39 changes: 39 additions & 0 deletions crates/rari-tools/src/wikihistory.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use std::collections::BTreeMap;
use std::fs::{self, File};
use std::io::{BufWriter, Write};
use std::path::Path;

use rari_doc::utils::root_for_locale;
use rari_types::locale::Locale;
use serde_json::Value;

use crate::error::ToolError;

pub fn update_wiki_history(locale: Locale, pairs: &Vec<(String, String)>) -> Result<(), ToolError> {
// Construct the path to "_wikihistory.json"
let locale_content_root = root_for_locale(locale)?;
let wiki_history_path = Path::new(locale_content_root)
.join(locale.as_folder_str())
.join("_wikihistory.json");

// Read the content of the JSON file
let wiki_history_content = fs::read_to_string(&wiki_history_path)?;

// Parse the JSON content into a BTreeMap (sorted map)
let mut all: BTreeMap<String, Value> = serde_json::from_str(&wiki_history_content)?;

for (old_slug, new_slug) in pairs {
if let Some(to) = all.remove(old_slug) {
all.insert(new_slug.to_string(), to);
}
}

let file = File::create(&wiki_history_path)?;
let mut buffer = BufWriter::new(file);
// Write the updated pretty JSON back to the file
serde_json::to_writer_pretty(&mut buffer, &all)?;
// Add a trailing newline
buffer.write_all(b"\n")?;

Ok(())
}
4 changes: 4 additions & 0 deletions crates/rari-types/src/settings.rs
Original file line number Diff line number Diff line change
@@ -49,6 +49,10 @@ impl Settings {
"CONTENT_ROOT",
std::env::var("TESTING_CONTENT_ROOT").unwrap(),
);
std::env::set_var(
"CONTENT_TRANSLATED_ROOT",
std::env::var("TESTING_CONTENT_TRANSLATED_ROOT").unwrap(),
);
Self::new_internal()
}
#[cfg(not(feature = "testing"))]
File renamed without changes.
11 changes: 0 additions & 11 deletions tests/data/content/files/en-us/basic/index.md

This file was deleted.

Empty file.