more improvements

This commit is contained in:
avitex 2021-03-04 11:54:34 +11:00
parent 738285dced
commit 3bc02dcb02
Signed by: avitex
GPG Key ID: 38C76CBF3749D62C
21 changed files with 307 additions and 178 deletions

View File

@ -7,7 +7,7 @@ publish = false
build = "build.rs" build = "build.rs"
[features] [features]
default = ["cli"] default = ["cli", "mdbook-renderer"]
cli = ["structopt", "tokio/macros", "tokio/rt-multi-thread"] cli = ["structopt", "tokio/macros", "tokio/rt-multi-thread"]
mdbook-renderer = ["mdbook", "includedir", "includedir_codegen", "glob", "phf"] mdbook-renderer = ["mdbook", "includedir", "includedir_codegen", "glob", "phf"]

View File

@ -6,18 +6,26 @@
{% macro references(refs) %} {% macro references(refs) %}
{%- if refs | length == 0 %}No references{% endif -%} {%- if refs | length == 0 %}No references{% endif -%}
{%- for ref in refs -%} {%- for ref in refs -%}
- {{ ref | autolink }} {%- if ref is url -%}
- <{{ ref }}>
{%- else -%}
- {{ ref }}
{%- endif -%}
{% endfor -%} {% endfor -%}
{% endmacro references %} {% endmacro references %}
{% macro doc_title(doc) -%} {% macro doc_title(doc) -%}
{{ doc.name }} <small>([edit]({{ doc.id | domain_id_link(for="edit") }}))</small> {{ document.name }} <small>([edit]({{ link_for(for="document", id=document.id, edit=true) }}))</small>
{%- endmacro doc_title %} {%- endmacro doc_title %}
{% macro model_title(domain, model) -%}
{{ model | capitalize }}
{%- endmacro model_title %}
{% macro doc_details(doc) -%} {% macro doc_details(doc) -%}
| Title | {{ doc.name }} | | Title | {{ document.name }} |
|:---------------------------:|:------------------------| |:---------------------------:|:------------------------|
{{ self::doc_details_next(title="ID", value=doc.id)}} {{ self::doc_details_next(title="ID", value=document.id) }}
{%- endmacro doc_details %} {%- endmacro doc_details %}
{% macro doc_details_next(title, value) -%} {% macro doc_details_next(title, value) -%}
@ -37,21 +45,21 @@
{%- endmacro doc_details_tags %} {%- endmacro doc_details_tags %}
{% macro doc_rich_link(doc) -%} {% macro doc_rich_link(doc) -%}
{{ doc.name }} ([{{ doc.id }}]({{ global(key="site_url")}}{{ doc.id | domain_id_link }})) {{ document.name }} ([{{ document.id }}]({{ link_for(for="document", id=document.id) }}))
{%- endmacro doc_rich_link %} {%- endmacro doc_rich_link %}
{% macro summary_table(instances) -%} {% macro summary_table(items) -%}
| ID | Name | | ID | Name |
|:---------------------------:|:------------------------| |:---------------------------:|:------------------------|
{% for item in instances -%} {% for item in items -%}
| [{{ item.id }}]({{ global(key="site_url") }}{{ item.id | domain_id_link }}) | {{ item.name }} | | [{{ item.id }}]({{ link_for(for="document", id=item.id) }}) | {{ item.name }} |
{% endfor %} {% endfor %}
{%- endmacro summary_table %} {%- endmacro summary_table %}
{% macro summary_list(instances) %} {% macro summary_list(items) %}
{%- set last_domain = "" -%} {%- set last_domain = "" -%}
{%- set last_model = "" -%} {%- set last_model = "" -%}
{%- for item in summary -%} {%- for item in items -%}
{%- set id_parts = item.id | domain_id -%} {%- set id_parts = item.id | domain_id -%}
{%- if last_domain != id_parts.domain -%} {%- if last_domain != id_parts.domain -%}
{%- set_global last_domain = id_parts.domain %} {%- set_global last_domain = id_parts.domain %}
@ -59,8 +67,13 @@
{%- endif -%} {%- endif -%}
{%- if last_model != id_parts.model -%} {%- if last_model != id_parts.model -%}
{%- set_global last_model = id_parts.model %} {%- set_global last_model = id_parts.model %}
- [{{ id_parts.model | capitalize }}](./{{ item.id | domain_id_link(for="model") }}) - [{{ id_parts.model | capitalize }}]({{ id_parts.domain ~ "/" ~ id_parts.model ~ ".md" }})
{%- endif %} {%- endif %}
- [{{ item.name }}](./{{ item.id | domain_id_link }}) - [{{ item.name }}]({{ id_parts.domain ~ "/" ~ id_parts.model ~ "/" ~id_parts.instance ~ ".md" }})
{%- endfor -%} {%- endfor -%}
{% endmacro summary_list %} {% endmacro summary_list %}
{% macro registry_content(key, default="") %}
{%- set content = global(key="registry") | get(key="content") %}
{%- if content is containing(key) %}{{ get(key=key) }}{% else %}{{ default }}{% endif -%}
{% endmacro registry_content %}

