use crate::Error;
use crate::IncludeToken;
use crate::ParseContainer;
use crate::PathResolver;
use crate::{PlantUmlLine, StartLine};
use nom::IResult;
use nom::{
error::{Error as NomError, ErrorKind as NomErrorKind},
Err as NomErr,
};
use std::collections::HashMap;
use std::fs::read_to_string;
use std::path::PathBuf;
use std::sync::Arc;
pub struct PlantUmlFile {
path: PathBuf,
data: PlantUmlFileData,
}
#[derive(Clone, Debug)]
pub struct PlantUmlFileData {
contents: Vec<PlantUmlContent>,
}
#[derive(Clone, Debug)]
pub struct PlantUmlContent {
lines: Vec<PlantUmlLine>,
includes_memo: Vec<IncludeToken>,
}
impl PlantUmlFile {
pub fn read(path: PathBuf) -> Result<Self, Error> {
let raw = read_to_string(&path)?;
let container = ParseContainer::new(Arc::new(raw));
let data = PlantUmlFileData::parse(container)?;
Ok(Self { path, data })
}
pub fn path(&self) -> &PathBuf {
&self.path
}
pub fn data(&self) -> &PlantUmlFileData {
&self.data
}
}
impl PlantUmlFileData {
pub fn parse_from_str<S>(input: S) -> Result<Self, Error>
where
S: Into<String>,
{
let input = ParseContainer::new(Arc::new(input.into()));
Self::parse(input)
}
pub fn parse(input: ParseContainer) -> Result<Self, Error> {
let mut contents = vec![];
let mut input = input;
let (mut rest, mut line) = PlantUmlLine::parse(input.clone())?;
'outer: while !rest.is_empty() {
while line.empty().is_some() {
(rest, line) = PlantUmlLine::parse(input.clone())?;
if line.empty().is_none() {
break;
}
if rest.is_empty() {
break 'outer;
}
input = rest;
}
let (tmp_rest, content) = PlantUmlContent::parse(input)?;
input = tmp_rest;
contents.push(content);
if input.is_empty() {
break;
}
(rest, line) = PlantUmlLine::parse(input.clone())?;
}
Ok(Self { contents })
}
pub fn get_by_token(&self, token: IncludeToken) -> Option<&PlantUmlContent> {
if let Some(content) = token.id().and_then(|id| self.get_by_id(id)) {
Some(content)
} else if let Some(content) = token.index().and_then(|index| self.get(index)) {
Some(content)
} else {
self.get(0)
}
}
pub fn get(&self, index: usize) -> Option<&PlantUmlContent> {
self.contents.get(index)
}
pub fn get_by_id(&self, id: &str) -> Option<&PlantUmlContent> {
self.contents.iter().find(|x| x.id() == Some(id))
}
pub fn iter(&self) -> std::slice::Iter<'_, PlantUmlContent> {
self.contents.iter()
}
}
impl PlantUmlContent {
fn parse(input: ParseContainer) -> IResult<ParseContainer, Self> {
let mut lines = vec![];
let mut includes_memo = vec![];
let (mut rest, mut line) = PlantUmlLine::parse(input.clone())?;
if line.start().is_none() {
StartLine::parse(input.clone())?;
return Err(NomErr::Failure(NomError::new(input, NomErrorKind::Fail)));
}
let line2 = line.clone();
let diagram_kind = line2.diagram_kind().unwrap();
lines.push(line.clone());
while line.end().is_none() {
let (tmp_rest, tmp_line) = PlantUmlLine::parse(rest)?;
(rest, line) = (tmp_rest, tmp_line);
if line.end().map(|x| x.eq_diagram_kind(diagram_kind)) == Some(false) {
return Err(NomErr::Failure(NomError::new(input, NomErrorKind::Fail)));
}
if let Some(line) = line.include() {
includes_memo.push(line.token());
}
lines.push(line.clone());
}
Ok((
rest,
Self {
lines,
includes_memo,
},
))
}
pub fn id(&self) -> Option<&str> {
self.lines[0].start().unwrap().id()
}
pub fn construct(
&self,
base: PathBuf,
includes: &HashMap<PathBuf, PlantUmlFileData>,
) -> String {
[
self.lines[0].raw_str(),
&self.construct_inner(base, includes),
self.lines[self.lines.len() - 1].raw_str(),
]
.join("")
}
fn construct_inner(
&self,
base: PathBuf,
includes: &HashMap<PathBuf, PlantUmlFileData>,
) -> String {
let resolver = PathResolver::new(base);
self.lines[1..self.lines.len() - 1]
.iter()
.map(|x| {
let Some(include) = x.include() else {
return x.raw_str().to_string();
};
let mut inner_resolver = resolver.clone();
inner_resolver.add(include.filepath().into());
let path = inner_resolver.build();
let Some(data) = includes.get(&path) else {
tracing::info!("file not loaded: path = {path:?}");
return x.raw_str().to_string();
};
let Some(content) = data.get_by_token(include.token()) else {
tracing::info!(
"specified content not found in file: path = {path:?}, token.id = {:?}, token.index = {:?}",
include.token().id(),
include.token().index(),
);
return x.raw_str().to_string();
};
content.construct_inner(path, includes)
})
.collect::<Vec<_>>()
.join("")
}
pub fn inner(&self) -> String {
self.lines[1..self.lines.len() - 1]
.iter()
.map(|x| x.raw_str())
.collect::<Vec<_>>()
.join("")
}
pub fn includes(&self) -> &[IncludeToken] {
&self.includes_memo
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_plant_uml_file_data() -> anyhow::Result<()> {
let testdata = r#"@startuml
@enduml
@startuml a
a -> b
@enduml
@startuml(id=b)
b -> a
@enduml
"#;
let parsed = PlantUmlFileData::parse(testdata.into())?;
assert_eq!(parsed.contents.len(), 3);
let content_0 = parsed.get(0).unwrap();
assert!(content_0.id().is_none());
let content_1 = parsed.get(1).unwrap();
assert_eq!(content_1.id(), Some("a"));
assert_eq!(content_1.inner(), " a -> b\n");
let content_a = parsed.get_by_id("a").unwrap();
assert_eq!(content_a.id(), Some("a"));
assert_eq!(content_a.inner(), " a -> b\n");
let content_2 = parsed.get(2).unwrap();
assert_eq!(content_2.id(), Some("b"));
assert_eq!(content_2.inner(), " b -> a\n");
let content_b = parsed.get_by_id("b").unwrap();
assert_eq!(content_b.id(), Some("b"));
assert_eq!(content_b.inner(), " b -> a\n");
assert!(parsed.get(3).is_none());
Ok(())
}
#[test]
fn test_parse_plant_uml_content() -> anyhow::Result<()> {
let testdata = r#"@startuml
@enduml"#;
let (rest, parsed) = PlantUmlContent::parse(testdata.into())?;
assert_eq!(rest, "");
assert_eq!(parsed.lines.len(), 2);
assert_eq!(parsed.inner(), "");
let testdata = r#"@startuml
a -> b
@enduml"#;
let (rest, parsed) = PlantUmlContent::parse(testdata.into())?;
assert_eq!(rest, "");
assert_eq!(parsed.lines.len(), 3);
assert_eq!(parsed.inner(), " a -> b\n");
let testdata = r#"@startuml aaa
a -> b
@enduml"#;
let (rest, parsed) = PlantUmlContent::parse(testdata.into())?;
assert_eq!(rest, "");
assert_eq!(parsed.lines.len(), 3);
assert_eq!(parsed.inner(), " a -> b\n");
assert_eq!(parsed.id(), Some("aaa"));
let testdata = r#"@startuml(id=bbb)
a -> b
@enduml"#;
let (rest, parsed) = PlantUmlContent::parse(testdata.into())?;
assert_eq!(rest, "");
assert_eq!(parsed.lines.len(), 3);
assert_eq!(parsed.inner(), " a -> b\n");
assert_eq!(parsed.id(), Some("bbb"));
let testdata = r#"@startuml
a -> b
@endfoo"#;
assert!(PlantUmlContent::parse(testdata.into()).is_err());
let testdata = r#"@startuml(id=ccc)
a -> b
!include shared.iuml
a -> b
!include functions.puml!aaa
a -> b
@enduml"#;
let (rest, parsed) = PlantUmlContent::parse(testdata.into())?;
assert_eq!(rest, "");
assert_eq!(parsed.lines.len(), 7);
assert_eq!(parsed.inner(), " a -> b\n !include shared.iuml\n a -> b\n !include functions.puml!aaa\n a -> b\n");
assert_eq!(parsed.id(), Some("ccc"));
let mut includes = parsed.includes().iter();
let include = includes.next().unwrap();
assert_eq!(include.filepath(), "shared.iuml");
assert_eq!(include.index(), None);
assert_eq!(include.id(), None);
let include = includes.next().unwrap();
assert_eq!(include.filepath(), "functions.puml");
assert_eq!(include.index(), None);
assert_eq!(include.id(), Some("aaa"));
assert!(includes.next().is_none());
Ok(())
}
}