1use std::borrow::Cow;
33use std::fmt;
34
35use thiserror::Error;
36
37use crate::event::{
38 Alphabet, Coordinate, CoordinateError, Event, EventBuilder, EventId, EventIdError, Kind,
39 SingleLetterTag, Tag, TagKind, Tags,
40};
41use crate::key::{PublicKey, PublicKeyError};
42use crate::types::{RelayUrl, RelayUrlError};
43
44pub const KIND_WIKI_ARTICLE: Kind = Kind::WIKI_ARTICLE;
46
47pub const KIND_WIKI_REDIRECT: Kind = Kind::WIKI_REDIRECT;
49
50pub const KIND_WIKI_MERGE_REQUEST: Kind = Kind::WIKI_MERGE_REQUEST;
52
53const TITLE_TAG: &str = "title";
54const SUMMARY_TAG: &str = "summary";
55const SOURCE_MARKER: &str = "source";
56const FORK_MARKER: &str = "fork";
57const DEFER_MARKER: &str = "defer";
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
61pub enum Relation {
62 Fork,
64 Defer,
66}
67
68impl Relation {
69 #[must_use]
71 pub const fn as_str(self) -> &'static str {
72 match self {
73 Self::Fork => FORK_MARKER,
74 Self::Defer => DEFER_MARKER,
75 }
76 }
77
78 #[must_use]
80 pub fn parse(token: &str) -> Option<Self> {
81 match token {
82 FORK_MARKER => Some(Self::Fork),
83 DEFER_MARKER => Some(Self::Defer),
84 _ => None,
85 }
86 }
87}
88
89impl fmt::Display for Relation {
90 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91 f.write_str(self.as_str())
92 }
93}
94
95#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct RelationRef {
102 pub relation: Relation,
104 pub coordinate: Option<Coordinate>,
106 pub coordinate_relay_hint: Option<RelayUrl>,
108 pub event_id: Option<EventId>,
110 pub event_relay_hint: Option<RelayUrl>,
112}
113
114#[must_use]
123pub fn normalize_d_tag(input: &str) -> String {
124 normalize_d_tag_cow(input).into_owned()
125}
126
127#[must_use]
133pub fn normalize_d_tag_cow(input: &str) -> Cow<'_, str> {
134 if is_canonical_d_tag(input) {
135 return Cow::Borrowed(input);
136 }
137 let mut out = String::with_capacity(input.len());
138 let mut prev_dash = true;
139 for ch in input.chars() {
140 if ch.is_alphabetic() || ch.is_numeric() {
141 for low in ch.to_lowercase() {
142 out.push(low);
143 }
144 prev_dash = false;
145 } else if (ch.is_whitespace() || ch == '-' || ch == '_') && !prev_dash {
146 out.push('-');
147 prev_dash = true;
148 }
149 }
152 while out.ends_with('-') {
153 out.pop();
154 }
155 Cow::Owned(out)
156}
157
158#[must_use]
161pub fn is_canonical_d_tag(input: &str) -> bool {
162 if input.starts_with('-') || input.ends_with('-') {
163 return false;
164 }
165 let mut prev_dash = false;
166 for ch in input.chars() {
167 if ch.is_uppercase() {
168 return false;
169 }
170 if ch == '-' {
171 if prev_dash {
172 return false;
173 }
174 prev_dash = true;
175 } else if ch.is_alphabetic() || ch.is_numeric() {
176 prev_dash = false;
177 } else {
178 return false;
180 }
181 }
182 true
183}
184
185#[derive(Debug, Clone, PartialEq, Eq, Default)]
187pub struct WikiArticle {
188 pub identifier: String,
190 pub content: String,
192 pub title: Option<String>,
194 pub summary: Option<String>,
196 pub relations: Vec<RelationRef>,
198 pub extra_tags: Vec<Tag>,
200}
201
202impl WikiArticle {
203 #[must_use]
208 pub fn new(identifier: impl AsRef<str>) -> Self {
209 Self {
210 identifier: normalize_d_tag(identifier.as_ref()),
211 ..Self::default()
212 }
213 }
214
215 #[must_use]
217 pub fn content(mut self, content: impl Into<String>) -> Self {
218 self.content = content.into();
219 self
220 }
221
222 #[must_use]
224 pub fn title(mut self, title: impl Into<String>) -> Self {
225 self.title = Some(title.into());
226 self
227 }
228
229 #[must_use]
231 pub fn summary(mut self, summary: impl Into<String>) -> Self {
232 self.summary = Some(summary.into());
233 self
234 }
235
236 #[must_use]
238 pub fn relation(mut self, relation: RelationRef) -> Self {
239 self.relations.push(relation);
240 self
241 }
242
243 #[must_use]
245 pub fn coordinate(&self, author: PublicKey) -> Coordinate {
246 Coordinate::new(KIND_WIKI_ARTICLE, author, self.identifier.clone())
247 }
248
249 pub fn from_event(event: &Event) -> Result<Self, WikiError> {
257 if event.kind != KIND_WIKI_ARTICLE {
258 return Err(WikiError::WrongKind(event.kind));
259 }
260 let identifier = d_value(&event.tags)
261 .ok_or(WikiError::MissingIdentifier)?
262 .to_owned();
263 let mut article = Self {
264 identifier,
265 content: event.content.clone(),
266 ..Self::default()
267 };
268 let mut pending_a: Vec<(Coordinate, Option<RelayUrl>, Option<Relation>)> = Vec::new();
269 let mut pending_e: Vec<(EventId, Option<RelayUrl>, Option<Relation>)> = Vec::new();
270 for tag in &event.tags {
271 match tag.kind() {
272 TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::D => {}
273 TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::A => {
274 absorb_marker_tag_a(tag, &mut pending_a, &mut article)?;
275 }
276 TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::E => {
277 absorb_marker_tag_e(tag, &mut pending_e, &mut article)?;
278 }
279 _ if tag.name() == TITLE_TAG => article.title = tag.get(1).map(str::to_owned),
280 _ if tag.name() == SUMMARY_TAG => article.summary = tag.get(1).map(str::to_owned),
281 _ => article.extra_tags.push(tag.clone()),
282 }
283 }
284 article.relations = pair_relations(pending_a, pending_e);
285 Ok(article)
286 }
287}
288
289fn absorb_marker_tag_a(
290 tag: &Tag,
291 pending: &mut Vec<(Coordinate, Option<RelayUrl>, Option<Relation>)>,
292 article: &mut WikiArticle,
293) -> Result<(), WikiError> {
294 let (coord, relay, marker) = parse_a_tag_with_marker(tag)?;
295 if let Some(rel) = marker.as_ref().and_then(|m| Relation::parse(m)) {
296 pending.push((coord, relay, Some(rel)));
297 } else {
298 article.extra_tags.push(tag.clone());
299 }
300 Ok(())
301}
302
303fn absorb_marker_tag_e(
304 tag: &Tag,
305 pending: &mut Vec<(EventId, Option<RelayUrl>, Option<Relation>)>,
306 article: &mut WikiArticle,
307) -> Result<(), WikiError> {
308 let (id, relay, marker) = parse_e_tag_with_marker(tag)?;
309 if let Some(rel) = marker.as_ref().and_then(|m| Relation::parse(m)) {
310 pending.push((id, relay, Some(rel)));
311 } else {
312 article.extra_tags.push(tag.clone());
313 }
314 Ok(())
315}
316
317fn absorb_merge_e_tag(
318 tag: &Tag,
319 source: &mut Option<(EventId, Option<RelayUrl>)>,
320 base: &mut Option<(EventId, Option<RelayUrl>)>,
321 extra_tags: &mut Vec<Tag>,
322) -> Result<(), WikiError> {
323 let (id, relay, marker) = parse_e_tag_with_marker(tag)?;
324 if marker.as_deref() == Some(SOURCE_MARKER) {
325 *source = Some((id, relay));
326 } else if base.is_none() {
327 *base = Some((id, relay));
328 } else {
329 extra_tags.push(tag.clone());
330 }
331 Ok(())
332}
333
334fn pair_relations(
335 a_tags: Vec<(Coordinate, Option<RelayUrl>, Option<Relation>)>,
336 e_tags: Vec<(EventId, Option<RelayUrl>, Option<Relation>)>,
337) -> Vec<RelationRef> {
338 let mut out: Vec<RelationRef> = Vec::new();
339 let mut e_iter = e_tags.into_iter();
340 for (coord, coord_relay, rel) in a_tags {
341 let relation = rel.unwrap_or(Relation::Fork);
342 let companion = e_iter.next();
343 out.push(RelationRef {
344 relation,
345 coordinate: Some(coord),
346 coordinate_relay_hint: coord_relay,
347 event_id: companion.as_ref().map(|(id, _, _)| *id),
348 event_relay_hint: companion.and_then(|(_, relay, _)| relay),
349 });
350 }
351 for (id, relay, rel) in e_iter {
353 let relation = rel.unwrap_or(Relation::Fork);
354 out.push(RelationRef {
355 relation,
356 coordinate: None,
357 coordinate_relay_hint: None,
358 event_id: Some(id),
359 event_relay_hint: relay,
360 });
361 }
362 out
363}
364
365#[derive(Debug, Clone, PartialEq, Eq)]
367pub struct WikiRedirect {
368 pub identifier: String,
370 pub target: Coordinate,
372 pub target_relay_hint: Option<RelayUrl>,
374 pub extra_tags: Vec<Tag>,
376}
377
378impl WikiRedirect {
379 #[must_use]
382 pub fn new(identifier: impl AsRef<str>, target: Coordinate) -> Self {
383 Self {
384 identifier: normalize_d_tag(identifier.as_ref()),
385 target,
386 target_relay_hint: None,
387 extra_tags: Vec::new(),
388 }
389 }
390
391 #[must_use]
393 pub fn target_relay_hint(mut self, relay: RelayUrl) -> Self {
394 self.target_relay_hint = Some(relay);
395 self
396 }
397
398 #[must_use]
400 pub fn coordinate(&self, author: PublicKey) -> Coordinate {
401 Coordinate::new(KIND_WIKI_REDIRECT, author, self.identifier.clone())
402 }
403
404 pub fn from_event(event: &Event) -> Result<Self, WikiError> {
413 if event.kind != KIND_WIKI_REDIRECT {
414 return Err(WikiError::WrongKind(event.kind));
415 }
416 let identifier = d_value(&event.tags)
417 .ok_or(WikiError::MissingIdentifier)?
418 .to_owned();
419 let mut target: Option<(Coordinate, Option<RelayUrl>)> = None;
420 let mut extra_tags: Vec<Tag> = Vec::new();
421 for tag in &event.tags {
422 match tag.kind() {
423 TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::D => {}
424 TagKind::SingleLetter(s)
425 if !s.uppercase && s.character == Alphabet::A && target.is_none() =>
426 {
427 let (coord, relay, _) = parse_a_tag_with_marker(tag)?;
428 target = Some((coord, relay));
429 }
430 _ => extra_tags.push(tag.clone()),
431 }
432 }
433 let (target, target_relay_hint) = target.ok_or(WikiError::MissingTarget)?;
434 Ok(Self {
435 identifier,
436 target,
437 target_relay_hint,
438 extra_tags,
439 })
440 }
441}
442
443#[derive(Debug, Clone, PartialEq, Eq)]
445pub struct MergeRequest {
446 pub destination: Coordinate,
448 pub destination_relay_hint: Option<RelayUrl>,
450 pub destination_pubkey: PublicKey,
452 pub base_revision: Option<EventId>,
454 pub base_revision_relay_hint: Option<RelayUrl>,
456 pub source_revision: EventId,
460 pub source_revision_relay_hint: Option<RelayUrl>,
462 pub content: String,
464 pub extra_tags: Vec<Tag>,
466}
467
468impl MergeRequest {
469 #[must_use]
471 pub const fn new(
472 destination: Coordinate,
473 destination_pubkey: PublicKey,
474 source_revision: EventId,
475 ) -> Self {
476 Self {
477 destination,
478 destination_relay_hint: None,
479 destination_pubkey,
480 base_revision: None,
481 base_revision_relay_hint: None,
482 source_revision,
483 source_revision_relay_hint: None,
484 content: String::new(),
485 extra_tags: Vec::new(),
486 }
487 }
488
489 #[must_use]
491 pub fn destination_relay_hint(mut self, relay: RelayUrl) -> Self {
492 self.destination_relay_hint = Some(relay);
493 self
494 }
495
496 #[must_use]
498 pub const fn base_revision(mut self, id: EventId) -> Self {
499 self.base_revision = Some(id);
500 self
501 }
502
503 #[must_use]
505 pub fn base_revision_relay_hint(mut self, relay: RelayUrl) -> Self {
506 self.base_revision_relay_hint = Some(relay);
507 self
508 }
509
510 #[must_use]
512 pub fn source_revision_relay_hint(mut self, relay: RelayUrl) -> Self {
513 self.source_revision_relay_hint = Some(relay);
514 self
515 }
516
517 #[must_use]
519 pub fn content(mut self, content: impl Into<String>) -> Self {
520 self.content = content.into();
521 self
522 }
523
524 pub fn from_event(event: &Event) -> Result<Self, WikiError> {
535 if event.kind != KIND_WIKI_MERGE_REQUEST {
536 return Err(WikiError::WrongKind(event.kind));
537 }
538 let mut destination: Option<(Coordinate, Option<RelayUrl>)> = None;
539 let mut destination_pubkey: Option<PublicKey> = None;
540 let mut base_revision: Option<(EventId, Option<RelayUrl>)> = None;
541 let mut source_revision: Option<(EventId, Option<RelayUrl>)> = None;
542 let mut extra_tags: Vec<Tag> = Vec::new();
543 for tag in &event.tags {
544 match tag.kind() {
545 TagKind::SingleLetter(s)
546 if !s.uppercase && s.character == Alphabet::A && destination.is_none() =>
547 {
548 let (coord, relay, _) = parse_a_tag_with_marker(tag)?;
549 destination = Some((coord, relay));
550 }
551 TagKind::SingleLetter(s)
552 if !s.uppercase
553 && s.character == Alphabet::P
554 && destination_pubkey.is_none() =>
555 {
556 let pk_hex = tag.get(1).ok_or(WikiError::MissingMergePubkey)?;
557 destination_pubkey = Some(PublicKey::parse(pk_hex)?);
558 }
559 TagKind::SingleLetter(s) if !s.uppercase && s.character == Alphabet::E => {
560 absorb_merge_e_tag(
561 tag,
562 &mut source_revision,
563 &mut base_revision,
564 &mut extra_tags,
565 )?;
566 }
567 _ => extra_tags.push(tag.clone()),
568 }
569 }
570 let (destination, destination_relay_hint) = destination.ok_or(WikiError::MissingTarget)?;
571 let destination_pubkey = destination_pubkey.ok_or(WikiError::MissingMergePubkey)?;
572 let (source_revision, source_revision_relay_hint) =
573 source_revision.ok_or(WikiError::MissingMergeSource)?;
574 let (base_revision, base_revision_relay_hint) =
575 base_revision.map_or((None, None), |(id, relay)| (Some(id), relay));
576 Ok(Self {
577 destination,
578 destination_relay_hint,
579 destination_pubkey,
580 base_revision,
581 base_revision_relay_hint,
582 source_revision,
583 source_revision_relay_hint,
584 content: event.content.clone(),
585 extra_tags,
586 })
587 }
588}
589
590fn parse_a_tag_with_marker(
591 tag: &Tag,
592) -> Result<(Coordinate, Option<RelayUrl>, Option<String>), WikiError> {
593 let coord_str = tag.get(1).ok_or(WikiError::MalformedAddressTag)?;
594 let coord = Coordinate::parse(coord_str)?;
595 let relay = match tag.get(2) {
596 Some(s) if !s.is_empty() => Some(RelayUrl::parse(s)?),
597 _ => None,
598 };
599 let marker = tag.get(3).filter(|s| !s.is_empty()).map(str::to_owned);
600 Ok((coord, relay, marker))
601}
602
603fn parse_e_tag_with_marker(
604 tag: &Tag,
605) -> Result<(EventId, Option<RelayUrl>, Option<String>), WikiError> {
606 let id_hex = tag.get(1).ok_or(WikiError::MalformedEventTag)?;
607 let id = EventId::parse(id_hex)?;
608 let relay = match tag.get(2) {
609 Some(s) if !s.is_empty() => Some(RelayUrl::parse(s)?),
610 _ => None,
611 };
612 let marker = tag.get(3).filter(|s| !s.is_empty()).map(str::to_owned);
613 Ok((id, relay, marker))
614}
615
616fn d_value(tags: &Tags) -> Option<&str> {
617 let head = TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::D));
618 tags.find_first(&head).and_then(|tag| tag.get(1))
619}
620
621#[derive(Debug, Error)]
623#[non_exhaustive]
624pub enum WikiError {
625 #[error("unexpected kind for NIP-54 event: {}", .0.as_u16())]
627 WrongKind(Kind),
628 #[error("NIP-54 event missing `d` tag")]
630 MissingIdentifier,
631 #[error("NIP-54 event missing target `a` tag")]
633 MissingTarget,
634 #[error("NIP-54 merge request missing `p` tag")]
636 MissingMergePubkey,
637 #[error("NIP-54 merge request missing source revision `e` tag")]
639 MissingMergeSource,
640 #[error("`a` tag missing coordinate")]
642 MalformedAddressTag,
643 #[error("`e` tag missing event id")]
645 MalformedEventTag,
646 #[error(transparent)]
648 InvalidCoordinate(#[from] CoordinateError),
649 #[error(transparent)]
651 InvalidEventId(#[from] EventIdError),
652 #[error(transparent)]
654 InvalidRelayUrl(#[from] RelayUrlError),
655 #[error(transparent)]
657 InvalidPublicKey(#[from] PublicKeyError),
658}
659
660impl EventBuilder {
661 #[must_use]
663 pub fn wiki_article(article: &WikiArticle) -> Self {
664 let mut builder = Self::new(KIND_WIKI_ARTICLE, article.content.clone());
665 builder = builder.tag(Tag::d(&article.identifier));
666 if let Some(title) = &article.title {
667 builder = builder.tag(Tag::with(&TagKind::from_wire(TITLE_TAG), [title.clone()]));
668 }
669 if let Some(summary) = &article.summary {
670 builder = builder.tag(Tag::with(
671 &TagKind::from_wire(SUMMARY_TAG),
672 [summary.clone()],
673 ));
674 }
675 for rel in &article.relations {
676 builder = push_relation_tags(rel, builder);
677 }
678 for tag in &article.extra_tags {
679 builder = builder.tag(tag.clone());
680 }
681 builder
682 }
683
684 #[must_use]
686 pub fn wiki_redirect(redirect: &WikiRedirect) -> Self {
687 let mut builder = Self::new(KIND_WIKI_REDIRECT, "");
688 builder = builder.tag(Tag::d(&redirect.identifier));
689 let head = TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::A));
690 builder = builder.tag(redirect.target_relay_hint.as_ref().map_or_else(
691 || Tag::with(&head, [redirect.target.to_wire()]),
692 |relay| {
693 Tag::with(
694 &head,
695 [redirect.target.to_wire(), relay.as_str().to_owned()],
696 )
697 },
698 ));
699 for tag in &redirect.extra_tags {
700 builder = builder.tag(tag.clone());
701 }
702 builder
703 }
704
705 #[must_use]
710 pub fn wiki_merge_request(merge: &MergeRequest) -> Self {
711 let mut builder = Self::new(KIND_WIKI_MERGE_REQUEST, merge.content.clone());
712 let head_a = TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::A));
713 builder = builder.tag(merge.destination_relay_hint.as_ref().map_or_else(
714 || Tag::with(&head_a, [merge.destination.to_wire()]),
715 |relay| {
716 Tag::with(
717 &head_a,
718 [merge.destination.to_wire(), relay.as_str().to_owned()],
719 )
720 },
721 ));
722 if let Some(base) = merge.base_revision {
723 let head_e = TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::E));
724 builder = builder.tag(merge.base_revision_relay_hint.as_ref().map_or_else(
725 || Tag::with(&head_e, [base.to_hex()]),
726 |relay| Tag::with(&head_e, [base.to_hex(), relay.as_str().to_owned()]),
727 ));
728 }
729 let head_p = TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::P));
730 builder = builder.tag(Tag::with(&head_p, [merge.destination_pubkey.to_hex()]));
731 let head_e = TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::E));
732 let source_relay_str = merge
733 .source_revision_relay_hint
734 .as_ref()
735 .map_or_else(String::new, |r| r.as_str().to_owned());
736 builder = builder.tag(Tag::with(
737 &head_e,
738 [
739 merge.source_revision.to_hex(),
740 source_relay_str,
741 SOURCE_MARKER.to_owned(),
742 ],
743 ));
744 for tag in &merge.extra_tags {
745 builder = builder.tag(tag.clone());
746 }
747 builder
748 }
749}
750
751fn push_relation_tags(rel: &RelationRef, mut builder: EventBuilder) -> EventBuilder {
752 if let Some(coord) = &rel.coordinate {
753 let head = TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::A));
754 let relay_str = rel
755 .coordinate_relay_hint
756 .as_ref()
757 .map_or_else(String::new, |r| r.as_str().to_owned());
758 builder = builder.tag(Tag::with(
759 &head,
760 [coord.to_wire(), relay_str, rel.relation.as_str().to_owned()],
761 ));
762 }
763 if let Some(id) = rel.event_id {
764 let head = TagKind::single_letter(SingleLetterTag::lowercase(Alphabet::E));
765 let relay_str = rel
766 .event_relay_hint
767 .as_ref()
768 .map_or_else(String::new, |r| r.as_str().to_owned());
769 builder = builder.tag(Tag::with(
770 &head,
771 [id.to_hex(), relay_str, rel.relation.as_str().to_owned()],
772 ));
773 }
774 builder
775}
776
777#[cfg(test)]
778mod tests {
779 use super::*;
780 use crate::Keys;
781
782 fn keys() -> Keys {
783 Keys::parse("0000000000000000000000000000000000000000000000000000000000000003").unwrap()
784 }
785
786 #[test]
787 fn d_tag_normalisation_examples() {
788 assert_eq!(normalize_d_tag("Wiki Article"), "wiki-article");
789 assert_eq!(normalize_d_tag("What's Up?"), "whats-up");
790 assert_eq!(normalize_d_tag(" Hello World "), "hello-world");
791 assert_eq!(normalize_d_tag("Article 1"), "article-1");
792 assert_eq!(normalize_d_tag("ウィキペディア"), "ウィキペディア");
793 assert_eq!(normalize_d_tag("日本語 Article"), "日本語-article");
794 }
795
796 #[test]
797 fn is_canonical_d_tag_basic_cases() {
798 assert!(is_canonical_d_tag("wiki-article"));
799 assert!(is_canonical_d_tag("article-1"));
800 assert!(!is_canonical_d_tag("Wiki Article"));
801 assert!(!is_canonical_d_tag("-leading"));
802 assert!(!is_canonical_d_tag("trailing-"));
803 assert!(!is_canonical_d_tag("double--dash"));
804 assert!(!is_canonical_d_tag("with.punct"));
805 }
806
807 #[test]
808 fn wiki_article_round_trip() {
809 let article = WikiArticle::new("Wiki Article")
810 .content("Wiki body")
811 .title("Wiki Article")
812 .summary("Short");
813 let event = EventBuilder::wiki_article(&article)
814 .sign_with_keys(&keys())
815 .unwrap();
816 let parsed = WikiArticle::from_event(&event).unwrap();
817 assert_eq!(parsed, article);
818 assert_eq!(parsed.identifier, "wiki-article");
819 }
820
821 #[test]
822 fn wiki_redirect_round_trip() {
823 let target = Coordinate::new(
824 KIND_WIKI_ARTICLE,
825 *keys().public_key(),
826 "bitcoin".to_owned(),
827 );
828 let redirect = WikiRedirect::new("BTC", target)
829 .target_relay_hint(RelayUrl::parse("wss://relay.example/").unwrap());
830 let event = EventBuilder::wiki_redirect(&redirect)
831 .sign_with_keys(&keys())
832 .unwrap();
833 let parsed = WikiRedirect::from_event(&event).unwrap();
834 assert_eq!(parsed, redirect);
835 assert_eq!(parsed.identifier, "btc");
836 }
837
838 #[test]
839 fn merge_request_round_trip() {
840 let destination = Coordinate::new(
841 KIND_WIKI_ARTICLE,
842 *keys().public_key(),
843 "bitcoin".to_owned(),
844 );
845 let base = EventId::from_byte_array([0xaa; 32]);
846 let src = EventId::from_byte_array([0xbb; 32]);
847 let merge = MergeRequest::new(destination, *keys().public_key(), src)
848 .base_revision(base)
849 .content("Added section about block size");
850 let event = EventBuilder::wiki_merge_request(&merge)
851 .sign_with_keys(&keys())
852 .unwrap();
853 let parsed = MergeRequest::from_event(&event).unwrap();
854 assert_eq!(parsed, merge);
855 }
856
857 #[test]
858 fn wiki_article_wrong_kind_is_rejected() {
859 let event = EventBuilder::text_note("nope")
860 .sign_with_keys(&keys())
861 .unwrap();
862 assert!(matches!(
863 WikiArticle::from_event(&event),
864 Err(WikiError::WrongKind(_))
865 ));
866 }
867
868 #[test]
869 fn wiki_redirect_missing_target_is_rejected() {
870 let event = EventBuilder::new(KIND_WIKI_REDIRECT, "")
871 .tag(Tag::d("btc"))
872 .sign_with_keys(&keys())
873 .unwrap();
874 assert!(matches!(
875 WikiRedirect::from_event(&event),
876 Err(WikiError::MissingTarget)
877 ));
878 }
879
880 #[test]
881 fn merge_request_missing_source_is_rejected() {
882 let destination = Coordinate::new(
883 KIND_WIKI_ARTICLE,
884 *keys().public_key(),
885 "bitcoin".to_owned(),
886 );
887 let event = EventBuilder::new(KIND_WIKI_MERGE_REQUEST, "")
888 .tag(Tag::a(&destination))
889 .tag(Tag::p(*keys().public_key()))
890 .sign_with_keys(&keys())
891 .unwrap();
892 assert!(matches!(
893 MergeRequest::from_event(&event),
894 Err(WikiError::MissingMergeSource)
895 ));
896 }
897}