View File

@ -1,20 +1,20 @@
{% import "macros.tera" as macros %} {% import "macros.tera" as macros %}
# {{ macros::doc_title(doc=doc) }} # {{ macros::doc_title(doc=document) }}
{{ macros::doc_details(doc=doc) }} {{ macros::doc_details(doc=document) }}
{%- if doc.WindowsEvent %} {%- if document.WindowsEvent %}
{{ macros::doc_details_next(title="Type", value="Windows event") }} {{ macros::doc_details_next(title="Type", value="Windows event") }}
{% endif -%} {% endif -%}
{{ macros::doc_details_next(title="Description", value=doc.description) }} {{ macros::doc_details_next(title="Description", value=document.description) }}
## Description ## Description
{{ macros::content(content=doc.content) }} {{ macros::content(content=document.content) }}
{% if doc.WindowsEvent %} {% if document.WindowsEvent %}
## Samples ## Samples
{% for sample in doc.WindowsEvent.sample %} {% for sample in document.WindowsEvent.sample %}
```xml ```xml
{{ sample.xml }} {{ sample.xml }}
``` ```

View File

@ -1,7 +1,11 @@
{% import "macros.tera" as macros %} {% import "macros.tera" as macros %}
# Event # {{ macros::model_title(domain="observe", model="event") }}
{% if content %}
{{ content }}
{% else %}
Observable items use to detect and respond to threat actor behaviour. Observable items use to detect and respond to threat actor behaviour.
{% endif %}
{{ macros::summary_table(instances=instances) }} {{ macros::summary_table(items=instances) }}

View File

@ -1,17 +1,17 @@
{% import "macros.tera" as macros %} {% import "macros.tera" as macros %}
{% set stage = doc.stage | get_doc %} {% set stage = document(id=document.stage) %}
# {{ macros::doc_title(doc=doc) }} # {{ macros::doc_title(doc=document) }}
{{ macros::doc_details(doc=doc) }} {{ macros::doc_details(doc=document) }}
{{ macros::doc_details_next(title="Stage", value=macros::doc_rich_link(doc=stage)) }} {{ macros::doc_details_next(title="Stage", value=macros::doc_rich_link(doc=stage)) }}
{{ macros::doc_details_next(title="Description", value=doc.description) }} {{ macros::doc_details_next(title="Description", value=document.description) }}
{{ macros::doc_details_tags(tags=doc.tags) }} {{ macros::doc_details_tags(tags=document.tags) }}
## Description ## Description
{{ macros::content(content=doc.content) }} {{ macros::content(content=document.content) }}
## References ## References
{{ macros::references(refs=doc.references) }} {{ macros::references(refs=document.references) }}

View File

@ -1,7 +1,11 @@
{% import "macros.tera" as macros %} {% import "macros.tera" as macros %}
# Action # {{ macros::model_title(domain="react", model="action") }}
{% if content %}
{{ content }}
{% else %}
An atomic human action assigned to an response stage. An atomic human action assigned to an response stage.
{% endif %}
{{ macros::summary_table(instances=instances) }} {{ macros::summary_table(items=instances) }}

View File

@ -1,10 +1,10 @@
{% import "macros.tera" as macros %} {% import "macros.tera" as macros %}
# {{ macros::doc_title(doc=doc) }} # {{ macros::doc_title(doc=document) }}
{{ macros::doc_details(doc=doc) }} {{ macros::doc_details(doc=document) }}
{{ macros::doc_details_next(title="Description", value=doc.description) }} {{ macros::doc_details_next(title="Description", value=document.description) }}
## Description ## Description
{{ macros::content(content=doc.content) }} {{ macros::content(content=document.content) }}

View File

@ -1,7 +1,11 @@
{% import "macros.tera" as macros %} {% import "macros.tera" as macros %}
# Stage # {{ macros::model_title(domain="react", model="stage") }}
{% if content %}
{{ content }}
{% else %}
Phase of a response to observed threat behaviour. Phase of a response to observed threat behaviour.
{% endif %}
{{ macros::summary_table(instances=instances) }} {{ macros::summary_table(items=instances) }}

View File

@ -1,14 +1,15 @@
{% import "macros.tera" as macros %} {% import "macros.tera" as macros %}
{% set provider = document(id=document.provider) %}
# {{ macros::doc_title(doc=doc) }} # {{ macros::doc_title(doc=document) }}
{{ macros::doc_details(doc=doc) }} {{ macros::doc_details(doc=document) }}
{{ macros::doc_details_next(title="Provider", value=doc.provider) }} {{ macros::doc_details_next(title="Provider", value=macros::doc_rich_link(doc=provider)) }}
## Description ## Description
{{ macros::content(content=doc.content) }} {{ macros::content(content=document.content) }}
## References ## References
{{ macros::references(refs=doc.references) }} {{ macros::references(refs=document.references) }}

