ckb_resource/
template.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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
/// Default chain spec.
pub const DEFAULT_SPEC: &str = "mainnet";
/// The list of bundled chain specs.
pub const AVAILABLE_SPECS: &[&str] = &["mainnet", "testnet", "preview", "staging", "dev"];
/// The default RPC listen port *8114*.
pub const DEFAULT_RPC_PORT: &str = "8114";
/// The default P2P listen port *8115*.
pub const DEFAULT_P2P_PORT: &str = "8115";

const START_MARKER: &str = " # {{";
const END_MAKER: &str = "# }}";
const WILDCARD_BRANCH: &str = "# _ => ";

use std::collections::HashMap;
use std::io;

/// A simple template which supports spec branches and variables.
///
/// The template is designed so that without expanding the template, it is still a valid TOML file.
///
/// ### Spec Branches
///
/// A spec branches block replaces a line with a branch matching the given spec name.
///
/// The block starts with the line ending with ` # {{` (the leading space is required) and ends
/// with a line `# }}`.
///
/// Between the start and end markers, every line is a branch starting with `# SPEC => CONTENT`, where
/// `SPEC` is the branch spec name, and `CONTENT` is the text to be replaced for the spec.
/// A special spec name `_` acts as a wildcard which matches any spec name.
///
/// The spec name is required to render the template, see [`Template::new`]. The block including
/// the **whole** starting line which ends with ` # {{` will be replaced by the branch `CONTENT`
/// which `SPEC` is `_` or equals to the given spec name.
///
/// In the `CONTENT`, variables are expanded and all the escape sequences `\n` are replaced by new
/// lines.
///
/// ```
/// use ckb_resource::{Template, TemplateContext};
///
/// let template = Template::new(
///     r#"filter = "debug" # {{
/// ## mainnet => filter = "error"
/// ## _ => filter = "info"
/// ## }}"#
///         .to_string(),
/// );
/// let mainnet_result = template.render(&TemplateContext::new("mainnet", Vec::new()));
/// assert_eq!("filter = \"error\"\n", mainnet_result.unwrap());
/// let testnet_result = template.render(&TemplateContext::new("testnet", Vec::new()));
/// assert_eq!("filter = \"info\"\n", testnet_result.unwrap());
/// ```
///
/// ### Template Variables
///
/// Template variables are defined as key value dictionary in [`TemplateContext`] via
/// [`TemplateContext::new`] or [`TemplateContext::insert`].
///
/// Template uses variables by surrounding the variable names with curly brackets.
///
/// The variables expansions **only** happen inside the spec branches in the spec `CONTENT`.
/// It is a trick to use a wildcard branch as in the following example.
///
/// ```
/// use ckb_resource::{Template, TemplateContext};
///
/// let template = Template::new(
///     r#"# # {{
/// ## _ => listen_address = "127.0.0.1:{rpc_port}"
/// ## }}"#
///         .to_string(),
/// );
/// let text = template.render(&TemplateContext::new("dev", vec![("rpc_port", "18114")]));
/// assert_eq!("listen_address = \"127.0.0.1:18114\"\n", text.unwrap());
/// ```
///
/// [`TemplateContext`]: struct.TemplateContext.html
/// [`TemplateContext::new`]: struct.TemplateContext.html#method_new
/// [`TemplateContext::insert`]: struct.TemplateContext.html#method_insert
pub struct Template(String);

/// The context used to expand the [`Template`](struct.Template.html).
pub struct TemplateContext<'a> {
    spec: &'a str,
    kvs: HashMap<&'a str, &'a str>,
}

impl<'a> TemplateContext<'a> {
    /// Creates a new template.
    ///
    /// * `spec` - the chain spec name for template spec branch.
    /// * `kvs` - the initial template variables.
    ///
    /// ## Examples
    ///
    /// ```
    /// use ckb_resource::TemplateContext;
    /// // Creates a context for *dev* chain and initializes variables:
    /// //
    /// //     rpc_port      => 8114
    /// //     p2p_port      => 8115
    /// TemplateContext::new("dev", vec![("rpc_port", "8114"), ("p2p_port", "8115")]);
    /// ```
    pub fn new<I>(spec: &'a str, kvs: I) -> Self
    where
        I: IntoIterator<Item = (&'a str, &'a str)>,
    {
        Self {
            spec,
            kvs: kvs.into_iter().collect(),
        }
    }

    /// Inserts a new variable into the context.
    ///
    /// * `key` - the variable name
    /// * `value` - the variable value
    pub fn insert(&mut self, key: &'a str, value: &'a str) {
        self.kvs.insert(key, value);
    }
}

impl Template {
    /// Creates the template with the specified content.
    pub fn new(content: String) -> Self {
        Template(content)
    }
}

fn writeln<W: io::Write>(w: &mut W, s: &str, context: &TemplateContext) -> io::Result<()> {
    #[cfg(docker)]
    let s = s.replace("127.0.0.1:{rpc_port}", "0.0.0.0:{rpc_port}");
    writeln!(
        w,
        "{}",
        context
            .kvs
            .iter()
            .fold(s.replace("\\n", "\n"), |s, (key, value)| s
                .replace(format!("{{{key}}}").as_str(), value))
    )
}

#[derive(Debug)]
pub enum TemplateState<'a> {
    SearchStartMarker,
    MatchBranch(&'a str),
    SearchEndMarker,
}

impl Template {
    /// Expands the template using the context and writes the result via the writer `w`.
    ///
    /// ## Errors
    ///
    /// This method returns `std::io::Error` when it fails to write the chunks to the underlying
    /// writer.
    pub fn render_to<W: io::Write>(
        &self,
        w: &mut W,
        context: &TemplateContext<'_>,
    ) -> io::Result<()> {
        let spec_branch = format!("# {} => ", context.spec);

        let mut state = TemplateState::SearchStartMarker;
        for line in self.0.lines() {
            // dbg!((line, &state));
            match state {
                TemplateState::SearchStartMarker => {
                    if line.ends_with(START_MARKER) {
                        state = TemplateState::MatchBranch(line);
                    } else {
                        writeln!(w, "{line}")?;
                    }
                }
                TemplateState::MatchBranch(start_line) => {
                    if line == END_MAKER {
                        writeln!(
                            w,
                            "{}",
                            &start_line[..(start_line.len() - START_MARKER.len())],
                        )?;
                        state = TemplateState::SearchStartMarker;
                    } else if line.starts_with(&spec_branch) {
                        writeln(w, &line[spec_branch.len()..], context)?;
                        state = TemplateState::SearchEndMarker;
                    } else if let Some(c) = line.strip_prefix(WILDCARD_BRANCH) {
                        writeln(w, c, context)?;
                        state = TemplateState::SearchEndMarker;
                    }
                }
                TemplateState::SearchEndMarker => {
                    if line == END_MAKER {
                        state = TemplateState::SearchStartMarker;
                    }
                }
            }
        }

        if let TemplateState::MatchBranch(start_line) = state {
            writeln!(
                w,
                "{}",
                &start_line[..(start_line.len() - START_MARKER.len())],
            )?;
        }

        Ok(())
    }

    /// Renders the template and returns the result as a string.
    ///
    /// ## Errors
    ///
    /// This method returns `std::io::Error` when it fails to write the chunks to the underlying
    /// writer or it failed to convert the result text to UTF-8.
    pub fn render(&self, context: &TemplateContext<'_>) -> io::Result<String> {
        let mut out = Vec::new();
        self.render_to(&mut out, context)?;
        String::from_utf8(out)
            .map_err(|from_utf8_err| io::Error::new(io::ErrorKind::InvalidInput, from_utf8_err))
    }
}