Skip to main content

parse_book_source/source/
op.rs

1//! 操作规则:搜索 / 浏览 / 书详情 / 目录 / 正文 的字段抽取与分页配置,以及黄金样例。
2
3use super::http::{PreStep, Request};
4use super::rule::Rule;
5use serde::{Deserialize, Serialize};
6use std::collections::BTreeMap;
7
8/// 一本书的字段抽取规则(均可省略)。
9#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "camelCase", deny_unknown_fields)]
11#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
12pub struct BookRules {
13    /// 列表项:指向书详情页的链接(搜索/浏览结果用;bookInfo 阶段忽略)。
14    #[serde(default, skip_serializing_if = "Option::is_none")]
15    pub book_url: Option<Rule>,
16    #[serde(default, skip_serializing_if = "Option::is_none")]
17    pub name: Option<Rule>,
18    #[serde(default, skip_serializing_if = "Option::is_none")]
19    pub author: Option<Rule>,
20    #[serde(default, skip_serializing_if = "Option::is_none")]
21    pub cover: Option<Rule>,
22    #[serde(default, skip_serializing_if = "Option::is_none")]
23    pub intro: Option<Rule>,
24    #[serde(default, skip_serializing_if = "Option::is_none")]
25    pub kind: Option<Rule>,
26    #[serde(default, skip_serializing_if = "Option::is_none")]
27    pub last_chapter: Option<Rule>,
28    #[serde(default, skip_serializing_if = "Option::is_none")]
29    pub toc_url: Option<Rule>,
30    #[serde(default, skip_serializing_if = "Option::is_none")]
31    pub word_count: Option<Rule>,
32}
33
34/// 书详情操作:详情字段抽取(同 [`BookRules`])+ 可选前置请求链(见 design D7-bis)。
35/// 字段与 `BookRules` 同名同序,故现有 `bookInfo:{...}` JSON 逐字节解析等价;引擎经
36/// [`BookInfoOp::as_book_rules`] 复用既有 `eval_book_info`(不用 `flatten` 以保 `deny_unknown_fields`)。
37#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
38#[serde(rename_all = "camelCase", deny_unknown_fields)]
39#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
40pub struct BookInfoOp {
41    #[serde(default, skip_serializing_if = "Option::is_none")]
42    pub book_url: Option<Rule>,
43    #[serde(default, skip_serializing_if = "Option::is_none")]
44    pub name: Option<Rule>,
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub author: Option<Rule>,
47    #[serde(default, skip_serializing_if = "Option::is_none")]
48    pub cover: Option<Rule>,
49    #[serde(default, skip_serializing_if = "Option::is_none")]
50    pub intro: Option<Rule>,
51    #[serde(default, skip_serializing_if = "Option::is_none")]
52    pub kind: Option<Rule>,
53    #[serde(default, skip_serializing_if = "Option::is_none")]
54    pub last_chapter: Option<Rule>,
55    #[serde(default, skip_serializing_if = "Option::is_none")]
56    pub toc_url: Option<Rule>,
57    #[serde(default, skip_serializing_if = "Option::is_none")]
58    pub word_count: Option<Rule>,
59    /// 详情主请求前的前置请求链;空 = 现状(直接 fetch book_url)。
60    #[serde(default, skip_serializing_if = "Vec::is_empty")]
61    pub prelude: Vec<PreStep>,
62}
63
64impl BookInfoOp {
65    /// 取详情字段抽取规则视图(供引擎复用 `eval_book_info(&BookRules)`,不暴露 `prelude`)。
66    pub fn as_book_rules(&self) -> BookRules {
67        BookRules {
68            book_url: self.book_url.clone(),
69            name: self.name.clone(),
70            author: self.author.clone(),
71            cover: self.cover.clone(),
72            intro: self.intro.clone(),
73            kind: self.kind.clone(),
74            last_chapter: self.last_chapter.clone(),
75            toc_url: self.toc_url.clone(),
76            word_count: self.word_count.clone(),
77        }
78    }
79}
80
81/// 共享列表页规格:search 与 explore 取一页书共用。承载前置链、请求(render / interceptApi /
82/// readyFor / totalPages / hasMore / pageBy / vars 均在 [`Request`] 上)、列表选择与每项字段抽取。
83/// 引擎以 `run_list_page(spec, vars, page, page_size)` 统一执行。
84///
85/// 这正是历史 `SearchOp` 的形状——前序 change 已把全部取页旋钮收敛到 [`Request`],故 search/explore
86/// 共用同一规格,无需各维护一套列表求值分支。
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
88#[serde(rename_all = "camelCase", deny_unknown_fields)]
89#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
90pub struct ListPageSpec {
91    /// 主请求之前按序执行的前置请求链(见 design D7-bis);空 = 单发(现状)。
92    #[serde(default, skip_serializing_if = "Vec::is_empty")]
93    pub prelude: Vec<PreStep>,
94    pub request: Request,
95    pub list: Rule,
96    pub item: BookRules,
97}
98
99/// 搜索操作 == 共享列表页规格(裸用,不包 `page` 层;search JSON 形状不变)。
100pub type SearchOp = ListPageSpec;
101
102/// 一个静态入口:固定标题 + 取页变量(字面量)。供「全部·最热」这类固定入口声明。
103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
104#[serde(rename_all = "camelCase", deny_unknown_fields)]
105#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
106pub struct StaticEntry {
107    pub title: String,
108    /// 取页变量:与 base/page/pageSize 合并后运行 `explore.page`。值为字面量。
109    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
110    pub vars: BTreeMap<String, String>,
111}
112
113/// fetch 入口项的生成规则:标题 + 变量。两者对「当前数据项(ctx)+ 外层 forEach 变量(vars)」求值,
114/// 故 `title`/`vars` 规则既能 `via:json` 读数据项字段,也能 `{{name}}` 引用循环变量。
115#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
116#[serde(rename_all = "camelCase", deny_unknown_fields)]
117#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
118pub struct FetchEntryItem {
119    pub title: Rule,
120    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
121    pub vars: BTreeMap<String, Rule>,
122}
123
124/// 远端抓取入口源:请求远端分类数据 → `list` 抽数组 → 每项经 `item` 生成入口。
125/// 可选 `forEach` 按多组变量重复请求并合并入口(空 = 执行一次)。
126#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
127#[serde(rename_all = "camelCase", deny_unknown_fields)]
128#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
129pub struct FetchEntrySource {
130    /// 循环变量集:对每组变量各请求一次、合并结果;变量在请求模板与 `item` 规则中可见。空 = 一次。
131    #[serde(default, skip_serializing_if = "Vec::is_empty")]
132    pub for_each: Vec<BTreeMap<String, String>>,
133    /// 入口数据请求(复用 [`Request`]:支持 render / interceptApi / charset / headers / 模板)。
134    pub request: Request,
135    /// 从响应抽取数据项数组(每项作为 `item` 规则的上下文)。
136    pub list: Rule,
137    pub item: FetchEntryItem,
138}
139
140/// 入口源:静态固定入口 或 远端抓取入口。`explore.entries` 是其数组,按声明顺序合并
141/// (无独立 chain 类型——「按序合并」即数组遍历)。与 [`Rule`] 一致用 untagged + 唯一键判别
142/// (`static` / `fetch`)。
143#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
144#[serde(untagged)]
145#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
146pub enum EntrySource {
147    /// 静态入口列表。
148    Static {
149        #[serde(rename = "static")]
150        static_entries: Vec<StaticEntry>,
151    },
152    /// 远端抓取动态入口(`Box`:`FetchEntrySource` 含完整 `Request`,避免枚举变体尺寸悬殊;
153    /// `Box<T>` 对 serde/schemars 透明,JSON 与 schema 形状不变)。
154    Fetch { fetch: Box<FetchEntrySource> },
155}
156
157/// 浏览操作:两阶段——`entries` 生成可选择的入口,`page` 用选中入口的变量取书。
158#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
159#[serde(rename_all = "camelCase", deny_unknown_fields)]
160#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
161pub struct ExploreOp {
162    /// 入口源数组(按声明顺序合并;static + fetch 组合)。
163    pub entries: Vec<EntrySource>,
164    /// 共享列表页规格:用选中入口变量 + page/pageSize 取书。
165    pub page: ListPageSpec,
166}
167
168fn default_max_pages() -> u32 {
169    100
170}
171
172/// 目录规则(章节 + 分卷 + 可选分页)。
173#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
174#[serde(rename_all = "camelCase", deny_unknown_fields)]
175#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
176pub struct TocRules {
177    /// 目录主请求之前按序执行的前置请求链(见 design D7-bis);空 = 现状。
178    #[serde(default, skip_serializing_if = "Vec::is_empty")]
179    pub prelude: Vec<PreStep>,
180    pub list: Rule,
181    pub name: Rule,
182    pub url: Rule,
183    #[serde(default, skip_serializing_if = "Option::is_none")]
184    pub is_volume: Option<Rule>,
185    #[serde(default, skip_serializing_if = "Option::is_none")]
186    pub next_page: Option<Rule>,
187    #[serde(default = "default_max_pages")]
188    pub max_pages: u32,
189}
190
191/// 正文规则(可选分页)。
192#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
193#[serde(rename_all = "camelCase", deny_unknown_fields)]
194#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
195pub struct ContentRules {
196    /// 正文主请求之前按序执行的前置请求链(见 design D7-bis);空 = 现状。
197    #[serde(default, skip_serializing_if = "Vec::is_empty")]
198    pub prelude: Vec<PreStep>,
199    pub value: Rule,
200    #[serde(default, skip_serializing_if = "Option::is_none")]
201    pub next_page: Option<Rule>,
202    #[serde(default = "default_max_pages")]
203    pub max_pages: u32,
204}
205
206/// 样例期望不变量。
207#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
208#[serde(rename_all = "camelCase")]
209#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
210pub struct Expect {
211    #[serde(default, skip_serializing_if = "Option::is_none")]
212    pub name: Option<String>,
213    #[serde(default, skip_serializing_if = "Option::is_none")]
214    pub min_chapters: Option<usize>,
215    #[serde(default, skip_serializing_if = "Option::is_none")]
216    pub volumes: Option<usize>,
217    #[serde(default, skip_serializing_if = "Option::is_none")]
218    pub min_content_chars: Option<usize>,
219}
220
221/// 黄金样例。
222#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
223#[serde(rename_all = "camelCase", deny_unknown_fields)]
224#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
225pub struct Sample {
226    pub book_url: String,
227    #[serde(default)]
228    pub expect: Expect,
229}