View File

@ -1,7 +1,11 @@
{% import "macros.tera" as macros %} {% import "macros.tera" as macros %}
# Intelligence # {{ macros::model_title(domain="source", model="intelligence") }}
{% if content %}
{{ content }}
{% else %}
Feed or result of a query used to satisfy intelligence requirements. Feed or result of a query used to satisfy intelligence requirements.
{% endif %}
{{ macros::summary_table(instances=instances) }} {{ macros::summary_table(items=instances) }}

View File

@ -1,13 +1,13 @@
{% import "macros.tera" as macros %} {% import "macros.tera" as macros %}
# {{ macros::doc_title(doc=doc) }} # {{ macros::doc_title(doc=document) }}
{{ macros::doc_details(doc=doc) }} {{ macros::doc_details(doc=document) }}
## Description ## Description
{{ macros::content(content=doc.content) }} {{ macros::content(content=document.content) }}
## References ## References
{{ macros::references(refs=doc.references) }} {{ macros::references(refs=document.references) }}

View File

@ -1,7 +1,13 @@
{% import "macros.tera" as macros %} {% import "macros.tera" as macros %}
# Provider # {{ macros::model_title(domain="source", model="provider") }}
{% if content %}
{{ content }}
{% else %}
An internal or external supplier of intelligence. An internal or external supplier of intelligence.
{% endif %}
{{ macros::summary_table(instances=instances) }} {{ macros::registry_content(key="source-provider") }}
{{ macros::summary_table(items=instances) }}

View File

@ -1,13 +1,13 @@
{% import "macros.tera" as macros %} {% import "macros.tera" as macros %}
# {{ macros::doc_title(doc=doc) }} # {{ macros::doc_title(doc=document) }}
{{ macros::doc_details(doc=doc) }} {{ macros::doc_details(doc=document) }}
## Description ## Description
{{ macros::content(content=doc.content) }} {{ macros::content(content=document.content) }}
## References ## References
{{ macros::references(refs=doc.references) }} {{ macros::references(refs=document.references) }}

View File

@ -1,7 +1,11 @@
{% import "macros.tera" as macros %} {% import "macros.tera" as macros %}
# Requirement # {{ macros::model_title(domain="source", model="requirement") }}
{% if content %}
{{ content }}
{% else %}
Collection of intelligence used by actions, detections and mitigations. Collection of intelligence used by actions, detections and mitigations.
{% endif %}
{{ macros::summary_table(instances=instances) }} {{ macros::summary_table(items=instances) }}

View File

@ -4,4 +4,4 @@
[STORM](./index.md) [STORM](./index.md)
{{ macros::summary_list(instances=summary) }} {{ macros::summary_list(items=summary) }}

View File

@ -1 +1,5 @@
edit-link = "https://localhost/registry/{{domain}}/{{model}}/{{model_id}}.md" [edit]
page = "https://localhost/{{page}}.md"
document = "https://localhost/registry/{{domain}}/{{model}}/{{instance}}.md"
[content]

View File

