more improvements
This commit is contained in:
parent
738285dced
commit
3bc02dcb02
@ -7,7 +7,7 @@ publish = false
|
||||
build = "build.rs"
|
||||
|
||||
[features]
|
||||
default = ["cli"]
|
||||
default = ["cli", "mdbook-renderer"]
|
||||
cli = ["structopt", "tokio/macros", "tokio/rt-multi-thread"]
|
||||
mdbook-renderer = ["mdbook", "includedir", "includedir_codegen", "glob", "phf"]
|
||||
|
||||
|
@ -6,18 +6,26 @@
|
||||
{% macro references(refs) %}
|
||||
{%- if refs | length == 0 %}No references{% endif -%}
|
||||
{%- for ref in refs -%}
|
||||
- {{ ref | autolink }}
|
||||
{%- if ref is url -%}
|
||||
- <{{ ref }}>
|
||||
{%- else -%}
|
||||
- {{ ref }}
|
||||
{%- endif -%}
|
||||
{% endfor -%}
|
||||
{% endmacro references %}
|
||||
|
||||
{% 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 %}
|
||||
|
||||
{% macro model_title(domain, model) -%}
|
||||
{{ model | capitalize }}
|
||||
{%- endmacro model_title %}
|
||||
|
||||
{% 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 %}
|
||||
|
||||
{% macro doc_details_next(title, value) -%}
|
||||
@ -37,21 +45,21 @@
|
||||
{%- endmacro doc_details_tags %}
|
||||
|
||||
{% 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 %}
|
||||
|
||||
{% macro summary_table(instances) -%}
|
||||
{% macro summary_table(items) -%}
|
||||
| ID | Name |
|
||||
|:---------------------------:|:------------------------|
|
||||
{% for item in instances -%}
|
||||
| [{{ item.id }}]({{ global(key="site_url") }}{{ item.id | domain_id_link }}) | {{ item.name }} |
|
||||
{% for item in items -%}
|
||||
| [{{ item.id }}]({{ link_for(for="document", id=item.id) }}) | {{ item.name }} |
|
||||
{% endfor %}
|
||||
{%- endmacro summary_table %}
|
||||
|
||||
{% macro summary_list(instances) %}
|
||||
{% macro summary_list(items) %}
|
||||
{%- set last_domain = "" -%}
|
||||
{%- set last_model = "" -%}
|
||||
{%- for item in summary -%}
|
||||
{%- for item in items -%}
|
||||
{%- set id_parts = item.id | domain_id -%}
|
||||
{%- if last_domain != id_parts.domain -%}
|
||||
{%- set_global last_domain = id_parts.domain %}
|
||||
@ -59,8 +67,13 @@
|
||||
{%- endif -%}
|
||||
{%- if 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 %}
|
||||
- [{{ item.name }}](./{{ item.id | domain_id_link }})
|
||||
- [{{ item.name }}]({{ id_parts.domain ~ "/" ~ id_parts.model ~ "/" ~id_parts.instance ~ ".md" }})
|
||||
{%- endfor -%}
|
||||
{% 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 %}
|
||||
|
@ -1,20 +1,20 @@
|
||||
{% import "macros.tera" as macros %}
|
||||
|
||||
# {{ macros::doc_title(doc=doc) }}
|
||||
# {{ macros::doc_title(doc=document) }}
|
||||
|
||||
{{ macros::doc_details(doc=doc) }}
|
||||
{%- if doc.WindowsEvent %}
|
||||
{{ macros::doc_details(doc=document) }}
|
||||
{%- if document.WindowsEvent %}
|
||||
{{ macros::doc_details_next(title="Type", value="Windows event") }}
|
||||
{% endif -%}
|
||||
{{ macros::doc_details_next(title="Description", value=doc.description) }}
|
||||
{{ macros::doc_details_next(title="Description", value=document.description) }}
|
||||
|
||||
## Description
|
||||
|
||||
{{ macros::content(content=doc.content) }}
|
||||
{{ macros::content(content=document.content) }}
|
||||
|
||||
{% if doc.WindowsEvent %}
|
||||
{% if document.WindowsEvent %}
|
||||
## Samples
|
||||
{% for sample in doc.WindowsEvent.sample %}
|
||||
{% for sample in document.WindowsEvent.sample %}
|
||||
```xml
|
||||
{{ sample.xml }}
|
||||
```
|
||||
|
@ -1,7 +1,11 @@
|
||||
{% 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.
|
||||
{% endif %}
|
||||
|
||||
{{ macros::summary_table(instances=instances) }}
|
||||
{{ macros::summary_table(items=instances) }}
|
||||
|
@ -1,17 +1,17 @@
|
||||
{% 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="Description", value=doc.description) }}
|
||||
{{ macros::doc_details_tags(tags=doc.tags) }}
|
||||
{{ macros::doc_details_next(title="Description", value=document.description) }}
|
||||
{{ macros::doc_details_tags(tags=document.tags) }}
|
||||
|
||||
## Description
|
||||
|
||||
{{ macros::content(content=doc.content) }}
|
||||
{{ macros::content(content=document.content) }}
|
||||
|
||||
## References
|
||||
|
||||
{{ macros::references(refs=doc.references) }}
|
||||
{{ macros::references(refs=document.references) }}
|
||||
|
@ -1,7 +1,11 @@
|
||||
{% 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.
|
||||
{% endif %}
|
||||
|
||||
{{ macros::summary_table(instances=instances) }}
|
||||
{{ macros::summary_table(items=instances) }}
|
||||
|
@ -1,10 +1,10 @@
|
||||
{% import "macros.tera" as macros %}
|
||||
|
||||
# {{ macros::doc_title(doc=doc) }}
|
||||
# {{ macros::doc_title(doc=document) }}
|
||||
|
||||
{{ macros::doc_details(doc=doc) }}
|
||||
{{ macros::doc_details_next(title="Description", value=doc.description) }}
|
||||
{{ macros::doc_details(doc=document) }}
|
||||
{{ macros::doc_details_next(title="Description", value=document.description) }}
|
||||
|
||||
## Description
|
||||
|
||||
{{ macros::content(content=doc.content) }}
|
||||
{{ macros::content(content=document.content) }}
|
||||
|
@ -1,7 +1,11 @@
|
||||
{% 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.
|
||||
{% endif %}
|
||||
|
||||
{{ macros::summary_table(instances=instances) }}
|
||||
{{ macros::summary_table(items=instances) }}
|
||||
|
@ -1,14 +1,15 @@
|
||||
{% 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_next(title="Provider", value=doc.provider) }}
|
||||
{{ macros::doc_details(doc=document) }}
|
||||
{{ macros::doc_details_next(title="Provider", value=macros::doc_rich_link(doc=provider)) }}
|
||||
|
||||
## Description
|
||||
|
||||
{{ macros::content(content=doc.content) }}
|
||||
{{ macros::content(content=document.content) }}
|
||||
|
||||
## References
|
||||
|
||||
{{ macros::references(refs=doc.references) }}
|
||||
{{ macros::references(refs=document.references) }}
|
||||
|
@ -1,7 +1,11 @@
|
||||
{% 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.
|
||||
{% endif %}
|
||||
|
||||
{{ macros::summary_table(instances=instances) }}
|
||||
{{ macros::summary_table(items=instances) }}
|
||||
|
@ -1,13 +1,13 @@
|
||||
{% 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
|
||||
|
||||
{{ macros::content(content=doc.content) }}
|
||||
{{ macros::content(content=document.content) }}
|
||||
|
||||
## References
|
||||
|
||||
{{ macros::references(refs=doc.references) }}
|
||||
{{ macros::references(refs=document.references) }}
|
||||
|
@ -1,7 +1,13 @@
|
||||
{% import "macros.tera" as macros %}
|
||||
|
||||
# Provider
|
||||
# {{ macros::model_title(domain="source", model="provider") }}
|
||||
|
||||
{% if content %}
|
||||
{{ content }}
|
||||
{% else %}
|
||||
An internal or external supplier of intelligence.
|
||||
{% endif %}
|
||||
|
||||
{{ macros::summary_table(instances=instances) }}
|
||||
{{ macros::registry_content(key="source-provider") }}
|
||||
|
||||
{{ macros::summary_table(items=instances) }}
|
||||
|
@ -1,13 +1,13 @@
|
||||
{% 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
|
||||
|
||||
{{ macros::content(content=doc.content) }}
|
||||
{{ macros::content(content=document.content) }}
|
||||
|
||||
## References
|
||||
|
||||
{{ macros::references(refs=doc.references) }}
|
||||
{{ macros::references(refs=document.references) }}
|
||||
|
@ -1,7 +1,11 @@
|
||||
{% 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.
|
||||
{% endif %}
|
||||
|
||||
{{ macros::summary_table(instances=instances) }}
|
||||
{{ macros::summary_table(items=instances) }}
|
||||
|
@ -4,4 +4,4 @@
|
||||
|
||||
[STORM](./index.md)
|
||||
|
||||
{{ macros::summary_list(instances=summary) }}
|
||||
{{ macros::summary_list(items=summary) }}
|
||||
|
@ -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]
|
||||
|
@ -115,12 +115,12 @@ impl DomainModelKind {
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Copy)]
|
||||
pub struct ModelId {
|
||||
pub struct Instance {
|
||||
pub major: u16,
|
||||
pub minor: Option<NonZeroU16>,
|
||||
}
|
||||
|
||||
impl ModelId {
|
||||
impl Instance {
|
||||
pub fn new(major: u16) -> Self {
|
||||
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 {
|
||||
write!(f, "{:0>5}", self.major)?;
|
||||
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;
|
||||
|
||||
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>
|
||||
where
|
||||
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>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
@ -177,12 +177,12 @@ impl<'de> Deserialize<'de> for ModelId {
|
||||
#[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Copy)]
|
||||
pub struct DomainId {
|
||||
pub kind: DomainModelKind,
|
||||
pub model_id: ModelId,
|
||||
pub instance: Instance,
|
||||
}
|
||||
|
||||
impl DomainId {
|
||||
pub fn new(kind: DomainModelKind, model_id: ModelId) -> Self {
|
||||
Self { kind, model_id }
|
||||
pub fn new(kind: DomainModelKind, instance: Instance) -> Self {
|
||||
Self { kind, instance }
|
||||
}
|
||||
}
|
||||
|
||||
@ -196,7 +196,7 @@ impl fmt::Debug for DomainId {
|
||||
|
||||
impl fmt::Display for DomainId {
|
||||
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> {
|
||||
if s.len() > 3 && s.is_char_boundary(3) {
|
||||
let (kind, model_id) = s.split_at(3);
|
||||
if let (Ok(kind), Ok(model_id)) =
|
||||
(DomainModelKind::from_str(kind), ModelId::from_str(model_id))
|
||||
{
|
||||
return Ok(Self { kind, model_id });
|
||||
let (kind, instance) = s.split_at(3);
|
||||
if let (Ok(kind), Ok(instance)) = (
|
||||
DomainModelKind::from_str(kind),
|
||||
Instance::from_str(instance),
|
||||
) {
|
||||
return Ok(Self { kind, instance });
|
||||
}
|
||||
}
|
||||
Err("invalid storm id")
|
||||
|
@ -27,7 +27,7 @@ impl DirectoryRegistry {
|
||||
fn domain_id_path(&self, id: DomainId) -> PathBuf {
|
||||
let (domain, model) = id.kind.parts();
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
M: DomainModel,
|
||||
{
|
||||
let id = DomainId::new(M::kind(), model_id);
|
||||
let id = DomainId::new(M::kind(), instance);
|
||||
let path = self.domain_id_path(id);
|
||||
if !path.is_file() {
|
||||
return Err(RegistryError::NotFound(id));
|
||||
@ -61,7 +61,7 @@ impl Registry for DirectoryRegistry {
|
||||
let ids = self.get_document_ids(M::kind()).await?;
|
||||
let mut docs = Vec::with_capacity(ids.len());
|
||||
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)
|
||||
}
|
||||
@ -80,9 +80,9 @@ impl Registry for DirectoryRegistry {
|
||||
let path = entry.path();
|
||||
if path.is_file() {
|
||||
let filestem = path.file_stem().unwrap().to_string_lossy();
|
||||
let model_id: ModelId = filestem.as_ref().parse().unwrap();
|
||||
if !model_id.is_example() {
|
||||
ids.push(DomainId::new(kind, model_id))
|
||||
let instance: Instance = filestem.as_ref().parse().unwrap();
|
||||
if !instance.is_example() {
|
||||
ids.push(DomainId::new(kind, instance))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -91,13 +91,13 @@ impl Registry for DirectoryRegistry {
|
||||
|
||||
async fn put_document<M>(
|
||||
&self,
|
||||
model_id: ModelId,
|
||||
instance: Instance,
|
||||
document: Document<M>,
|
||||
) -> Result<(), RegistryError>
|
||||
where
|
||||
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();
|
||||
fs::write(path, document.as_str()).await?;
|
||||
Ok(())
|
||||
|
@ -1,5 +1,6 @@
|
||||
mod directory;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Debug;
|
||||
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)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
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 {
|
||||
pub fn 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")))?;
|
||||
pub fn document_edit_link(&self, domain_id: DomainId) -> Result<String, RegistryError> {
|
||||
let (domain, model) = domain_id.kind.parts();
|
||||
let mut context = Context::new();
|
||||
context.insert("id", &domain_id);
|
||||
context.insert("domain", domain);
|
||||
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)
|
||||
.context("error rendering edit link")
|
||||
.map_err(RegistryError::Config)
|
||||
pub fn page_edit_link(&self, page: &str) -> Result<String, RegistryError> {
|
||||
let mut context = Context::new();
|
||||
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 {
|
||||
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
|
||||
M: DomainModel;
|
||||
|
||||
@ -74,7 +94,7 @@ pub trait Registry: Send + Sync {
|
||||
|
||||
async fn put_document<M>(
|
||||
&self,
|
||||
model_id: ModelId,
|
||||
instance: Instance,
|
||||
document: Document<M>,
|
||||
) -> Result<(), RegistryError>
|
||||
where
|
||||
@ -90,11 +110,11 @@ where
|
||||
(**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
|
||||
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>
|
||||
@ -113,13 +133,13 @@ where
|
||||
|
||||
async fn put_document<M>(
|
||||
&self,
|
||||
model_id: ModelId,
|
||||
instance: Instance,
|
||||
document: Document<M>,
|
||||
) -> Result<(), RegistryError>
|
||||
where
|
||||
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
|
||||
M: DomainModel,
|
||||
{
|
||||
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>(
|
||||
&self,
|
||||
model_id: ModelId,
|
||||
instance: Instance,
|
||||
document: Document<M>,
|
||||
) -> Result<(), RegistryError>
|
||||
where
|
||||
M: DomainModel,
|
||||
{
|
||||
match self {
|
||||
Self::Directory(r) => r.put_document(model_id, document).await,
|
||||
Self::Directory(r) => r.put_document(instance, document).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ use tokio::fs;
|
||||
include!(concat!(env!("OUT_DIR"), "/mdbook-templates.rs"));
|
||||
|
||||
use crate::document::Document;
|
||||
use crate::domains::common::{DomainId, DomainModel, ModelId};
|
||||
use crate::domains::common::{DomainId, DomainModel, Instance};
|
||||
use crate::domains::GenericDocument;
|
||||
use crate::registry::{Registry, RegistryConfig};
|
||||
|
||||
@ -52,11 +52,11 @@ pub struct MDBookEngine {
|
||||
enum Inner {
|
||||
Start {
|
||||
src: PathBuf,
|
||||
global: Map<String, Value>,
|
||||
site_url: Option<String>,
|
||||
},
|
||||
LoadAndRender {
|
||||
src: PathBuf,
|
||||
global: Map<String, Value>,
|
||||
site_url: Option<String>,
|
||||
templates: Tera,
|
||||
summary: Vec<SummaryItem>,
|
||||
documents: DocumentMap,
|
||||
@ -66,17 +66,17 @@ enum Inner {
|
||||
|
||||
impl MDBookEngine {
|
||||
pub fn new(root: &Path, config: &MDBookConfig) -> Self {
|
||||
let mut global = Map::new();
|
||||
if let Ok(Some(HtmlConfig { site_url, .. })) = config.get_deserialized_opt("output.html") {
|
||||
global.insert(
|
||||
"site_url".to_owned(),
|
||||
site_url.as_deref().unwrap_or("/").to_owned().into(),
|
||||
);
|
||||
}
|
||||
let site_url = if let Ok(Some(HtmlConfig { site_url, .. })) =
|
||||
config.get_deserialized_opt("output.html")
|
||||
{
|
||||
site_url
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Self {
|
||||
inner: Inner::Start {
|
||||
src: root.join(&config.book.src),
|
||||
global,
|
||||
site_url,
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -89,13 +89,13 @@ impl Engine for MDBookEngine {
|
||||
where
|
||||
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 documents = HashMap::new();
|
||||
let summary = Vec::new();
|
||||
self.inner = Inner::LoadAndRender {
|
||||
src,
|
||||
global,
|
||||
site_url,
|
||||
templates,
|
||||
documents,
|
||||
summary,
|
||||
@ -106,7 +106,7 @@ impl Engine for MDBookEngine {
|
||||
|
||||
async fn push_document<R>(
|
||||
&mut self,
|
||||
model_id: ModelId,
|
||||
instance: Instance,
|
||||
doc: GenericDocument,
|
||||
_registry: &R,
|
||||
) -> Result<(), RendererError>
|
||||
@ -124,7 +124,7 @@ impl Engine for MDBookEngine {
|
||||
match 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 {
|
||||
src,
|
||||
global,
|
||||
site_url,
|
||||
mut templates,
|
||||
documents,
|
||||
summary,
|
||||
@ -169,8 +169,13 @@ impl Engine for MDBookEngine {
|
||||
let summary = Arc::new(summary);
|
||||
let documents = Arc::new(documents);
|
||||
let registry_config = registry.get_config().await?;
|
||||
register_filters(&mut templates, documents.clone(), registry_config, global);
|
||||
render_indexes(&templates, &src, &summary).await?;
|
||||
register_filters(
|
||||
&mut templates,
|
||||
documents.clone(),
|
||||
registry_config.clone(),
|
||||
site_url,
|
||||
);
|
||||
render_indexes(&templates, &src, &summary, registry_config).await?;
|
||||
render_summary(&templates, &src, &summary).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>(
|
||||
summary: &mut Vec<SummaryItem>,
|
||||
documents: &mut DocumentMap,
|
||||
model_id: ModelId,
|
||||
instance: Instance,
|
||||
doc: &Document<M>,
|
||||
) where
|
||||
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() {
|
||||
document.insert("id".to_owned(), tera::to_value(&id).unwrap());
|
||||
documents.insert(id, document);
|
||||
@ -233,10 +238,10 @@ async fn render_documents(
|
||||
let template_path = format!("{}.instance.tera", model_path);
|
||||
let output_path = src
|
||||
.join(model_path)
|
||||
.join(id.model_id.to_string())
|
||||
.join(id.instance.to_string())
|
||||
.with_extension("md");
|
||||
let mut context = Context::new();
|
||||
context.insert("doc", &document);
|
||||
context.insert("document", &document);
|
||||
let contents = templates.render(&template_path, &context)?;
|
||||
fs::write(output_path, contents).await?;
|
||||
}
|
||||
@ -250,7 +255,6 @@ async fn render_summary(
|
||||
) -> Result<(), MDBookEngineError> {
|
||||
let mut context = Context::new();
|
||||
context.insert("summary", summary);
|
||||
|
||||
let contents = templates.render("summary.tera", &context)?;
|
||||
let summary_path = src.join("SUMMARY.md");
|
||||
fs::write(summary_path, contents).await?;
|
||||
@ -261,6 +265,7 @@ async fn render_indexes(
|
||||
templates: &Tera,
|
||||
src: &Path,
|
||||
summary: &[SummaryItem],
|
||||
registry_config: Arc<RegistryConfig>,
|
||||
) -> Result<(), MDBookEngineError> {
|
||||
let mut last_i = 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?;
|
||||
}
|
||||
|
||||
let content = registry_config
|
||||
.content
|
||||
.get(&format!("{}-{}", domain, model));
|
||||
let mut context = Context::new();
|
||||
context.insert("content", &content);
|
||||
context.insert("instances", &summary[last_i..i]);
|
||||
|
||||
let model_index = model_dir.with_extension("md");
|
||||
@ -302,83 +311,138 @@ fn register_filters(
|
||||
tera: &mut Tera,
|
||||
documents: Arc<DocumentMap>,
|
||||
registry_config: Arc<RegistryConfig>,
|
||||
global: Map<String, Value>,
|
||||
site_url: Option<String>,
|
||||
) {
|
||||
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>| {
|
||||
if let Some(Value::String(key)) = args.get("key") {
|
||||
Ok(global.get(key).cloned().unwrap_or(Value::Null))
|
||||
if let Some(Value::String(full_key)) = args.get("key") {
|
||||
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 {
|
||||
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> {
|
||||
let id = try_get_value!("link", "value", String, value);
|
||||
id.parse()
|
||||
.map_err(|_| Error::msg(format_args!("failed to parse domain id `{}`", id)))
|
||||
}
|
||||
|
||||
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(_) => {
|
||||
tera.register_function("link_for", move |args: &HashMap<String, Value>| {
|
||||
let link_for = if let Some(Value::String(link_for)) = args.get("for") {
|
||||
link_for
|
||||
} else {
|
||||
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))
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
tera.register_filter(
|
||||
"domain_id",
|
||||
move |value: &Value, _: &HashMap<String, 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)?;
|
||||
tera.register_function("document", move |args: &HashMap<String, Value>| {
|
||||
if let Some(value) = args.get("id") {
|
||||
let id = parse_domain_id("document", value)?;
|
||||
documents
|
||||
.get(&id)
|
||||
.cloned()
|
||||
.map(Value::Object)
|
||||
.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! {
|
||||
static ref URL: Regex = Regex::new(
|
||||
r"(?ix)
|
||||
\b(([\w-]+://?|www[.])[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/)))
|
||||
^\b(([\w-]+://?|www[.])[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/)))$
|
||||
",
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
let text = try_get_value!("autolink", "value", String, value);
|
||||
let text = if text.is_empty() {
|
||||
text
|
||||
if let Some(Value::String(text)) = value {
|
||||
Ok(URL.is_match(text))
|
||||
} else {
|
||||
URL.replace_all(text.as_str(), "[$0]($0)").into_owned()
|
||||
};
|
||||
Ok(Value::String(text))
|
||||
Err(Error::msg("expected string for `url` test"))
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ use async_trait::async_trait;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::document::Document;
|
||||
use crate::domains::common::{DomainId, DomainModel, ModelId};
|
||||
use crate::domains::common::{DomainId, DomainModel, Instance};
|
||||
use crate::domains::GenericDocument;
|
||||
use crate::registry::{Registry, RegistryError};
|
||||
|
||||
@ -39,7 +39,7 @@ where
|
||||
$(
|
||||
for (id, doc) in self.get_documents().await? {
|
||||
self.engine
|
||||
.push_document(id.model_id, GenericDocument::$kind(doc), &self.registry)
|
||||
.push_document(id.instance, GenericDocument::$kind(doc), &self.registry)
|
||||
.await?;
|
||||
}
|
||||
)*
|
||||
@ -92,7 +92,7 @@ pub trait Engine {
|
||||
|
||||
async fn push_document<R>(
|
||||
&mut self,
|
||||
id: ModelId,
|
||||
id: Instance,
|
||||
doc: GenericDocument,
|
||||
registry: &R,
|
||||
) -> Result<(), RendererError>
|
||||
|
Loading…
Reference in New Issue
Block a user