mirror of
https://github.com/bevyengine/bevy
synced 2024-11-14 00:47:32 +00:00
054134fba2
# Objective Fix https://github.com/bevyengine/bevy/issues/11657 ## Solution Add a `ReflectKind` enum, add `Reflect::reflect_kind` which returns a `ReflectKind`, and add `kind` method implementions to `ReflectRef`, `ReflectMut`, and `ReflectOwned`, which returns a `ReflectKind`. I also changed `AccessError` to use this new struct instead of it's own `TypeKind` struct. --- ## Changelog - Added `ReflectKind`, an enumeration over the kinds of a reflected type without its data. - Added `Reflect::reflect_kind` (with default implementation) - Added implementation for the `kind` method on `ReflectRef`, `ReflectMut`, and `ReflectOwned` which gives their kind without any information, as a `ReflectKind`
1070 lines
39 KiB
Rust
1070 lines
39 KiB
Rust
use crate::io::AssetSourceId;
|
|
use bevy_reflect::{
|
|
std_traits::ReflectDefault, utility::NonGenericTypeInfoCell, FromReflect, FromType,
|
|
GetTypeRegistration, Reflect, ReflectDeserialize, ReflectFromPtr, ReflectFromReflect,
|
|
ReflectKind, ReflectMut, ReflectOwned, ReflectRef, ReflectSerialize, TypeInfo, TypePath,
|
|
TypeRegistration, Typed, ValueInfo,
|
|
};
|
|
use bevy_utils::CowArc;
|
|
use serde::{de::Visitor, Deserialize, Serialize};
|
|
use std::{
|
|
fmt::{Debug, Display},
|
|
hash::{Hash, Hasher},
|
|
ops::Deref,
|
|
path::{Path, PathBuf},
|
|
};
|
|
use thiserror::Error;
|
|
|
|
/// Represents a path to an asset in a "virtual filesystem".
|
|
///
|
|
/// Asset paths consist of three main parts:
|
|
/// * [`AssetPath::source`]: The name of the [`AssetSource`](crate::io::AssetSource) to load the asset from.
|
|
/// This is optional. If one is not set the default source will be used (which is the `assets` folder by default).
|
|
/// * [`AssetPath::path`]: The "virtual filesystem path" pointing to an asset source file.
|
|
/// * [`AssetPath::label`]: An optional "named sub asset". When assets are loaded, they are
|
|
/// allowed to load "sub assets" of any type, which are identified by a named "label".
|
|
///
|
|
/// Asset paths are generally constructed (and visualized) as strings:
|
|
///
|
|
/// ```no_run
|
|
/// # use bevy_asset::{Asset, AssetServer, Handle};
|
|
/// # use bevy_reflect::TypePath;
|
|
/// #
|
|
/// # #[derive(Asset, TypePath, Default)]
|
|
/// # struct Mesh;
|
|
/// #
|
|
/// # #[derive(Asset, TypePath, Default)]
|
|
/// # struct Scene;
|
|
/// #
|
|
/// # let asset_server: AssetServer = panic!();
|
|
/// // This loads the `my_scene.scn` base asset from the default asset source.
|
|
/// let scene: Handle<Scene> = asset_server.load("my_scene.scn");
|
|
///
|
|
/// // This loads the `PlayerMesh` labeled asset from the `my_scene.scn` base asset in the default asset source.
|
|
/// let mesh: Handle<Mesh> = asset_server.load("my_scene.scn#PlayerMesh");
|
|
///
|
|
/// // This loads the `my_scene.scn` base asset from a custom 'remote' asset source.
|
|
/// let scene: Handle<Scene> = asset_server.load("remote://my_scene.scn");
|
|
/// ```
|
|
///
|
|
/// [`AssetPath`] implements [`From`] for `&'static str`, `&'static Path`, and `&'a String`,
|
|
/// which allows us to optimize the static cases.
|
|
/// This means that the common case of `asset_server.load("my_scene.scn")` when it creates and
|
|
/// clones internal owned [`AssetPaths`](AssetPath).
|
|
/// This also means that you should use [`AssetPath::parse`] in cases where `&str` is the explicit type.
|
|
#[derive(Eq, PartialEq, Hash, Clone, Default)]
|
|
pub struct AssetPath<'a> {
|
|
source: AssetSourceId<'a>,
|
|
path: CowArc<'a, Path>,
|
|
label: Option<CowArc<'a, str>>,
|
|
}
|
|
|
|
impl<'a> Debug for AssetPath<'a> {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
Display::fmt(self, f)
|
|
}
|
|
}
|
|
|
|
impl<'a> Display for AssetPath<'a> {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
if let AssetSourceId::Name(name) = self.source() {
|
|
write!(f, "{name}://")?;
|
|
}
|
|
write!(f, "{}", self.path.display())?;
|
|
if let Some(label) = &self.label {
|
|
write!(f, "#{label}")?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// An error that occurs when parsing a string type to create an [`AssetPath`] fails, such as during [`AssetPath::parse`] or [`AssetPath::from<'static str>`].
|
|
#[derive(Error, Debug, PartialEq, Eq)]
|
|
pub enum ParseAssetPathError {
|
|
/// Error that occurs when the [`AssetPath::source`] section of a path string contains the [`AssetPath::label`] delimiter `#`. E.g. `bad#source://file.test`.
|
|
#[error("Asset source must not contain a `#` character")]
|
|
InvalidSourceSyntax,
|
|
/// Error that occurs when the [`AssetPath::label`] section of a path string contains the [`AssetPath::source`] delimiter `://`. E.g. `source://file.test#bad://label`.
|
|
#[error("Asset label must not contain a `://` substring")]
|
|
InvalidLabelSyntax,
|
|
/// Error that occurs when a path string has an [`AssetPath::source`] delimiter `://` with no characters preceeding it. E.g. `://file.test`.
|
|
#[error("Asset source must be at least one character. Either specify the source before the '://' or remove the `://`")]
|
|
MissingSource,
|
|
/// Error that occurs when a path string has an [`AssetPath::label`] delimiter `#` with no characters succeeding it. E.g. `file.test#`
|
|
#[error("Asset label must be at least one character. Either specify the label after the '#' or remove the '#'")]
|
|
MissingLabel,
|
|
}
|
|
|
|
impl<'a> AssetPath<'a> {
|
|
/// Creates a new [`AssetPath`] from a string in the asset path format:
|
|
/// * An asset at the root: `"scene.gltf"`
|
|
/// * An asset nested in some folders: `"some/path/scene.gltf"`
|
|
/// * An asset with a "label": `"some/path/scene.gltf#Mesh0"`
|
|
/// * An asset with a custom "source": `"custom://some/path/scene.gltf#Mesh0"`
|
|
///
|
|
/// Prefer [`From<'static str>`] for static strings, as this will prevent allocations
|
|
/// and reference counting for [`AssetPath::into_owned`].
|
|
///
|
|
/// # Panics
|
|
/// Panics if the asset path is in an invalid format. Use [`AssetPath::try_parse`] for a fallible variant
|
|
pub fn parse(asset_path: &'a str) -> AssetPath<'a> {
|
|
Self::try_parse(asset_path).unwrap()
|
|
}
|
|
|
|
/// Creates a new [`AssetPath`] from a string in the asset path format:
|
|
/// * An asset at the root: `"scene.gltf"`
|
|
/// * An asset nested in some folders: `"some/path/scene.gltf"`
|
|
/// * An asset with a "label": `"some/path/scene.gltf#Mesh0"`
|
|
/// * An asset with a custom "source": `"custom://some/path/scene.gltf#Mesh0"`
|
|
///
|
|
/// Prefer [`From<'static str>`] for static strings, as this will prevent allocations
|
|
/// and reference counting for [`AssetPath::into_owned`].
|
|
///
|
|
/// This will return a [`ParseAssetPathError`] if `asset_path` is in an invalid format.
|
|
pub fn try_parse(asset_path: &'a str) -> Result<AssetPath<'a>, ParseAssetPathError> {
|
|
let (source, path, label) = Self::parse_internal(asset_path)?;
|
|
Ok(Self {
|
|
source: match source {
|
|
Some(source) => AssetSourceId::Name(CowArc::Borrowed(source)),
|
|
None => AssetSourceId::Default,
|
|
},
|
|
path: CowArc::Borrowed(path),
|
|
label: label.map(CowArc::Borrowed),
|
|
})
|
|
}
|
|
|
|
// Attempts to Parse a &str into an `AssetPath`'s `AssetPath::source`, `AssetPath::path`, and `AssetPath::label` components.
|
|
fn parse_internal(
|
|
asset_path: &str,
|
|
) -> Result<(Option<&str>, &Path, Option<&str>), ParseAssetPathError> {
|
|
let chars = asset_path.char_indices();
|
|
let mut source_range = None;
|
|
let mut path_range = 0..asset_path.len();
|
|
let mut label_range = None;
|
|
|
|
// Loop through the characters of the passed in &str to accomplish the following:
|
|
// 1. Seach for the first instance of the `://` substring. If the `://` substring is found,
|
|
// store the range of indices representing everything before the `://` substring as the `source_range`.
|
|
// 2. Seach for the last instance of the `#` character. If the `#` character is found,
|
|
// store the range of indices representing everything after the `#` character as the `label_range`
|
|
// 3. Set the `path_range` to be everything in between the `source_range` and `label_range`,
|
|
// excluding the `://` substring and `#` character.
|
|
// 4. Verify that there are no `#` characters in the `AssetPath::source` and no `://` substrings in the `AssetPath::label`
|
|
let mut source_delimiter_chars_matched = 0;
|
|
let mut last_found_source_index = 0;
|
|
for (index, char) in chars {
|
|
match char {
|
|
':' => {
|
|
source_delimiter_chars_matched = 1;
|
|
}
|
|
'/' => {
|
|
match source_delimiter_chars_matched {
|
|
1 => {
|
|
source_delimiter_chars_matched = 2;
|
|
}
|
|
2 => {
|
|
// If we haven't found our first `AssetPath::source` yet, check to make sure it is valid and then store it.
|
|
if source_range.is_none() {
|
|
// If the `AssetPath::source` containes a `#` character, it is invalid.
|
|
if label_range.is_some() {
|
|
return Err(ParseAssetPathError::InvalidSourceSyntax);
|
|
}
|
|
source_range = Some(0..index - 2);
|
|
path_range.start = index + 1;
|
|
}
|
|
last_found_source_index = index - 2;
|
|
source_delimiter_chars_matched = 0;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
'#' => {
|
|
path_range.end = index;
|
|
label_range = Some(index + 1..asset_path.len());
|
|
source_delimiter_chars_matched = 0;
|
|
}
|
|
_ => {
|
|
source_delimiter_chars_matched = 0;
|
|
}
|
|
}
|
|
}
|
|
// If we found an `AssetPath::label`
|
|
if let Some(range) = label_range.clone() {
|
|
// If the `AssetPath::label` contained a `://` substring, it is invalid.
|
|
if range.start <= last_found_source_index {
|
|
return Err(ParseAssetPathError::InvalidLabelSyntax);
|
|
}
|
|
}
|
|
// Try to parse the range of indices that represents the `AssetPath::source` portion of the `AssetPath` to make sure it is not empty.
|
|
// This would be the case if the input &str was something like `://some/file.test`
|
|
let source = match source_range {
|
|
Some(source_range) => {
|
|
if source_range.is_empty() {
|
|
return Err(ParseAssetPathError::MissingSource);
|
|
}
|
|
Some(&asset_path[source_range])
|
|
}
|
|
None => None,
|
|
};
|
|
// Try to parse the range of indices that represents the `AssetPath::label` portion of the `AssetPath` to make sure it is not empty.
|
|
// This would be the case if the input &str was something like `some/file.test#`.
|
|
let label = match label_range {
|
|
Some(label_range) => {
|
|
if label_range.is_empty() {
|
|
return Err(ParseAssetPathError::MissingLabel);
|
|
}
|
|
Some(&asset_path[label_range])
|
|
}
|
|
None => None,
|
|
};
|
|
|
|
let path = Path::new(&asset_path[path_range]);
|
|
Ok((source, path, label))
|
|
}
|
|
|
|
/// Creates a new [`AssetPath`] from a [`Path`].
|
|
#[inline]
|
|
pub fn from_path(path: &'a Path) -> AssetPath<'a> {
|
|
AssetPath {
|
|
path: CowArc::Borrowed(path),
|
|
source: AssetSourceId::Default,
|
|
label: None,
|
|
}
|
|
}
|
|
|
|
/// Gets the "asset source", if one was defined. If none was defined, the default source
|
|
/// will be used.
|
|
#[inline]
|
|
pub fn source(&self) -> &AssetSourceId {
|
|
&self.source
|
|
}
|
|
|
|
/// Gets the "sub-asset label".
|
|
#[inline]
|
|
pub fn label(&self) -> Option<&str> {
|
|
self.label.as_deref()
|
|
}
|
|
|
|
/// Gets the "sub-asset label".
|
|
#[inline]
|
|
pub fn label_cow(&self) -> Option<CowArc<'a, str>> {
|
|
self.label.clone()
|
|
}
|
|
|
|
/// Gets the path to the asset in the "virtual filesystem".
|
|
#[inline]
|
|
pub fn path(&self) -> &Path {
|
|
self.path.deref()
|
|
}
|
|
|
|
/// Gets the path to the asset in the "virtual filesystem" without a label (if a label is currently set).
|
|
#[inline]
|
|
pub fn without_label(&self) -> AssetPath<'_> {
|
|
Self {
|
|
source: self.source.clone(),
|
|
path: self.path.clone(),
|
|
label: None,
|
|
}
|
|
}
|
|
|
|
/// Removes a "sub-asset label" from this [`AssetPath`], if one was set.
|
|
#[inline]
|
|
pub fn remove_label(&mut self) {
|
|
self.label = None;
|
|
}
|
|
|
|
/// Takes the "sub-asset label" from this [`AssetPath`], if one was set.
|
|
#[inline]
|
|
pub fn take_label(&mut self) -> Option<CowArc<'a, str>> {
|
|
self.label.take()
|
|
}
|
|
|
|
/// Returns this asset path with the given label. This will replace the previous
|
|
/// label if it exists.
|
|
#[inline]
|
|
pub fn with_label(self, label: impl Into<CowArc<'a, str>>) -> AssetPath<'a> {
|
|
AssetPath {
|
|
source: self.source,
|
|
path: self.path,
|
|
label: Some(label.into()),
|
|
}
|
|
}
|
|
|
|
/// Returns this asset path with the given asset source. This will replace the previous asset
|
|
/// source if it exists.
|
|
#[inline]
|
|
pub fn with_source(self, source: impl Into<AssetSourceId<'a>>) -> AssetPath<'a> {
|
|
AssetPath {
|
|
source: source.into(),
|
|
path: self.path,
|
|
label: self.label,
|
|
}
|
|
}
|
|
|
|
/// Returns an [`AssetPath`] for the parent folder of this path, if there is a parent folder in the path.
|
|
pub fn parent(&self) -> Option<AssetPath<'a>> {
|
|
let path = match &self.path {
|
|
CowArc::Borrowed(path) => CowArc::Borrowed(path.parent()?),
|
|
CowArc::Static(path) => CowArc::Static(path.parent()?),
|
|
CowArc::Owned(path) => path.parent()?.to_path_buf().into(),
|
|
};
|
|
Some(AssetPath {
|
|
source: self.source.clone(),
|
|
label: None,
|
|
path,
|
|
})
|
|
}
|
|
|
|
/// Converts this into an "owned" value. If internally a value is borrowed, it will be cloned into an "owned [`Arc`]".
|
|
/// If internally a value is a static reference, the static reference will be used unchanged.
|
|
/// If internally a value is an "owned [`Arc`]", it will remain unchanged.
|
|
///
|
|
/// [`Arc`]: std::sync::Arc
|
|
pub fn into_owned(self) -> AssetPath<'static> {
|
|
AssetPath {
|
|
source: self.source.into_owned(),
|
|
path: self.path.into_owned(),
|
|
label: self.label.map(|l| l.into_owned()),
|
|
}
|
|
}
|
|
|
|
/// Clones this into an "owned" value. If internally a value is borrowed, it will be cloned into an "owned [`Arc`]".
|
|
/// If internally a value is a static reference, the static reference will be used unchanged.
|
|
/// If internally a value is an "owned [`Arc`]", the [`Arc`] will be cloned.
|
|
///
|
|
/// [`Arc`]: std::sync::Arc
|
|
#[inline]
|
|
pub fn clone_owned(&self) -> AssetPath<'static> {
|
|
self.clone().into_owned()
|
|
}
|
|
|
|
/// Resolves a relative asset path via concatenation. The result will be an `AssetPath` which
|
|
/// is resolved relative to this "base" path.
|
|
///
|
|
/// ```
|
|
/// # use bevy_asset::AssetPath;
|
|
/// assert_eq!(AssetPath::parse("a/b").resolve("c"), Ok(AssetPath::parse("a/b/c")));
|
|
/// assert_eq!(AssetPath::parse("a/b").resolve("./c"), Ok(AssetPath::parse("a/b/c")));
|
|
/// assert_eq!(AssetPath::parse("a/b").resolve("../c"), Ok(AssetPath::parse("a/c")));
|
|
/// assert_eq!(AssetPath::parse("a/b").resolve("c.png"), Ok(AssetPath::parse("a/b/c.png")));
|
|
/// assert_eq!(AssetPath::parse("a/b").resolve("/c"), Ok(AssetPath::parse("c")));
|
|
/// assert_eq!(AssetPath::parse("a/b.png").resolve("#c"), Ok(AssetPath::parse("a/b.png#c")));
|
|
/// assert_eq!(AssetPath::parse("a/b.png#c").resolve("#d"), Ok(AssetPath::parse("a/b.png#d")));
|
|
/// ```
|
|
///
|
|
/// There are several cases:
|
|
///
|
|
/// If the `path` argument begins with `#`, then it is considered an asset label, in which case
|
|
/// the result is the base path with the label portion replaced.
|
|
///
|
|
/// If the path argument begins with '/', then it is considered a 'full' path, in which
|
|
/// case the result is a new `AssetPath` consisting of the base path asset source
|
|
/// (if there is one) with the path and label portions of the relative path. Note that a 'full'
|
|
/// asset path is still relative to the asset source root, and not necessarily an absolute
|
|
/// filesystem path.
|
|
///
|
|
/// If the `path` argument begins with an asset source (ex: `http://`) then the entire base
|
|
/// path is replaced - the result is the source, path and label (if any) of the `path`
|
|
/// argument.
|
|
///
|
|
/// Otherwise, the `path` argument is considered a relative path. The result is concatenated
|
|
/// using the following algorithm:
|
|
///
|
|
/// * The base path and the `path` argument are concatenated.
|
|
/// * Path elements consisting of "/." or "<name>/.." are removed.
|
|
///
|
|
/// If there are insufficient segments in the base path to match the ".." segments,
|
|
/// then any left-over ".." segments are left as-is.
|
|
pub fn resolve(&self, path: &str) -> Result<AssetPath<'static>, ParseAssetPathError> {
|
|
self.resolve_internal(path, false)
|
|
}
|
|
|
|
/// Resolves an embedded asset path via concatenation. The result will be an `AssetPath` which
|
|
/// is resolved relative to this path. This is similar in operation to `resolve`, except that
|
|
/// the the 'file' portion of the base path (that is, any characters after the last '/')
|
|
/// is removed before concatenation, in accordance with the behavior specified in
|
|
/// IETF RFC 1808 "Relative URIs".
|
|
///
|
|
/// The reason for this behavior is that embedded URIs which start with "./" or "../" are
|
|
/// relative to the *directory* containing the asset, not the asset file. This is consistent
|
|
/// with the behavior of URIs in `JavaScript`, CSS, HTML and other web file formats. The
|
|
/// primary use case for this method is resolving relative paths embedded within asset files,
|
|
/// which are relative to the asset in which they are contained.
|
|
///
|
|
/// ```
|
|
/// # use bevy_asset::AssetPath;
|
|
/// assert_eq!(AssetPath::parse("a/b").resolve_embed("c"), Ok(AssetPath::parse("a/c")));
|
|
/// assert_eq!(AssetPath::parse("a/b").resolve_embed("./c"), Ok(AssetPath::parse("a/c")));
|
|
/// assert_eq!(AssetPath::parse("a/b").resolve_embed("../c"), Ok(AssetPath::parse("c")));
|
|
/// assert_eq!(AssetPath::parse("a/b").resolve_embed("c.png"), Ok(AssetPath::parse("a/c.png")));
|
|
/// assert_eq!(AssetPath::parse("a/b").resolve_embed("/c"), Ok(AssetPath::parse("c")));
|
|
/// assert_eq!(AssetPath::parse("a/b.png").resolve_embed("#c"), Ok(AssetPath::parse("a/b.png#c")));
|
|
/// assert_eq!(AssetPath::parse("a/b.png#c").resolve_embed("#d"), Ok(AssetPath::parse("a/b.png#d")));
|
|
/// ```
|
|
pub fn resolve_embed(&self, path: &str) -> Result<AssetPath<'static>, ParseAssetPathError> {
|
|
self.resolve_internal(path, true)
|
|
}
|
|
|
|
fn resolve_internal(
|
|
&self,
|
|
path: &str,
|
|
replace: bool,
|
|
) -> Result<AssetPath<'static>, ParseAssetPathError> {
|
|
if let Some(label) = path.strip_prefix('#') {
|
|
// It's a label only
|
|
Ok(self.clone_owned().with_label(label.to_owned()))
|
|
} else {
|
|
let (source, rpath, rlabel) = AssetPath::parse_internal(path)?;
|
|
let mut base_path = PathBuf::from(self.path());
|
|
if replace && !self.path.to_str().unwrap().ends_with('/') {
|
|
// No error if base is empty (per RFC 1808).
|
|
base_path.pop();
|
|
}
|
|
|
|
// Strip off leading slash
|
|
let mut is_absolute = false;
|
|
let rpath = match rpath.strip_prefix("/") {
|
|
Ok(p) => {
|
|
is_absolute = true;
|
|
p
|
|
}
|
|
_ => rpath,
|
|
};
|
|
|
|
let mut result_path = PathBuf::new();
|
|
if !is_absolute && source.is_none() {
|
|
for elt in base_path.iter() {
|
|
if elt == "." {
|
|
// Skip
|
|
} else if elt == ".." {
|
|
if !result_path.pop() {
|
|
// Preserve ".." if insufficient matches (per RFC 1808).
|
|
result_path.push(elt);
|
|
}
|
|
} else {
|
|
result_path.push(elt);
|
|
}
|
|
}
|
|
}
|
|
|
|
for elt in rpath.iter() {
|
|
if elt == "." {
|
|
// Skip
|
|
} else if elt == ".." {
|
|
if !result_path.pop() {
|
|
// Preserve ".." if insufficient matches (per RFC 1808).
|
|
result_path.push(elt);
|
|
}
|
|
} else {
|
|
result_path.push(elt);
|
|
}
|
|
}
|
|
|
|
Ok(AssetPath {
|
|
source: match source {
|
|
Some(source) => AssetSourceId::Name(CowArc::Owned(source.into())),
|
|
None => self.source.clone_owned(),
|
|
},
|
|
path: CowArc::Owned(result_path.into()),
|
|
label: rlabel.map(|l| CowArc::Owned(l.into())),
|
|
})
|
|
}
|
|
}
|
|
|
|
/// Returns the full extension (including multiple '.' values).
|
|
/// Ex: Returns `"config.ron"` for `"my_asset.config.ron"`
|
|
pub fn get_full_extension(&self) -> Option<String> {
|
|
let file_name = self.path().file_name()?.to_str()?;
|
|
let index = file_name.find('.')?;
|
|
let extension = file_name[index + 1..].to_lowercase();
|
|
Some(extension)
|
|
}
|
|
|
|
pub(crate) fn iter_secondary_extensions(full_extension: &str) -> impl Iterator<Item = &str> {
|
|
full_extension.chars().enumerate().filter_map(|(i, c)| {
|
|
if c == '.' {
|
|
Some(&full_extension[i + 1..])
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
impl From<&'static str> for AssetPath<'static> {
|
|
#[inline]
|
|
fn from(asset_path: &'static str) -> Self {
|
|
let (source, path, label) = Self::parse_internal(asset_path).unwrap();
|
|
AssetPath {
|
|
source: source.into(),
|
|
path: CowArc::Static(path),
|
|
label: label.map(CowArc::Static),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a> From<&'a String> for AssetPath<'a> {
|
|
#[inline]
|
|
fn from(asset_path: &'a String) -> Self {
|
|
AssetPath::parse(asset_path.as_str())
|
|
}
|
|
}
|
|
|
|
impl From<String> for AssetPath<'static> {
|
|
#[inline]
|
|
fn from(asset_path: String) -> Self {
|
|
AssetPath::parse(asset_path.as_str()).into_owned()
|
|
}
|
|
}
|
|
|
|
impl From<&'static Path> for AssetPath<'static> {
|
|
#[inline]
|
|
fn from(path: &'static Path) -> Self {
|
|
Self {
|
|
source: AssetSourceId::Default,
|
|
path: CowArc::Static(path),
|
|
label: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<PathBuf> for AssetPath<'static> {
|
|
#[inline]
|
|
fn from(path: PathBuf) -> Self {
|
|
Self {
|
|
source: AssetSourceId::Default,
|
|
path: path.into(),
|
|
label: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a, 'b> From<&'a AssetPath<'b>> for AssetPath<'b> {
|
|
fn from(value: &'a AssetPath<'b>) -> Self {
|
|
value.clone()
|
|
}
|
|
}
|
|
|
|
impl<'a> From<AssetPath<'a>> for PathBuf {
|
|
fn from(value: AssetPath<'a>) -> Self {
|
|
value.path().to_path_buf()
|
|
}
|
|
}
|
|
|
|
impl<'a> Serialize for AssetPath<'a> {
|
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: serde::Serializer,
|
|
{
|
|
self.to_string().serialize(serializer)
|
|
}
|
|
}
|
|
|
|
impl<'de> Deserialize<'de> for AssetPath<'static> {
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
where
|
|
D: serde::Deserializer<'de>,
|
|
{
|
|
deserializer.deserialize_string(AssetPathVisitor)
|
|
}
|
|
}
|
|
|
|
struct AssetPathVisitor;
|
|
|
|
impl<'de> Visitor<'de> for AssetPathVisitor {
|
|
type Value = AssetPath<'static>;
|
|
|
|
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
formatter.write_str("string AssetPath")
|
|
}
|
|
|
|
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
|
|
where
|
|
E: serde::de::Error,
|
|
{
|
|
Ok(AssetPath::parse(v).into_owned())
|
|
}
|
|
|
|
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
|
|
where
|
|
E: serde::de::Error,
|
|
{
|
|
Ok(AssetPath::from(v))
|
|
}
|
|
}
|
|
|
|
// NOTE: We manually implement "reflect value" because deriving Reflect on `AssetPath` breaks dynamic linking
|
|
// See https://github.com/bevyengine/bevy/issues/9747
|
|
// NOTE: This could use `impl_reflect_value` if it supported static lifetimes.
|
|
|
|
impl GetTypeRegistration for AssetPath<'static> {
|
|
fn get_type_registration() -> TypeRegistration {
|
|
let mut registration = TypeRegistration::of::<Self>();
|
|
registration.insert::<ReflectFromPtr>(FromType::<Self>::from_type());
|
|
registration.insert::<ReflectFromReflect>(FromType::<Self>::from_type());
|
|
registration.insert::<ReflectSerialize>(FromType::<Self>::from_type());
|
|
registration.insert::<ReflectDeserialize>(FromType::<Self>::from_type());
|
|
registration.insert::<ReflectDefault>(FromType::<Self>::from_type());
|
|
registration
|
|
}
|
|
}
|
|
|
|
impl TypePath for AssetPath<'static> {
|
|
fn type_path() -> &'static str {
|
|
"bevy_asset::path::AssetPath<'static>"
|
|
}
|
|
fn short_type_path() -> &'static str {
|
|
"AssetPath<'static>"
|
|
}
|
|
fn type_ident() -> Option<&'static str> {
|
|
Some("AssetPath<'static>")
|
|
}
|
|
fn crate_name() -> Option<&'static str> {
|
|
None
|
|
}
|
|
fn module_path() -> Option<&'static str> {
|
|
None
|
|
}
|
|
}
|
|
impl Typed for AssetPath<'static> {
|
|
fn type_info() -> &'static TypeInfo {
|
|
static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new();
|
|
CELL.get_or_set(|| {
|
|
let info = ValueInfo::new::<Self>();
|
|
TypeInfo::Value(info)
|
|
})
|
|
}
|
|
}
|
|
impl Reflect for AssetPath<'static> {
|
|
#[inline]
|
|
fn get_represented_type_info(&self) -> Option<&'static TypeInfo> {
|
|
Some(<Self as Typed>::type_info())
|
|
}
|
|
#[inline]
|
|
fn into_any(self: Box<Self>) -> Box<dyn core::any::Any> {
|
|
self
|
|
}
|
|
#[inline]
|
|
fn as_any(&self) -> &dyn core::any::Any {
|
|
self
|
|
}
|
|
#[inline]
|
|
fn as_any_mut(&mut self) -> &mut dyn core::any::Any {
|
|
self
|
|
}
|
|
#[inline]
|
|
fn into_reflect(self: Box<Self>) -> Box<dyn Reflect> {
|
|
self
|
|
}
|
|
#[inline]
|
|
fn as_reflect(&self) -> &dyn Reflect {
|
|
self
|
|
}
|
|
#[inline]
|
|
fn as_reflect_mut(&mut self) -> &mut dyn Reflect {
|
|
self
|
|
}
|
|
#[inline]
|
|
fn apply(&mut self, value: &dyn Reflect) {
|
|
let value = Reflect::as_any(value);
|
|
if let Some(value) = value.downcast_ref::<Self>() {
|
|
*self = value.clone();
|
|
} else {
|
|
panic!("Value is not {}.", std::any::type_name::<Self>());
|
|
}
|
|
}
|
|
#[inline]
|
|
fn set(
|
|
&mut self,
|
|
value: Box<dyn bevy_reflect::Reflect>,
|
|
) -> Result<(), Box<dyn bevy_reflect::Reflect>> {
|
|
*self = <dyn bevy_reflect::Reflect>::take(value)?;
|
|
Ok(())
|
|
}
|
|
fn reflect_kind(&self) -> ReflectKind {
|
|
ReflectKind::Value
|
|
}
|
|
fn reflect_ref(&self) -> ReflectRef {
|
|
ReflectRef::Value(self)
|
|
}
|
|
fn reflect_mut(&mut self) -> ReflectMut {
|
|
ReflectMut::Value(self)
|
|
}
|
|
fn reflect_owned(self: Box<Self>) -> ReflectOwned {
|
|
ReflectOwned::Value(self)
|
|
}
|
|
#[inline]
|
|
fn clone_value(&self) -> Box<dyn Reflect> {
|
|
Box::new(self.clone())
|
|
}
|
|
fn reflect_hash(&self) -> Option<u64> {
|
|
let mut hasher = bevy_reflect::utility::reflect_hasher();
|
|
Hash::hash(&::core::any::Any::type_id(self), &mut hasher);
|
|
Hash::hash(self, &mut hasher);
|
|
Some(Hasher::finish(&hasher))
|
|
}
|
|
fn reflect_partial_eq(&self, value: &dyn Reflect) -> Option<bool> {
|
|
let value = <dyn Reflect>::as_any(value);
|
|
if let Some(value) = <dyn core::any::Any>::downcast_ref::<Self>(value) {
|
|
Some(PartialEq::eq(self, value))
|
|
} else {
|
|
Some(false)
|
|
}
|
|
}
|
|
fn debug(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
|
::core::fmt::Debug::fmt(self, f)
|
|
}
|
|
}
|
|
impl FromReflect for AssetPath<'static> {
|
|
fn from_reflect(reflect: &dyn Reflect) -> Option<Self> {
|
|
Some(Clone::clone(<dyn core::any::Any>::downcast_ref::<
|
|
AssetPath<'static>,
|
|
>(<dyn Reflect>::as_any(reflect))?))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::AssetPath;
|
|
use std::path::Path;
|
|
|
|
#[test]
|
|
fn parse_asset_path() {
|
|
let result = AssetPath::parse_internal("a/b.test");
|
|
assert_eq!(result, Ok((None, Path::new("a/b.test"), None)));
|
|
|
|
let result = AssetPath::parse_internal("http://a/b.test");
|
|
assert_eq!(result, Ok((Some("http"), Path::new("a/b.test"), None)));
|
|
|
|
let result = AssetPath::parse_internal("http://a/b.test#Foo");
|
|
assert_eq!(
|
|
result,
|
|
Ok((Some("http"), Path::new("a/b.test"), Some("Foo")))
|
|
);
|
|
|
|
let result = AssetPath::parse_internal("localhost:80/b.test");
|
|
assert_eq!(result, Ok((None, Path::new("localhost:80/b.test"), None)));
|
|
|
|
let result = AssetPath::parse_internal("http://localhost:80/b.test");
|
|
assert_eq!(
|
|
result,
|
|
Ok((Some("http"), Path::new("localhost:80/b.test"), None))
|
|
);
|
|
|
|
let result = AssetPath::parse_internal("http://localhost:80/b.test#Foo");
|
|
assert_eq!(
|
|
result,
|
|
Ok((Some("http"), Path::new("localhost:80/b.test"), Some("Foo")))
|
|
);
|
|
|
|
let result = AssetPath::parse_internal("#insource://a/b.test");
|
|
assert_eq!(result, Err(crate::ParseAssetPathError::InvalidSourceSyntax));
|
|
|
|
let result = AssetPath::parse_internal("source://a/b.test#://inlabel");
|
|
assert_eq!(result, Err(crate::ParseAssetPathError::InvalidLabelSyntax));
|
|
|
|
let result = AssetPath::parse_internal("#insource://a/b.test#://inlabel");
|
|
assert!(
|
|
result == Err(crate::ParseAssetPathError::InvalidSourceSyntax)
|
|
|| result == Err(crate::ParseAssetPathError::InvalidLabelSyntax)
|
|
);
|
|
|
|
let result = AssetPath::parse_internal("http://");
|
|
assert_eq!(result, Ok((Some("http"), Path::new(""), None)));
|
|
|
|
let result = AssetPath::parse_internal("://x");
|
|
assert_eq!(result, Err(crate::ParseAssetPathError::MissingSource));
|
|
|
|
let result = AssetPath::parse_internal("a/b.test#");
|
|
assert_eq!(result, Err(crate::ParseAssetPathError::MissingLabel));
|
|
}
|
|
|
|
#[test]
|
|
fn test_parent() {
|
|
// Parent consumes path segments, returns None when insufficient
|
|
let result = AssetPath::from("a/b.test");
|
|
assert_eq!(result.parent(), Some(AssetPath::from("a")));
|
|
assert_eq!(result.parent().unwrap().parent(), Some(AssetPath::from("")));
|
|
assert_eq!(result.parent().unwrap().parent().unwrap().parent(), None);
|
|
|
|
// Parent cannot consume asset source
|
|
let result = AssetPath::from("http://a");
|
|
assert_eq!(result.parent(), Some(AssetPath::from("http://")));
|
|
assert_eq!(result.parent().unwrap().parent(), None);
|
|
|
|
// Parent consumes labels
|
|
let result = AssetPath::from("http://a#Foo");
|
|
assert_eq!(result.parent(), Some(AssetPath::from("http://")));
|
|
}
|
|
|
|
#[test]
|
|
fn test_with_source() {
|
|
let result = AssetPath::from("http://a#Foo");
|
|
assert_eq!(result.with_source("ftp"), AssetPath::from("ftp://a#Foo"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_without_label() {
|
|
let result = AssetPath::from("http://a#Foo");
|
|
assert_eq!(result.without_label(), AssetPath::from("http://a"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_resolve_full() {
|
|
// A "full" path should ignore the base path.
|
|
let base = AssetPath::from("alice/bob#carol");
|
|
assert_eq!(
|
|
base.resolve("/joe/next").unwrap(),
|
|
AssetPath::from("joe/next")
|
|
);
|
|
assert_eq!(
|
|
base.resolve_embed("/joe/next").unwrap(),
|
|
AssetPath::from("joe/next")
|
|
);
|
|
assert_eq!(
|
|
base.resolve("/joe/next#dave").unwrap(),
|
|
AssetPath::from("joe/next#dave")
|
|
);
|
|
assert_eq!(
|
|
base.resolve_embed("/joe/next#dave").unwrap(),
|
|
AssetPath::from("joe/next#dave")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_resolve_implicit_relative() {
|
|
// A path with no inital directory separator should be considered relative.
|
|
let base = AssetPath::from("alice/bob#carol");
|
|
assert_eq!(
|
|
base.resolve("joe/next").unwrap(),
|
|
AssetPath::from("alice/bob/joe/next")
|
|
);
|
|
assert_eq!(
|
|
base.resolve_embed("joe/next").unwrap(),
|
|
AssetPath::from("alice/joe/next")
|
|
);
|
|
assert_eq!(
|
|
base.resolve("joe/next#dave").unwrap(),
|
|
AssetPath::from("alice/bob/joe/next#dave")
|
|
);
|
|
assert_eq!(
|
|
base.resolve_embed("joe/next#dave").unwrap(),
|
|
AssetPath::from("alice/joe/next#dave")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_resolve_explicit_relative() {
|
|
// A path which begins with "./" or "../" is treated as relative
|
|
let base = AssetPath::from("alice/bob#carol");
|
|
assert_eq!(
|
|
base.resolve("./martin#dave").unwrap(),
|
|
AssetPath::from("alice/bob/martin#dave")
|
|
);
|
|
assert_eq!(
|
|
base.resolve_embed("./martin#dave").unwrap(),
|
|
AssetPath::from("alice/martin#dave")
|
|
);
|
|
assert_eq!(
|
|
base.resolve("../martin#dave").unwrap(),
|
|
AssetPath::from("alice/martin#dave")
|
|
);
|
|
assert_eq!(
|
|
base.resolve_embed("../martin#dave").unwrap(),
|
|
AssetPath::from("martin#dave")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_resolve_trailing_slash() {
|
|
// A path which begins with "./" or "../" is treated as relative
|
|
let base = AssetPath::from("alice/bob/");
|
|
assert_eq!(
|
|
base.resolve("./martin#dave").unwrap(),
|
|
AssetPath::from("alice/bob/martin#dave")
|
|
);
|
|
assert_eq!(
|
|
base.resolve_embed("./martin#dave").unwrap(),
|
|
AssetPath::from("alice/bob/martin#dave")
|
|
);
|
|
assert_eq!(
|
|
base.resolve("../martin#dave").unwrap(),
|
|
AssetPath::from("alice/martin#dave")
|
|
);
|
|
assert_eq!(
|
|
base.resolve_embed("../martin#dave").unwrap(),
|
|
AssetPath::from("alice/martin#dave")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_resolve_canonicalize() {
|
|
// Test that ".." and "." are removed after concatenation.
|
|
let base = AssetPath::from("alice/bob#carol");
|
|
assert_eq!(
|
|
base.resolve("./martin/stephan/..#dave").unwrap(),
|
|
AssetPath::from("alice/bob/martin#dave")
|
|
);
|
|
assert_eq!(
|
|
base.resolve_embed("./martin/stephan/..#dave").unwrap(),
|
|
AssetPath::from("alice/martin#dave")
|
|
);
|
|
assert_eq!(
|
|
base.resolve("../martin/.#dave").unwrap(),
|
|
AssetPath::from("alice/martin#dave")
|
|
);
|
|
assert_eq!(
|
|
base.resolve_embed("../martin/.#dave").unwrap(),
|
|
AssetPath::from("martin#dave")
|
|
);
|
|
assert_eq!(
|
|
base.resolve("/martin/stephan/..#dave").unwrap(),
|
|
AssetPath::from("martin#dave")
|
|
);
|
|
assert_eq!(
|
|
base.resolve_embed("/martin/stephan/..#dave").unwrap(),
|
|
AssetPath::from("martin#dave")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_resolve_canonicalize_base() {
|
|
// Test that ".." and "." are removed after concatenation even from the base path.
|
|
let base = AssetPath::from("alice/../bob#carol");
|
|
assert_eq!(
|
|
base.resolve("./martin/stephan/..#dave").unwrap(),
|
|
AssetPath::from("bob/martin#dave")
|
|
);
|
|
assert_eq!(
|
|
base.resolve_embed("./martin/stephan/..#dave").unwrap(),
|
|
AssetPath::from("martin#dave")
|
|
);
|
|
assert_eq!(
|
|
base.resolve("../martin/.#dave").unwrap(),
|
|
AssetPath::from("martin#dave")
|
|
);
|
|
assert_eq!(
|
|
base.resolve_embed("../martin/.#dave").unwrap(),
|
|
AssetPath::from("../martin#dave")
|
|
);
|
|
assert_eq!(
|
|
base.resolve("/martin/stephan/..#dave").unwrap(),
|
|
AssetPath::from("martin#dave")
|
|
);
|
|
assert_eq!(
|
|
base.resolve_embed("/martin/stephan/..#dave").unwrap(),
|
|
AssetPath::from("martin#dave")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_resolve_canonicalize_with_source() {
|
|
// Test that ".." and "." are removed after concatenation.
|
|
let base = AssetPath::from("source://alice/bob#carol");
|
|
assert_eq!(
|
|
base.resolve("./martin/stephan/..#dave").unwrap(),
|
|
AssetPath::from("source://alice/bob/martin#dave")
|
|
);
|
|
assert_eq!(
|
|
base.resolve_embed("./martin/stephan/..#dave").unwrap(),
|
|
AssetPath::from("source://alice/martin#dave")
|
|
);
|
|
assert_eq!(
|
|
base.resolve("../martin/.#dave").unwrap(),
|
|
AssetPath::from("source://alice/martin#dave")
|
|
);
|
|
assert_eq!(
|
|
base.resolve_embed("../martin/.#dave").unwrap(),
|
|
AssetPath::from("source://martin#dave")
|
|
);
|
|
assert_eq!(
|
|
base.resolve("/martin/stephan/..#dave").unwrap(),
|
|
AssetPath::from("source://martin#dave")
|
|
);
|
|
assert_eq!(
|
|
base.resolve_embed("/martin/stephan/..#dave").unwrap(),
|
|
AssetPath::from("source://martin#dave")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_resolve_absolute() {
|
|
// Paths beginning with '/' replace the base path
|
|
let base = AssetPath::from("alice/bob#carol");
|
|
assert_eq!(
|
|
base.resolve("/martin/stephan").unwrap(),
|
|
AssetPath::from("martin/stephan")
|
|
);
|
|
assert_eq!(
|
|
base.resolve_embed("/martin/stephan").unwrap(),
|
|
AssetPath::from("martin/stephan")
|
|
);
|
|
assert_eq!(
|
|
base.resolve("/martin/stephan#dave").unwrap(),
|
|
AssetPath::from("martin/stephan/#dave")
|
|
);
|
|
assert_eq!(
|
|
base.resolve_embed("/martin/stephan#dave").unwrap(),
|
|
AssetPath::from("martin/stephan/#dave")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_resolve_asset_source() {
|
|
// Paths beginning with 'source://' replace the base path
|
|
let base = AssetPath::from("alice/bob#carol");
|
|
assert_eq!(
|
|
base.resolve("source://martin/stephan").unwrap(),
|
|
AssetPath::from("source://martin/stephan")
|
|
);
|
|
assert_eq!(
|
|
base.resolve_embed("source://martin/stephan").unwrap(),
|
|
AssetPath::from("source://martin/stephan")
|
|
);
|
|
assert_eq!(
|
|
base.resolve("source://martin/stephan#dave").unwrap(),
|
|
AssetPath::from("source://martin/stephan/#dave")
|
|
);
|
|
assert_eq!(
|
|
base.resolve_embed("source://martin/stephan#dave").unwrap(),
|
|
AssetPath::from("source://martin/stephan/#dave")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_resolve_label() {
|
|
// A relative path with only a label should replace the label portion
|
|
let base = AssetPath::from("alice/bob#carol");
|
|
assert_eq!(
|
|
base.resolve("#dave").unwrap(),
|
|
AssetPath::from("alice/bob#dave")
|
|
);
|
|
assert_eq!(
|
|
base.resolve_embed("#dave").unwrap(),
|
|
AssetPath::from("alice/bob#dave")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_resolve_insufficient_elements() {
|
|
// Ensure that ".." segments are preserved if there are insufficient elements to remove them.
|
|
let base = AssetPath::from("alice/bob#carol");
|
|
assert_eq!(
|
|
base.resolve("../../joe/next").unwrap(),
|
|
AssetPath::from("joe/next")
|
|
);
|
|
assert_eq!(
|
|
base.resolve_embed("../../joe/next").unwrap(),
|
|
AssetPath::from("../joe/next")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_get_extension() {
|
|
let result = AssetPath::from("http://a.tar.gz#Foo");
|
|
assert_eq!(result.get_full_extension(), Some("tar.gz".to_string()));
|
|
|
|
let result = AssetPath::from("http://a#Foo");
|
|
assert_eq!(result.get_full_extension(), None);
|
|
}
|
|
}
|