| use std::collections::BTreeSet; | |
| use std::fmt::{self, Write as _}; | |
| use std::marker::PhantomData; | |
| use std::str::FromStr; | |
| use itertools::Itertools as _; | |
| use serde::{Deserialize, Serialize}; | |
| /// Append-only templates for sorted, deduplicated lists of items. | |
| /// | |
| /// Last line of the rendered output is a comment encoding the next insertion point. | |
| #[derive(Debug, Clone)] | |
| pub(crate) struct SortedTemplate<F> { | |
| format: PhantomData<F>, | |
| before: String, | |
| after: String, | |
| fragments: BTreeSet<String>, | |
| } | |
| /// Written to last line of file to specify the location of each fragment | |
| #[derive(Serialize, Deserialize, Debug, Clone)] | |
| struct Offset { | |
| /// Index of the first byte in the template | |
| start: usize, | |
| /// The length of each fragment in the encoded template, including the separator | |
| fragment_lengths: Vec<usize>, | |
| } | |
| impl<F> SortedTemplate<F> { | |
| /// Generate this template from arbitrary text. | |
| /// Will insert wherever the substring `delimiter` can be found. | |
| /// Errors if it does not appear exactly once. | |
| pub(crate) fn from_template(template: &str, delimiter: &str) -> Result<Self, Error> { | |
| let mut split = template.split(delimiter); | |
| let before = split.next().ok_or(Error("delimiter should appear at least once"))?; | |
| let after = split.next().ok_or(Error("delimiter should appear at least once"))?; | |
| // not `split_once` because we want to check for too many occurrences | |
| if split.next().is_some() { | |
| return Err(Error("delimiter should appear at most once")); | |
| } | |
| Ok(Self::from_before_after(before, after)) | |
| } | |
| /// Template will insert fragments between `before` and `after` | |
| pub(crate) fn from_before_after<S: ToString, T: ToString>(before: S, after: T) -> Self { | |
| let before = before.to_string(); | |
| let after = after.to_string(); | |
| Self { format: PhantomData, before, after, fragments: Default::default() } | |
| } | |
| } | |
| impl<F> SortedTemplate<F> { | |
| /// Adds this text to the template | |
| pub(crate) fn append(&mut self, insert: String) { | |
| self.fragments.insert(insert); | |
| } | |
| } | |
| impl<F: FileFormat> fmt::Display for SortedTemplate<F> { | |
| fn fmt(&self, mut f: &mut fmt::Formatter<'_>) -> fmt::Result { | |
| let mut fragment_lengths = Vec::default(); | |
| write!(f, "{}", self.before)?; | |
| for (p, fragment) in self.fragments.iter().with_position() { | |
| let mut f = DeltaWriter { inner: &mut f, delta: 0 }; | |
| let sep = if p.is_first() { "" } else { F::SEPARATOR }; | |
| f.write_str(sep)?; | |
| f.write_str(fragment)?; | |
| fragment_lengths.push(f.delta); | |
| } | |
| let offset = Offset { start: self.before.len(), fragment_lengths }; | |
| let offset = serde_json::to_string(&offset).unwrap(); | |
| write!(f, "{}\n{}{}{}", self.after, F::COMMENT_START, offset, F::COMMENT_END) | |
| } | |
| } | |
| impl<F: FileFormat> FromStr for SortedTemplate<F> { | |
| type Err = Error; | |
| fn from_str(s: &str) -> Result<Self, Self::Err> { | |
| let (s, offset) = s | |
| .rsplit_once("\n") | |
| .ok_or(Error("invalid format: should have a newline on the last line"))?; | |
| let offset = offset | |
| .strip_prefix(F::COMMENT_START) | |
| .ok_or(Error("last line expected to start with a comment"))?; | |
| let offset = offset | |
| .strip_suffix(F::COMMENT_END) | |
| .ok_or(Error("last line expected to end with a comment"))?; | |
| let offset: Offset = serde_json::from_str(offset).map_err(|_| { | |
| Error("could not find insertion location descriptor object on last line") | |
| })?; | |
| let (before, mut s) = | |
| s.split_at_checked(offset.start).ok_or(Error("invalid start: out of bounds"))?; | |
| let mut fragments = BTreeSet::default(); | |
| for (p, &index) in offset.fragment_lengths.iter().with_position() { | |
| let (fragment, rest) = | |
| s.split_at_checked(index).ok_or(Error("invalid fragment length: out of bounds"))?; | |
| s = rest; | |
| let sep = if p.is_first() { "" } else { F::SEPARATOR }; | |
| let fragment = fragment | |
| .strip_prefix(sep) | |
| .ok_or(Error("invalid fragment length: expected to find separator here"))?; | |
| fragments.insert(fragment.to_string()); | |
| } | |
| Ok(Self { | |
| format: PhantomData, | |
| before: before.to_string(), | |
| after: s.to_string(), | |
| fragments, | |
| }) | |
| } | |
| } | |
| pub(crate) trait FileFormat { | |
| const COMMENT_START: &'static str; | |
| const COMMENT_END: &'static str; | |
| const SEPARATOR: &'static str; | |
| } | |
| #[derive(Debug, Clone)] | |
| pub(crate) struct Html; | |
| impl FileFormat for Html { | |
| const COMMENT_START: &'static str = "<!--"; | |
| const COMMENT_END: &'static str = "-->"; | |
| const SEPARATOR: &'static str = ""; | |
| } | |
| #[derive(Debug, Clone)] | |
| pub(crate) struct Js; | |
| impl FileFormat for Js { | |
| const COMMENT_START: &'static str = "//"; | |
| const COMMENT_END: &'static str = ""; | |
| const SEPARATOR: &'static str = ","; | |
| } | |
| #[derive(Debug, Clone)] | |
| pub(crate) struct Error(&'static str); | |
| impl fmt::Display for Error { | |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | |
| write!(f, "invalid template: {}", self.0) | |
| } | |
| } | |
| struct DeltaWriter<W> { | |
| inner: W, | |
| delta: usize, | |
| } | |
| impl<W: fmt::Write> fmt::Write for DeltaWriter<W> { | |
| fn write_str(&mut self, s: &str) -> fmt::Result { | |
| self.inner.write_str(s)?; | |
| self.delta += s.len(); | |
| Ok(()) | |
| } | |
| } | |
| #[cfg(test)] | |
| mod tests; |