meta_language/program_representation/
snapshot.rs1use serde_json::{Value, json};
2
3use super::{
4 LinkType, ProgramProjectContext, ProgramProjectSource, ProgramRepresentation,
5 ProgramRepresentationError, construct_program,
6};
7
8pub const PROGRAM_SNAPSHOT_SCHEMA_VERSION: u32 = 1;
10
11impl ProgramRepresentation {
12 #[must_use]
14 pub fn serialize_snapshot(&self) -> String {
15 let fragments = retained_fragments(self);
16 let mut project = json!({
17 "root": self.project.root(),
18 "files": self.project.files(),
19 "dependencies": self.project.dependencies(),
20 "extensions": self.project.extensions(),
21 });
22 if let Some(entry) = self.project.entry() {
24 project["entry"] = json!(entry);
25 project["sources"] = self
26 .project
27 .sources()
28 .iter()
29 .map(|file| json!({ "path": file.path(), "source": file.source() }))
30 .collect();
31 }
32 json!({
33 "schemaVersion": PROGRAM_SNAPSHOT_SCHEMA_VERSION,
34 "language": self.language,
35 "project": project,
36 "fragments": fragments,
37 })
38 .to_string()
39 }
40
41 pub fn from_snapshot(snapshot: &str) -> Result<Self, ProgramRepresentationError> {
43 let value: Value = serde_json::from_str(snapshot).map_err(|error| {
44 ProgramRepresentationError::InvalidSnapshot(format!("invalid JSON: {error}"))
45 })?;
46 let object = value.as_object().ok_or_else(|| {
47 ProgramRepresentationError::InvalidSnapshot("expected an object".to_string())
48 })?;
49 let version = object.get("schemaVersion").and_then(Value::as_u64);
50 if version != Some(u64::from(PROGRAM_SNAPSHOT_SCHEMA_VERSION)) {
51 return Err(ProgramRepresentationError::InvalidSnapshot(format!(
52 "unsupported schema version {version:?}"
53 )));
54 }
55 let language = required_string(object.get("language"), "language")?;
56 let project = read_project(object.get("project"))?;
57 let fragments = object
58 .get("fragments")
59 .and_then(Value::as_array)
60 .ok_or_else(|| {
61 ProgramRepresentationError::InvalidSnapshot(
62 "fragments must be an array".to_string(),
63 )
64 })?;
65 let mut source = String::new();
66 let mut expected_start = 0_u64;
67 for fragment in fragments {
68 let fragment = fragment.as_object().ok_or_else(|| {
69 ProgramRepresentationError::InvalidSnapshot(
70 "fragment must be an object".to_string(),
71 )
72 })?;
73 let byte_start = fragment.get("byteStart").and_then(Value::as_u64);
74 let byte_end = fragment.get("byteEnd").and_then(Value::as_u64);
75 let text = fragment.get("text").and_then(Value::as_str);
76 let (Some(byte_start), Some(byte_end), Some(text)) = (byte_start, byte_end, text)
77 else {
78 return Err(ProgramRepresentationError::InvalidSnapshot(
79 "fragment fields have invalid types".to_string(),
80 ));
81 };
82 let text_length = u64::try_from(text.len()).map_err(|_| {
83 ProgramRepresentationError::InvalidSnapshot(
84 "fragment text length does not fit u64".to_string(),
85 )
86 })?;
87 if byte_start != expected_start
88 || byte_end < byte_start
89 || text_length != byte_end - byte_start
90 {
91 return Err(ProgramRepresentationError::InvalidSnapshot(format!(
92 "invalid fragment span at byte {expected_start}"
93 )));
94 }
95 source.push_str(text);
96 expected_start = byte_end;
97 }
98 construct_program(&source, language, project)
99 }
100}
101
102pub fn construct_program_from_fragments<S: AsRef<str>>(
104 fragments: &[S],
105 language: &str,
106 project: ProgramProjectContext,
107) -> Result<ProgramRepresentation, ProgramRepresentationError> {
108 let source = fragments
109 .iter()
110 .fold(String::new(), |mut source, fragment| {
111 source.push_str(fragment.as_ref());
112 source
113 });
114 construct_program(&source, language, project)
115}
116
117fn retained_fragments(program: &ProgramRepresentation) -> Vec<Value> {
118 let mut tokens = program
119 .network
120 .links()
121 .filter(|link| link.metadata().link_type() == Some(LinkType::Token))
122 .filter(|link| !link.metadata().flags().is_missing())
123 .filter_map(|link| {
124 let range = link.metadata().span()?.byte_range();
125 Some((
126 range.start(),
127 link.id().as_u64(),
128 range.end(),
129 link.metadata().term()?.to_string(),
130 ))
131 })
132 .collect::<Vec<_>>();
133 tokens.sort_by_key(|(start, id, _end, _text)| (*start, *id));
134 let mut covered_until = 0;
135 let mut fragments = Vec::new();
136 for (start, _id, end, text) in tokens {
137 if start < covered_until {
138 continue;
139 }
140 debug_assert_eq!(
141 start, covered_until,
142 "source-token spans must be contiguous"
143 );
144 debug_assert_eq!(text.len(), end - start, "source-token span must match text");
145 fragments.push(json!({ "byteStart": start, "byteEnd": end, "text": text }));
146 covered_until = end;
147 }
148 debug_assert_eq!(
149 fragments
150 .iter()
151 .filter_map(|fragment| fragment["text"].as_str())
152 .collect::<String>(),
153 program.source
154 );
155 fragments
156}
157
158fn read_project(
159 value: Option<&Value>,
160) -> Result<ProgramProjectContext, ProgramRepresentationError> {
161 let project = value.and_then(Value::as_object).ok_or_else(|| {
162 ProgramRepresentationError::InvalidSnapshot("project must be an object".to_string())
163 })?;
164 let context = ProgramProjectContext::new(
165 required_string(project.get("root"), "project.root")?,
166 string_array(project.get("files"), "project.files")?,
167 string_array(project.get("dependencies"), "project.dependencies")?,
168 )
169 .with_extensions(string_array(
170 project.get("extensions"),
171 "project.extensions",
172 )?);
173 if project.get("entry").is_none() && project.get("sources").is_none() {
174 return Ok(context);
175 }
176 let entry = required_string(project.get("entry"), "project.entry")?;
177 let sources = project
178 .get("sources")
179 .and_then(Value::as_array)
180 .ok_or_else(|| {
181 ProgramRepresentationError::InvalidSnapshot(
182 "project.sources must be an array".to_string(),
183 )
184 })?
185 .iter()
186 .map(|file| {
187 match (
188 file.get("path").and_then(Value::as_str),
189 file.get("source").and_then(Value::as_str),
190 ) {
191 (Some(path), Some(source)) => Ok(ProgramProjectSource::new(path, source)),
192 _ => Err(ProgramRepresentationError::InvalidSnapshot(
193 "project.sources entries must have a path and a source".to_string(),
194 )),
195 }
196 })
197 .collect::<Result<Vec<_>, _>>()?;
198 Ok(context.with_entry(entry, sources))
199}
200
201fn required_string<'a>(
202 value: Option<&'a Value>,
203 name: &str,
204) -> Result<&'a str, ProgramRepresentationError> {
205 value.and_then(Value::as_str).ok_or_else(|| {
206 ProgramRepresentationError::InvalidSnapshot(format!("{name} must be a string"))
207 })
208}
209
210fn string_array(
211 value: Option<&Value>,
212 name: &str,
213) -> Result<Vec<String>, ProgramRepresentationError> {
214 value
215 .and_then(Value::as_array)
216 .ok_or_else(|| {
217 ProgramRepresentationError::InvalidSnapshot(format!("{name} must be an array"))
218 })?
219 .iter()
220 .map(|entry| {
221 entry.as_str().map(ToString::to_string).ok_or_else(|| {
222 ProgramRepresentationError::InvalidSnapshot(format!(
223 "{name} entries must be strings"
224 ))
225 })
226 })
227 .collect()
228}