flatland_cli/
characters.rs1use uuid::Uuid;
4
5use crate::api::ControlPlaneClient;
6use crate::auth_context::load_play_auth;
7use crate::config::SessionConfig;
8
9#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
10pub struct CharacterSummary {
11 pub id: Uuid,
12 pub name: String,
13}
14
15#[derive(serde::Deserialize)]
16struct CharacterList {
17 characters: Vec<CharacterSummary>,
18}
19
20#[derive(serde::Deserialize)]
21struct CreateCharacter {
22 character: CharacterSummary,
23}
24
25pub fn validate_character_name(name: &str) -> anyhow::Result<()> {
27 let name = name.trim();
28 if name.is_empty() || name.len() > 32 {
29 anyhow::bail!("character name must be 1–32 characters");
30 }
31 let mut chars = name.chars();
32 let Some(first) = chars.next() else {
33 anyhow::bail!("character name must be 1–32 characters");
34 };
35 if !first.is_ascii_alphabetic() {
36 anyhow::bail!("character name must start with a letter (A–Z)");
37 }
38 if !chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') {
39 anyhow::bail!(
40 "character name may only contain letters, digits, '_' and '-' (no spaces or ':')"
41 );
42 }
43 Ok(())
44}
45
46pub async fn list_characters() -> anyhow::Result<Vec<CharacterSummary>> {
47 let auth = load_play_auth()?;
48 let client = ControlPlaneClient::new(&auth.api_base);
49 let data: CharacterList = client.get("/v1/characters", &auth.bearer).await?;
50 Ok(data.characters)
51}
52
53pub async fn create_character(name: &str) -> anyhow::Result<CharacterSummary> {
54 let name = name.trim();
55 validate_character_name(name)?;
56
57 let auth = load_play_auth()?;
58 let client = ControlPlaneClient::new(&auth.api_base);
59 let created: CreateCharacter = client
60 .post(
61 "/v1/characters",
62 &serde_json::json!({ "name": name }),
63 Some(&auth.bearer),
64 )
65 .await?;
66
67 if let Ok(mut session) = SessionConfig::load() {
68 let _ = session.remember_character(created.character.id);
69 }
70
71 Ok(created.character)
72}
73
74#[allow(dead_code)] pub fn remember_character(id: Uuid) -> anyhow::Result<()> {
76 let mut session = SessionConfig::load()?;
77 session.remember_character(id)
78}
79
80#[cfg(test)]
81mod tests {
82 use super::validate_character_name;
83
84 #[test]
85 fn accepts_simple_names() {
86 assert!(validate_character_name("madsin").is_ok());
87 assert!(validate_character_name("Mad_Sin-2").is_ok());
88 }
89
90 #[test]
91 fn rejects_colon_junk() {
92 assert!(validate_character_name("madsin:3A").is_err());
93 assert!(validate_character_name("mad sin").is_err());
94 assert!(validate_character_name("1mad").is_err());
95 }
96}