rune/doc/
artifacts.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
use rust_alloc::string::ToString;

use std::fs;
use std::io;

use rust_alloc::borrow::ToOwned;
use std::path::Path;

use crate::alloc::borrow::Cow;
use crate::alloc::{String, Vec};
use crate::runtime::Protocol;
use crate::ItemBuf;

use anyhow::{Context as _, Error, Result};
use base64::display::Base64Display;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use relative_path::{RelativePath, RelativePathBuf};
use sha2::{Digest, Sha256};

/// Test parameters.
#[derive(Debug, Default, Clone, Copy)]
pub(crate) struct TestParams {
    /// If the test should not run.
    pub(crate) no_run: bool,
    /// If the test should panic.
    pub(crate) should_panic: bool,
    /// Ignore the test.
    pub(crate) ignore: bool,
}

/// The kind of a test.
#[derive(Default, Debug, Clone, Copy)]
pub(crate) enum TestKind {
    /// The test originates from a free function.
    #[default]
    Free,
    /// The test originates from a protocol function.
    Protocol(&'static Protocol),
}

/// A discovered test.
pub(crate) struct Test {
    /// Item of the test.
    pub(crate) item: ItemBuf,
    /// The kind of a test.
    pub(crate) kind: TestKind,
    /// Lines that make up the tests.
    pub(crate) content: String,
    /// Test parameters.
    pub(crate) params: TestParams,
}

/// A collection of artifacts produced by a documentation build.
///
/// This can be disabled through the [`AssetsQueue::disabled`] constructor in
/// case you don't want any static assets to be built.
pub(crate) struct Artifacts {
    pub(crate) enabled: bool,
    assets: Vec<Asset>,
    tests: Vec<Test>,
}

impl Artifacts {
    /// Construct a new assets queue.
    pub(crate) fn new() -> Self {
        Self {
            enabled: true,
            assets: Vec::new(),
            tests: Vec::new(),
        }
    }

    /// Build a disabled assets queue.
    pub(crate) fn without_assets() -> Self {
        Self {
            enabled: false,
            assets: Vec::new(),
            tests: Vec::new(),
        }
    }

    /// Set captures tests.
    pub(crate) fn set_tests(&mut self, tests: Vec<Test>) {
        self.tests = tests;
    }

    /// Iterate over assets produced by this documentation build.
    ///
    /// This is always empty if the [`Artifacts::without_assets`] constructor
    /// was used.
    pub(crate) fn assets(&self) -> impl Iterator<Item = &Asset> {
        self.assets.iter()
    }

    /// Iterate over tests produced by this documentation build.
    pub(crate) fn tests(&self) -> impl Iterator<Item = &Test> {
        self.tests.iter()
    }

    /// Define an asset artifact.
    pub(crate) fn asset<P, F>(
        &mut self,
        hash: bool,
        path: &P,
        content: F,
    ) -> Result<RelativePathBuf>
    where
        P: ?Sized + AsRef<RelativePath>,
        F: FnOnce() -> Result<Cow<'static, [u8]>>,
    {
        if !self.enabled {
            return Ok(path.as_ref().to_owned());
        }

        let content = content().context("Building asset content")?;

        let path = if hash {
            let mut hasher = Sha256::new();
            hasher.update(content.as_ref());
            let result = hasher.finalize();
            let hash = Base64Display::new(&result[..], &URL_SAFE_NO_PAD);

            let path = path.as_ref();
            let stem = path.file_stem().context("Missing file stem")?;
            let ext = path.extension().context("Missing file extension")?;
            path.with_file_name(format!("{stem}-{hash}.{ext}"))
        } else {
            path.as_ref().to_owned()
        };

        self.assets.try_push(Asset {
            path: path.clone(),
            content,
        })?;

        Ok(path)
    }
}

/// Asset builder.
pub(crate) struct Asset {
    path: RelativePathBuf,
    content: Cow<'static, [u8]>,
}

impl Asset {
    /// Build the given asset.
    pub(crate) fn build(&self, root: &Path) -> Result<()> {
        let p = self.path.to_path(root);
        tracing::info!("Writing: {}", p.display());
        ensure_parent_dir(&p)?;
        fs::write(&p, &self.content).with_context(|| p.display().to_string())?;
        Ok(())
    }
}

/// Ensure parent dir exists.
fn ensure_parent_dir(path: &Path) -> Result<()> {
    if let Some(p) = path.parent() {
        if p.is_dir() {
            return Ok(());
        }

        tracing::info!("create dir: {}", p.display());

        match fs::create_dir_all(p) {
            Ok(()) => {}
            Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {}
            Err(e) => return Err(Error::from(e)).context(p.display().to_string()),
        }
    }

    Ok(())
}