use crate::convert::doc::common::ArgumentType;
use crate::convert::doc::common::Formatter;
use crate::errors::into_generator_error;
use sdml_core::error::Error;
use sdml_core::load::ModuleLoader;
use sdml_core::model::identifiers::{Identifier, IdentifierReference};
use sdml_core::model::modules::Module;
use sdml_core::store::ModuleStore;
use serde::de::Visitor;
use serde::Deserializer;
use serde::Serializer;
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use std::fs::read_to_string;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use tracing::error;
#[derive(Clone, Debug)]
pub struct Heading {
level: u8,
title: String,
label: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct AnnotationCategories {
labels: Vec<IdentifierReference>,
definitions: Vec<IdentifierReference>,
descriptions: Vec<IdentifierReference>,
comments: Vec<IdentifierReference>,
references: Vec<IdentifierReference>,
}
pub trait DocumentationWriter<TInclude, TSource, F>
where
TInclude: ArgumentType,
TSource: ArgumentType,
F: Formatter<TInclude, TSource>,
{
fn formatter() -> &'static F;
fn write_book<T>(
&mut self,
loader: &mut T,
cache: &mut impl ModuleStore,
book_config: BookConfig,
) -> Result<(), Error>
where
T: ModuleLoader;
fn write_preamble<W>(
&mut self,
title: &str,
language: &str,
include_toc: bool,
writer: &mut W,
) -> Result<(), Error>
where
W: Write + Sized;
fn write_module_uml_overview<W>(
&mut self,
module: &Module,
cache: &impl ModuleStore,
writer: &mut W,
) -> Result<(), Error>
where
W: Write + Sized;
fn write_module_description<W>(
&mut self,
heading: Heading,
module: &Module,
cache: &impl ModuleStore,
writer: &mut W,
) -> Result<(), Error>
where
W: Write + Sized;
fn write_module_definitions<W>(
&mut self,
heading_level: u8,
module: &Module,
cache: &impl ModuleStore,
writer: &mut W,
) -> Result<(), Error>
where
W: Write + Sized;
fn write_module_sdml_listing<W>(
&mut self,
heading: Heading,
module: &Module,
writer: &mut W,
) -> Result<(), Error>
where
W: Write + Sized;
fn write_module_rdf_listing<W>(
&mut self,
heading: Heading,
module: &Module,
cache: &impl ModuleStore,
writer: &mut W,
) -> Result<(), Error>
where
W: Write + Sized;
fn write_module_dependency_table<W>(
&mut self,
module: &Module,
cache: &impl ModuleStore,
writer: &mut W,
) -> Result<(), Error>
where
W: Write + Sized;
fn write_module_dependency_graph<W>(
&mut self,
heading: Heading,
module: &Module,
cache: &impl ModuleStore,
writer: &mut W,
) -> Result<(), Error>
where
W: Write + Sized;
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct ContentSection {
heading: String,
#[serde(default)]
items: Vec<ContentItem>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(untagged)]
pub enum ContentItem {
SourceFile { module_path: PathBuf },
SourceModule {
#[serde(serialize_with = "identifier_to_string")]
#[serde(deserialize_with = "identifier_from_string")]
module: Identifier,
},
Include { include_file_path: PathBuf },
Section { sub_section: ContentSection },
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum DocumentFormat {
#[default]
OrgMode,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct BookConfig {
title: String,
#[serde(skip_serializing_if = "Option::is_none")]
introduction: Option<PathBuf>,
content: ContentSection,
#[serde(default)]
output_format: DocumentFormat,
output_file: PathBuf,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
language: Option<String>,
#[serde(flatten)]
options: BookOptions,
#[serde(skip)]
annotation_categories: AnnotationCategories,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)]
pub struct BookOptions {
#[serde(default = "default_to_true")]
include_toc: bool,
#[serde(default = "default_to_true")]
multi_part: bool,
#[serde(default)]
copy_includes: bool,
#[serde(default)]
skip_dependency_graphs: bool,
#[serde(default)]
skip_sdml_listings: bool,
#[serde(default)]
skip_rdf_listings: bool,
}
#[inline(always)]
fn default_to_true() -> bool {
true
}
impl Default for AnnotationCategories {
fn default() -> Self {
Self {
labels: vec![
IdentifierReference::from_str("skos:altLabel").unwrap(),
IdentifierReference::from_str("rdfs:label").unwrap(),
IdentifierReference::from_str("sdml:srcLabel").unwrap(),
],
definitions: vec![IdentifierReference::from_str("skos:definition").unwrap()],
descriptions: vec![
IdentifierReference::from_str("dc:description").unwrap(),
IdentifierReference::from_str("dc_terms:description").unwrap(),
],
comments: vec![
IdentifierReference::from_str("rdfs:comment").unwrap(),
IdentifierReference::from_str("skos:note").unwrap(),
IdentifierReference::from_str("skos:changeNote").unwrap(),
IdentifierReference::from_str("skos:editorialNote").unwrap(),
IdentifierReference::from_str("skos:historyNote").unwrap(),
IdentifierReference::from_str("skos:scopeNote").unwrap(),
IdentifierReference::from_str("skos:example").unwrap(),
],
references: vec![
IdentifierReference::from_str("rdfs:seeAlso").unwrap(),
IdentifierReference::from_str("rdfs:isDefinedBy").unwrap(),
IdentifierReference::from_str("dc_terms:alternative").unwrap(),
IdentifierReference::from_str("dc_terms:isFormatOf").unwrap(),
IdentifierReference::from_str("dc_terms:isPartOf").unwrap(),
IdentifierReference::from_str("dc_terms:isReferencedBy").unwrap(),
IdentifierReference::from_str("dc_terms:isReplacedBy").unwrap(),
IdentifierReference::from_str("dc_terms:isRequiredBy").unwrap(),
IdentifierReference::from_str("dc_terms:isRequiredBy").unwrap(),
IdentifierReference::from_str("dc_terms:isVersionOf").unwrap(),
],
}
}
}
impl AnnotationCategories {
pub const fn label_properties(&self) -> &Vec<IdentifierReference> {
&self.labels
}
pub const fn definition_properties(&self) -> &Vec<IdentifierReference> {
&self.definitions
}
pub const fn description_properties(&self) -> &Vec<IdentifierReference> {
&self.descriptions
}
pub const fn comment_properties(&self) -> &Vec<IdentifierReference> {
&self.comments
}
pub const fn reference_properties(&self) -> &Vec<IdentifierReference> {
&self.references
}
}
impl Heading {
const LEVEL_SECTION: u8 = 1;
const LEVEL_SUBSECTION: u8 = 2;
const LEVEL_SUBSUBSECTION: u8 = 3;
pub fn new<S>(level: u8, title: S) -> Self
where
S: Into<String>,
{
Self {
level,
title: title.into(),
label: Default::default(),
}
}
pub fn with_label<S>(mut self, label: S) -> Self
where
S: Into<String>,
{
self.label = Some(label.into());
self
}
pub fn new_section<S>(title: S) -> Self
where
S: Into<String>,
{
Self::new(Self::LEVEL_SECTION, title)
}
pub fn new_subsection<S>(title: S) -> Self
where
S: Into<String>,
{
Self::new(Self::LEVEL_SUBSECTION, title)
}
pub fn new_subsubsection<S>(title: S) -> Self
where
S: Into<String>,
{
Self::new(Self::LEVEL_SUBSUBSECTION, title)
}
pub const fn level(&self) -> u8 {
self.level
}
pub const fn is_section(&self) -> bool {
self.level == Self::LEVEL_SECTION
}
pub const fn is_subsection(&self) -> bool {
self.level == Self::LEVEL_SUBSECTION
}
pub const fn is_subsubsection(&self) -> bool {
self.level == Self::LEVEL_SUBSUBSECTION
}
pub fn title(&self) -> &str {
self.title.as_ref()
}
pub const fn label(&self) -> Option<&String> {
self.label.as_ref()
}
}
impl Default for BookOptions {
fn default() -> Self {
Self {
include_toc: true,
multi_part: true,
copy_includes: Default::default(),
skip_dependency_graphs: Default::default(),
skip_sdml_listings: Default::default(),
skip_rdf_listings: Default::default(),
}
}
}
impl BookOptions {
pub const fn with_toc(self, include_toc: bool) -> Self {
let mut self_mut = self;
self_mut.include_toc = include_toc;
self_mut
}
pub const fn include_toc(&self) -> bool {
self.include_toc
}
pub const fn with_multi_part(self, multi_part: bool) -> Self {
let mut self_mut = self;
self_mut.multi_part = multi_part;
self_mut
}
pub const fn multi_part(&self) -> bool {
self.multi_part
}
pub const fn with_copy_includes(self, copy_includes: bool) -> Self {
let mut self_mut = self;
self_mut.copy_includes = !copy_includes;
self_mut
}
pub const fn copy_includes(&self) -> bool {
self.copy_includes
}
pub const fn with_dependency_graphs(self, dependency_graphs: bool) -> Self {
let mut self_mut = self;
self_mut.skip_dependency_graphs = !dependency_graphs;
self_mut
}
pub const fn skip_dependency_graphs(&self) -> bool {
self.skip_dependency_graphs
}
pub const fn with_sdml_listings(self, sdml_listings: bool) -> Self {
let mut self_mut = self;
self_mut.skip_sdml_listings = !sdml_listings;
self_mut
}
pub const fn skip_sdml_listings(&self) -> bool {
self.skip_sdml_listings
}
pub const fn with_rdf_listings(self, rdf_listings: bool) -> Self {
let mut self_mut = self;
self_mut.skip_rdf_listings = !rdf_listings;
self_mut
}
pub const fn skip_rdf_listings(&self) -> bool {
self.skip_rdf_listings
}
}
impl Default for BookConfig {
fn default() -> Self {
Self {
title: Default::default(),
introduction: Default::default(),
content: Default::default(),
output_format: DocumentFormat::OrgMode,
output_file: "index.org".into(),
language: Default::default(),
options: Default::default(),
annotation_categories: Default::default(),
}
}
}
impl FromStr for BookConfig {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match serde_json::from_str(s) {
Ok(config) => Ok(config),
Err(err) => {
error!(
?s,
?err,
"Could not de-serialize BookConfig from provided string"
);
Err(into_generator_error("doc-book", err))
}
}
}
}
impl BookConfig {
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
let content = read_to_string(path)?;
Self::from_str(&content)
}
pub fn item_count(&self) -> usize {
self.introduction.as_ref().map(|_| 1).unwrap_or_default() + self.content.item_count()
}
pub fn with_title<S>(self, title: S) -> Self
where
S: Into<String>,
{
let mut self_mut = self;
self_mut.title = title.into();
self_mut
}
pub const fn title(&self) -> &String {
&self.title
}
pub fn with_introduction<P>(self, introduction_file: P) -> Self
where
P: Into<PathBuf>,
{
let mut self_mut = self;
self_mut.introduction = Some(introduction_file.into());
self_mut
}
pub const fn introduction(&self) -> Option<&PathBuf> {
self.introduction.as_ref()
}
pub fn with_content(self, content: ContentSection) -> Self {
let mut self_mut = self;
self_mut.content = content;
self_mut
}
pub const fn content(&self) -> &ContentSection {
&self.content
}
pub fn with_output_format<S>(self, output_format: DocumentFormat) -> Self {
let mut self_mut = self;
self_mut.output_format = output_format;
self_mut
}
pub const fn output_format(&self) -> DocumentFormat {
self.output_format
}
pub fn with_output_file<P>(self, output_file: P) -> Self
where
P: Into<PathBuf>,
{
let mut self_mut = self;
self_mut.output_file = output_file.into();
self_mut
}
pub const fn output_file(&self) -> &PathBuf {
&self.output_file
}
pub fn with_language<S>(self, language: S) -> Self
where
S: Into<String>,
{
let mut self_mut = self;
self_mut.language = Some(language.into());
self_mut
}
pub const fn language(&self) -> Option<&String> {
self.language.as_ref()
}
pub fn with_options(self, options: BookOptions) -> Self {
let mut self_mut = self;
self_mut.options = options;
self_mut
}
pub const fn options(&self) -> &BookOptions {
&self.options
}
pub fn with_annotation_categories(self, annotation_categories: AnnotationCategories) -> Self {
let mut self_mut = self;
self_mut.annotation_categories = annotation_categories;
self_mut
}
pub const fn annotation_categories(&self) -> &AnnotationCategories {
&self.annotation_categories
}
}
impl ContentSection {
pub fn new<S, I>(heading: S, items: Vec<I>) -> Self
where
S: Into<String>,
I: Into<ContentItem>,
{
Self {
heading: heading.into(),
items: items.into_iter().map(|i| i.into()).collect(),
}
}
pub fn item_count(&self) -> usize {
self.items
.iter()
.map(|item| match item {
ContentItem::SourceFile { .. } => 1,
ContentItem::SourceModule { .. } => 1,
ContentItem::Include { .. } => 1,
ContentItem::Section { sub_section } => sub_section.item_count(),
})
.sum()
}
pub fn with_heading<S>(self, heading: S) -> Self
where
S: Into<String>,
{
let mut self_mut = self;
self_mut.heading = heading.into();
self_mut
}
pub fn heading(&self) -> &String {
&self.heading
}
pub fn with_items<I>(self, items: Vec<I>) -> Self
where
I: Into<ContentItem>,
{
let mut self_mut = self;
self_mut.items = items.into_iter().map(|i| i.into()).collect();
self_mut
}
pub fn with_item<I>(self, item: I) -> Self
where
I: Into<ContentItem>,
{
let mut self_mut = self;
self_mut.items.push(item.into());
self_mut
}
pub fn with_source_file(self, module_path: PathBuf) -> Self {
Self::with_item(self, ContentItem::SourceFile { module_path })
}
pub fn with_source_module(self, module: Identifier) -> Self {
Self::with_item(self, ContentItem::SourceModule { module })
}
pub fn with_include(self, include_file_path: PathBuf) -> Self {
Self::with_item(self, ContentItem::Include { include_file_path })
}
pub fn with_sub_section(self, sub_section: ContentSection) -> Self {
Self::with_item(self, ContentItem::Section { sub_section })
}
pub fn items(&self) -> &Vec<ContentItem> {
&self.items
}
}
impl From<ContentSection> for ContentItem {
fn from(sub_section: ContentSection) -> Self {
Self::Section { sub_section }
}
}
impl From<Identifier> for ContentItem {
fn from(module: Identifier) -> Self {
Self::SourceModule { module }
}
}
fn identifier_to_string<S>(name: &Identifier, se: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
se.serialize_str(name.as_ref())
}
struct IdentifierVisitor;
impl Visitor<'_> for IdentifierVisitor {
type Value = Identifier;
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("a string representing an SDML Identifier")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Identifier::from_str(v).map_err(|e| E::custom(e))
}
}
fn identifier_from_string<'de, D>(de: D) -> Result<Identifier, D::Error>
where
D: Deserializer<'de>,
{
let visitor = IdentifierVisitor;
de.deserialize_str(visitor)
}
pub mod markdown;
pub mod org_mode;
pub mod common;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_json_writer() {
let config = BookConfig::default()
.with_title("My Project")
.with_options(BookOptions::default().with_toc(false))
.with_introduction(PathBuf::from("./introduction.org"))
.with_content(
ContentSection::default()
.with_heading("Content Heading")
.with_include(PathBuf::from("./accounts.org"))
.with_source_module(Identifier::new_unchecked("account"))
.with_source_module(Identifier::new_unchecked("customer"))
.with_source_file(PathBuf::from("./account_enums.sdml")),
);
let json = serde_json::to_string_pretty(&config).unwrap();
println!("{json}");
}
#[test]
fn test_config_json_reader() {
const JSON: &str = r##"{
"title": "My Project",
"introduction": "./introduction.org",
"content": {
"heading": "Content Heading",
"items": [
{ "include_file_path": "./accounts.org" },
{ "module": "account" },
{ "module": "customer" },
{ "module_path": "./account_enums.sdml" }
]
},
"output_format": "org-mode",
"output_file": "index.org",
"options": {
"include_toc": false,
"multi_part": false,
"copy_includes": false
}
}"##;
let config: BookConfig = serde_json::from_str(JSON).unwrap();
println!("{config:?}");
}
}