1use thiserror::Error;
43
44use crate::event::{Alphabet, Event, EventBuilder, Kind, SingleLetterTag, Tag, TagKind, Tags};
45
46pub const KIND_APPLICATION_DATA: Kind = Kind::new(30_078);
48
49#[derive(Debug, Clone, PartialEq, Eq, Default)]
55pub struct ApplicationData {
56 pub identifier: String,
60 pub content: String,
62 pub extra_tags: Vec<Tag>,
66}
67
68impl ApplicationData {
69 #[must_use]
71 pub fn new(identifier: impl Into<String>) -> Self {
72 Self {
73 identifier: identifier.into(),
74 content: String::new(),
75 extra_tags: Vec::new(),
76 }
77 }
78
79 #[must_use]
81 pub fn content(mut self, content: impl Into<String>) -> Self {
82 self.content = content.into();
83 self
84 }
85
86 #[must_use]
92 pub fn tag(mut self, tag: Tag) -> Self {
93 if !is_d_tag(&tag) {
94 self.extra_tags.push(tag);
95 }
96 self
97 }
98
99 #[must_use]
101 pub fn tags<I>(mut self, tags: I) -> Self
102 where
103 I: IntoIterator<Item = Tag>,
104 {
105 for tag in tags {
106 if !is_d_tag(&tag) {
107 self.extra_tags.push(tag);
108 }
109 }
110 self
111 }
112
113 pub fn from_event(event: &Event) -> Result<Self, ApplicationDataError> {
121 if event.kind != KIND_APPLICATION_DATA {
122 return Err(ApplicationDataError::WrongKind(event.kind));
123 }
124 let identifier = d_value(&event.tags)
125 .ok_or(ApplicationDataError::MissingIdentifier)?
126 .to_owned();
127 let extra_tags: Vec<Tag> = event
128 .tags
129 .iter()
130 .filter(|tag| !is_d_tag(tag))
131 .cloned()
132 .collect();
133 Ok(Self {
134 identifier,
135 content: event.content.clone(),
136 extra_tags,
137 })
138 }
139}
140
141#[derive(Debug, Error)]
143#[non_exhaustive]
144pub enum ApplicationDataError {
145 #[error("expected kind 30078 (application data), got kind {}", .0.as_u16())]
147 WrongKind(Kind),
148 #[error("NIP-78 event must carry a `d` tag")]
150 MissingIdentifier,
151}
152
153fn is_d_tag(tag: &Tag) -> bool {
154 matches!(
155 tag.kind(),
156 TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::D
157 )
158}
159
160fn d_value(tags: &Tags) -> Option<&str> {
161 let head = TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::D));
162 tags.find_first(&head).and_then(|tag| tag.get(1))
163}
164
165impl EventBuilder {
166 #[must_use]
171 pub fn application_data(data: &ApplicationData) -> Self {
172 let mut builder =
173 Self::new(KIND_APPLICATION_DATA, data.content.clone()).tag(Tag::d(&data.identifier));
174 for tag in &data.extra_tags {
175 builder = builder.tag(tag.clone());
176 }
177 builder
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184 use crate::Keys;
185
186 fn keys() -> Keys {
187 Keys::parse("0000000000000000000000000000000000000000000000000000000000000003").unwrap()
188 }
189
190 #[test]
191 fn round_trip_with_only_required_fields() {
192 let data = ApplicationData::new("com.example.app");
193 let event = EventBuilder::application_data(&data)
194 .sign_with_keys(&keys())
195 .unwrap();
196 assert_eq!(event.kind, KIND_APPLICATION_DATA);
197 assert_eq!(event.tags.len(), 1, "exactly one tag: the d tag");
198 let parsed = ApplicationData::from_event(&event).unwrap();
199 assert_eq!(parsed, data);
200 }
201
202 #[test]
203 fn round_trip_preserves_content_and_extra_tags_in_order() {
204 let data = ApplicationData::new("vendor/v1")
205 .content(r#"{"theme":"dark"}"#)
206 .tag(Tag::with(&TagKind::Custom("color".to_owned()), ["blue"]))
207 .tag(Tag::with(&TagKind::Custom("color".to_owned()), ["red"]))
208 .tag(Tag::title("preferences"));
209 let event = EventBuilder::application_data(&data)
210 .sign_with_keys(&keys())
211 .unwrap();
212 let parsed = ApplicationData::from_event(&event).unwrap();
213 assert_eq!(parsed, data);
214 let names: Vec<&str> = parsed.extra_tags.iter().map(Tag::name).collect();
216 assert_eq!(names, ["color", "color", "title"]);
217 }
218
219 #[test]
220 fn user_supplied_d_tags_are_silently_dropped() {
221 let data = ApplicationData::new("vendor/v1")
222 .tag(Tag::d("not-the-real-id"))
223 .tag(Tag::title("ok"));
224 assert_eq!(data.extra_tags.len(), 1);
226 assert_eq!(data.extra_tags[0].name(), "title");
227 }
228
229 #[test]
230 fn missing_d_tag_is_rejected_when_parsing() {
231 let event = EventBuilder::new(KIND_APPLICATION_DATA, "")
232 .sign_with_keys(&keys())
233 .unwrap();
234 assert!(matches!(
235 ApplicationData::from_event(&event),
236 Err(ApplicationDataError::MissingIdentifier)
237 ));
238 }
239
240 #[test]
241 fn wrong_kind_is_rejected_when_parsing() {
242 let event = EventBuilder::text_note("nope")
243 .sign_with_keys(&keys())
244 .unwrap();
245 assert!(matches!(
246 ApplicationData::from_event(&event),
247 Err(ApplicationDataError::WrongKind(_))
248 ));
249 }
250
251 #[test]
252 fn empty_identifier_is_allowed_per_spec() {
253 let data = ApplicationData::new("");
255 let event = EventBuilder::application_data(&data)
256 .sign_with_keys(&keys())
257 .unwrap();
258 let parsed = ApplicationData::from_event(&event).unwrap();
259 assert_eq!(parsed.identifier, "");
260 }
261}