scratch_test_model/reader.rs
1use crate::aux::JsonCtx;
2use crate::error::ModelError;
3use crate::scopes::{ProjectDoc, Target};
4
5#[cfg(feature = "sb3")]
6#[allow(unused)]
7pub use sb3::{json_from_sb3_file, json_from_sb3_stream};
8
9impl ProjectDoc {
10 /// Parses a document from a json value
11 pub fn from_json<'a>(doc: &'a serde_json::Value) -> Result<ProjectDoc, ModelError<'a>> {
12 use crate::aux::WithJsonContextExt;
13 let semver = doc["meta"]["semver"].as_str().map(svalue::ARc::from);
14 let targets = doc["targets"]
15 .as_array()
16 .ok_or(ModelError::NoTargetsArray(doc))?;
17 let targets: Result<Vec<Target>, JsonCtx<'a, _>> = targets
18 .iter()
19 .map(|x| Target::from_json.with_ctx(x))
20 .collect();
21 Ok(ProjectDoc {
22 targets: targets?.into(),
23 semver,
24 })
25 }
26}
27
28#[cfg(feature = "sb3")]
29mod sb3 {
30 use std::{
31 io::{Read, Seek},
32 path::Path,
33 };
34
35 use crate::{
36 ProjectDoc,
37 error::{DocError, ModelError},
38 };
39
40 fn read_json_from_file<R: Read + Seek, T: std::fmt::Debug>(
41 reader: &mut R,
42 tag: T,
43 ) -> Result<serde_json::Value, DocError> {
44 let mut archive = zip::ZipArchive::new(reader)?;
45 for idx in 0..archive.len() {
46 let file = match archive.by_index(idx) {
47 Ok(ok) => ok,
48 Err(err) => {
49 log::error!("Error encountered while reading sb3 {tag:?}: {err}");
50 return Err(DocError::Io(err.into()));
51 }
52 };
53 if file.name()?.to_lowercase().ends_with(".json") {
54 return Ok(serde_json::from_reader(file)?);
55 }
56 }
57 Err(DocError::NoDocument)
58 }
59
60 pub fn json_from_sb3_stream<R: Read + Seek, T: std::fmt::Debug>(
61 handle: &mut R,
62 tag: Option<T>,
63 ) -> Result<serde_json::Value, DocError> {
64 let tag = tag.map(|s| format!("{s:?}")).unwrap_or_default();
65 read_json_from_file(handle, tag)
66 }
67 pub fn json_from_sb3_file(path: impl AsRef<Path>) -> Result<serde_json::Value, DocError> {
68 // wrapper is generic and will be duplicated for every concrete type
69 // while this implementation can be used for all of them
70 fn from_sb3_file_impl(path: &Path) -> Result<serde_json::Value, DocError> {
71 let mut handle = std::fs::File::open(path)
72 .map_err(|err| DocError::FileRead(path.to_path_buf(), err))?;
73 read_json_from_file(&mut handle, path)
74 }
75 let path = path.as_ref();
76 from_sb3_file_impl(path)
77 }
78
79 // TODO: think about public API
80
81 #[ouroboros::self_referencing]
82 pub struct InnerParseResult {
83 value: serde_json::Value,
84 #[borrows(value)]
85 #[covariant]
86 result: Result<ProjectDoc, ModelError<'this>>,
87 }
88 pub struct ParseResult(InnerParseResult);
89
90 #[allow(unused)]
91 impl ParseResult {
92 pub fn json(&self) -> &serde_json::Value {
93 self.0.borrow_value()
94 }
95 pub fn result(&self) -> &Result<ProjectDoc, ModelError<'_>> {
96 self.0.borrow_result()
97 }
98 pub fn ok(&self) -> Option<&ProjectDoc> {
99 self.result().as_ref().ok()
100 }
101 pub fn err(&self) -> Option<&ModelError<'_>> {
102 self.result().as_ref().err()
103 }
104 }
105
106 pub type ReadResult = Result<ParseResult, DocError>;
107
108 impl ProjectDoc {
109 // TODO: think about public API
110 pub(crate) fn from_owned_json(doc: serde_json::Value) -> ParseResult {
111 let i = InnerParseResultBuilder {
112 value: doc,
113 result_builder: |doc| ProjectDoc::from_json(doc),
114 }
115 .build();
116 ParseResult(i)
117 }
118 #[allow(unused)]
119 // TODO: think about public API
120 /// Read a sb3 file by [`Path`]
121 ///
122 /// This is a convenience method that will:
123 ///
124 /// 1. Open the provided path
125 /// - `Err(`[`DocError::FileRead`]`)` in case of IO error
126 /// 2. Extract `project.json`
127 /// - `Err(`[`DocError::Io`]`)` in case of IO error while reading the zip file
128 /// - `Err(`[`DocError::NoDocument`]`)` if file list ends before
129 /// `project.json` was found
130 /// 3. Parse `project.json`
131 /// - `Err(`[`DocError::Json`]`)` if the `project.json` fails to parse
132 /// 4. Read the information to generate a valid document
133 /// - `Ok(...(`[`ModelError`]`)...)` if the read data is represents
134 /// a semantically invalid program.
135 /// - `Ok(...(`[`ProjectDoc`]`)...)` if everythinf is valid
136 ///
137 /// The real inner `Result` can be borrowed via [`ParseResult::result`]
138 ///
139 ///
140 pub(crate) fn from_sb3_file<'a>(path: impl AsRef<Path> + 'a) -> ReadResult {
141 // wrapper is generic and will be duplicated for every concrete type
142 // while this implementation can be used for all of them
143 fn from_sb3_file_impl(path: &Path) -> ReadResult {
144 let mut handle = std::fs::File::open(path)
145 .map_err(|err| DocError::FileRead(path.to_path_buf(), err))?;
146 loop {
147 match zip::read::read_zipfile_from_stream(&mut handle) {
148 Ok(Some(file)) => {
149 if file.name()?.to_lowercase().ends_with(".json") {
150 let value: serde_json::Value = serde_json::from_reader(file)?;
151 return Ok(ProjectDoc::from_owned_json(value));
152 }
153 }
154 Ok(None) => Err(DocError::NoDocument)?,
155 Err(e) => {
156 log::error!("Error encountered while reading sb3 {path:?}: {e:?}");
157 Err(DocError::Io(e.into()))?
158 }
159 }
160 }
161 }
162 let path = path.as_ref();
163 from_sb3_file_impl(path)
164 }
165 /// Read sb3 data as a stream from an instance of [`Read`]
166 /// with a suffix string used for context in error logs
167 ///
168 /// The suffix should be empty or start with a space
169 /// (that's why this method is private and should be used only
170 /// by functions of this type) and will be used to give additional
171 /// context to the log messages for errors.
172 /// This could for example be a file name.
173 ///
174 /// This is a convenience method that will:
175 ///
176 /// 1. Extract `project.json`
177 /// - `Err(`[`DocError::Io`]`)` in case of IO error while reading the zip file
178 /// - `Err(`[`DocError::NoDocument`]`)` if file list ends before
179 /// `project.json` was found
180 /// 2. Parse `project.json`
181 /// - `Err(`[`DocError::Json`]`)` if the `project.json` fails to parse
182 /// 3. Read the information to generate a valid document
183 /// - `Ok(...(`[`ModelError`]`)...)` if the read data is represents
184 /// a semantically invalid program.
185 /// - `Ok(...(`[`ProjectDoc`]`)...)` if everythinf is valid
186 ///
187 /// The real inner `Result` can be borrowed via [`ParseResult::result`]
188 ///
189 fn _inner_from_sb3_stream<R: Read>(handle: &mut R, suffix: &str) -> ReadResult {
190 loop {
191 match zip::read::read_zipfile_from_stream(handle) {
192 Ok(Some(file)) => {
193 if file.name()?.to_lowercase().ends_with(".json") {
194 let value: serde_json::Value = serde_json::from_reader(file)?;
195 return Ok(Self::from_owned_json(value));
196 }
197 }
198 Ok(None) => Err(DocError::NoDocument)?,
199 Err(e) => {
200 log::error!("Error encountered while reading sb3{suffix}: {e:?}");
201 Err(DocError::Io(e.into()))?
202 }
203 }
204 }
205 }
206 #[allow(unused)]
207 // TODO: think about public API
208 /// Read sb3 data as a stream from an instance of [`Read`]
209 /// where a tag with extra context for log messages is provided
210 ///
211 /// This is a convenience method that will:
212 ///
213 /// 1. Extract `project.json`
214 /// - `Err(`[`DocError::Io`]`)` in case of IO error while reading the zip file
215 /// - `Err(`[`DocError::NoDocument`]`)` if file list ends before
216 /// `project.json` was found
217 /// 2. Parse `project.json`
218 /// - `Err(`[`DocError::Json`]`)` if the `project.json` fails to parse
219 /// 3. Read the information to generate a valid document
220 /// - `Ok(...(`[`ModelError`]`)...)` if the read data is represents
221 /// a semantically invalid program.
222 /// - `Ok(...(`[`ProjectDoc`]`)...)` if everythinf is valid
223 ///
224 /// The real inner `Result` can be borrowed via [`ParseResult::result`]
225 ///
226 pub(crate) fn from_tagged_sb3_stream<R: Read, T: std::fmt::Display>(
227 handle: &mut R,
228 tag: &T,
229 ) -> ReadResult {
230 Self::_inner_from_sb3_stream(handle, &format!(" {tag}"))
231 }
232 #[allow(unused)]
233 // TODO: think about public API
234 /// Read sb3 data as a stream from an instance of [`Read`]
235 ///
236 /// (If you want to tag this stream with a filename
237 /// use [`Self::from_tagged_sb3_stream`])
238 ///
239 /// This is a convenience method that will:
240 ///
241 /// 1. Extract `project.json`
242 /// - `Err(`[`DocError::Io`]`)` in case of IO error while reading the zip file
243 /// - `Err(`[`DocError::NoDocument`]`)` if file list ends before
244 /// `project.json` was found
245 /// 2. Parse `project.json`
246 /// - `Err(`[`DocError::Json`]`)` if the `project.json` fails to parse
247 /// 3. Read the information to generate a valid document
248 /// - `Ok(...(`[`ModelError`]`)...)` if the read data is represents
249 /// a semantically invalid program.
250 /// - `Ok(...(`[`ProjectDoc`]`)...)` if everythinf is valid
251 ///
252 /// The real inner `Result` can be borrowed via [`ParseResult::result`]
253 ///
254 pub(crate) fn from_sb3_stream<R: Read>(handle: &mut R) -> ReadResult {
255 Self::_inner_from_sb3_stream(handle, "")
256 }
257 }
258}