Skip to main content

shuyi_assets/
ron.rs

1//! Reading RON assets with `file:line:col` errors. As a format (ADR-0007),
2//! [`Ron`] reads any kind that serde reads: a game's own data tables.
3use crate::prelude::*;
4
5use core::fmt;
6use core::marker::PhantomData;
7
8use serde::de::{DeserializeOwned, DeserializeSeed};
9
10use crate::bundle::Bundle;
11use crate::error::{ContentError, codes};
12use crate::format::{AssetKind, AssetPath, DecodeCx, Format, TEXT_MAX_BYTES, text};
13
14/// A RON file of a kind serde reads (`ron`, `.ron`, unless
15/// [`Ron::named`] says), read as [`parse_ron`] says: a game's data table,
16/// say, whose own checks run after.
17pub struct Ron<T> {
18    id: &'static str,
19    extensions: &'static [&'static str],
20    kind: PhantomData<fn() -> T>,
21}
22
23impl<T> Ron<T> {
24    /// The format `ron`, of `.ron` files.
25    pub const fn new() -> Self {
26        Self::named("ron", &["ron"])
27    }
28
29    /// The format `id`, of files with `extensions`.
30    pub const fn named(id: &'static str, extensions: &'static [&'static str]) -> Self {
31        Self {
32            id,
33            extensions,
34            kind: PhantomData,
35        }
36    }
37}
38
39impl<T> Default for Ron<T> {
40    fn default() -> Self {
41        Self::new()
42    }
43}
44
45impl<T> fmt::Debug for Ron<T> {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        f.debug_struct("Ron")
48            .field("id", &self.id)
49            .field("extensions", &self.extensions)
50            .finish()
51    }
52}
53
54impl<T: AssetKind + DeserializeOwned> Format for Ron<T> {
55    type Asset = T;
56
57    fn id(&self) -> &'static str {
58        self.id
59    }
60
61    fn extensions(&self) -> &'static [&'static str] {
62        self.extensions
63    }
64
65    fn max_bytes(&self) -> usize {
66        TEXT_MAX_BYTES
67    }
68
69    fn decode(
70        &self,
71        file: &AssetPath,
72        bytes: &[u8],
73        _cx: &DecodeCx<'_>,
74    ) -> Result<T, Vec<ContentError>> {
75        parse_ron(file.shown(), text(file, bytes)?).map_err(|e| vec![e])
76    }
77}
78
79/// Loads and deserializes the RON file at `path` in `bundle`.
80pub fn load_ron<T: DeserializeOwned>(bundle: &Bundle, path: &str) -> Result<T, ContentError> {
81    let display = bundle.display_path(path);
82    let text = bundle
83        .file(path)
84        .ok_or_else(|| ContentError::not_found(&display))?;
85    parse_ron(&display, text)
86}
87
88/// Deserializes RON `source`; errors are attributed to `file` (a
89/// human-facing path) with RON's 1-based line and column.
90pub fn parse_ron<T: DeserializeOwned>(file: &str, source: &str) -> Result<T, ContentError> {
91    ::ron::from_str(source).map_err(|e| error(file, &e))
92}
93
94/// Like [`parse_ron`], with RON's `implicit_some` on: an optional field
95/// is written as its value, not in `Some(…)`.
96pub fn parse_ron_implicit_some<T: DeserializeOwned>(
97    file: &str,
98    source: &str,
99) -> Result<T, ContentError> {
100    ::ron::Options::default()
101        .with_default_extension(::ron::extensions::Extensions::IMPLICIT_SOME)
102        .from_str(source)
103        .map_err(|e| error(file, &e))
104}
105
106/// Like [`parse_ron`], reading with `seed`: for a value whose shape is
107/// known only at run time.
108pub fn parse_ron_seed<'de, S: DeserializeSeed<'de>>(
109    file: &str,
110    source: &'de str,
111    seed: S,
112) -> Result<S::Value, ContentError> {
113    ::ron::Options::default()
114        .from_str_seed(source, seed)
115        .map_err(|e| error(file, &e))
116}
117
118/// `e` as an error of `file`.
119pub(crate) fn error(file: &str, e: &::ron::error::SpannedError) -> ContentError {
120    // `span.end` is the offending token; `span.start` can be the
121    // whitespace before it.
122    let pos = e.span.end;
123    ContentError::new(file, e.code.to_string())
124        .at(to_u32(pos.line), Some(to_u32(pos.col)))
125        .with_code(codes::PARSE)
126}
127
128fn to_u32(n: usize) -> u32 {
129    u32::try_from(n).unwrap_or(u32::MAX)
130}
131
132#[cfg(test)]
133mod tests {
134    use std::collections::BTreeMap;
135
136    use super::*;
137    use crate::bundle::Layer;
138
139    #[test]
140    fn parses_valid_ron() {
141        let m: BTreeMap<String, u8> =
142            parse_ron("x.ron", "{\"a\": 1, \"b\": 2}").unwrap_or_default();
143        assert_eq!(m.get("b"), Some(&2));
144    }
145
146    #[test]
147    fn syntax_error_reports_file_line_and_column() {
148        // Line 3, column 10 is the stray `?` where a value should start.
149        let src = "{\n    \"a\": 1,\n    \"b\": ?,\n}";
150        let err = parse_ron::<BTreeMap<String, u8>>("base/data/t.ron", src)
151            .err()
152            .unwrap_or_else(|| ContentError::new("", "expected an error"));
153        assert_eq!(err.file, "base/data/t.ron");
154        assert_eq!(err.line, Some(3));
155        assert_eq!(err.column, Some(10));
156        assert_eq!(err.code, Some(codes::PARSE));
157        assert!(err.to_string().starts_with("base/data/t.ron:3:10: "));
158    }
159
160    #[test]
161    fn type_error_reports_position() {
162        let src = "{\n\"a\": \"not a number\"}";
163        let err = parse_ron::<BTreeMap<String, u8>>("t.ron", src)
164            .err()
165            .unwrap_or_else(|| ContentError::new("", "expected an error"));
166        assert_eq!(err.line, Some(2));
167    }
168
169    #[test]
170    fn huge_positions_saturate() {
171        assert_eq!(to_u32(usize::MAX), u32::MAX);
172        assert_eq!(to_u32(7), 7);
173    }
174
175    static FILES: &[(&str, &[u8])] = &[
176        ("data/bad.ron", b"{\n\"a\": ?}"),
177        ("data/good.ron", b"{\"a\": 1}"),
178    ];
179
180    fn bundle() -> Bundle {
181        Bundle::new(vec![Layer::table("base", FILES)])
182    }
183
184    #[test]
185    fn loads_a_file_of_the_bundle() {
186        let m: Result<BTreeMap<String, u8>, _> = load_ron(&bundle(), "data/good.ron");
187        assert_eq!(m.ok().and_then(|m| m.get("a").copied()), Some(1));
188    }
189
190    #[test]
191    fn an_error_names_the_file_as_the_bundle_shows_it() {
192        let err = load_ron::<BTreeMap<String, u8>>(&bundle(), "data/bad.ron")
193            .err()
194            .unwrap_or_else(|| ContentError::new("", "expected an error"));
195        assert_eq!(
196            (err.file.as_str(), err.line),
197            ("base/data/bad.ron", Some(2))
198        );
199    }
200
201    #[test]
202    fn missing_file_names_the_path() {
203        let err = load_ron::<BTreeMap<String, u8>>(&bundle(), "data/nope.ron")
204            .err()
205            .unwrap_or_else(|| ContentError::new("", "expected an error"));
206        assert_eq!(err.file, "base/data/nope.ron");
207        assert_eq!(err.line, None);
208        assert!(err.message.contains("not found"));
209    }
210}