@ -115,12 +115,12 @@ impl DomainModelKind {
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Copy)] #[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Copy)]
pub struct ModelId { pub struct Instance {
pub major: u16, pub major: u16,
pub minor: Option<NonZeroU16>, pub minor: Option<NonZeroU16>,
} }
impl ModelId { impl Instance {
pub fn new(major: u16) -> Self { pub fn new(major: u16) -> Self {
Self { major, minor: None } Self { major, minor: None }
} }
@ -130,7 +130,7 @@ impl ModelId {
} }
} }
impl fmt::Display for ModelId { impl fmt::Display for Instance {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:0>5}", self.major)?; write!(f, "{:0>5}", self.major)?;
if let Some(minor) = self.minor { if let Some(minor) = self.minor {
@ -140,7 +140,7 @@ impl fmt::Display for ModelId {
} }
} }
impl FromStr for ModelId { impl FromStr for Instance {
type Err = ParseIntError; type Err = ParseIntError;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
@ -153,7 +153,7 @@ impl FromStr for ModelId {
} }
} }
impl Serialize for ModelId { impl Serialize for Instance {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where where
S: Serializer, S: Serializer,
@ -162,7 +162,7 @@ impl Serialize for ModelId {
} }
} }
impl<'de> Deserialize<'de> for ModelId { impl<'de> Deserialize<'de> for Instance {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where where
D: Deserializer<'de>, D: Deserializer<'de>,
@ -177,12 +177,12 @@ impl<'de> Deserialize<'de> for ModelId {
#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Copy)] #[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Copy)]
pub struct DomainId { pub struct DomainId {
pub kind: DomainModelKind, pub kind: DomainModelKind,
pub model_id: ModelId, pub instance: Instance,
} }
impl DomainId { impl DomainId {
pub fn new(kind: DomainModelKind, model_id: ModelId) -> Self { pub fn new(kind: DomainModelKind, instance: Instance) -> Self {
Self { kind, model_id } Self { kind, instance }
} }
} }
@ -196,7 +196,7 @@ impl fmt::Debug for DomainId {
impl fmt::Display for DomainId { impl fmt::Display for DomainId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}{}", <&'static str>::from(&self.kind), self.model_id) write!(f, "{}{}", <&'static str>::from(&self.kind), self.instance)
} }
} }
@ -205,11 +205,12 @@ impl FromStr for DomainId {
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.len() > 3 && s.is_char_boundary(3) { if s.len() > 3 && s.is_char_boundary(3) {
let (kind, model_id) = s.split_at(3); let (kind, instance) = s.split_at(3);
if let (Ok(kind), Ok(model_id)) = if let (Ok(kind), Ok(instance)) = (
(DomainModelKind::from_str(kind), ModelId::from_str(model_id)) DomainModelKind::from_str(kind),
{ Instance::from_str(instance),
return Ok(Self { kind, model_id }); ) {
return Ok(Self { kind, instance });
} }
} }
Err("invalid storm id") Err("invalid storm id")

View File

@ -27,7 +27,7 @@ impl DirectoryRegistry {
fn domain_id_path(&self, id: DomainId) -> PathBuf { fn domain_id_path(&self, id: DomainId) -> PathBuf {
let (domain, model) = id.kind.parts(); let (domain, model) = id.kind.parts();
self.path self.path
.join(format!("{}/{}/{}.md", domain, model, id.model_id)) .join(format!("{}/{}/{}.md", domain, model, id.instance))
} }
} }
@ -41,11 +41,11 @@ impl Registry for DirectoryRegistry {
.map_err(Into::into) .map_err(Into::into)
} }
async fn get_document<M>(&self, model_id: ModelId) -> Result<Document<M>, RegistryError> async fn get_document<M>(&self, instance: Instance) -> Result<Document<M>, RegistryError>
where where
M: DomainModel, M: DomainModel,
{ {
let id = DomainId::new(M::kind(), model_id); let id = DomainId::new(M::kind(), instance);
let path = self.domain_id_path(id); let path = self.domain_id_path(id);
if !path.is_file() { if !path.is_file() {
return Err(RegistryError::NotFound(id)); return Err(RegistryError::NotFound(id));
@ -61,7 +61,7 @@ impl Registry for DirectoryRegistry {
let ids = self.get_document_ids(M::kind()).await?; let ids = self.get_document_ids(M::kind()).await?;
let mut docs = Vec::with_capacity(ids.len()); let mut docs = Vec::with_capacity(ids.len());
for id in ids { for id in ids {
docs.push((id, self.get_document(id.model_id).await?)); docs.push((id, self.get_document(id.instance).await?));
} }
Ok(docs) Ok(docs)
} }
@ -80,9 +80,9 @@ impl Registry for DirectoryRegistry {
let path = entry.path(); let path = entry.path();
if path.is_file() { if path.is_file() {
let filestem = path.file_stem().unwrap().to_string_lossy(); let filestem = path.file_stem().unwrap().to_string_lossy();
let model_id: ModelId = filestem.as_ref().parse().unwrap(); let instance: Instance = filestem.as_ref().parse().unwrap();
if !model_id.is_example() { if !instance.is_example() {
ids.push(DomainId::new(kind, model_id)) ids.push(DomainId::new(kind, instance))
} }
} }
} }
@ -91,13 +91,13 @@ impl Registry for DirectoryRegistry {
async fn put_document<M>( async fn put_document<M>(
&self, &self,
model_id: ModelId, instance: Instance,
document: Document<M>, document: Document<M>,
) -> Result<(), RegistryError> ) -> Result<(), RegistryError>
where where
M: DomainModel, M: DomainModel,
{ {
let path = self.domain_id_path(DomainId::new(M::kind(), model_id)); let path = self.domain_id_path(DomainId::new(M::kind(), instance));
let document = document.to_string(); let document = document.to_string();
fs::write(path, document.as_str()).await?; fs::write(path, document.as_str()).await?;
Ok(()) Ok(())

View File

@ -1,5 +1,6 @@
mod directory; mod directory;
use std::collections::HashMap;
use std::fmt::Debug; use std::fmt::Debug;
use std::sync::Arc; use std::sync::Arc;
@ -32,28 +33,47 @@ impl From<std::io::Error> for RegistryError {
} }
} }
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct EditLinks {
page: Option<String>,
document: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct RegistryConfig { pub struct RegistryConfig {
edit_link: Option<String>, pub edit: EditLinks,
pub content: HashMap<String, String>,
}
fn build_edit_link(
name: &str,
template: Option<&str>,
context: Context,
) -> Result<String, RegistryError> {
let template =
template.ok_or_else(|| RegistryError::Config(anyhow!("link for `{}` not set", name)))?;
Tera::one_off(template, &context, false)
.context("error rendering edit link")
.map_err(RegistryError::Config)
} }
impl RegistryConfig { impl RegistryConfig {
pub fn edit_link(&self, domain_id: DomainId) -> Result<String, RegistryError> { pub fn document_edit_link(&self, domain_id: DomainId) -> Result<String, RegistryError> {
let edit_link = self
.edit_link
.as_deref()
.ok_or_else(|| RegistryError::Config(anyhow!("edit-link option not set")))?;
let (domain, model) = domain_id.kind.parts(); let (domain, model) = domain_id.kind.parts();
let mut context = Context::new(); let mut context = Context::new();
context.insert("id", &domain_id); context.insert("id", &domain_id);
context.insert("domain", domain); context.insert("domain", domain);
context.insert("model", model); context.insert("model", model);
context.insert("model_id", &domain_id.model_id); context.insert("instance", &domain_id.instance);
build_edit_link("document", self.edit.document.as_deref(), context)
}
Tera::one_off(edit_link, &context, false) pub fn page_edit_link(&self, page: &str) -> Result<String, RegistryError> {
.context("error rendering edit link") let mut context = Context::new();
.map_err(RegistryError::Config) context.insert("page", &page);
build_edit_link("page", self.edit.page.as_deref(), context)
} }
} }
@ -61,7 +81,7 @@ impl RegistryConfig {
pub trait Registry: Send + Sync { pub trait Registry: Send + Sync {
async fn get_config(&self) -> Result<Arc<RegistryConfig>, RegistryError>; async fn get_config(&self) -> Result<Arc<RegistryConfig>, RegistryError>;
async fn get_document<M>(&self, model_id: ModelId) -> Result<Document<M>, RegistryError> async fn get_document<M>(&self, instance: Instance) -> Result<Document<M>, RegistryError>
where where
M: DomainModel; M: DomainModel;
@ -74,7 +94,7 @@ pub trait Registry: Send + Sync {
async fn put_document<M>( async fn put_document<M>(
&self, &self,
model_id: ModelId, instance: Instance,
document: Document<M>, document: Document<M>,
) -> Result<(), RegistryError> ) -> Result<(), RegistryError>
where where
@ -90,11 +110,11 @@ where
(**self).get_config().await (**self).get_config().await
} }
async fn get_document<M>(&self, model_id: ModelId) -> Result<Document<M>, RegistryError> async fn get_document<M>(&self, instance: Instance) -> Result<Document<M>, RegistryError>
where where
M: DomainModel, M: DomainModel,
{ {
(**self).get_document(model_id).await (**self).get_document(instance).await
} }
async fn get_documents<M>(&self) -> Result<Vec<(DomainId, Document<M>)>, RegistryError> async fn get_documents<M>(&self) -> Result<Vec<(DomainId, Document<M>)>, RegistryError>
@ -113,13 +133,13 @@ where
async fn put_document<M>( async fn put_document<M>(
&self, &self,
model_id: ModelId, instance: Instance,
document: Document<M>, document: Document<M>,
) -> Result<(), RegistryError> ) -> Result<(), RegistryError>
where where
M: DomainModel, M: DomainModel,
{ {
(**self).put_document(model_id, document).await (**self).put_document(instance, document).await
} }
} }
@ -136,12 +156,12 @@ impl Registry for SupportedRegistry {
} }
} }
async fn get_document<M>(&self, model_id: ModelId) -> Result<Document<M>, RegistryError> async fn get_document<M>(&self, instance: Instance) -> Result<Document<M>, RegistryError>
where where
M: DomainModel, M: DomainModel,
{ {
match self { match self {
Self::Directory(r) => r.get_document(model_id).await, Self::Directory(r) => r.get_document(instance).await,
} }
} }
@ -165,14 +185,14 @@ impl Registry for SupportedRegistry {
async fn put_document<M>( async fn put_document<M>(
&self, &self,
model_id: ModelId, instance: Instance,
document: Document<M>, document: Document<M>,
) -> Result<(), RegistryError> ) -> Result<(), RegistryError>
where where
M: DomainModel, M: DomainModel,
{ {
match self { match self {
Self::Directory(r) => r.put_document(model_id, document).await, Self::Directory(r) => r.put_document(instance, document).await,
} }
} }
} }

