1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
4pub struct IdentityId(String);
5
6#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
7pub struct Did(String);
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub struct Handle(String);
11
12#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
13pub struct PeerRef(String);
14
15#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
16pub struct GroupRef(String);
17
18#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
19pub struct MessageId(String);
20
21#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
22pub struct ThreadId(String);
23
24#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
25pub struct Cursor(String);
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28pub struct Page<T> {
29 pub items: Vec<T>,
30 pub next_cursor: Option<Cursor>,
31 pub has_more: bool,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
35pub struct PageLimit(pub u32);
36
37impl IdentityId {
38 pub fn parse(input: impl AsRef<str>) -> crate::ImResult<Self> {
39 parse_non_empty("identity_id", input).map(Self)
40 }
41
42 pub fn as_str(&self) -> &str {
43 &self.0
44 }
45}
46
47impl Did {
48 pub fn parse(input: impl AsRef<str>) -> crate::ImResult<Self> {
49 let value = parse_non_empty("did", input)?;
50 if !value.starts_with("did:") {
51 return Err(crate::ImError::invalid_input(
52 Some("did".to_string()),
53 "DID must start with did:",
54 ));
55 }
56 Ok(Self(value))
57 }
58
59 pub fn as_str(&self) -> &str {
60 &self.0
61 }
62}
63
64impl Handle {
65 pub fn parse(input: impl AsRef<str>, default_domain: &str) -> crate::ImResult<Self> {
66 let value = parse_non_empty("handle", input)?;
67 let normalized = if value.contains('@') || value.contains('.') || default_domain.is_empty()
68 {
69 value
70 } else {
71 format!("{value}.{default_domain}")
72 };
73 Ok(Self(
74 normalized.trim_start_matches('@').to_ascii_lowercase(),
75 ))
76 }
77
78 pub fn as_str(&self) -> &str {
79 &self.0
80 }
81}
82
83impl PeerRef {
84 pub fn parse(input: impl AsRef<str>, default_domain: &str) -> crate::ImResult<Self> {
85 let value = parse_non_empty("peer", input)?;
86 if value.starts_with("did:") {
87 return Ok(Self(value));
88 }
89 Ok(Self(
90 Handle::parse(value, default_domain)?.as_str().to_string(),
91 ))
92 }
93
94 pub fn as_str(&self) -> &str {
95 &self.0
96 }
97}
98
99impl GroupRef {
100 pub fn parse(input: impl AsRef<str>) -> crate::ImResult<Self> {
101 parse_non_empty("group", input).map(Self)
102 }
103
104 pub fn as_str(&self) -> &str {
105 &self.0
106 }
107}
108
109impl MessageId {
110 pub fn parse(input: impl AsRef<str>) -> crate::ImResult<Self> {
111 parse_non_empty("message_id", input).map(Self)
112 }
113
114 pub fn as_str(&self) -> &str {
115 &self.0
116 }
117}
118
119impl ThreadId {
120 pub fn parse(input: impl AsRef<str>) -> crate::ImResult<Self> {
121 parse_non_empty("thread_id", input).map(Self)
122 }
123
124 pub fn as_str(&self) -> &str {
125 &self.0
126 }
127}
128
129impl Cursor {
130 pub fn parse(input: impl AsRef<str>) -> crate::ImResult<Self> {
131 parse_non_empty("cursor", input).map(Self)
132 }
133
134 pub fn as_str(&self) -> &str {
135 &self.0
136 }
137}
138
139impl PageLimit {
140 pub fn new(value: u32) -> crate::ImResult<Self> {
141 if value == 0 {
142 return Err(crate::ImError::invalid_input(
143 Some("limit".to_string()),
144 "limit must be greater than zero",
145 ));
146 }
147 Ok(Self(value.min(100)))
148 }
149}
150
151fn parse_non_empty(field: &'static str, input: impl AsRef<str>) -> crate::ImResult<String> {
152 let value = input.as_ref().trim();
153 if value.is_empty() {
154 return Err(crate::ImError::invalid_input(
155 Some(field.to_string()),
156 format!("{field} must not be empty"),
157 ));
158 }
159 Ok(value.to_string())
160}