example_spec.rs

  1use serde::{Deserialize, Serialize};
  2use std::{fmt::Write as _, mem, path::Path, sync::Arc};
  3
  4#[derive(Clone, Debug, Serialize, Deserialize)]
  5pub struct ExampleSpec {
  6    #[serde(default)]
  7    pub name: String,
  8    pub repository_url: String,
  9    pub revision: String,
 10    #[serde(default)]
 11    pub uncommitted_diff: String,
 12    pub cursor_path: Arc<Path>,
 13    pub cursor_position: String,
 14    pub edit_history: String,
 15    pub expected_patch: String,
 16}
 17
 18const UNCOMMITTED_DIFF_HEADING: &str = "Uncommitted Diff";
 19const EDIT_HISTORY_HEADING: &str = "Edit History";
 20const CURSOR_POSITION_HEADING: &str = "Cursor Position";
 21const EXPECTED_PATCH_HEADING: &str = "Expected Patch";
 22const EXPECTED_CONTEXT_HEADING: &str = "Expected Context";
 23const REPOSITORY_URL_FIELD: &str = "repository_url";
 24const REVISION_FIELD: &str = "revision";
 25
 26impl ExampleSpec {
 27    /// Format this example spec as markdown.
 28    pub fn to_markdown(&self) -> String {
 29        let mut markdown = String::new();
 30
 31        _ = writeln!(markdown, "# {}", self.name);
 32        markdown.push('\n');
 33
 34        _ = writeln!(markdown, "repository_url = {}", self.repository_url);
 35        _ = writeln!(markdown, "revision = {}", self.revision);
 36        markdown.push('\n');
 37
 38        if !self.uncommitted_diff.is_empty() {
 39            _ = writeln!(markdown, "## {}", UNCOMMITTED_DIFF_HEADING);
 40            _ = writeln!(markdown);
 41            _ = writeln!(markdown, "```diff");
 42            markdown.push_str(&self.uncommitted_diff);
 43            if !markdown.ends_with('\n') {
 44                markdown.push('\n');
 45            }
 46            _ = writeln!(markdown, "```");
 47            markdown.push('\n');
 48        }
 49
 50        _ = writeln!(markdown, "## {}", EDIT_HISTORY_HEADING);
 51        _ = writeln!(markdown);
 52
 53        if self.edit_history.is_empty() {
 54            _ = writeln!(markdown, "(No edit history)");
 55            _ = writeln!(markdown);
 56        } else {
 57            _ = writeln!(markdown, "```diff");
 58            markdown.push_str(&self.edit_history);
 59            if !markdown.ends_with('\n') {
 60                markdown.push('\n');
 61            }
 62            _ = writeln!(markdown, "```");
 63            markdown.push('\n');
 64        }
 65
 66        _ = writeln!(markdown, "## {}", CURSOR_POSITION_HEADING);
 67        _ = writeln!(markdown);
 68        _ = writeln!(markdown, "```{}", self.cursor_path.to_string_lossy());
 69        markdown.push_str(&self.cursor_position);
 70        if !markdown.ends_with('\n') {
 71            markdown.push('\n');
 72        }
 73        _ = writeln!(markdown, "```");
 74        markdown.push('\n');
 75
 76        _ = writeln!(markdown, "## {}", EXPECTED_PATCH_HEADING);
 77        markdown.push('\n');
 78        _ = writeln!(markdown, "```diff");
 79        markdown.push_str(&self.expected_patch);
 80        if !markdown.ends_with('\n') {
 81            markdown.push('\n');
 82        }
 83        _ = writeln!(markdown, "```");
 84        markdown.push('\n');
 85
 86        markdown
 87    }
 88
 89    /// Parse an example spec from markdown.
 90    pub fn from_markdown(name: String, input: &str) -> anyhow::Result<Self> {
 91        use pulldown_cmark::{CodeBlockKind, CowStr, Event, HeadingLevel, Parser, Tag, TagEnd};
 92
 93        let parser = Parser::new(input);
 94
 95        let mut spec = ExampleSpec {
 96            name,
 97            repository_url: String::new(),
 98            revision: String::new(),
 99            uncommitted_diff: String::new(),
100            cursor_path: Path::new("").into(),
101            cursor_position: String::new(),
102            edit_history: String::new(),
103            expected_patch: String::new(),
104        };
105
106        let mut text = String::new();
107        let mut block_info: CowStr = "".into();
108
109        #[derive(PartialEq)]
110        enum Section {
111            Start,
112            UncommittedDiff,
113            EditHistory,
114            CursorPosition,
115            ExpectedExcerpts,
116            ExpectedPatch,
117            Other,
118        }
119
120        let mut current_section = Section::Start;
121
122        for event in parser {
123            match event {
124                Event::Text(line) => {
125                    text.push_str(&line);
126
127                    if let Section::Start = current_section
128                        && let Some((field, value)) = line.split_once('=')
129                    {
130                        match field.trim() {
131                            REPOSITORY_URL_FIELD => {
132                                spec.repository_url = value.trim().to_string();
133                            }
134                            REVISION_FIELD => {
135                                spec.revision = value.trim().to_string();
136                            }
137                            _ => {}
138                        }
139                    }
140                }
141                Event::End(TagEnd::Heading(HeadingLevel::H2)) => {
142                    let title = mem::take(&mut text);
143                    current_section = if title.eq_ignore_ascii_case(UNCOMMITTED_DIFF_HEADING) {
144                        Section::UncommittedDiff
145                    } else if title.eq_ignore_ascii_case(EDIT_HISTORY_HEADING) {
146                        Section::EditHistory
147                    } else if title.eq_ignore_ascii_case(CURSOR_POSITION_HEADING) {
148                        Section::CursorPosition
149                    } else if title.eq_ignore_ascii_case(EXPECTED_PATCH_HEADING) {
150                        Section::ExpectedPatch
151                    } else if title.eq_ignore_ascii_case(EXPECTED_CONTEXT_HEADING) {
152                        Section::ExpectedExcerpts
153                    } else {
154                        Section::Other
155                    };
156                }
157                Event::End(TagEnd::Heading(HeadingLevel::H3)) => {
158                    mem::take(&mut text);
159                }
160                Event::End(TagEnd::Heading(HeadingLevel::H4)) => {
161                    mem::take(&mut text);
162                }
163                Event::End(TagEnd::Heading(level)) => {
164                    anyhow::bail!("Unexpected heading level: {level}");
165                }
166                Event::Start(Tag::CodeBlock(kind)) => {
167                    match kind {
168                        CodeBlockKind::Fenced(info) => {
169                            block_info = info;
170                        }
171                        CodeBlockKind::Indented => {
172                            anyhow::bail!("Unexpected indented codeblock");
173                        }
174                    };
175                }
176                Event::Start(_) => {
177                    text.clear();
178                    block_info = "".into();
179                }
180                Event::End(TagEnd::CodeBlock) => {
181                    let block_info = block_info.trim();
182                    match current_section {
183                        Section::UncommittedDiff => {
184                            spec.uncommitted_diff = mem::take(&mut text);
185                        }
186                        Section::EditHistory => {
187                            spec.edit_history.push_str(&mem::take(&mut text));
188                        }
189                        Section::CursorPosition => {
190                            spec.cursor_path = Path::new(block_info).into();
191                            spec.cursor_position = mem::take(&mut text);
192                        }
193                        Section::ExpectedExcerpts => {
194                            mem::take(&mut text);
195                        }
196                        Section::ExpectedPatch => {
197                            spec.expected_patch = mem::take(&mut text);
198                        }
199                        Section::Start | Section::Other => {}
200                    }
201                }
202                _ => {}
203            }
204        }
205
206        if spec.cursor_path.as_ref() == Path::new("") || spec.cursor_position.is_empty() {
207            anyhow::bail!("Missing cursor position codeblock");
208        }
209
210        Ok(spec)
211    }
212}