es_fluent_shared/resource/
paths.rs1use crate::namespace::NamespacePathError;
2
3#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
5pub enum LocaleRelativeFtlPathError {
6 #[error("path must not be empty")]
8 Empty,
9 #[error("path must be relative")]
11 Absolute,
12 #[error("path must use '/' separators")]
14 Backslash,
15 #[error("path must end with .ftl")]
17 MissingFtlSuffix,
18 #[error("{0}")]
20 InvalidStem(#[from] NamespacePathError),
21}
22
23#[derive(
28 Clone,
29 Debug,
30 derive_more::AsRef,
31 derive_more::Deref,
32 derive_more::Display,
33 Eq,
34 Hash,
35 Ord,
36 PartialEq,
37 PartialOrd,
38)]
39#[as_ref(str)]
40#[deref(forward)]
41pub struct LocaleRelativeFtlPath(String);
42
43impl LocaleRelativeFtlPath {
44 pub fn try_new(path: impl Into<String>) -> Result<Self, LocaleRelativeFtlPathError> {
46 let path = path.into();
47 validate_locale_relative_ftl_path(&path)?;
48 Ok(Self(path))
49 }
50
51 #[allow(
53 clippy::panic,
54 reason = "static metadata uses literal paths; use try_new for dynamic input"
55 )]
56 pub fn from_static_path(path: &'static str) -> Self {
57 Self::try_new(path)
58 .unwrap_or_else(|error| panic!("invalid locale-relative FTL path '{path}': {error}"))
59 }
60
61 pub fn as_str(&self) -> &str {
63 &self.0
64 }
65
66 pub fn into_string(self) -> String {
68 self.0
69 }
70}
71
72impl PartialEq<&str> for LocaleRelativeFtlPath {
73 fn eq(&self, other: &&str) -> bool {
74 self.as_str() == *other
75 }
76}
77
78impl PartialEq<LocaleRelativeFtlPath> for &str {
79 fn eq(&self, other: &LocaleRelativeFtlPath) -> bool {
80 *self == other.as_str()
81 }
82}
83
84impl serde::Serialize for LocaleRelativeFtlPath {
85 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
86 where
87 S: serde::Serializer,
88 {
89 serializer.serialize_str(self.as_str())
90 }
91}
92
93impl<'de> serde::Deserialize<'de> for LocaleRelativeFtlPath {
94 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
95 where
96 D: serde::Deserializer<'de>,
97 {
98 let value = <String as serde::Deserialize>::deserialize(deserializer)?;
99 Self::try_new(value).map_err(serde::de::Error::custom)
100 }
101}
102
103fn validate_locale_relative_ftl_path(path: &str) -> Result<(), LocaleRelativeFtlPathError> {
104 if path.is_empty() {
105 return Err(LocaleRelativeFtlPathError::Empty);
106 }
107 if path.starts_with('/') {
108 return Err(LocaleRelativeFtlPathError::Absolute);
109 }
110 if path.contains('\\') {
111 return Err(LocaleRelativeFtlPathError::Backslash);
112 }
113
114 let Some(stem) = path.strip_suffix(".ftl") else {
115 return Err(LocaleRelativeFtlPathError::MissingFtlSuffix);
116 };
117
118 crate::namespace::validate_namespace_path_typed(stem)?;
119 Ok(())
120}