ckb_resource/
template.rs

1/// Default chain spec.
2pub const DEFAULT_SPEC: &str = "mainnet";
3/// The list of bundled chain specs.
4pub const AVAILABLE_SPECS: &[&str] = &["mainnet", "testnet", "preview", "staging", "dev"];
5/// The default RPC listen port *8114*.
6pub const DEFAULT_RPC_PORT: &str = "8114";
7/// The default P2P listen port *8115*.
8pub const DEFAULT_P2P_PORT: &str = "8115";
9
10const START_MARKER: &str = " # {{";
11const END_MAKER: &str = "# }}";
12const WILDCARD_BRANCH: &str = "# _ => ";
13
14use std::collections::HashMap;
15use std::io;
16
17/// A simple template which supports spec branches and variables.
18///
19/// The template is designed so that without expanding the template, it is still a valid TOML file.
20///
21/// ### Spec Branches
22///
23/// A spec branches block replaces a line with a branch matching the given spec name.
24///
25/// The block starts with the line ending with ` # {{` (the leading space is required) and ends
26/// with a line `# }}`.
27///
28/// Between the start and end markers, every line is a branch starting with `# SPEC => CONTENT`, where
29/// `SPEC` is the branch spec name, and `CONTENT` is the text to be replaced for the spec.
30/// A special spec name `_` acts as a wildcard which matches any spec name.
31///
32/// The spec name is required to render the template, see [`Template::new`]. The block including
33/// the **whole** starting line which ends with ` # {{` will be replaced by the branch `CONTENT`
34/// which `SPEC` is `_` or equals to the given spec name.
35///
36/// In the `CONTENT`, variables are expanded and all the escape sequences `\n` are replaced by new
37/// lines.
38///
39/// ```
40/// use ckb_resource::{Template, TemplateContext};
41///
42/// let template = Template::new(
43///     r#"filter = "debug" # {{
44/// ## mainnet => filter = "error"
45/// ## _ => filter = "info"
46/// ## }}"#
47///         .to_string(),
48/// );
49/// let mainnet_result = template.render(&TemplateContext::new("mainnet", Vec::new()));
50/// assert_eq!("filter = \"error\"\n", mainnet_result.unwrap());
51/// let testnet_result = template.render(&TemplateContext::new("testnet", Vec::new()));
52/// assert_eq!("filter = \"info\"\n", testnet_result.unwrap());
53/// ```
54///
55/// ### Template Variables
56///
57/// Template variables are defined as key value dictionary in [`TemplateContext`] via
58/// [`TemplateContext::new`] or [`TemplateContext::insert`].
59///
60/// Template uses variables by surrounding the variable names with curly brackets.
61///
62/// The variables expansions **only** happen inside the spec branches in the spec `CONTENT`.
63/// It is a trick to use a wildcard branch as in the following example.
64///
65/// ```
66/// use ckb_resource::{Template, TemplateContext};
67///
68/// let template = Template::new(
69///     r#"# # {{
70/// ## _ => listen_address = "127.0.0.1:{rpc_port}"
71/// ## }}"#
72///         .to_string(),
73/// );
74/// let text = template.render(&TemplateContext::new("dev", vec![("rpc_port", "18114")]));
75/// assert_eq!("listen_address = \"127.0.0.1:18114\"\n", text.unwrap());
76/// ```
77///
78/// [`TemplateContext`]: struct.TemplateContext.html
79/// [`TemplateContext::new`]: struct.TemplateContext.html#method_new
80/// [`TemplateContext::insert`]: struct.TemplateContext.html#method_insert
81pub struct Template(String);
82
83/// The context used to expand the [`Template`](struct.Template.html).
84pub struct TemplateContext<'a> {
85    spec: &'a str,
86    kvs: HashMap<&'a str, &'a str>,
87}
88
89impl<'a> TemplateContext<'a> {
90    /// Creates a new template.
91    ///
92    /// * `spec` - the chain spec name for template spec branch.
93    /// * `kvs` - the initial template variables.
94    ///
95    /// ## Examples
96    ///
97    /// ```
98    /// use ckb_resource::TemplateContext;
99    /// // Creates a context for *dev* chain and initializes variables:
100    /// //
101    /// //     rpc_port      => 8114
102    /// //     p2p_port      => 8115
103    /// TemplateContext::new("dev", vec![("rpc_port", "8114"), ("p2p_port", "8115")]);
104    /// ```
105    pub fn new<I>(spec: &'a str, kvs: I) -> Self
106    where
107        I: IntoIterator<Item = (&'a str, &'a str)>,
108    {
109        Self {
110            spec,
111            kvs: kvs.into_iter().collect(),
112        }
113    }
114
115    /// Inserts a new variable into the context.
116    ///
117    /// * `key` - the variable name
118    /// * `value` - the variable value
119    pub fn insert(&mut self, key: &'a str, value: &'a str) {
120        self.kvs.insert(key, value);
121    }
122}
123
124impl Template {
125    /// Creates the template with the specified content.
126    pub fn new(content: String) -> Self {
127        Template(content)
128    }
129}
130
131#[allow(unexpected_cfgs)]
132fn writeln<W: io::Write>(w: &mut W, s: &str, context: &TemplateContext) -> io::Result<()> {
133    #[cfg(docker)]
134    let s = s.replace("127.0.0.1:{rpc_port}", "0.0.0.0:{rpc_port}");
135    writeln!(
136        w,
137        "{}",
138        context
139            .kvs
140            .iter()
141            .fold(s.replace("\\n", "\n"), |s, (key, value)| s
142                .replace(format!("{{{key}}}").as_str(), value))
143    )
144}
145
146#[derive(Debug)]
147pub enum TemplateState<'a> {
148    SearchStartMarker,
149    MatchBranch(&'a str),
150    SearchEndMarker,
151}
152
153impl Template {
154    /// Expands the template using the context and writes the result via the writer `w`.
155    ///
156    /// ## Errors
157    ///
158    /// This method returns `std::io::Error` when it fails to write the chunks to the underlying
159    /// writer.
160    pub fn render_to<W: io::Write>(
161        &self,
162        w: &mut W,
163        context: &TemplateContext<'_>,
164    ) -> io::Result<()> {
165        let spec_branch = format!("# {} => ", context.spec);
166
167        let mut state = TemplateState::SearchStartMarker;
168        for line in self.0.lines() {
169            // dbg!((line, &state));
170            match state {
171                TemplateState::SearchStartMarker => {
172                    if line.ends_with(START_MARKER) {
173                        state = TemplateState::MatchBranch(line);
174                    } else {
175                        writeln!(w, "{line}")?;
176                    }
177                }
178                TemplateState::MatchBranch(start_line) => {
179                    if line == END_MAKER {
180                        writeln!(
181                            w,
182                            "{}",
183                            &start_line[..(start_line.len() - START_MARKER.len())],
184                        )?;
185                        state = TemplateState::SearchStartMarker;
186                    } else if line.starts_with(&spec_branch) {
187                        writeln(w, &line[spec_branch.len()..], context)?;
188                        state = TemplateState::SearchEndMarker;
189                    } else if let Some(c) = line.strip_prefix(WILDCARD_BRANCH) {
190                        writeln(w, c, context)?;
191                        state = TemplateState::SearchEndMarker;
192                    }
193                }
194                TemplateState::SearchEndMarker => {
195                    if line == END_MAKER {
196                        state = TemplateState::SearchStartMarker;
197                    }
198                }
199            }
200        }
201
202        if let TemplateState::MatchBranch(start_line) = state {
203            writeln!(
204                w,
205                "{}",
206                &start_line[..(start_line.len() - START_MARKER.len())],
207            )?;
208        }
209
210        Ok(())
211    }
212
213    /// Renders the template and returns the result as a string.
214    ///
215    /// ## Errors
216    ///
217    /// This method returns `std::io::Error` when it fails to write the chunks to the underlying
218    /// writer or it failed to convert the result text to UTF-8.
219    pub fn render(&self, context: &TemplateContext<'_>) -> io::Result<String> {
220        let mut out = Vec::new();
221        self.render_to(&mut out, context)?;
222        String::from_utf8(out)
223            .map_err(|from_utf8_err| io::Error::new(io::ErrorKind::InvalidInput, from_utf8_err))
224    }
225}