use std::borrow::Borrow;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::ops::Deref;
use crate::{ar, sdf};
#[derive(Debug, Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))]
pub struct AssetPath {
pub authored_path: String,
#[cfg_attr(feature = "serde", serde(skip))]
evaluated_path: Option<String>,
#[cfg_attr(feature = "serde", serde(skip))]
resolved_path: Option<String>,
}
impl AssetPath {
pub fn new(authored_path: impl Into<String>) -> Self {
Self {
authored_path: authored_path.into(),
evaluated_path: None,
resolved_path: None,
}
}
pub fn with_resolved_path(authored_path: impl Into<String>, resolved_path: impl Into<String>) -> Self {
Self {
authored_path: authored_path.into(),
evaluated_path: None,
resolved_path: Some(resolved_path.into()),
}
}
pub fn as_str(&self) -> &str {
&self.authored_path
}
pub fn asset_path(&self) -> &str {
self.evaluated_path.as_deref().unwrap_or(&self.authored_path)
}
pub fn evaluated_path(&self) -> Option<&str> {
self.evaluated_path.as_deref()
}
pub fn set_evaluated_path(&mut self, evaluated_path: impl Into<String>) {
self.evaluated_path = Some(evaluated_path.into());
}
pub fn resolved_path(&self) -> Option<&str> {
self.resolved_path.as_deref()
}
pub fn set_resolved_path(&mut self, resolved_path: impl Into<String>) {
self.resolved_path = Some(resolved_path.into());
}
pub fn is_empty(&self) -> bool {
self.authored_path.is_empty()
}
pub fn into_string(self) -> String {
self.authored_path
}
}
impl PartialEq for AssetPath {
fn eq(&self, other: &Self) -> bool {
self.authored_path == other.authored_path
}
}
impl Eq for AssetPath {}
impl Hash for AssetPath {
fn hash<H: Hasher>(&self, state: &mut H) {
self.authored_path.hash(state);
}
}
impl PartialOrd for AssetPath {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for AssetPath {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.authored_path.cmp(&other.authored_path)
}
}
impl Deref for AssetPath {
type Target = str;
fn deref(&self) -> &str {
&self.authored_path
}
}
impl AsRef<str> for AssetPath {
fn as_ref(&self) -> &str {
&self.authored_path
}
}
impl Borrow<str> for AssetPath {
fn borrow(&self) -> &str {
&self.authored_path
}
}
impl std::fmt::Display for AssetPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.authored_path)
}
}
impl From<String> for AssetPath {
fn from(authored_path: String) -> Self {
Self::new(authored_path)
}
}
impl From<&str> for AssetPath {
fn from(authored_path: &str) -> Self {
Self::new(authored_path)
}
}
impl From<AssetPath> for String {
fn from(asset: AssetPath) -> Self {
asset.authored_path
}
}
impl PartialEq<str> for AssetPath {
fn eq(&self, other: &str) -> bool {
self.authored_path == other
}
}
impl PartialEq<&str> for AssetPath {
fn eq(&self, other: &&str) -> bool {
self.authored_path == *other
}
}
impl PartialEq<String> for AssetPath {
fn eq(&self, other: &String) -> bool {
self.authored_path == *other
}
}
impl PartialEq<AssetPath> for str {
fn eq(&self, other: &AssetPath) -> bool {
other.authored_path == *self
}
}
impl PartialEq<AssetPath> for &str {
fn eq(&self, other: &AssetPath) -> bool {
other.authored_path == *self
}
}
impl PartialEq<AssetPath> for String {
fn eq(&self, other: &AssetPath) -> bool {
other.authored_path == *self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum AssetOutcome {
Failed,
None,
Evaluated,
}
#[derive(Debug)]
pub(crate) struct AssetExpressionFailure {
pub(crate) expression: String,
pub(crate) message: String,
}
pub(crate) fn resolve_asset_paths(
registry: &sdf::LayerRegistry,
anchor: Option<&ar::ResolvedPath>,
variables: Option<&HashMap<String, sdf::Value>>,
value: sdf::Value,
errors: &mut Vec<AssetExpressionFailure>,
) -> sdf::Value {
map_paths(value, |asset| resolve_path(registry, anchor, variables, asset, errors)).0
}
pub(crate) fn evaluate_asset_paths(
variables: Option<&HashMap<String, sdf::Value>>,
value: sdf::Value,
errors: &mut Vec<AssetExpressionFailure>,
) -> (sdf::Value, AssetOutcome) {
map_paths(value, |asset| evaluate_path(variables, asset, errors))
}
pub(crate) fn holds_asset_expression(value: &sdf::Value) -> bool {
match value {
sdf::Value::AssetPath(asset) => sdf::expr::is_expression(asset.as_str()),
sdf::Value::AssetPathVec(assets) => assets.iter().any(|a| sdf::expr::is_expression(a.as_str())),
_ => false,
}
}
fn map_paths(
value: sdf::Value,
mut f: impl FnMut(AssetPath) -> (AssetPath, AssetOutcome),
) -> (sdf::Value, AssetOutcome) {
let mut outcome = AssetOutcome::Evaluated;
let value = match value {
sdf::Value::AssetPath(asset) => {
let (asset, element) = f(asset);
outcome = element;
sdf::Value::AssetPath(asset)
}
sdf::Value::AssetPathVec(assets) => sdf::Value::AssetPathVec(
assets
.into_iter()
.map(|asset| {
let (asset, element) = f(asset);
outcome = outcome.min(element);
asset
})
.collect(),
),
other => other,
};
(value, outcome)
}
fn evaluate_path(
variables: Option<&HashMap<String, sdf::Value>>,
asset: AssetPath,
errors: &mut Vec<AssetExpressionFailure>,
) -> (AssetPath, AssetOutcome) {
let mut asset = AssetPath::new(asset.into_string());
if asset.is_empty() || !sdf::expr::is_expression(asset.as_str()) {
return (asset, AssetOutcome::Evaluated);
}
let Some(variables) = variables else {
return (asset, AssetOutcome::None);
};
let evaluated = sdf::expr::evaluate_string(asset.as_str(), variables);
match evaluated.value {
Some(path) => {
asset.set_evaluated_path(path);
(asset, AssetOutcome::Evaluated)
}
None if evaluated.errors.is_empty() => (asset, AssetOutcome::None),
None => {
errors.push(AssetExpressionFailure {
expression: asset.as_str().to_string(),
message: evaluated.errors.join("; "),
});
(asset, AssetOutcome::Failed)
}
}
}
fn resolve_path(
registry: &sdf::LayerRegistry,
anchor: Option<&ar::ResolvedPath>,
variables: Option<&HashMap<String, sdf::Value>>,
asset: AssetPath,
errors: &mut Vec<AssetExpressionFailure>,
) -> (AssetPath, AssetOutcome) {
let (mut asset, outcome) = evaluate_path(variables, asset, errors);
if asset.is_empty() || outcome != AssetOutcome::Evaluated {
return (asset, outcome);
}
let identifier = registry.create_identifier(asset.asset_path(), anchor);
if let Some(resolved) = registry.resolve(&identifier) {
asset.set_resolved_path(resolved.to_string_lossy().into_owned());
}
(asset, outcome)
}
#[cfg(test)]
mod tests {
use std::fs;
use super::*;
#[test]
fn string_like() {
let asset = AssetPath::new("./tex.png");
assert_eq!(asset.len(), "./tex.png".len());
assert!(asset.ends_with(".png"));
assert_eq!(asset.as_ref() as &str, "./tex.png");
assert_eq!(asset, "./tex.png");
assert_eq!("./tex.png", asset);
assert_eq!(asset, String::from("./tex.png"));
assert_eq!(asset.to_string(), "./tex.png");
assert_eq!(String::from(asset), "./tex.png");
assert!(!AssetPath::new("./tex.png").is_empty());
assert!(AssetPath::default().is_empty());
}
#[test]
fn no_scope_skips_expression() {
let registry = sdf::LayerRegistry::default();
let mut errors = Vec::new();
let value = sdf::Value::AssetPath(AssetPath::new("`${A}`"));
let value = resolve_asset_paths(®istry, None, None, value, &mut errors);
let asset = value.try_as_asset_path().expect("an asset value stays one");
assert_eq!(asset.as_str(), "`${A}`");
assert_eq!(asset.evaluated_path(), None);
assert_eq!(asset.resolved_path(), None);
assert!(errors.is_empty());
}
#[test]
fn absolute_resolves_unanchored() {
let dir = tempfile::tempdir().expect("tempdir");
let texture = dir.path().join("tex.png");
fs::write(&texture, b"png").expect("write texture");
let authored = texture.to_string_lossy().replace('\\', "/");
let registry = sdf::LayerRegistry::default();
let mut errors = Vec::new();
let value = sdf::Value::AssetPath(AssetPath::new(authored));
let value = resolve_asset_paths(®istry, None, None, value, &mut errors);
let asset = value.try_as_asset_path().expect("an asset value stays one");
assert!(asset.resolved_path().is_some(), "an absolute path needs no anchor");
assert!(errors.is_empty());
}
}