Skip to main content

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}