nreplops_tool/
sources.rs

1// sources.rs
2// Copyright 2022 Matti Hänninen
3//
4// Licensed under the Apache License, Version 2.0 (the "License"); you may not
5// use this file except in compliance with the License. You may obtain a copy of
6// the License at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13// License for the specific language governing permissions and limitations under
14// the License.
15
16use std::{
17  borrow::Cow,
18  collections::HashMap,
19  fs,
20  io::{self, Read},
21  rc::Rc,
22};
23
24use crate::{cli, error::Error};
25
26#[derive(Debug)]
27pub struct Source {
28  pub content: String,
29  pub file: Option<String>,
30}
31
32pub fn load_sources(
33  source_args: &[cli::SourceArg],
34  template_args: &[cli::TemplateArg],
35) -> Result<Vec<Source>, Error> {
36  let context = Context::from(template_args);
37  let mut result = Vec::new();
38  for source_arg in source_args.iter() {
39    let (file, raw_content) = load_content(source_arg)?;
40    let content = render_source(&context, raw_content.as_ref());
41    result.push(Source { content, file });
42  }
43  Ok(result)
44}
45
46fn load_content(
47  source_arg: &cli::SourceArg,
48) -> Result<(Option<String>, Cow<'_, str>), Error> {
49  use cli::SourceArg::*;
50  match source_arg {
51    Pipe => {
52      let stdin = io::stdin();
53      let mut handle = stdin.lock();
54      let mut buffer = String::new();
55      handle
56        .read_to_string(&mut buffer)
57        .map_err(|_| Error::CannotReadStdIn)?;
58      Ok((None, Cow::Owned(buffer)))
59    }
60    Expr(e) => Ok((None, Cow::Borrowed(e.as_str()))),
61    File(f) => {
62      let mut file = fs::File::open(f)
63        .map_err(|_| Error::CannotReadFile(f.to_string_lossy().to_string()))?;
64      let mut buffer = String::new();
65      file
66        .read_to_string(&mut buffer)
67        .map_err(|_| Error::CannotReadFile(f.to_string_lossy().to_string()))?;
68      Ok((Some(f.to_string_lossy().to_string()), Cow::Owned(buffer)))
69    }
70  }
71}
72
73// XXX(soija) This needs work
74// This rendering has the following limitations:
75// - does not catch '#nr[...]' exprs without value arg (nREPL catches this though)
76// - is not easy to extend supporting '#nr[<var> <default>]'
77// - let alone '#nr[<var-1> ... <var-n> <default>]'
78// - does not captures values from environment variables (e.g. NR_VAR_1)
79fn render_source(context: &Context, source: &str) -> String {
80  let after_shebang = if source.starts_with("#!") {
81    match source.split_once('\n') {
82      Some((_, remaining)) => remaining,
83      None => "",
84    }
85  } else {
86    source
87  }
88  .trim();
89  if let Some(ref regex) = context.regex {
90    let mut fragments = Vec::<Rc<str>>::new();
91    let mut remaining = after_shebang;
92    while let Some(captures) = regex.captures(remaining) {
93      let full_match = captures.get(0).unwrap();
94      let (upto, after) = remaining.split_at(full_match.end());
95      let (before, _) = upto.split_at(full_match.start());
96      fragments.push(before.to_string().into());
97      let value = context
98        .table
99        .get(captures.get(1).unwrap().as_str())
100        .unwrap();
101      fragments.push(value.clone());
102      remaining = after;
103    }
104    fragments.push(remaining.to_string().into());
105    fragments.join("")
106  } else {
107    after_shebang.into()
108  }
109}
110
111#[derive(Debug)]
112struct Context {
113  table: HashMap<Rc<str>, Rc<str>>,
114  regex: Option<regex::Regex>,
115}
116
117impl From<&[cli::TemplateArg]> for Context {
118  fn from(template_args: &[cli::TemplateArg]) -> Self {
119    let table = template_args.iter().fold(HashMap::new(), |mut m, a| {
120      if let Some(ref n) = a.name {
121        m.insert(n.clone(), a.value.clone());
122      }
123      if let Some(i) = a.pos {
124        m.insert((i + 1).to_string().into(), a.value.clone());
125      }
126      m
127    });
128    let regex = if table.is_empty() {
129      None
130    } else {
131      let keys = table
132        .keys()
133        .map(|s| regex::escape(s))
134        .collect::<Vec<String>>();
135      let key_union = keys.join("|");
136      let pat = format!(r#"#nr\s*\[\s*({})\s*\]"#, key_union,);
137      // XXX(soija) Once https://github.com/rust-lang/rust/issues/79524 lands, use
138      // intersperse.
139      /*
140      let pat = format!(
141          r#"#nr\s*\[\s*({})\s*\]"#,
142          table
143              .keys()
144              .map(|k| regex::escape(k))
145              .intersperse("|".to_string())
146              .collect::<String>(),
147      );
148      */
149      Some(regex::Regex::new(&pat).unwrap())
150    };
151    Self { table, regex }
152  }
153}