1mod structure;
2mod utils;
3
4use std::io::Cursor;
5use std::path::PathBuf;
6
7use chrono::{DateTime, Utc};
8use chrono_tz::Asia::Shanghai;
9use chrono_tz::Tz;
10use image::{DynamicImage, ImageReader};
11use tokio::sync::OnceCell;
12use url::Url;
13
14use self::structure::*;
15use crate::{
16 Category, ChapterInfo, Client, Comment, CommentType, ContentInfo, ContentInfos, Error,
17 FindImageResult, FindTextResult, HTTPClient, LongComment, NovelDB, NovelInfo, Options,
18 ShortComment, Tag, UserInfo, VolumeInfo, VolumeInfos, WordCountRange,
19};
20
21#[must_use]
23pub struct SfacgClient {
24 proxy: Option<Url>,
25 no_proxy: bool,
26 cert_path: Option<PathBuf>,
27
28 client: OnceCell<HTTPClient>,
29 client_rss: OnceCell<HTTPClient>,
30
31 db: OnceCell<NovelDB>,
32}
33
34impl Client for SfacgClient {
35 fn proxy(&mut self, proxy: Url) {
36 self.proxy = Some(proxy);
37 }
38
39 fn no_proxy(&mut self) {
40 self.no_proxy = true;
41 }
42
43 fn cert(&mut self, cert_path: PathBuf) {
44 self.cert_path = Some(cert_path);
45 }
46
47 async fn shutdown(&self) -> Result<(), Error> {
48 self.client().await?.save_cookies()
49 }
50
51 async fn add_cookie(&self, cookie_str: &str, url: &Url) -> Result<(), Error> {
52 self.client().await?.add_cookie(cookie_str, url)
53 }
54
55 async fn log_in(&self, username: String, password: Option<String>) -> Result<(), Error> {
56 assert!(!username.is_empty());
57 assert!(password.is_some());
58
59 let password = password.unwrap();
60
61 let response: GenericResponse = self
62 .post("/sessions", LogInRequest { username, password })
63 .await?;
64 response.status.check()?;
65
66 Ok(())
67 }
68
69 async fn logged_in(&self) -> Result<bool, Error> {
70 let response: GenericResponse = self.get("/user").await?;
71
72 if response.status.unauthorized() {
73 Ok(false)
74 } else {
75 response.status.check()?;
76 Ok(true)
77 }
78 }
79
80 async fn user_info(&self) -> Result<UserInfo, Error> {
81 let response: UserInfoResponse = self.get("/user").await?;
82 response.status.check()?;
83 let data = response.data.unwrap();
84
85 Ok(UserInfo {
86 nickname: data.nick_name.trim().to_string(),
87 avatar: Some(data.avatar),
88 })
89 }
90
91 async fn money(&self) -> Result<u32, Error> {
92 let response: MoneyResponse = self.get("/user/money").await?;
93 response.status.check()?;
94 let data = response.data.unwrap();
95
96 Ok(data.fire_money_remain + data.coupons_remain)
97 }
98
99 async fn sign_in(&self) -> Result<(), Error> {
100 let now: DateTime<Tz> = Utc::now().with_timezone(&Shanghai);
101
102 let response: GenericResponse = self
103 .put(
104 "/user/newSignInfo",
105 SignRequest {
106 sign_date: now.format("%Y-%m-%d").to_string(),
107 },
108 )
109 .await?;
110 if response.status.already_signed_in() {
111 tracing::info!("{}", response.status.msg.unwrap().trim())
112 } else {
113 response.status.check()?;
114 }
115
116 Ok(())
117 }
118
119 async fn bookshelf_infos(&self) -> Result<Vec<u32>, Error> {
120 let response: BookshelfInfoResponse = self
121 .get_query("/user/Pockets", BookshelfInfoRequest { expand: "novels" })
122 .await?;
123 response.status.check()?;
124 let data = response.data.unwrap();
125
126 let mut result = Vec::with_capacity(32);
127 for info in data {
128 if info.expand.is_some() {
129 let novels = info.expand.unwrap().novels;
130
131 if let Some(novels) = novels {
132 for novel_info in novels {
133 result.push(novel_info.novel_id);
134 }
135 }
136 }
137 }
138
139 Ok(result)
140 }
141
142 async fn novel_info(&self, id: u32) -> Result<Option<NovelInfo>, Error> {
143 assert!(id > 0 && id <= i32::MAX as u32);
144
145 let response: NovelInfoResponse = self
146 .get_query(
147 format!("/novels/{id}"),
148 NovelInfoRequest {
149 expand: "intro,typeName,sysTags",
150 },
151 )
152 .await?;
153 if response.status.not_found() {
154 return Ok(None);
155 }
156 response.status.check()?;
157 let data = response.data.unwrap();
158
159 let category = Category {
160 id: Some(data.type_id),
161 parent_id: None,
162 name: data.expand.type_name.trim().to_string(),
163 };
164
165 let novel_info = NovelInfo {
166 id,
167 name: data.novel_name.trim().to_string(),
168 author_name: data.author_name.trim().to_string(),
169 cover_url: Some(data.novel_cover),
170 introduction: super::parse_multi_line(data.expand.intro),
171 word_count: SfacgClient::parse_word_count(data.char_count),
172 is_vip: Some(data.sign_status == "VIP"),
173 is_finished: Some(data.is_finish),
174 create_time: Some(data.add_time),
175 update_time: Some(data.last_update_time),
176 category: Some(category),
177 tags: self.parse_tags(data.expand.sys_tags).await?,
178 };
179
180 Ok(Some(novel_info))
181 }
182
183 async fn comments(
184 &self,
185 id: u32,
186 comment_type: CommentType,
187 need_replies: bool,
188 page: u16,
189 size: u16,
190 ) -> Result<Option<Vec<Comment>>, Error> {
191 assert!(id <= i32::MAX as u32);
192
193 match comment_type {
194 CommentType::Short => self.do_short_comments(id, need_replies, page, size).await,
195 CommentType::Long => self.do_long_comments(id, need_replies, page, size).await,
196 }
197 }
198
199 async fn volume_infos(&self, id: u32) -> Result<Option<VolumeInfos>, Error> {
200 assert!(id <= i32::MAX as u32);
201
202 let response: VolumeInfosResponse = self.get(format!("/novels/{id}/dirs")).await?;
203
204 if response.status.not_available() {
205 return Ok(None);
206 }
207
208 response.status.check()?;
209 let data = response.data.unwrap();
210
211 let mut volumes = VolumeInfos::with_capacity(8);
212 for volume in data.volume_list {
213 let mut volume_info = VolumeInfo {
214 id: volume.volume_id,
215 title: volume.title.trim().to_string(),
216 chapter_infos: Vec::with_capacity(volume.chapter_list.len()),
217 };
218
219 for chapter in volume.chapter_list {
220 let chapter_info = ChapterInfo {
221 novel_id: Some(chapter.novel_id),
222 id: chapter.chap_id,
223 title: chapter.title.trim().to_string(),
224 word_count: Some(chapter.char_count),
225 create_time: Some(chapter.add_time),
226 update_time: chapter.update_time,
227 is_vip: Some(chapter.is_vip),
228 price: Some(chapter.need_fire_money),
229 payment_required: Some(chapter.need_fire_money != 0),
230 is_valid: None,
231 };
232
233 volume_info.chapter_infos.push(chapter_info);
234 }
235
236 volumes.push(volume_info);
237 }
238
239 Ok(Some(volumes))
240 }
241
242 async fn content_infos(&self, info: &ChapterInfo) -> Result<ContentInfos, Error> {
243 let content;
244
245 match self.db().await?.find_text(info).await? {
246 FindTextResult::Ok(str) => {
247 content = str;
248 }
249 other => {
250 let response: ContentInfosResponse = self
251 .get_query(
252 format!("/Chaps/{}", info.id),
253 ContentInfosRequest {
254 expand: "content,isContentEncrypted",
255 },
256 )
257 .await?;
258 response.status.check()?;
259 let data = response.data.unwrap();
260
261 if data.expand.is_content_encrypted {
262 content = SfacgClient::convert(data.expand.content);
263 } else {
264 content = data.expand.content;
265 }
266
267 if content.trim().is_empty() {
268 return Err(Error::NovelApi(String::from("Content is empty")));
269 }
270
271 match other {
272 FindTextResult::None => self.db().await?.insert_text(info, &content).await?,
273 FindTextResult::Outdate => self.db().await?.update_text(info, &content).await?,
274 FindTextResult::Ok(_) => (),
275 }
276 }
277 }
278
279 let mut content_infos = ContentInfos::with_capacity(128);
280 for line in content
281 .lines()
282 .map(|line| line.trim())
283 .filter(|line| !line.is_empty())
284 {
285 if line.starts_with("[img") {
286 match SfacgClient::parse_image_url(line) {
287 Ok(url) => content_infos.push(ContentInfo::Image(url)),
288 Err(err) => tracing::error!("{err}"),
289 }
290 } else {
291 content_infos.push(ContentInfo::Text(line.to_string()));
292 }
293 }
294
295 Ok(content_infos)
296 }
297
298 async fn content_infos_multiple(
299 &self,
300 infos: &[ChapterInfo],
301 ) -> Result<Vec<ContentInfos>, Error> {
302 let mut result = Vec::new();
303
304 for info in infos {
305 result.push(self.content_infos(info).await?);
306 }
307
308 Ok(result)
309 }
310
311 async fn order_chapter(&self, info: &ChapterInfo) -> Result<(), Error> {
312 let response: GenericResponse = self
313 .post(
314 &format!("/novels/{}/orderedchaps", info.novel_id.unwrap()),
315 OrderRequest {
316 order_all: false,
317 auto_order: false,
318 chap_ids: vec![info.id],
319 order_type: "readOrder",
320 },
321 )
322 .await?;
323 if response.status.already_ordered() {
324 tracing::info!("{}", response.status.msg.unwrap().trim())
325 } else {
326 response.status.check()?;
327 }
328
329 Ok(())
330 }
331
332 async fn order_novel(&self, id: u32, _: &VolumeInfos) -> Result<(), Error> {
333 assert!(id > 0 && id <= i32::MAX as u32);
334
335 let response: GenericResponse = self
336 .post(
337 &format!("/novels/{id}/orderedchaps",),
338 OrderRequest {
339 order_all: true,
340 auto_order: false,
341 chap_ids: vec![],
342 order_type: "readOrder",
343 },
344 )
345 .await?;
346 if response.status.already_ordered() {
347 tracing::info!("{}", response.status.msg.unwrap().trim())
348 } else {
349 response.status.check()?;
350 }
351
352 Ok(())
353 }
354
355 async fn image(&self, url: &Url) -> Result<DynamicImage, Error> {
356 match self.db().await?.find_image(url).await? {
357 FindImageResult::Ok(image) => Ok(image),
358 FindImageResult::None => {
359 let response = self.get_rss(url).await?;
360 let bytes = response.bytes().await?;
361
362 let image = ImageReader::new(Cursor::new(&bytes))
363 .with_guessed_format()?
364 .decode()?;
365
366 self.db().await?.insert_image(url, bytes).await?;
367
368 Ok(image)
369 }
370 }
371 }
372
373 async fn categories(&self) -> Result<&Vec<Category>, Error> {
374 static CATEGORIES: OnceCell<Vec<Category>> = OnceCell::const_new();
375
376 CATEGORIES
377 .get_or_try_init(|| async {
378 let response: CategoryResponse = self.get("/noveltypes").await?;
379 response.status.check()?;
380 let data = response.data.unwrap();
381
382 let mut result = Vec::with_capacity(8);
383 for tag_data in data {
384 result.push(Category {
385 id: Some(tag_data.type_id),
386 parent_id: None,
387 name: tag_data.type_name.trim().to_string(),
388 });
389 }
390
391 result.sort_unstable_by_key(|x| x.id.unwrap());
392
393 Ok(result)
394 })
395 .await
396 }
397
398 async fn tags(&self) -> Result<&Vec<Tag>, Error> {
399 static TAGS: OnceCell<Vec<Tag>> = OnceCell::const_new();
400
401 TAGS.get_or_try_init(|| async {
402 let response: TagResponse = self.get("/novels/0/sysTags").await?;
403 response.status.check()?;
404 let data = response.data.unwrap();
405
406 let mut result = Vec::with_capacity(64);
407 for tag_data in data {
408 result.push(Tag {
409 id: Some(tag_data.sys_tag_id),
410 name: tag_data.tag_name.trim().to_string(),
411 });
412 }
413
414 result.push(Tag {
416 id: Some(74),
417 name: "百合".to_string(),
418 });
419
420 result.sort_unstable_by_key(|x| x.id.unwrap());
421
422 Ok(result)
423 })
424 .await
425 }
426
427 async fn search_infos(
428 &self,
429 option: &Options,
430 page: u16,
431 size: u16,
432 ) -> Result<Option<Vec<u32>>, Error> {
433 assert!(size <= 50, "The maximum number of items per page is 50");
434
435 if option.keyword.is_some() {
436 self.do_search_with_keyword(option, page, size).await
437 } else {
438 self.do_search_without_keyword(option, page, size).await
439 }
440 }
441
442 fn has_this_type_of_comments(comment_type: CommentType) -> bool {
443 match comment_type {
444 CommentType::Short => true,
445 CommentType::Long => true,
446 }
447 }
448}
449
450impl SfacgClient {
451 async fn do_short_comments(
452 &self,
453 id: u32,
454 need_replies: bool,
455 page: u16,
456 size: u16,
457 ) -> Result<Option<Vec<Comment>>, Error> {
458 assert!(size <= 50);
459
460 let response: CommentResponse = self
461 .get_query(
462 format!("/novels/{id}/Cmts"),
463 ShortCommentRequest {
464 page,
465 size,
466 r#type: "clear",
467 sort: "smart",
468 },
469 )
470 .await?;
471 response.status.check()?;
472 let data = response.data.unwrap();
473
474 if data.is_empty() {
475 return Ok(None);
476 }
477
478 let mut result = Vec::with_capacity(data.len());
479
480 for comment in data {
481 let Some(content) = super::parse_multi_line(comment.content) else {
482 continue;
483 };
484
485 result.push(Comment::Short(ShortComment {
486 id: comment.comment_id,
487 user: UserInfo {
488 nickname: comment.display_name.trim().to_string(),
489 avatar: Some(comment.avatar),
490 },
491 content,
492 create_time: Some(comment.create_time),
493 like_count: Some(comment.fav_count),
494 replies: if need_replies && comment.reply_num > 0 {
495 self.comment_replies(comment.comment_id, CommentType::Short, comment.reply_num)
496 .await?
497 } else {
498 None
499 },
500 }));
501 }
502
503 Ok(Some(result))
504 }
505
506 async fn do_long_comments(
507 &self,
508 id: u32,
509 need_replies: bool,
510 page: u16,
511 size: u16,
512 ) -> Result<Option<Vec<Comment>>, Error> {
513 assert!(size <= 20);
514
515 let response: CommentResponse = self
516 .get_query(
517 format!("/novels/{id}/lcmts"),
518 LongCommentRequest {
519 page,
520 size,
521 charlen: 140,
522 sort: "addtime",
523 },
524 )
525 .await?;
526 response.status.check()?;
527 let data = response.data.unwrap();
528
529 if data.is_empty() {
530 return Ok(None);
531 }
532
533 let mut result = Vec::with_capacity(data.len());
534
535 for comment in data {
536 result.push(Comment::Long(LongComment {
537 id: comment.comment_id,
538 user: UserInfo {
539 nickname: comment.display_name.trim().to_string(),
540 avatar: Some(comment.avatar),
541 },
542 title: comment.title.unwrap().trim().to_string(),
543 content: self.long_comment_content(comment.comment_id).await?,
544 create_time: Some(comment.create_time),
545 like_count: Some(comment.fav_count),
546 replies: if need_replies && comment.reply_num > 0 {
547 self.comment_replies(comment.comment_id, CommentType::Long, comment.reply_num)
548 .await?
549 } else {
550 None
551 },
552 }));
553 }
554
555 Ok(Some(result))
556 }
557
558 async fn long_comment_content(&self, comment_id: u32) -> Result<Vec<String>, Error> {
559 let response: LongCommentContentResponse = self.get(format!("/lcmts/{comment_id}")).await?;
560 response.status.check()?;
561 let data = response.data.unwrap();
562
563 Ok(super::parse_multi_line(data.content).unwrap())
564 }
565
566 async fn comment_replies(
567 &self,
568 comment_id: u32,
569 comment_type: CommentType,
570 total: u16,
571 ) -> Result<Option<Vec<ShortComment>>, Error> {
572 let url = match comment_type {
573 CommentType::Short => format!("/cmts/{comment_id}/replys"),
574 CommentType::Long => format!("/lcmts/{comment_id}/replys"),
575 };
576
577 let mut page = 0;
578 let size = 50;
579 let total_page = if total.is_multiple_of(size) {
580 total / size
581 } else {
582 total / size + 1
583 };
584 let mut reply_list = Vec::with_capacity(total as usize);
585
586 while page < total_page {
587 let response: ReplyResponse = self.get_query(&url, ReplyRequest { page, size }).await?;
588 response.status.check()?;
589 let data = response.data.unwrap();
590
591 for reply in data {
592 let Some(content) = super::parse_multi_line(reply.content) else {
593 continue;
594 };
595
596 reply_list.push(ShortComment {
597 id: reply.reply_id,
598 user: UserInfo {
599 nickname: reply.display_name.trim().to_string(),
600 avatar: Some(reply.avatar),
601 },
602 content,
603 create_time: Some(reply.create_time),
604 like_count: None,
605 replies: None,
606 });
607 }
608
609 page += 1;
610 }
611
612 if reply_list.is_empty() {
613 Ok(None)
614 } else {
615 reply_list.sort_unstable_by_key(|x| x.create_time.unwrap());
616 reply_list.dedup();
617 Ok(Some(reply_list))
618 }
619 }
620
621 async fn do_search_with_keyword(
622 &self,
623 option: &Options,
624 page: u16,
625 size: u16,
626 ) -> Result<Option<Vec<u32>>, Error> {
627 let is_finish = if option.is_finished.is_none() {
631 -1
632 } else if *option.is_finished.as_ref().unwrap() {
633 1
634 } else {
635 0
636 };
637
638 let update_days = if option.update_days.is_none() {
640 -1
641 } else {
642 option.update_days.unwrap() as i8
643 };
644
645 let response: SearchResponse = self
646 .get_query(
647 "/search/novels/result/new",
648 SearchRequest {
649 q: option.keyword.as_ref().unwrap().to_string(),
650 is_finish,
651 update_days,
652 systagids: SfacgClient::tag_ids(&option.tags),
653 page,
654 size,
655 sort: "hot",
661 expand: "sysTags",
662 },
663 )
664 .await?;
665 response.status.check()?;
666 let data = response.data.unwrap();
667
668 if data.novels.is_empty() {
669 return Ok(None);
670 }
671
672 let mut result = Vec::new();
673 let sys_tags = self.tags().await?;
674
675 for novel_info in data.novels {
676 let mut tag_ids = vec![];
677
678 for tag in novel_info.expand.sys_tags {
679 if let Some(sys_tag) = sys_tags.iter().find(|x| x.id.unwrap() == tag.sys_tag_id) {
680 tag_ids.push(sys_tag.id.unwrap());
681 }
682 }
683
684 if SfacgClient::match_category(option, novel_info.type_id)
685 && SfacgClient::match_excluded_tags(option, tag_ids)
686 && SfacgClient::match_vip(option, &novel_info.sign_status)
687 && SfacgClient::match_word_count(option, novel_info.char_count)
688 {
689 result.push(novel_info.novel_id);
690 }
691 }
692
693 Ok(Some(result))
694 }
695
696 async fn do_search_without_keyword(
697 &self,
698 option: &Options,
699 page: u16,
700 size: u16,
701 ) -> Result<Option<Vec<u32>>, Error> {
702 let mut category_id = 0;
703 if option.category.is_some() {
704 category_id = option.category.as_ref().unwrap().id.unwrap();
705 }
706
707 let updatedays = if option.update_days.is_none() {
709 -1
710 } else {
711 option.update_days.unwrap() as i8
712 };
713
714 let isfinish = SfacgClient::bool_to_str(&option.is_finished);
715 let isfree = SfacgClient::bool_to_str(&option.is_vip.as_ref().map(|x| !x));
716
717 let systagids = SfacgClient::tag_ids(&option.tags);
718 let notexcludesystagids = SfacgClient::tag_ids(&option.excluded_tags);
719
720 let mut charcountbegin = 0;
721 let mut charcountend = 0;
722
723 if option.word_count.is_some() {
724 match option.word_count.as_ref().unwrap() {
725 WordCountRange::Range(range) => {
726 charcountbegin = range.start;
727 charcountend = range.end;
728 }
729 WordCountRange::RangeFrom(range_from) => charcountbegin = range_from.start,
730 WordCountRange::RangeTo(range_to) => charcountend = range_to.end,
731 }
732 }
733
734 let response: NovelsResponse = self
735 .get_query(
736 format!("/novels/{category_id}/sysTags/novels"),
737 NovelsRequest {
738 charcountbegin,
739 charcountend,
740 isfinish,
741 isfree,
742 systagids,
743 notexcludesystagids,
744 updatedays,
745 page,
746 size,
747 sort: "viewtimes",
753 },
754 )
755 .await?;
756 response.status.check()?;
757 let data = response.data.unwrap();
758
759 if data.is_empty() {
760 return Ok(None);
761 }
762
763 let mut result = Vec::new();
764 for novel_data in data {
765 result.push(novel_data.novel_id);
766 }
767
768 Ok(Some(result))
769 }
770
771 fn parse_word_count(word_count: i32) -> Option<u32> {
772 if word_count <= 0 {
774 None
775 } else {
776 Some(word_count as u32)
777 }
778 }
779
780 async fn parse_tags(&self, tag_list: Vec<NovelInfoSysTag>) -> Result<Option<Vec<Tag>>, Error> {
781 let sys_tags = self.tags().await?;
782
783 let mut result = Vec::new();
784 for tag in tag_list {
785 let id = tag.sys_tag_id;
786 let name = tag.tag_name.trim().to_string();
787
788 if sys_tags.iter().any(|sys_tag| sys_tag.id.unwrap() == id) {
790 result.push(Tag { id: Some(id), name });
791 } else {
792 tracing::info!("This tag is not a system tag and is ignored: {name}");
793 }
794 }
795
796 if result.is_empty() {
797 Ok(None)
798 } else {
799 result.sort_unstable_by_key(|x| x.id.unwrap());
800 Ok(Some(result))
801 }
802 }
803
804 fn parse_image_url(line: &str) -> Result<Url, Error> {
805 let begin = line.find("http");
806 let end = line.find("[/img]");
807
808 if begin.is_none() || end.is_none() {
809 return Err(Error::NovelApi(format!(
810 "Image URL format is incorrect: {line}"
811 )));
812 }
813
814 let begin = begin.unwrap();
815 let end = end.unwrap();
816
817 let url = line
818 .chars()
819 .skip(begin)
820 .take(end - begin)
821 .collect::<String>()
822 .trim()
823 .to_string();
824
825 match Url::parse(&url) {
826 Ok(url) => Ok(url),
827 Err(error) => Err(Error::NovelApi(format!(
828 "Image URL parse failed: {error}, content: {line}"
829 ))),
830 }
831 }
832
833 fn bool_to_str(flag: &Option<bool>) -> &'static str {
834 if flag.is_some() {
835 if *flag.as_ref().unwrap() { "is" } else { "not" }
836 } else {
837 "both"
838 }
839 }
840
841 fn tag_ids(tags: &Option<Vec<Tag>>) -> Option<String> {
842 tags.as_ref().map(|tags| {
843 tags.iter()
844 .map(|tag| tag.id.unwrap().to_string())
845 .collect::<Vec<String>>()
846 .join(",")
847 })
848 }
849
850 fn match_vip(option: &Options, sign_status: &str) -> bool {
851 if option.is_vip.is_none() {
852 return true;
853 }
854
855 if *option.is_vip.as_ref().unwrap() {
856 sign_status == "VIP"
857 } else {
858 sign_status != "VIP"
859 }
860 }
861
862 fn match_excluded_tags(option: &Options, tag_ids: Vec<u16>) -> bool {
863 if option.excluded_tags.is_none() {
864 return true;
865 }
866
867 tag_ids.iter().all(|id| {
868 !option
869 .excluded_tags
870 .as_ref()
871 .unwrap()
872 .iter()
873 .any(|tag| tag.id.unwrap() == *id)
874 })
875 }
876
877 fn match_category(option: &Options, category_id: u16) -> bool {
878 if option.category.is_none() {
879 return true;
880 }
881
882 let category = option.category.as_ref().unwrap();
883 category.id.unwrap() == category_id
884 }
885
886 fn match_word_count(option: &Options, word_count: i32) -> bool {
887 if option.word_count.is_none() {
888 return true;
889 }
890
891 if word_count <= 0 {
892 return true;
893 }
894
895 let word_count = word_count as u32;
896 match option.word_count.as_ref().unwrap() {
897 WordCountRange::Range(range) => {
898 if word_count >= range.start && word_count < range.end {
899 return true;
900 }
901 }
902 WordCountRange::RangeFrom(range_from) => {
903 if word_count >= range_from.start {
904 return true;
905 }
906 }
907 WordCountRange::RangeTo(rang_to) => {
908 if word_count < rang_to.end {
909 return true;
910 }
911 }
912 }
913
914 false
915 }
916}