use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
pub(crate) fn is_kebab(s: &str) -> bool {
!s.is_empty()
&& s.split('-').all(|seg| {
!seg.is_empty()
&& seg
.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit())
})
}
pub(crate) fn is_prefixed(s: &str, prefix: &str) -> bool {
match s.strip_prefix(prefix).and_then(|r| r.strip_prefix('/')) {
Some(rest) => is_kebab(rest),
None => false,
}
}
macro_rules! prefixed_id {
($(#[$m:meta])* $name:ident, $prefix:literal) => {
$(#[$m])*
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(transparent)]
#[schemars(transparent)]
pub struct $name(pub String);
impl $name {
pub const PREFIX: &'static str = $prefix;
pub fn as_str(&self) -> &str {
&self.0
}
pub fn is_valid_syntax(&self) -> bool {
is_prefixed(&self.0, Self::PREFIX)
}
}
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
};
}
prefixed_id!(
AreaId, "area");
prefixed_id!(
NpcId, "npc");
prefixed_id!(
ClassId, "class");
prefixed_id!(
QuestId, "quest");
prefixed_id!(
PrefabId, "prefab");
prefixed_id!(
PoolId, "pool");
prefixed_id!(
DialogueId, "dlg");
prefixed_id!(
ObjectiveId, "obj");
prefixed_id!(
AnchorId, "anchor");
prefixed_id!(
WaveId, "wave");
prefixed_id!(
FlagId, "flag");
prefixed_id!(
TriggerId, "trigger");
prefixed_id!(
ActorId, "actor");
prefixed_id!(
TrapId, "trap");
prefixed_id!(
TimedGateId, "timed-gate");
prefixed_id!(
AmbushId, "ambush");
prefixed_id!(
ShortcutId, "shortcut");
prefixed_id!(
BranchPointId, "branch-point");
prefixed_id!(
BranchId, "branch");
prefixed_id!(
EndingId, "ending");
prefixed_id!(
LootId, "loot");
prefixed_id!(
EditBatchId, "batch");
prefixed_id!(
RegionId, "region");
#[derive(
Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
)]
#[serde(transparent)]
#[schemars(transparent)]
pub struct CampaignId(pub String);
impl CampaignId {
pub fn as_str(&self) -> &str {
&self.0
}
pub fn is_valid_syntax(&self) -> bool {
is_kebab(&self.0)
}
}
impl std::fmt::Display for CampaignId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn kebab_rules() {
assert!(is_kebab("open-the-door"));
assert!(is_kebab("keep"));
assert!(is_kebab("area1"));
assert!(!is_kebab("Open"));
assert!(!is_kebab("open_the_door"));
assert!(!is_kebab("open--the"));
assert!(!is_kebab("-open"));
assert!(!is_kebab(""));
}
#[test]
fn prefixed_rules() {
assert!(AreaId("area/keep".into()).is_valid_syntax());
assert!(!AreaId("area/Keep".into()).is_valid_syntax());
assert!(!AreaId("keep".into()).is_valid_syntax());
assert!(!AreaId("npc/keeper".into()).is_valid_syntax());
assert!(NpcId("npc/keeper".into()).is_valid_syntax());
}
}