frontend-utils: Add initial bundle infrastructure
This commit is contained in:
parent
906837c6a1
commit
961bc0a7c5
|
@ -4285,6 +4285,8 @@ dependencies = [
|
|||
name = "ruffle_frontend_utils"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"toml_edit 0.22.9",
|
||||
"tracing",
|
||||
"url",
|
||||
|
|
|
@ -14,3 +14,7 @@ workspace = true
|
|||
toml_edit = { version = "0.22.9", features = ["parse"] }
|
||||
url = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
|
@ -0,0 +1,148 @@
|
|||
use crate::bundle::info::{
|
||||
BundleInformation, BundleInformationParseError, BUNDLE_INFORMATION_FILENAME,
|
||||
};
|
||||
use crate::bundle::source::BundleSource;
|
||||
use std::path::Path;
|
||||
|
||||
pub mod info;
|
||||
pub mod source;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum BundleError {
|
||||
#[error("Invalid ruffle-bundle.toml")]
|
||||
InvalidBundleInformation(#[from] BundleInformationParseError),
|
||||
|
||||
#[error("Missing or corrupt ruffle-bundle.toml")]
|
||||
MissingBundleInformation,
|
||||
|
||||
#[error("Invalid bundle source")]
|
||||
InvalidSource(#[from] source::BundleSourceError),
|
||||
|
||||
#[error("Bundle does not exist")]
|
||||
BundleDoesntExist,
|
||||
}
|
||||
|
||||
pub struct Bundle {
|
||||
source: BundleSource,
|
||||
information: BundleInformation,
|
||||
warnings: Vec<String>,
|
||||
}
|
||||
|
||||
impl Bundle {
|
||||
pub fn from_path(path: impl AsRef<Path>) -> Result<Bundle, BundleError> {
|
||||
let path = path.as_ref();
|
||||
if !path.exists() {
|
||||
return Err(BundleError::BundleDoesntExist);
|
||||
}
|
||||
let source = BundleSource::from_path(path)?;
|
||||
let info_file = source
|
||||
.read_file(BUNDLE_INFORMATION_FILENAME)
|
||||
.map_err(|_| BundleError::MissingBundleInformation)?;
|
||||
let info_text =
|
||||
String::from_utf8(info_file).map_err(|_| BundleError::MissingBundleInformation)?;
|
||||
let information = BundleInformation::parse(&info_text)?;
|
||||
|
||||
Ok(Bundle {
|
||||
source,
|
||||
information: information.result.take(),
|
||||
warnings: information.warnings,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn source(&self) -> &BundleSource {
|
||||
&self.source
|
||||
}
|
||||
|
||||
pub fn warnings(&self) -> &[String] {
|
||||
&self.warnings
|
||||
}
|
||||
|
||||
pub fn information(&self) -> &BundleInformation {
|
||||
&self.information
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::bundle::info::{
|
||||
BundleInformation, BundleInformationParseError, BUNDLE_INFORMATION_FILENAME,
|
||||
};
|
||||
use crate::bundle::{Bundle, BundleError};
|
||||
use tempfile::tempdir;
|
||||
use url::Url;
|
||||
|
||||
#[test]
|
||||
fn from_path_nonexistent() {
|
||||
assert!(matches!(
|
||||
Bundle::from_path("/this/path/likely/doesnt/exist"),
|
||||
Err(BundleError::BundleDoesntExist)
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_path_directory_without_bundle() {
|
||||
let tmp_dir = tempdir().unwrap();
|
||||
let result = Bundle::from_path(tmp_dir.path());
|
||||
drop(tmp_dir);
|
||||
assert!(matches!(result, Err(BundleError::MissingBundleInformation)))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_path_directory_with_folder_as_info() {
|
||||
let tmp_dir = tempdir().unwrap();
|
||||
let _ = std::fs::create_dir(tmp_dir.path().join(BUNDLE_INFORMATION_FILENAME));
|
||||
let result = Bundle::from_path(tmp_dir.path());
|
||||
drop(tmp_dir);
|
||||
assert!(matches!(result, Err(BundleError::MissingBundleInformation)))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_path_directory_with_binary_info() {
|
||||
let tmp_dir = tempdir().unwrap();
|
||||
let _ = std::fs::write(
|
||||
tmp_dir.path().join(BUNDLE_INFORMATION_FILENAME),
|
||||
[0, 159, 146, 150],
|
||||
);
|
||||
let result = Bundle::from_path(tmp_dir.path());
|
||||
drop(tmp_dir);
|
||||
assert!(matches!(result, Err(BundleError::MissingBundleInformation)))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_path_directory_with_bad_toml() {
|
||||
let tmp_dir = tempdir().unwrap();
|
||||
let _ = std::fs::write(tmp_dir.path().join(BUNDLE_INFORMATION_FILENAME), "???");
|
||||
let result = Bundle::from_path(tmp_dir.path());
|
||||
drop(tmp_dir);
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(BundleError::InvalidBundleInformation(
|
||||
BundleInformationParseError::InvalidToml(_)
|
||||
))
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_path_directory_valid() {
|
||||
let tmp_dir = tempdir().unwrap();
|
||||
let _ = std::fs::write(
|
||||
tmp_dir.path().join(BUNDLE_INFORMATION_FILENAME),
|
||||
r#"
|
||||
[bundle]
|
||||
name = "Cool Game!"
|
||||
url = "file:///game.swf"
|
||||
"#,
|
||||
);
|
||||
let result = Bundle::from_path(tmp_dir.path());
|
||||
drop(tmp_dir);
|
||||
let result = result.unwrap();
|
||||
assert_eq!(
|
||||
BundleInformation {
|
||||
name: "Cool Game!".to_string(),
|
||||
url: Url::parse("file:///game.swf").unwrap()
|
||||
},
|
||||
result.information
|
||||
);
|
||||
assert_eq!(Vec::<String>::new(), result.warnings);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
# Ruffle Bundle (.ruf) format specification
|
||||
A Ruffle Bundle is an easy way to package and share Flash games and any assets that are required to make the game work.
|
||||
|
||||
A bundle can be a directory or a renamed zip file, and must contain at minimum a `bundle.toml` file.
|
||||
|
||||
<!-- TOC -->
|
||||
* [Ruffle Bundle (.ruf) format specification](#ruffle-bundle-ruf-format-specification)
|
||||
* [Directory structure](#directory-structure)
|
||||
* [`ruffle-bundle.toml` (Bundle information)](#ruffle-bundletoml-bundle-information)
|
||||
* [`content/` (Flash content)](#content-flash-content)
|
||||
* [`ruffle-bundle.toml` file specification](#ruffle-bundletoml-file-specification)
|
||||
* [`[bundle]`](#bundle)
|
||||
* [`name` - The name of the bundle](#name---the-name-of-the-bundle)
|
||||
* [`url` - The url of the Flash content to open](#url---the-url-of-the-flash-content-to-open)
|
||||
<!-- TOC -->
|
||||
|
||||
## Directory structure
|
||||
|
||||
- `ruffle-bundle.toml` - **required**, the bundle information
|
||||
- `content/` - a directory containing any swf files, assets they need, etc.
|
||||
|
||||
More files and folders may be added in the future, as this format is expanded upon.
|
||||
|
||||
### `ruffle-bundle.toml` (Bundle information)
|
||||
This [toml](https://toml.io/) file is required and contains information that Ruffle needs to run this bundle.
|
||||
|
||||
See [the ruffle-bundle.toml file specification](#bundletoml-file-specification) for more details.
|
||||
|
||||
### `content/` (Flash content)
|
||||
Every file (and subdirectory) within this directory will be accessible to the Flash content, exposed as a **virtual filesystem**.
|
||||
|
||||
To Flash content, this is accessible through `file:///` - for example, the file `/content/game.swf` is `file:///content.swf`.
|
||||
The file `/content/locale/en.xml` is `file:///locale/en.xml`.
|
||||
|
||||
You'll want to put the `.swf` file in here, along with any extra files it may need. Files outside this directory are **not** accessible to the content.
|
||||
|
||||
## `ruffle-bundle.toml` file specification
|
||||
The absolute minimum `ruffle-bundle.toml` looks like this:
|
||||
```toml
|
||||
[bundle]
|
||||
name = "Super Mario 63"
|
||||
url = "file:///game.swf"
|
||||
```
|
||||
|
||||
If either `bundle.name` or `bundle.url` is invalid or missing, the bundle is considered to be invalid and will not open.
|
||||
The same is true if the toml document is malformed or corrupt.
|
||||
|
||||
All other fields are absolutely optional and reasonable defaults will be assumed if they're missing or invalid.
|
||||
|
||||
### `[bundle]`
|
||||
This section is required to exist, and contains the two required fields for a bundle to work in Ruffle:
|
||||
|
||||
#### `name` - The name of the bundle
|
||||
This can be anything, and is shown to the user in UI.
|
||||
Try to keep this a reasonable length, and descriptive about what this bundle actually *is*.
|
||||
|
||||
#### `url` - The url of the Flash content to open
|
||||
Whilst this **can** be a URL on the internet (for example, `url = "https://ruffle.rs/demo/logo-anim.swf"` is totally valid),
|
||||
we would recommend that it instead be the path to a file within the `content/` directory inside the bundle.
|
||||
|
||||
This way, an internet connection is not required, and the bundle won't stop working in 5 years when the website changes.
|
||||
|
||||
Remember - the `content/` directory is accessible through `file:///` - so if you have a game at `content/game.swf`, you'll want to use `url = "file:///game.swf"`.
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
use crate::parse::{DocumentHolder, ParseContext, ParseDetails, ReadExt};
|
||||
use toml_edit::DocumentMut;
|
||||
use url::Url;
|
||||
|
||||
pub const BUNDLE_INFORMATION_FILENAME: &str = "ruffle-bundle.toml";
|
||||
|
||||
#[derive(Debug, thiserror::Error, PartialEq)]
|
||||
pub enum BundleInformationParseError {
|
||||
#[error("File is not valid TOML")]
|
||||
InvalidToml(#[from] toml_edit::TomlError),
|
||||
|
||||
#[error("Invalid or missing [bundle] section")]
|
||||
InvalidBundleSection,
|
||||
|
||||
#[error("Invalid or missing bundle.name")]
|
||||
InvalidName,
|
||||
|
||||
#[error("Invalid or missing bundle.url")]
|
||||
InvalidUrl,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct BundleInformation {
|
||||
pub name: String,
|
||||
pub url: Url,
|
||||
}
|
||||
|
||||
impl BundleInformation {
|
||||
pub fn parse(
|
||||
input: &str,
|
||||
) -> Result<ParseDetails<BundleInformation>, BundleInformationParseError> {
|
||||
let document = input.parse::<DocumentMut>()?;
|
||||
|
||||
let mut cx = ParseContext::default();
|
||||
|
||||
let result = document
|
||||
.get_table_like(&mut cx, "bundle", |cx, bundle| {
|
||||
let Some(name) = bundle.parse_from_str(cx, "name") else {
|
||||
return Err(BundleInformationParseError::InvalidName);
|
||||
};
|
||||
let Some(url) = bundle.parse_from_str(cx, "url") else {
|
||||
return Err(BundleInformationParseError::InvalidUrl);
|
||||
};
|
||||
Ok(BundleInformation { name, url })
|
||||
})
|
||||
.unwrap_or(Err(BundleInformationParseError::InvalidBundleSection))?;
|
||||
|
||||
Ok(ParseDetails {
|
||||
result: DocumentHolder::new(result, document),
|
||||
warnings: cx.warnings,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::bundle::info::{BundleInformation, BundleInformationParseError};
|
||||
use url::Url;
|
||||
|
||||
fn read(input: &str) -> Result<(BundleInformation, Vec<String>), BundleInformationParseError> {
|
||||
BundleInformation::parse(input).map(|details| (details.result.take(), details.warnings))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_toml() {
|
||||
// [NA] Can't construct TomlError to be able to test this properly
|
||||
assert!(matches!(
|
||||
read("???"),
|
||||
Err(BundleInformationParseError::InvalidToml(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
assert_eq!(
|
||||
read(""),
|
||||
Err(BundleInformationParseError::InvalidBundleSection)
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_name() {
|
||||
assert_eq!(
|
||||
read("[bundle]"),
|
||||
Err(BundleInformationParseError::InvalidName)
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_name() {
|
||||
assert_eq!(
|
||||
read(
|
||||
r#"
|
||||
[bundle]
|
||||
name = 1234
|
||||
"#
|
||||
),
|
||||
Err(BundleInformationParseError::InvalidName)
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_url() {
|
||||
assert_eq!(
|
||||
read(
|
||||
r#"
|
||||
[bundle]
|
||||
name = "Cool Game!"
|
||||
"#
|
||||
),
|
||||
Err(BundleInformationParseError::InvalidUrl)
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_url_type() {
|
||||
assert_eq!(
|
||||
read(
|
||||
r#"
|
||||
[bundle]
|
||||
name = "Cool Game!"
|
||||
url = 1234
|
||||
"#
|
||||
),
|
||||
Err(BundleInformationParseError::InvalidUrl)
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_url_value() {
|
||||
assert_eq!(
|
||||
read(
|
||||
r#"
|
||||
[bundle]
|
||||
name = "Cool Game!"
|
||||
url = "invalid"
|
||||
"#
|
||||
),
|
||||
Err(BundleInformationParseError::InvalidUrl)
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn minimally_valid() {
|
||||
assert_eq!(
|
||||
read(
|
||||
r#"
|
||||
[bundle]
|
||||
name = "Cool Game!"
|
||||
url = "file:///game.swf"
|
||||
"#
|
||||
),
|
||||
Ok((
|
||||
BundleInformation {
|
||||
name: "Cool Game!".to_string(),
|
||||
url: Url::parse("file:///game.swf").unwrap(),
|
||||
},
|
||||
vec![]
|
||||
))
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
use std::io::{Error, Read};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub mod directory;
|
||||
|
||||
trait BundleSourceImpl {
|
||||
type Read: Read;
|
||||
|
||||
/// Reads any file from the bundle.
|
||||
fn read_file(&self, path: &str) -> Result<Self::Read, Error>;
|
||||
|
||||
/// Reads a file specifically from the content directory of the bundle.
|
||||
fn read_content(&self, path: &str) -> Result<Self::Read, Error>;
|
||||
}
|
||||
|
||||
pub enum BundleSource {
|
||||
Directory(PathBuf),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum BundleSourceError {
|
||||
#[error("Unknown bundle source")]
|
||||
UnknownSource,
|
||||
}
|
||||
|
||||
impl BundleSource {
|
||||
pub fn from_path(path: impl AsRef<Path>) -> Result<Self, BundleSourceError> {
|
||||
let path = path.as_ref();
|
||||
if path.is_dir() {
|
||||
return Ok(Self::Directory(path.to_owned()));
|
||||
}
|
||||
|
||||
Err(BundleSourceError::UnknownSource)
|
||||
}
|
||||
|
||||
/// Reads any file from the bundle.
|
||||
pub fn read_file(&self, path: &str) -> Result<Vec<u8>, Error> {
|
||||
match self {
|
||||
BundleSource::Directory(directory) => {
|
||||
let mut file = directory.read_file(path)?;
|
||||
let mut data = vec![];
|
||||
file.read_to_end(&mut data)?;
|
||||
Ok(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads a file specifically from the content directory of the bundle.
|
||||
pub fn read_content(&self, path: &str) -> Result<Vec<u8>, Error> {
|
||||
match self {
|
||||
BundleSource::Directory(directory) => {
|
||||
let mut file = directory.read_content(path)?;
|
||||
let mut data = vec![];
|
||||
file.read_to_end(&mut data)?;
|
||||
Ok(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,170 @@
|
|||
use crate::bundle::source::BundleSourceImpl;
|
||||
use std::fs::File;
|
||||
use std::io::{Error, ErrorKind};
|
||||
use std::path::Path;
|
||||
|
||||
impl BundleSourceImpl for Path {
|
||||
type Read = File;
|
||||
|
||||
fn read_file(&self, path: &str) -> Result<Self::Read, Error> {
|
||||
let potential_path = self.join(path);
|
||||
if !potential_path.starts_with(self) {
|
||||
return Err(Error::from(ErrorKind::NotFound));
|
||||
}
|
||||
File::open(potential_path)
|
||||
}
|
||||
|
||||
fn read_content(&self, path: &str) -> Result<Self::Read, Error> {
|
||||
let root = self.join("content");
|
||||
let potential_path = root.join(path);
|
||||
if !potential_path.starts_with(root) {
|
||||
return Err(Error::from(ErrorKind::NotFound));
|
||||
}
|
||||
File::open(potential_path)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::bundle::source::BundleSourceImpl;
|
||||
use std::io::{ErrorKind, Read, Write};
|
||||
use tempfile::{tempdir, NamedTempFile};
|
||||
|
||||
/*
|
||||
[NA] Careful with panicking in these tests.
|
||||
tempdir() relies on Drop to clean up the directory,
|
||||
and since we're testing a real filesystem... it sucks if we leak.
|
||||
|
||||
Construct the test, perform the test, drop the tmp_dir and *then* assert.
|
||||
*/
|
||||
|
||||
#[test]
|
||||
fn read_file_not_found() {
|
||||
let tmp_dir = tempdir().unwrap();
|
||||
let success = matches!(
|
||||
tmp_dir.path().read_file("some_file"),
|
||||
Err(e) if e.kind() == ErrorKind::NotFound
|
||||
);
|
||||
drop(tmp_dir);
|
||||
assert!(success)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_file_invalid() {
|
||||
let tmp_dir = tempdir().unwrap();
|
||||
let success = matches!(
|
||||
tmp_dir.path().read_file("!?\\//"),
|
||||
Err(e) if e.kind() == ErrorKind::NotFound
|
||||
);
|
||||
drop(tmp_dir);
|
||||
assert!(success)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_file_works() {
|
||||
let tmp_dir = tempdir().unwrap();
|
||||
let _ = std::fs::write(tmp_dir.path().join("some_file.txt"), "Fancy!");
|
||||
let result = tmp_dir
|
||||
.path()
|
||||
.read_file("some_file.txt")
|
||||
.map_err(|e| e.to_string())
|
||||
.map(|mut f| {
|
||||
let mut result = String::new();
|
||||
let _ = f.read_to_string(&mut result);
|
||||
result
|
||||
});
|
||||
drop(tmp_dir);
|
||||
|
||||
assert_eq!(result.as_deref(), Ok("Fancy!"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_file_outside_directory() {
|
||||
let tmp_dir = tempdir().unwrap();
|
||||
let mut tmp_file = NamedTempFile::new().unwrap();
|
||||
let _ = tmp_file.write(&[1, 2, 3, 4]);
|
||||
let success = matches!(
|
||||
tmp_dir
|
||||
.path()
|
||||
.read_file(&tmp_file.path().to_string_lossy()),
|
||||
Err(e) if e.kind() == ErrorKind::NotFound
|
||||
);
|
||||
drop(tmp_file);
|
||||
drop(tmp_dir);
|
||||
|
||||
assert!(success)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_content_not_found() {
|
||||
let tmp_dir = tempdir().unwrap();
|
||||
let success = matches!(
|
||||
tmp_dir.path().read_content("some_file"),
|
||||
Err(e) if e.kind() == ErrorKind::NotFound
|
||||
);
|
||||
drop(tmp_dir);
|
||||
assert!(success)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_content_invalid() {
|
||||
let tmp_dir = tempdir().unwrap();
|
||||
let success = matches!(
|
||||
tmp_dir.path().read_content("!?\\//"),
|
||||
Err(e) if e.kind() == ErrorKind::NotFound
|
||||
);
|
||||
drop(tmp_dir);
|
||||
assert!(success)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_content_works() {
|
||||
let tmp_dir = tempdir().unwrap();
|
||||
let _ = std::fs::create_dir(tmp_dir.path().join("content"));
|
||||
let _ = std::fs::write(tmp_dir.path().join("content/some_file.txt"), "Fancy!");
|
||||
let result = tmp_dir
|
||||
.path()
|
||||
.read_content("some_file.txt")
|
||||
.map_err(|e| e.to_string())
|
||||
.map(|mut f| {
|
||||
let mut result = String::new();
|
||||
let _ = f.read_to_string(&mut result);
|
||||
result
|
||||
});
|
||||
drop(tmp_dir);
|
||||
|
||||
assert_eq!(result.as_deref(), Ok("Fancy!"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_content_outside_content_directory() {
|
||||
let tmp_dir = tempdir().unwrap();
|
||||
let _ = std::fs::write(tmp_dir.path().join("some_file.txt"), "Fancy!");
|
||||
let success = matches!(
|
||||
tmp_dir
|
||||
.path()
|
||||
.read_content("../some_file.txt"),
|
||||
Err(e) if e.kind() == ErrorKind::NotFound
|
||||
);
|
||||
drop(tmp_dir);
|
||||
|
||||
assert!(success)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_content_outside_root_directory() {
|
||||
let tmp_dir = tempdir().unwrap();
|
||||
let mut tmp_file = NamedTempFile::new().unwrap();
|
||||
let _ = tmp_file.write(&[1, 2, 3, 4]);
|
||||
let success = matches!(
|
||||
tmp_dir
|
||||
.path()
|
||||
.read_content(&tmp_file.path().to_string_lossy()),
|
||||
Err(e) if e.kind() == ErrorKind::NotFound
|
||||
);
|
||||
drop(tmp_file);
|
||||
drop(tmp_dir);
|
||||
|
||||
assert!(success)
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
pub mod bookmarks;
|
||||
pub mod bundle;
|
||||
pub mod parse;
|
||||
pub mod write;
|
||||
|
|
|
@ -125,17 +125,18 @@ impl ParseContext {
|
|||
pub trait ReadExt<'a> {
|
||||
fn get_impl(&'a self, key: &str) -> Option<&'a Item>;
|
||||
|
||||
fn get_table_like(
|
||||
fn get_table_like<R>(
|
||||
&'a self,
|
||||
cx: &mut ParseContext,
|
||||
key: &'static str,
|
||||
fun: impl FnOnce(&mut ParseContext, &dyn TableLike),
|
||||
) {
|
||||
fun: impl FnOnce(&mut ParseContext, &dyn TableLike) -> R,
|
||||
) -> Option<R> {
|
||||
let mut result = None;
|
||||
if let Some(item) = self.get_impl(key) {
|
||||
cx.push_key(key);
|
||||
|
||||
if let Some(table) = item.as_table_like() {
|
||||
fun(cx, table);
|
||||
result = Some(fun(cx, table));
|
||||
} else {
|
||||
cx.add_warning(format!(
|
||||
"Invalid {}: expected table but found {}",
|
||||
|
@ -146,19 +147,21 @@ pub trait ReadExt<'a> {
|
|||
|
||||
cx.pop_key();
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn get_array_of_tables(
|
||||
fn get_array_of_tables<R>(
|
||||
&'a self,
|
||||
cx: &mut ParseContext,
|
||||
key: &'static str,
|
||||
fun: impl FnOnce(&mut ParseContext, &ArrayOfTables),
|
||||
) {
|
||||
fun: impl FnOnce(&mut ParseContext, &ArrayOfTables) -> R,
|
||||
) -> Option<R> {
|
||||
let mut result = None;
|
||||
if let Some(item) = self.get_impl(key) {
|
||||
cx.push_key(key);
|
||||
|
||||
if let Some(array) = item.as_array_of_tables() {
|
||||
fun(cx, array);
|
||||
result = Some(fun(cx, array));
|
||||
} else {
|
||||
cx.add_warning(format!(
|
||||
"Invalid {}: expected array of tables but found {}",
|
||||
|
@ -169,6 +172,7 @@ pub trait ReadExt<'a> {
|
|||
|
||||
cx.pop_key();
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn parse_from_str<T: FromStr>(&'a self, cx: &mut ParseContext, key: &'static str) -> Option<T> {
|
||||
|
|
Loading…
Reference in New Issue