this repo has no description
1
2pub mod blog {
3 use anyhow::Result;
4 use std::iter::once as iter_once;
5 use std::iter::Once as OnceIter;
6 use std::ops::Not as _;
7 use std::result::Result as StdResult;
8 use std::slice::Iter as SliceIter;
9 use string_interner::DefaultSymbol;
10 use tagbuddy::generate_label;
11 use tagbuddy::parse::*;
12 use tagbuddy::storage::Storage;
13 use tagbuddy::tag::KeyValueTag;
14 use tagbuddy::tag::PlainTag;
15 use tagbuddy::tag::Tagged;
16 use tagbuddy::TagManager;
17
18 generate_label! {
19 pub Tags {}
20 pub Ratings {}
21 }
22
23 type PostTagsManager = TagManager<Tags, DefaultSymbol, PlainTag<Tags>, Plain<Tags>>;
24 type PostRatingsManager =
25 TagManager<Ratings, DefaultSymbol, KeyValueTag<Ratings>, KeyValue<Ratings>>;
26
27 pub struct Blog {
28 posts: Vec<BlogPost>,
29 tag_manager: PostTagsManager,
30 rating_manager: PostRatingsManager,
31 }
32
33 impl Blog {
34 /// Initialize a new blog.
35 pub fn new() -> Self {
36 let tag_manager = TagManager::builder()
37 .parser(Plain::new())
38 .storage(Storage::<Tags>::fresh())
39 .build();
40
41 let rating_manager = TagManager::builder()
42 .parser(KeyValue::new(KvPolicy::NoAmbiguousSep))
43 .storage(tag_manager.storage().shallow_clone::<Ratings>())
44 .build();
45
46 Self {
47 posts: Vec::new(),
48 tag_manager,
49 rating_manager,
50 }
51 }
52
53 /// Add a new post to the blog.
54 pub fn add_post(
55 &mut self,
56 title: &str,
57 content: &str,
58 tags: &[&str],
59 rating: &str,
60 ) -> Result<&mut Self> {
61 let title = title.to_owned();
62 let content = content.to_owned();
63
64 let tags = self
65 .tag_manager
66 .parse_tags_into::<StdResult<_, _>>(tags.into_iter().map(|t| *t))?;
67
68 let rating = self.rating_manager.parse_tag(rating)?;
69
70 self.posts.push(BlogPost {
71 title,
72 content,
73 tags,
74 rating,
75 });
76
77 Ok(self)
78 }
79
80 /// Get the posts in the blog.
81 pub fn posts(&self) -> impl Iterator<Item = &BlogPost> {
82 self.posts.iter()
83 }
84 }
85
86 /// A single post on the blog.
87 pub struct BlogPost {
88 /// The title of the post.
89 #[allow(unused)]
90 title: String,
91
92 /// The content of the post.
93 #[allow(unused)]
94 content: String,
95
96 /// The tags associated with the post.
97 tags: Vec<PlainTag<Tags>>,
98
99 /// The rating assigned to the post.
100 rating: KeyValueTag<Ratings>,
101 }
102
103 impl BlogPost {
104 /// Get the tags applied to a blog post.
105 pub fn tags(&self, blog: &Blog) -> Vec<String> {
106 // SAFETY: We know we're using the correct storage, so the tag data should always be valid.
107 blog.tag_manager
108 .resolve_tags_into::<StdResult<_, _>>(Tagged::<PlainTag<Tags>>::get_tags(self))
109 .expect("tags should always resolve successfully")
110 }
111
112 /// Get the rating of a blog post.
113 pub fn rating(&self, blog: &Blog) -> String {
114 // SAFETY: We know we're using the correct storage, so the rating data should always be valid.
115 blog.rating_manager
116 .resolve_tags_into::<StdResult<_, _>>(Tagged::<KeyValueTag<Ratings>>::get_tags(
117 self,
118 ))
119 .expect("ratings should always resolve successfully")
120 }
121 }
122
123 // Mark a blog post as being tagged with tags.
124 impl Tagged<PlainTag<Tags>> for BlogPost {
125 type TagIter<'iter> = SliceIter<'iter, PlainTag<Tags>>;
126
127 fn has_tags(&self) -> bool {
128 self.tags.is_empty().not()
129 }
130
131 fn get_tags(&self) -> Self::TagIter<'_> {
132 self.tags.iter()
133 }
134 }
135
136 // Mark a blog post as being tagged with a rating.
137 impl Tagged<KeyValueTag<Ratings>> for BlogPost {
138 type TagIter<'iter> = OnceIter<&'iter KeyValueTag<Ratings>>;
139
140 fn has_tags(&self) -> bool {
141 true
142 }
143
144 fn get_tags(&self) -> Self::TagIter<'_> {
145 iter_once(&self.rating)
146 }
147 }
148}
149
150use crate::blog::Blog;
151use anyhow::Result;
152
153#[test]
154fn blog_can_handle_tags_and_rating() -> Result<()> {
155 let mut blog = Blog::new();
156
157 blog.add_post("one", "1", &["hello", "my", "friend"], "score:1")?
158 .add_post("two", "2", &["goodbye", "your", "enemy"], "score:2")?
159 .add_post(
160 "three",
161 "3",
162 &["see you soon", "our", "acquaintance"],
163 "score:3",
164 )?;
165
166 assert_eq!(
167 blog.posts()
168 .flat_map(|post| post.tags(&blog))
169 .collect::<Vec<_>>(),
170 vec![
171 "hello",
172 "my",
173 "friend",
174 "goodbye",
175 "your",
176 "enemy",
177 "see you soon",
178 "our",
179 "acquaintance",
180 ]
181 );
182
183 assert_eq!(
184 blog.posts()
185 .map(|post| post.rating(&blog))
186 .collect::<Vec<_>>(),
187 vec!["score:1", "score:2", "score:3"]
188 );
189
190 Ok(())
191}