1use once_cell::sync::Lazy;
3use serde::{Deserialize, Serialize};
4use std::{
5 collections::BTreeMap,
6 fs::{self, File},
7 io::BufReader,
8 path::PathBuf,
9 sync::RwLock,
10};
11use strum::{AsRefStr, EnumIter};
12use syn::{
13 parse_str, Attribute, AttributeArgs, Error, Lit, LitInt, LitStr, Meta, MetaList, MetaNameValue,
14 NestedMeta, Path, Result,
15};
16
17const END: &str = " \n";
19pub static OUTPUT_DIR_PATH: &str = "user_doc";
21static OUTPUT_FILE_NAME: &str = "user_doc.json";
23pub static CUSTOM_OUTPUT_FILE_NAME: Lazy<RwLock<Option<String>>> = Lazy::new(|| RwLock::new(None));
25pub static DOCS: Lazy<RwLock<DocDict>> =
27 Lazy::new(|| RwLock::new(DocDict(BTreeMap::new(), Vec::new())));
28pub const PREVIEW_TEXT_LENGTH: usize = 16;
30
31#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
32pub enum DirectoryNamingScheme {
34 ChapterName,
36 ChapterNumber,
38}
39
40#[derive(Clone, Serialize, Deserialize, Debug, Eq, Ord, PartialEq, PartialOrd)]
41pub enum Documentable {
44 Doc(String, String),
45 BoxedDocDict(String, Box<DocDict>),
46}
47
48impl core::fmt::Display for Documentable {
49 fn fmt(
50 &self,
51 f: &'_ mut core::fmt::Formatter,
52 ) -> core::fmt::Result {
53 match self {
54 Self::Doc(ref name, ref text) => {
55 write!(
56 f,
57 "Documentable::Doc(`{}`: {})",
58 name,
59 if text.len() > PREVIEW_TEXT_LENGTH {
60 let mut text = text.to_string();
61 text.truncate(PREVIEW_TEXT_LENGTH - 3);
62 format!("{}...", text)
63 } else {
64 text.to_string()
65 }
66 )
67 }
68 Self::BoxedDocDict(ref name, ref boxed_doc_dict) => {
69 write!(
70 f,
71 "Documentable::BoxedDocDict(`{}`: {} subchapters)",
72 name,
73 boxed_doc_dict.len()
74 )
75 }
76 }
77 }
78}
79
80#[allow(clippy::enum_variant_names)]
81#[derive(AsRefStr, Clone, EnumIter, Debug, Eq, Ord, PartialOrd, PartialEq)]
82#[strum(serialize_all = "snake_case")]
83pub enum HelperAttr {
110 ChapterNum(usize),
112 ChapterNumSlug(Vec<usize>),
114 ChapterName(String),
116 ChapterNameSlug(Vec<String>),
118 ChapterBlurb(String),
120}
121
122impl From<&HelperAttr> for Path {
123 fn from(h: &HelperAttr) -> Self {
124 parse_str::<Self>(h.as_ref()).expect("Must create path from HelperAttr")
125 }
126}
127impl HelperAttr {
128 #[allow(clippy::ptr_arg)]
130 pub fn from_attribute_args(a: &AttributeArgs) -> Result<Vec<Self>> {
131 let mut selves = Vec::with_capacity(a.len());
132 for nested_meta in a {
133 match nested_meta {
134 NestedMeta::Meta(ref meta) => match meta {
135 Meta::Path(ref path) => {
136 return Err(Error::new_spanned(path, "unsupported attribute subpath"))
137 }
138 Meta::List(MetaList {
139 ref path,
140 ref nested,
141 ..
142 }) => {
143 if path.is_ident(Self::ChapterNameSlug(vec![]).as_ref()) {
144 let mut slugs = vec![];
145 for nested_meta in nested.iter() {
146 match nested_meta {
147 NestedMeta::Lit(Lit::Str(ref lit_str)) => {
148 slugs.push(lit_str.value());
149 }
150 _ => {
151 return Err(Error::new_spanned(
152 nested_meta,
153 "Unsupported nested meta attribute ",
154 ))
155 }
156 }
157 }
158 selves.push(Self::ChapterNameSlug(slugs));
159 } else if path.is_ident(Self::ChapterNumSlug(Vec::new()).as_ref()) {
160 let mut slugs = vec![];
161 for nested_meta in nested.iter() {
162 match nested_meta {
163 NestedMeta::Lit(Lit::Int(ref lit_int)) => {
164 slugs.push(lit_int.base10_parse()?);
165 }
166 _ => {
167 return Err(Error::new_spanned(
168 nested_meta,
169 "Unsupported nested meta attribute ",
170 ))
171 }
172 }
173 }
174 selves.push(Self::ChapterNumSlug(slugs));
175 } else {
176 return Err(Error::new_spanned(path, "Unsupported meta list path"));
177 }
178 }
179 Meta::NameValue(MetaNameValue {
180 ref path, ref lit, ..
181 }) => {
182 if path.is_ident(Self::ChapterName(String::new()).as_ref()) {
183 match lit {
184 Lit::Str(ref lit_str) => {
185 selves.push(Self::ChapterName(lit_str.value()));
186 }
187 bad_lit => {
188 return Err(Error::new_spanned(
189 bad_lit,
190 "Unsupported chapter name literal",
191 ));
192 }
193 }
194 } else if path.is_ident(Self::ChapterNum(0usize).as_ref()) {
195 match lit {
196 Lit::Int(ref lit_int) => {
197 selves.push(Self::ChapterNum(lit_int.base10_parse()?));
198 }
199 bad_lit => {
200 return Err(Error::new_spanned(
201 bad_lit,
202 "Unsupported chapter number literal",
203 ));
204 }
205 }
206 } else if path.is_ident(Self::ChapterBlurb(String::new()).as_ref()) {
207 match lit {
208 Lit::Str(ref lit_str) => {
209 selves.push(Self::ChapterBlurb(lit_str.value()));
210 }
211 bad_lit => {
212 return Err(Error::new_spanned(
213 bad_lit,
214 "Unsupported chapter blurb literal",
215 ));
216 }
217 }
218 } else {
219 return Err(Error::new_spanned(
220 path,
221 "unrecognized helper attribute inner",
222 ));
223 }
224 }
225 },
226 _ => {
227 return Err(Error::new_spanned(
228 nested_meta,
229 "unrecognized helper attribute",
230 ));
231 }
232 }
233 }
234 Ok(selves)
235 }
236
237 pub fn from_attribute(a: &Attribute) -> Result<Self> {
239 let Attribute {
240 path, tokens: _, ..
241 } = a;
242 if path.is_ident(&Self::ChapterNum(0).as_ref()) {
243 let chapter_num_lit_int = a.parse_args::<LitInt>()?;
244 Ok(Self::ChapterNum(chapter_num_lit_int.base10_parse()?))
245 } else if path.is_ident(&Self::ChapterName(String::new()).as_ref()) {
246 let chapter_name_lit_str = a.parse_args::<LitStr>()?;
247 Ok(Self::ChapterName(chapter_name_lit_str.value()))
248 } else if path.is_ident(&Self::ChapterBlurb(String::new()).as_ref()) {
249 let chapter_name_blurb_str = a.parse_args::<LitStr>()?;
250 Ok(Self::ChapterBlurb(chapter_name_blurb_str.value()))
251 } else if path.is_ident(&Self::ChapterNameSlug(Vec::new()).as_ref()) {
252 let meta = a.parse_meta()?;
253 match meta {
254 Meta::List(MetaList { nested, .. }) => {
255 let segments: Vec<String> = nested
256 .iter()
257 .filter_map(|nested_meta| match nested_meta {
258 NestedMeta::Lit(Lit::Str(lit_str)) => Some(lit_str.value()),
259 _ => None,
260 })
261 .collect();
262 Ok(Self::ChapterNameSlug(segments))
263 }
264 bad => Err(Error::new_spanned(
265 bad,
266 "unrecognized attribute payload for chapter_name_slug",
267 )),
268 }
269 } else if path.is_ident(&Self::ChapterNumSlug(Vec::new()).as_ref()) {
270 let meta = a.parse_meta()?;
271 match meta {
272 Meta::List(MetaList { nested, .. }) => {
273 let mut segments_results: Vec<Result<usize>> = nested
274 .iter()
275 .filter_map(|nested_meta| match nested_meta {
276 NestedMeta::Lit(Lit::Int(lit_int)) => Some(lit_int.base10_parse()),
277 _ => None,
278 })
279 .collect();
280 let mut segments: Vec<usize> = Vec::with_capacity(segments_results.len());
281 for segment_result in segments_results.drain(0..) {
282 segments.push(segment_result?);
283 }
284 Ok(Self::ChapterNumSlug(segments))
285 }
286 bad => Err(Error::new_spanned(
287 bad,
288 "unrecognized attribute payload for chapter_name_slug",
289 )),
290 }
291 } else {
292 Err(Error::new_spanned(path, "unrecognized helper attribute"))
293 }
294 }
295}
296
297pub type HeaderLevel = u8;
299fn constrain_header_level(header_level: HeaderLevel) -> HeaderLevel {
301 header_level.clamp(0, 6)
302}
303fn make_header(header_level: HeaderLevel) -> String {
305 let mut s = String::new();
306 for _i in 0..header_level {
307 s.push('#');
308 }
309 s
310}
311fn make_named_header(
313 header_level: HeaderLevel,
314 header_name: &str,
315) -> String {
316 format!("{} {}", make_header(header_level), header_name)
317}
318fn make_separator() -> String {
320 "---".to_string()
321}
322fn make_chapter_num_string(chap_nums_vec: &[usize]) -> String {
323 let mut s = String::new();
324 for n in chap_nums_vec.iter() {
325 s = format!("{}.{}", s, n);
326 }
327 s
328}
329impl core::cmp::PartialEq<&str> for Documentable {
330 fn eq(
331 &self,
332 rhs: &&str,
333 ) -> bool {
334 match self {
335 Self::Doc(_, contents) => contents.eq(rhs),
336 _ => false,
337 }
338 }
339}
340impl Documentable {
341 pub fn get_inner_boxed_doc_dict_mut_ref(&mut self) -> Option<&mut Box<DocDict>> {
343 match self {
344 Self::Doc(_, _) => None,
345 Self::BoxedDocDict(_, ref mut boxed_doc_dict) => Some(boxed_doc_dict),
346 }
347 }
348
349 pub fn name(&self) -> String {
351 match self {
352 Self::Doc(ref name, ..) => name.clone(),
353 Self::BoxedDocDict(ref name, ..) => name.clone(),
354 }
355 }
356
357 pub fn to_string(
359 &self,
360 header_level: HeaderLevel,
361 chapter_nums_opt: Option<Vec<usize>>,
362 ) -> String {
363 let chapter_nums = chapter_nums_opt.unwrap_or_default();
364 let header_level = constrain_header_level(header_level);
365 let mut s = String::new();
366 match self {
367 Documentable::Doc(ref name, ref doc_text) => {
368 s.push_str(&format!(
369 "{}{}{}",
370 make_named_header(header_level, name),
371 END,
372 doc_text
373 ));
374 }
375 Documentable::BoxedDocDict(ref name, ref boxed_doc_dict) => {
376 let count = boxed_doc_dict.len();
377 for (subchapter_num, (subchapter_name, documentable)) in boxed_doc_dict.iter() {
378 let mut chapter_nums = chapter_nums.clone();
379 chapter_nums.push(*subchapter_num);
380 let sep_string = if *subchapter_num < count - 1 {
381 make_separator()
382 } else {
383 String::new()
384 };
385 s.push_str(&format!(
386 "{}{}
387 {}{}
388 {}{}
389 {}{}
390 {}{}",
391 make_named_header(header_level, name),
392 END,
393 make_chapter_num_string(&chapter_nums),
394 END,
395 make_named_header(header_level + 1, subchapter_name),
396 END,
397 documentable.to_string(header_level + 1, Some(chapter_nums),),
398 END,
399 sep_string,
400 END
401 ))
402 }
403 }
404 }
405
406 s
407 }
408}
409
410pub type DocDictEntryValueType = (String, Documentable);
412pub type DocDictTree = BTreeMap<usize, DocDictEntryValueType>;
414#[derive(Clone, Default, Serialize, Deserialize, Debug, Eq, Ord, PartialEq, PartialOrd)]
415pub struct DocDict(
417 pub DocDictTree,
419 pub Vec<(Vec<usize>, String)>,
421);
422impl DocDict {
423 pub fn add_entry(
425 &mut self,
426 documentable: Documentable,
427 name_opt: Option<String>,
428 number_opt: Option<usize>,
429 overwrite_opt: Option<bool>,
430 ) -> anyhow::Result<()> {
431 let number = number_opt.unwrap_or_default();
432 let name = name_opt.unwrap_or_else(|| documentable.name()); let (already_has_number, already_has_name) = self.iter().fold(
434 (false, false),
435 |(has_number, has_name), (num_i, (nam_i, _))| {
436 (has_number || num_i == &number, has_name || nam_i == &name)
437 },
438 );
439 match (already_has_number, already_has_name) {
441 (false, false) => {
442 self.insert(number, (name, documentable));
443 Ok(())
444 }
445 (false, true) => {
446 self.insert(number, (name, documentable));
448 Ok(())
449 }
450 (true, false) => {
451 let number = self.find_next_entry_number();
453 self.insert(number, (name, documentable));
454 Ok(())
455 }
456 (true, true) => {
457 let overwrite_falsy_message = format!(
458 "Attempted to insert duplicate entry for (chapter, title) ({} {}).
459 \nTry setting overwrite_opt param to Some(true)",
460 number, name
461 );
462 if let Some(must_overwrite) = overwrite_opt {
463 if must_overwrite {
464 self.insert(number, (name, documentable));
465 Ok(())
466 } else {
467 anyhow::bail!(overwrite_falsy_message)
468 }
469 } else {
470 anyhow::bail!(overwrite_falsy_message);
472 }
473 }
474 }
475 }
476
477 pub fn add_path(
481 &mut self,
482 chapter_blurb_opt: &Option<String>,
483 name_opt: &Option<String>,
484 documentable_opt: Option<Documentable>,
485 overwrite_opt: Option<bool>,
486 path_names: &[String],
487 path_numbers: &[usize],
488 ) -> anyhow::Result<()> {
489 use anyhow::Context;
490 let mut i = 0;
492 let mut subdict = self;
493 let mut paths_found = vec![false; path_numbers.len()];
494 while i < path_numbers.len() {
495 let chapter_num = path_numbers
496 .get(i)
497 .cloned()
498 .unwrap_or_else(|| subdict.find_next_entry_number());
499 let chapter_name = path_names
500 .get(i)
501 .or(name_opt.as_ref())
502 .cloned()
503 .unwrap_or_default();
504 let empty_chapter = Documentable::BoxedDocDict(
505 chapter_name.clone(),
506 Box::new(DocDict(BTreeMap::new(), Vec::new())),
507 );
508 if !subdict
509 .iter()
510 .any(|(num, (_name, _contents))| num == &chapter_num)
511 {
512 let documentable = if i == path_numbers.len() - 1 {
513 documentable_opt.clone().unwrap_or(empty_chapter)
515 } else {
516 empty_chapter
518 };
519 subdict.add_entry(
521 documentable,
522 Some(chapter_name.clone()),
523 Some(chapter_num),
524 overwrite_opt,
525 )?;
526 if let Some(ref chapter_blurb) = chapter_blurb_opt {
528 let back_link = path_numbers.to_vec();
529 subdict.1.push((back_link, chapter_blurb.to_string()));
530 }
531 } else {
533 paths_found[i] = true;
535 }
536 if i < path_numbers.len() - 1 {
537 subdict = subdict
539 .get_mut(&chapter_num)
540 .with_context(|| {
541 format!(
542 "Must get new chapter ({} (name: {}))",
543 chapter_num, chapter_name,
544 )
545 })?
546 .1
547 .get_inner_boxed_doc_dict_mut_ref()
548 .with_context(|| {
549 format!(
550 "Must get newly inserted chapter ({} (name: {}))",
551 chapter_num, chapter_name,
552 )
553 })?;
554 }
555 i += 1;
557 }
558 Ok(())
559 }
560
561 pub fn deep_iter(
568 &self,
569 start_slug_opt: Option<Vec<usize>>,
570 ) -> std::collections::vec_deque::IntoIter<(Vec<usize>, &DocDictEntryValueType, usize)> {
571 use std::collections::VecDeque;
572 let mut vv: VecDeque<(Vec<usize>, &DocDictEntryValueType, usize)> = VecDeque::new();
573 let start_slug = start_slug_opt.unwrap_or_default();
574 if !self.0.is_empty() {
575 for (k, entry) in self.0.iter() {
577 let mut iter_slug = start_slug.clone();
579 iter_slug.push(*k);
580 if let Documentable::BoxedDocDict(_, dd) = &entry.1 {
581 vv.push_back((iter_slug.clone(), entry, (*dd).len()));
582 vv.extend(dd.deep_iter(Some(iter_slug)))
584 } else {
585 vv.push_back((iter_slug, entry, 0usize));
586 }
587 }
588 }
589 vv.into_iter()
590 }
591
592 pub fn expand_into_mdbook_dirs_at_path(
594 &self,
595 naming_scheme: DirectoryNamingScheme,
596 root_path: &str,
597 ) -> anyhow::Result<()> {
598 use anyhow::Context;
599 const README_NAME: &str = "README";
600 let dir_path: PathBuf = root_path.into();
601 if !dir_path.is_dir() {
602 fs::create_dir_all(dir_path.clone())
603 .with_context(|| format!("Must create root path {:?}", dir_path))?;
604 }
605 let one_indentation = " ";
606 let indent_for_depth = |depth: usize| -> String {
607 let v = vec![one_indentation; depth];
608 v.join("")
609 };
610 let mut slugs_to_paths: BTreeMap<Vec<usize>, PathBuf> = BTreeMap::new();
611 let mut summary_md_contents = String::from("# Summary");
612 if !self.1.is_empty() {
613 for chapter_summary_line in self.1.iter() {
614 summary_md_contents.push_str(&format!("\n{} ", chapter_summary_line.1));
615 }
616 }
617 for (iter_slug, (name, documentable), _sub_entries_len) in self.deep_iter(None) {
618 let depth = iter_slug.len() - 1;
619 if depth == 0 {
620 }
623 let mut subdir_path = slugs_to_paths
624 .get(&iter_slug[0..iter_slug.len() - 1])
625 .cloned()
626 .unwrap_or_else(|| dir_path.clone());
627 let number = iter_slug.last().expect("must get default name");
628 let name = if name.is_empty() {
629 format!("{}", number)
630 } else {
631 format!("{} - {}", number, name)
632 };
633 let mut contents_name = iter_slug[1..]
634 .iter()
635 .fold(iter_slug[0].to_string(), |s, ii| format!("{}.{}", s, ii));
636 match documentable {
637 Documentable::Doc(ref doc_name, ref contents) => {
638 subdir_path.push(name.clone());
639 subdir_path.set_extension("md");
640 let out_contents = format!("# {} \n{}", name, contents);
641 fs::write(subdir_path.clone(), out_contents)
642 .with_context(|| format!("must write to file {:?}", subdir_path))?;
643 if !doc_name.is_empty() {
644 contents_name.push_str(&format!(" - {}", doc_name));
645 }
646 summary_md_contents.push_str(&format!(
647 "\n{}- [{}](<{}>) ",
648 indent_for_depth(depth),
649 contents_name,
650 subdir_path
651 .strip_prefix(dir_path.clone())
652 .with_context(|| "Must create subdir path for summary.md".to_string())?
653 .to_string_lossy()
654 ));
655 }
656 Documentable::BoxedDocDict(chapter_name, boxed_doc_dict) => {
657 match &naming_scheme {
659 DirectoryNamingScheme::ChapterName => {
661 subdir_path.push(name.clone());
662 }
663 DirectoryNamingScheme::ChapterNumber => {
665 subdir_path.push(
666 iter_slug
667 .iter()
668 .last()
669 .expect("must get last slug element")
670 .to_string(),
671 );
672 }
673 }
674 fs::create_dir_all(subdir_path.clone())
676 .with_context(|| format!("Must create subdir path {:?}", subdir_path))?;
677 slugs_to_paths.insert(iter_slug.clone(), subdir_path.to_path_buf());
678 let mut chapter_readme_path = subdir_path.clone();
680 chapter_readme_path.push(README_NAME);
681 chapter_readme_path.set_extension("md");
682 contents_name.push_str(&format!(" - {}", chapter_name));
683 summary_md_contents.push_str(&format!(
684 "\n{}- [{}](<{}>) ",
685 indent_for_depth(depth),
686 contents_name,
687 chapter_readme_path
688 .strip_prefix(dir_path.clone())
689 .with_context(|| format!(
690 "Must create relative chapter readme path from {:?}",
691 dir_path
692 ))?
693 .to_string_lossy()
694 ));
695 let mut chapter_readme_contents = format!("# {}", chapter_name);
697
698 if !boxed_doc_dict.1.is_empty() {
699 for (chapter_i, chapter_documentable) in boxed_doc_dict.0.iter() {
701 let mut forward_link: PathBuf = match &naming_scheme {
702 DirectoryNamingScheme::ChapterName => {
704 let chapter_name = chapter_documentable.1.name();
705 if chapter_name.is_empty() {
706 format!("{}", chapter_i)
707 } else {
708 format!("{} - {}", chapter_i, chapter_name)
709 }
710 }
711 DirectoryNamingScheme::ChapterNumber => chapter_i.to_string(),
713 }
714 .into();
715 forward_link.set_extension("md");
716 let chapter_readme_blurb = boxed_doc_dict
717 .1
718 .iter()
719 .find(|(backlink, _blurb)| backlink == &iter_slug)
720 .map(|(_backlink, blurb)| blurb.to_string())
721 .unwrap_or_else(|| {
722 format!(
723 "\n- [Skip to {} ({})](<{}>) ",
724 chapter_documentable.0,
725 chapter_i,
726 forward_link.to_string_lossy(),
727 )
728 });
729
730 chapter_readme_contents.push_str(&format!("\n{}", chapter_readme_blurb));
731 }
732 }
733 fs::write(chapter_readme_path.clone(), chapter_readme_contents)
734 .with_context(|| format!("must write to summary.md file {:?}", chapter_readme_path))?;
735 }
736 }
737 }
738
739 let mut summary_md_path = dir_path;
741 summary_md_path.push("SUMMARY");
742 summary_md_path.set_extension("md");
743 fs::write(summary_md_path.clone(), summary_md_contents)
744 .with_context(|| format!("must write to summary.md file {:?}", summary_md_path))?;
745 Ok(())
746 }
747
748 pub fn find_next_entry_number(&self) -> usize {
750 let mut n = 0usize;
751
752 while self.keys().cloned().any(|x| x == n) {
753 n += 1;
754 }
755 n
756 }
757
758 #[allow(clippy::unnecessary_unwrap)]
759 pub fn get_entry_at_numeric_path(
762 &self,
763 path: &[usize],
764 ) -> Option<&DocDictEntryValueType> {
765 let mut map_pointer: &Self = self;
766 let max_depth = path.len() - 1;
767 for (depth, index) in path.iter().enumerate() {
768 let entry_opt = map_pointer.get(index);
769 if entry_opt.is_some() {
770 if depth == max_depth {
771 return entry_opt;
773 } else {
774 match &entry_opt.unwrap().1 {
775 Documentable::Doc(_, _) => {
776 return None;
778 }
779 Documentable::BoxedDocDict(_name, ref boxed_sub_dict) => {
780 map_pointer = boxed_sub_dict;
782 }
783 }
784 }
785 } else {
786 return None;
787 }
788 }
789 None
790 }
791
792 #[allow(clippy::unnecessary_unwrap)]
793 pub fn get_mut_entry_at_numeric_path(
796 &mut self,
797 path: &[usize],
798 ) -> Option<&mut DocDictEntryValueType> {
799 let mut map_pointer: &mut Self = self;
800 let max_depth = path.len() - 1;
801 for (depth, index) in path.iter().enumerate() {
802 let entry_opt = map_pointer.get_mut(index);
803 if entry_opt.is_some() {
804 if depth == max_depth {
805 return entry_opt;
807 } else {
808 match &mut entry_opt.unwrap().1 {
809 Documentable::Doc(_, _) => {
810 return None;
812 }
813 Documentable::BoxedDocDict(_name, ref mut boxed_sub_dict) => {
814 map_pointer = &mut *boxed_sub_dict;
816 }
817 }
818 }
819 } else {
820 return None;
821 }
822 }
823 None
824 }
825}
826impl core::ops::Deref for DocDict {
827 type Target = DocDictTree;
828 fn deref(&self) -> &Self::Target {
829 &self.0
830 }
831}
832impl core::ops::DerefMut for DocDict {
833 fn deref_mut(&mut self) -> &mut Self::Target {
834 &mut self.0
835 }
836}
837
838pub fn get_persistence_dir_path() -> PathBuf {
840 std::env::temp_dir().join(OUTPUT_DIR_PATH)
841}
842
843pub fn get_persistence_file_path() -> anyhow::Result<PathBuf> {
845 CUSTOM_OUTPUT_FILE_NAME.read().map_or_else(
846 |_| anyhow::bail!("Must get read lock on CUSTOM_OUTPUT_FILE_NAME"),
847 |custom_output_file_name_lock| {
848 Ok(
849 get_persistence_dir_path().join(
850 custom_output_file_name_lock
851 .as_ref()
852 .cloned()
853 .unwrap_or_else(|| OUTPUT_FILE_NAME.to_string()),
854 ),
855 )
856 },
857 )
858}
859
860pub fn make_string_key(
862 num: usize,
863 name: String,
864) -> String {
865 format!("({})({})", num, name)
866}
867
868pub fn persist_docs() -> anyhow::Result<()> {
875 use anyhow::Context;
876 let dir_path = get_persistence_dir_path();
877 if !dir_path.is_dir() {
879 fs::create_dir_all(dir_path)?;
880 }
881 let complete_path: PathBuf = get_persistence_file_path()?;
882 let file = File::create(complete_path)?;
884 let docs = &*DOCS;
885 let docs_read_lock = docs
886 .read()
887 .map_err(|poison_error| anyhow::anyhow!(format!("{:#?}", poison_error)))
888 .with_context(|| "Must get read lock on DOCS")?;
889 serde_json::to_writer(file, &*docs_read_lock).with_context(|| {
891 format!(
892 "Must write JSON from DOCS to file: \nResult:\n{:#?}",
893 serde_json::to_string(&*docs_read_lock)
894 )
895 })?;
896 Ok(())
897}
898
899pub fn load_global_docs(
906 file_name_opt: Option<&str>,
907 doc_dict_opt: Option<&mut DocDict>,
908) -> anyhow::Result<()> {
909 use anyhow::Context;
910 let complete_path: PathBuf = if let Some(file_name) = file_name_opt {
911 get_persistence_dir_path().join(file_name)
912 } else {
913 get_persistence_file_path()?
914 };
915 if !complete_path.is_file() {
917 anyhow::bail!(format!(
918 "The target docs directory ({:?}) is not a file",
919 complete_path
920 ));
921 }
922 std::println!("loading global docs from complete_path {:?}", complete_path);
923 let file = File::open(complete_path)?;
924 let file_reader = BufReader::new(file);
925 let docs = &*DOCS;
926 let doc_dict: DocDict = serde_json::from_reader(file_reader)
927 .with_context(|| "Must read JSON from file into docs".to_string())?;
928 match doc_dict_opt {
929 Some(existing_doc_dict) => {
930 *existing_doc_dict = doc_dict;
931 }
932 None => {
933 let mut docs_write_lock = docs
934 .write()
935 .map_err(|poison_error| anyhow::anyhow!(format!("{:#?}", poison_error)))
936 .with_context(|| "Must get write lock on DOCS")?;
937 *docs_write_lock = doc_dict;
939 }
940 }
941 Ok(())
942}
943
944#[cfg(test)]
945mod tests {
946 use super::*;
947
948 #[test]
949 fn deep_iter() {
950 let mut d: DocDict = DocDict(
951 BTreeMap::new(),
952 vec![
953 (
954 vec![],
955 String::from("A book about some things. Words sdrwo."),
956 ),
957 (vec![], String::from("\nAnd some other things.")),
958 ],
959 );
960
961 let slugs = vec![
962 vec![
963 (1, "buff".to_string()),
964 (2, "aztec".to_string()),
965 (3, "priestess".to_string()),
966 ],
967 vec![
968 (1, "buff".to_string()),
969 (2, "aztec".to_string()),
970 (4, "priest".to_string()),
971 ],
972 vec![
973 (1, "buff".to_string()),
974 (2, "aztec".to_string()),
975 (5, "eagle warrior".to_string()),
976 ],
977 vec![
978 (1, "buff".to_string()),
979 (3, "maya".to_string()),
980 (2, "princess".to_string()),
981 ],
982 vec![
983 (1, "buff".to_string()),
984 (3, "maya".to_string()),
985 (5, "prince".to_string()),
986 ],
987 ];
988 let target_ord = vec![
989 slugs[0][0].clone(),
990 slugs[0][1].clone(),
991 slugs[0][2].clone(),
992 slugs[1][2].clone(),
993 slugs[2][2].clone(),
994 slugs[3][1].clone(),
995 slugs[3][2].clone(),
996 slugs[4][2].clone(),
997 ];
998 let target_num_slugs = vec![
999 vec![1],
1000 vec![1, 2],
1001 vec![1, 2, 3],
1002 vec![1, 2, 4],
1003 vec![1, 2, 5],
1004 vec![1, 3],
1005 vec![1, 3, 2],
1006 vec![1, 3, 5],
1007 ];
1008 let target_sub_entries_lens = vec![2, 3, 0, 0, 0, 2, 0, 0];
1009 for slug in slugs {
1010 let (path_numbers, path_names): (Vec<_>, Vec<_>) = slug.iter().cloned().unzip();
1011 let name = slug.last().unwrap().1.to_string();
1012 d.add_path(
1013 &None,
1014 &Some(name.clone()),
1015 Some(Documentable::Doc(name.clone(), "dummy".to_string())),
1016 None,
1017 &path_names,
1018 &path_numbers,
1019 )
1020 .expect("must add path");
1021 }
1022 for (i, (iter_slug, (name, documentable), sub_entries_len)) in d.deep_iter(None).enumerate() {
1023 let target = &target_ord[i];
1024 let target_num_slug = &target_num_slugs[i];
1025 let target_sub_entries_len = target_sub_entries_lens[i];
1026 assert_eq!(&target.1, name, "{} th target name must match", i);
1027 assert_eq!(
1028 target_num_slug, &iter_slug,
1029 "{} th target slug must match",
1030 i
1031 );
1032 assert_eq!(
1033 target_sub_entries_len, sub_entries_len,
1034 "{} th target sub entries length must match",
1035 i
1036 );
1037
1038 std::println!("{} {}", i, documentable);
1039 }
1040 }
1041}