View File

@ -15,7 +15,7 @@ use tokio::fs;
include!(concat!(env!("OUT_DIR"), "/mdbook-templates.rs")); include!(concat!(env!("OUT_DIR"), "/mdbook-templates.rs"));
use crate::document::Document; use crate::document::Document;
use crate::domains::common::{DomainId, DomainModel, ModelId}; use crate::domains::common::{DomainId, DomainModel, Instance};
use crate::domains::GenericDocument; use crate::domains::GenericDocument;
use crate::registry::{Registry, RegistryConfig}; use crate::registry::{Registry, RegistryConfig};
@ -52,11 +52,11 @@ pub struct MDBookEngine {
enum Inner { enum Inner {
Start { Start {
src: PathBuf, src: PathBuf,
global: Map<String, Value>, site_url: Option<String>,
}, },
LoadAndRender { LoadAndRender {
src: PathBuf, src: PathBuf,
global: Map<String, Value>, site_url: Option<String>,
templates: Tera, templates: Tera,
summary: Vec<SummaryItem>, summary: Vec<SummaryItem>,
documents: DocumentMap, documents: DocumentMap,
@ -66,17 +66,17 @@ enum Inner {
impl MDBookEngine { impl MDBookEngine {
pub fn new(root: &Path, config: &MDBookConfig) -> Self { pub fn new(root: &Path, config: &MDBookConfig) -> Self {
let mut global = Map::new(); let site_url = if let Ok(Some(HtmlConfig { site_url, .. })) =
if let Ok(Some(HtmlConfig { site_url, .. })) = config.get_deserialized_opt("output.html") { config.get_deserialized_opt("output.html")
global.insert( {
"site_url".to_owned(), site_url
site_url.as_deref().unwrap_or("/").to_owned().into(), } else {
); None
} };
Self { Self {
inner: Inner::Start { inner: Inner::Start {
src: root.join(&config.book.src), src: root.join(&config.book.src),
global, site_url,
}, },
} }
} }
@ -89,13 +89,13 @@ impl Engine for MDBookEngine {
where where
R: Registry, R: Registry,
{ {
if let Inner::Start { src, global } = mem::replace(&mut self.inner, Inner::Finish) { if let Inner::Start { src, site_url } = mem::replace(&mut self.inner, Inner::Finish) {
let templates = load_templates(&src).await?; let templates = load_templates(&src).await?;
let documents = HashMap::new(); let documents = HashMap::new();
let summary = Vec::new(); let summary = Vec::new();
self.inner = Inner::LoadAndRender { self.inner = Inner::LoadAndRender {
src, src,
global, site_url,
templates, templates,
documents, documents,
summary, summary,
@ -106,7 +106,7 @@ impl Engine for MDBookEngine {
async fn push_document<R>( async fn push_document<R>(
&mut self, &mut self,
model_id: ModelId, instance: Instance,
doc: GenericDocument, doc: GenericDocument,
_registry: &R, _registry: &R,
) -> Result<(), RendererError> ) -> Result<(), RendererError>
@ -124,7 +124,7 @@ impl Engine for MDBookEngine {
match doc { match doc {
$( $(
GenericDocument::$kind(doc) => { GenericDocument::$kind(doc) => {
save_domain_model(summary, documents, model_id, &doc); save_domain_model(summary, documents, instance, &doc);
} }
)+ )+
} }
@ -160,7 +160,7 @@ impl Engine for MDBookEngine {
{ {
if let Inner::LoadAndRender { if let Inner::LoadAndRender {
src, src,
global, site_url,
mut templates, mut templates,
documents, documents,
summary, summary,
@ -169,8 +169,13 @@ impl Engine for MDBookEngine {
let summary = Arc::new(summary); let summary = Arc::new(summary);
let documents = Arc::new(documents); let documents = Arc::new(documents);
let registry_config = registry.get_config().await?; let registry_config = registry.get_config().await?;
register_filters(&mut templates, documents.clone(), registry_config, global); register_filters(
render_indexes(&templates, &src, &summary).await?; &mut templates,
documents.clone(),
registry_config.clone(),
site_url,
);
render_indexes(&templates, &src, &summary, registry_config).await?;
render_summary(&templates, &src, &summary).await?; render_summary(&templates, &src, &summary).await?;
render_documents(&templates, &src, documents).await?; render_documents(&templates, &src, documents).await?;
} }
@ -206,12 +211,12 @@ async fn load_templates(src: &Path) -> Result<Tera, MDBookEngineError> {
fn save_domain_model<M>( fn save_domain_model<M>(
summary: &mut Vec<SummaryItem>, summary: &mut Vec<SummaryItem>,
documents: &mut DocumentMap, documents: &mut DocumentMap,
model_id: ModelId, instance: Instance,
doc: &Document<M>, doc: &Document<M>,
) where ) where
M: DomainModel, M: DomainModel,
{ {
let id = DomainId::new(M::kind(), model_id); let id = DomainId::new(M::kind(), instance);
if let Value::Object(mut document) = tera::to_value(&doc).unwrap() { if let Value::Object(mut document) = tera::to_value(&doc).unwrap() {
document.insert("id".to_owned(), tera::to_value(&id).unwrap()); document.insert("id".to_owned(), tera::to_value(&id).unwrap());
documents.insert(id, document); documents.insert(id, document);
@ -233,10 +238,10 @@ async fn render_documents(
let template_path = format!("{}.instance.tera", model_path); let template_path = format!("{}.instance.tera", model_path);
let output_path = src let output_path = src
.join(model_path) .join(model_path)
.join(id.model_id.to_string()) .join(id.instance.to_string())
.with_extension("md"); .with_extension("md");
let mut context = Context::new(); let mut context = Context::new();
context.insert("doc", &document); context.insert("document", &document);
let contents = templates.render(&template_path, &context)?; let contents = templates.render(&template_path, &context)?;
fs::write(output_path, contents).await?; fs::write(output_path, contents).await?;
} }
@ -250,7 +255,6 @@ async fn render_summary(
) -> Result<(), MDBookEngineError> { ) -> Result<(), MDBookEngineError> {
let mut context = Context::new(); let mut context = Context::new();
context.insert("summary", summary); context.insert("summary", summary);
let contents = templates.render("summary.tera", &context)?; let contents = templates.render("summary.tera", &context)?;
let summary_path = src.join("SUMMARY.md"); let summary_path = src.join("SUMMARY.md");
fs::write(summary_path, contents).await?; fs::write(summary_path, contents).await?;
@ -261,6 +265,7 @@ async fn render_indexes(
templates: &Tera, templates: &Tera,
src: &Path, src: &Path,
summary: &[SummaryItem], summary: &[SummaryItem],
registry_config: Arc<RegistryConfig>,
) -> Result<(), MDBookEngineError> { ) -> Result<(), MDBookEngineError> {
let mut last_i = 0; let mut last_i = 0;
let mut last_item = if let Some(item) = summary.get(0) { let mut last_item = if let Some(item) = summary.get(0) {
@ -282,7 +287,11 @@ async fn render_indexes(
fs::create_dir(&model_dir).await?; fs::create_dir(&model_dir).await?;
} }
let content = registry_config
.content
.get(&format!("{}-{}", domain, model));
let mut context = Context::new(); let mut context = Context::new();
context.insert("content", &content);
context.insert("instances", &summary[last_i..i]); context.insert("instances", &summary[last_i..i]);
let model_index = model_dir.with_extension("md"); let model_index = model_dir.with_extension("md");
@ -302,83 +311,138 @@ fn register_filters(
tera: &mut Tera, tera: &mut Tera,
documents: Arc<DocumentMap>, documents: Arc<DocumentMap>,
registry_config: Arc<RegistryConfig>, registry_config: Arc<RegistryConfig>,
global: Map<String, Value>, site_url: Option<String>,
) { ) {
use tera::{try_get_value, Error}; use tera::{try_get_value, Error};
let site_url = site_url.as_deref().unwrap_or("/").to_owned();
let mut global: HashMap<&str, Value> = HashMap::new();
global.insert("site_url", tera::to_value(&site_url).unwrap());
global.insert("registry", tera::to_value(&*registry_config).unwrap());
tera.register_function("global", move |args: &HashMap<String, Value>| { tera.register_function("global", move |args: &HashMap<String, Value>| {
if let Some(Value::String(key)) = args.get("key") { if let Some(Value::String(full_key)) = args.get("key") {
Ok(global.get(key).cloned().unwrap_or(Value::Null)) let mut keys = full_key.split('.');
let mut value = global.get(keys.next().unwrap());
for key in keys {
match value {
Some(Value::Object(map)) => {
value = map.get(key);
}
None => {
return Err(Error::msg(format_args!(
"no key `{}` section of `{}`",
key, full_key
)))
}
Some(_) => {
return Err(Error::msg(format_args!(
"value cannot be indexed with `{}`",
key
)))
}
}
}
Ok(value.cloned().unwrap_or(Value::Null))
} else { } else {
Err(Error::msg("expected `key` for `global` function")) Err(Error::msg("expected valid `key` arg for `global` function"))
} }
}); });
fn parse_domain_id(value: &Value) -> Result<DomainId, Error> { tera.register_function("link_for", move |args: &HashMap<String, Value>| {
let id = try_get_value!("link", "value", String, value); let link_for = if let Some(Value::String(link_for)) = args.get("for") {
id.parse() link_for
.map_err(|_| Error::msg(format_args!("failed to parse domain id `{}`", id))) } else {
}
tera.register_filter(
"domain_id_link",
move |value: &Value, args: &HashMap<String, Value>| {
let id = parse_domain_id(value)?;
let (domain, model) = id.kind.parts();
let link = match args.get("for").and_then(|v| v.as_str()) {
None => format!("{}/{}/{}.md", domain, model, id.model_id),
Some("model") => format!("{}/{}.md", domain, model),
Some("edit") => registry_config.edit_link(id).map_err(Error::msg)?,
Some(_) => {
return Err(Error::msg( return Err(Error::msg(
"domain_id_link `for` arg can only be `model|edit`", "expected string `for` arg for `link_for` function",
));
};
let edit = match args.get("edit") {
Some(Value::Bool(edit)) => *edit,
Some(_) => return Err(Error::msg("`edit` arg for `link_for` must be a boolean")),
None => false,
};
let link = match link_for.as_str() {
"document" => match args.get("id") {
Some(value) => {
let id = parse_domain_id("link_for", value)?;
if edit {
registry_config.document_edit_link(id).map_err(Error::msg)?
} else {
let (domain, model) = id.kind.parts();
format!("{}{}/{}/{}.md", site_url, domain, model, id.instance)
}
}
_ => {
return Err(Error::msg(
"`link_for(for=\"document\")` `` expects `id` arg",
))
}
},
"page" => match args.get("path") {
Some(Value::String(page)) if edit => {
registry_config.page_edit_link(page).map_err(Error::msg)?
}
Some(Value::String(page)) => {
format!("{}{}.md", site_url, page)
}
_ => return Err(Error::msg("`link_for(for=\"path\")` `` expects `path` arg")),
},
_ => {
return Err(Error::msg(
"`link_for` `for` arg can only be `document|page`",
)) ))
} }
}; };
Ok(Value::String(link)) Ok(Value::String(link))
}, });
);
tera.register_filter( tera.register_function("document", move |args: &HashMap<String, Value>| {
"domain_id", if let Some(value) = args.get("id") {
move |value: &Value, _: &HashMap<String, Value>| { let id = parse_domain_id("document", value)?;
let id = parse_domain_id(value)?;
let (domain, model) = id.kind.parts();
let mut parts = Map::with_capacity(4);
parts.insert("domain".to_owned(), domain.into());
parts.insert("model".to_owned(), model.into());
parts.insert("model_id".to_owned(), id.model_id.to_string().into());
Ok(Value::Object(parts))
},
);
tera.register_filter(
"get_doc",
move |value: &Value, _: &HashMap<String, Value>| {
let id = parse_domain_id(value)?;
documents documents
.get(&id) .get(&id)
.cloned() .cloned()
.map(Value::Object) .map(Value::Object)
.ok_or_else(|| Error::msg(format_args!("unknown document id {}", id))) .ok_or_else(|| Error::msg(format_args!("unknown document id {}", id)))
} else {
Err(Error::msg("expected `id` arg for `document` function"))
}
});
fn parse_domain_id(within: &str, value: &Value) -> Result<DomainId, Error> {
let id = try_get_value!(within, "id", String, value);
id.parse()
.map_err(|_| Error::msg(format_args!("failed to parse domain id `{}`", id)))
}
tera.register_filter(
"domain_id",
move |value: &Value, _: &HashMap<String, Value>| {
let id = parse_domain_id("domain_id", value)?;
let (domain, model) = id.kind.parts();
let mut parts = Map::with_capacity(4);
parts.insert("domain".to_owned(), domain.into());
parts.insert("model".to_owned(), model.into());
parts.insert("instance".to_owned(), id.instance.to_string().into());
Ok(Value::Object(parts))
}, },
); );
tera.register_filter("autolink", |value: &Value, _: &HashMap<String, Value>| { tera.register_tester("url", |value: Option<&Value>, _: &[Value]| {
lazy_static! { lazy_static! {
static ref URL: Regex = Regex::new( static ref URL: Regex = Regex::new(
r"(?ix) r"(?ix)
\b(([\w-]+://?|www[.])[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/))) ^\b(([\w-]+://?|www[.])[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/)))$
", ",
) )
.unwrap(); .unwrap();
} }
let text = try_get_value!("autolink", "value", String, value); if let Some(Value::String(text)) = value {
let text = if text.is_empty() { Ok(URL.is_match(text))
text
} else { } else {
URL.replace_all(text.as_str(), "[$0]($0)").into_owned() Err(Error::msg("expected string for `url` test"))
}; }
Ok(Value::String(text))
}); });
} }

View File

@ -5,7 +5,7 @@ use async_trait::async_trait;
use thiserror::Error; use thiserror::Error;
use crate::document::Document; use crate::document::Document;
use crate::domains::common::{DomainId, DomainModel, ModelId}; use crate::domains::common::{DomainId, DomainModel, Instance};
use crate::domains::GenericDocument; use crate::domains::GenericDocument;
use crate::registry::{Registry, RegistryError}; use crate::registry::{Registry, RegistryError};
@ -39,7 +39,7 @@ where
$( $(
for (id, doc) in self.get_documents().await? { for (id, doc) in self.get_documents().await? {
self.engine self.engine
.push_document(id.model_id, GenericDocument::$kind(doc), &self.registry) .push_document(id.instance, GenericDocument::$kind(doc), &self.registry)
.await?; .await?;
} }
)* )*
@ -92,7 +92,7 @@ pub trait Engine {
async fn push_document<R>( async fn push_document<R>(
&mut self, &mut self,
id: ModelId, id: Instance,
doc: GenericDocument, doc: GenericDocument,
registry: &R, registry: &R,
) -> Result<(), RendererError> ) -> Result<(), RendererError>