1use std::path::Path;
6
7use serde_json::{Map as JMap, Value as JValue};
8use toml_edit::{DocumentMut, Item, Table};
9
10use crate::error::{Error, Issue, Result, code};
11use crate::util;
12
13pub const TOP_BARE_KEYS: &[&str] = &[
17 "str",
18 "spec",
19 "kind",
20 "id",
21 "name",
22 "type",
23 "title",
24 "summary",
25 "tags",
26 "revision",
27 "created_at",
28 "updated_at",
29 "schema",
30];
31
32pub const TABLE_ORDER: &[&str] = &["policies", "authors", "refs", "entries", "ext"];
34
35pub const POLICIES_KEYS: &[&str] = &[
37 "id_version",
38 "max_depth",
39 "manifest",
40 "sha256",
41 "large_asset_bytes",
42 "deep_tree_warn",
43];
44
45pub const AUTHOR_KEYS: &[&str] = &["id", "name", "role", "at"];
47
48pub const REF_KEYS: &[&str] = &["id", "target", "rel", "title", "order", "note"];
50
51pub const ENTRY_KEYS: &[&str] = &[
53 "path",
54 "role",
55 "id",
56 "type",
57 "title",
58 "summary",
59 "order",
60 "media_type",
61 "size",
62 "sha256",
63 "count",
64 "schema",
65 "optional",
66 "note",
67];
68
69pub const ROOT_ALLOWED: &[&str] = &[
71 "str", "spec", "kind", "id", "name", "type", "title", "summary", "tags", "revision",
72 "created_at", "updated_at", "schema", "policies", "authors", "refs", "entries", "ext",
73];
74
75pub const BRANCH_ALLOWED: &[&str] = &[
77 "str", "spec", "kind", "id", "name", "type", "title", "summary", "tags", "revision",
78 "created_at", "updated_at", "schema", "authors", "refs", "entries", "ext",
79];
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum Kind {
86 Root,
88 Node,
90 Branch,
92}
93
94impl Kind {
95 pub fn as_str(self) -> &'static str {
97 match self {
98 Kind::Root => "root",
99 Kind::Node => "node",
100 Kind::Branch => "branch",
101 }
102 }
103
104 pub fn parse(s: &str) -> Option<Kind> {
106 match s {
107 "root" => Some(Kind::Root),
108 "node" => Some(Kind::Node),
109 "branch" => Some(Kind::Branch),
110 _ => None,
111 }
112 }
113
114 pub fn allowed_top_keys(self) -> &'static [&'static str] {
116 match self {
117 Kind::Root => ROOT_ALLOWED,
118 _ => BRANCH_ALLOWED,
119 }
120 }
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub enum ManifestPolicy {
126 Strict,
128 Advisory,
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub enum ShaPolicy {
135 Required,
137 Optional,
139 Off,
141}
142
143#[derive(Debug, Clone, PartialEq, Eq)]
145pub struct Policies {
146 pub id_version: usize,
148 pub max_depth: usize,
150 pub manifest: ManifestPolicy,
152 pub sha256: ShaPolicy,
154 pub large_asset_bytes: u64,
156 pub deep_tree_warn: usize,
158}
159
160impl Default for Policies {
161 fn default() -> Self {
162 Self {
163 id_version: 7,
164 max_depth: 32,
165 manifest: ManifestPolicy::Strict,
166 sha256: ShaPolicy::Required,
167 large_asset_bytes: 10 * 1024 * 1024,
168 deep_tree_warn: 16,
169 }
170 }
171}
172
173#[derive(Debug, Clone, Default, PartialEq, Eq)]
175pub struct Author {
176 pub id: String,
178 pub name: Option<String>,
180 pub role: String,
182 pub at: Option<String>,
184}
185
186#[derive(Debug, Clone, Default, PartialEq, Eq)]
188pub struct RefItem {
189 pub id: String,
191 pub target: String,
193 pub rel: String,
195 pub title: Option<String>,
197 pub order: Option<i64>,
199 pub note: Option<String>,
201}
202
203#[derive(Debug, Clone, Default, PartialEq, Eq)]
205pub struct Entry {
206 pub path: String,
208 pub role: String,
210 pub id: Option<String>,
212 pub r#type: Option<String>,
214 pub title: Option<String>,
216 pub summary: Option<String>,
218 pub order: Option<i64>,
220 pub media_type: Option<String>,
222 pub size: Option<i64>,
224 pub sha256: Option<String>,
226 pub count: Option<i64>,
228 pub schema: Option<String>,
230 pub optional: bool,
232 pub note: Option<String>,
234}
235
236impl Entry {
237 pub fn is_branch(&self) -> bool {
239 self.role == "node" || self.role == "branch"
240 }
241
242 pub fn is_file_like(&self) -> bool {
244 self.role == "payload" || self.role == "asset"
245 }
246}
247
248#[derive(Debug)]
250pub struct Meta {
251 pub doc: DocumentMut,
253 pub str_version: Option<i64>,
255 pub spec: Option<String>,
257 pub kind_raw: Option<String>,
259 pub kind: Option<Kind>,
261 pub id: Option<String>,
263 pub name: Option<String>,
265 pub r#type: Option<String>,
267 pub title: Option<String>,
269 pub summary: Option<String>,
271 pub tags: Vec<String>,
273 pub revision: Option<i64>,
275 pub created_at: Option<String>,
277 pub updated_at: Option<String>,
279 pub schema: Option<String>,
281 pub policies: Policies,
283 pub authors: Vec<Author>,
285 pub refs: Vec<RefItem>,
287 pub entries: Vec<Entry>,
289}
290
291pub enum MetaLoad {
293 Ok(Box<Meta>, Vec<Issue>),
295 Failed(Vec<Issue>),
297}
298
299pub fn load(path: &Path, rel: &str) -> Result<MetaLoad> {
301 let bytes = std::fs::read(path).map_err(|e| Error::io(path, e))?;
302 let mut issues = Vec::new();
303
304 if bytes.starts_with(&[0xEF, 0xBB, 0xBF]) {
306 issues.push(Issue::error(
307 code::PARSE,
308 rel,
309 "文件含 UTF-8 BOM,规范要求 UTF-8 无 BOM",
310 ));
311 return Ok(MetaLoad::Failed(issues));
312 }
313 let text = match std::str::from_utf8(&bytes) {
314 Ok(t) => t,
315 Err(e) => {
316 issues.push(Issue::error(
317 code::PARSE,
318 rel,
319 format!("编码非 UTF-8:{e}"),
320 ));
321 return Ok(MetaLoad::Failed(issues));
322 }
323 };
324
325 let doc: DocumentMut = match text.parse() {
326 Ok(d) => d,
327 Err(e) => {
328 issues.push(Issue::error(code::PARSE, rel, format!("TOML 解析失败:{e}")));
329 return Ok(MetaLoad::Failed(issues));
330 }
331 };
332
333 let (meta, mut field_issues) = extract(doc, rel);
334 issues.append(&mut field_issues);
335 Ok(MetaLoad::Ok(Box::new(meta), issues))
336}
337
338pub fn extract(doc: DocumentMut, rel: &str) -> (Meta, Vec<Issue>) {
340 let mut cx = Ctx {
341 rel: rel.to_string(),
342 issues: Vec::new(),
343 };
344 let table = doc.as_table().clone();
345
346 let str_version = cx.opt_int(&table, "str");
347 let spec = cx.opt_str(&table, "spec");
348 let kind_raw = cx.opt_str(&table, "kind");
349 let kind = match kind_raw.as_deref() {
350 Some(s) => match Kind::parse(s) {
351 Some(k) => Some(k),
352 None => {
353 cx.err(
354 code::KIND_INVALID,
355 format!("`kind` = {s:?} 非法(应为 root / node / branch)"),
356 );
357 None
358 }
359 },
360 None => {
361 cx.err(code::SCHEMA_FIELD, "缺少必填字段 `kind`");
362 None
363 }
364 };
365
366 let allowed = kind.map(|k| k.allowed_top_keys()).unwrap_or(BRANCH_ALLOWED);
367 cx.check_unknown(&table, allowed);
368
369 for key in ["id", "revision", "created_at", "updated_at"] {
372 if table.get(key).is_none() {
373 cx.err(code::SCHEMA_FIELD, format!("缺少必填字段 `{key}`"));
374 }
375 }
376
377 let id = cx.opt_str(&table, "id");
378 let name = cx.opt_str(&table, "name");
379 let r#type = cx.opt_str(&table, "type");
380 let title = cx.opt_str(&table, "title");
381 let summary = cx.opt_str(&table, "summary");
382 let tags = cx.opt_str_array(&table, "tags").unwrap_or_default();
383 let revision = cx.opt_int(&table, "revision");
384 let created_at = cx.opt_dt(&table, "created_at");
385 let updated_at = cx.opt_dt(&table, "updated_at");
386 let schema = cx.opt_str(&table, "schema");
387
388 let mut policies = Policies::default();
390 if let Some(item) = table.get("policies") {
391 match item.as_table() {
392 Some(pt) => {
393 if kind != Some(Kind::Root) {
394 cx.err(
395 code::SCHEMA_FIELD,
396 "`[policies]` 只能出现在 root 的 `._meta` 中",
397 );
398 }
399 cx.check_unknown(pt, POLICIES_KEYS);
400 if let Some(v) = cx.opt_int(pt, "id_version") {
401 policies.id_version = v.max(0) as usize;
402 }
403 if let Some(v) = cx.opt_int(pt, "max_depth") {
404 policies.max_depth = v.max(1) as usize;
405 }
406 if let Some(v) = cx.opt_str(pt, "manifest") {
407 policies.manifest = match v.as_str() {
408 "strict" => ManifestPolicy::Strict,
409 "advisory" => ManifestPolicy::Advisory,
410 _ => {
411 cx.err(
412 code::SCHEMA_FIELD,
413 format!("`policies.manifest` = {v:?} 非法(strict / advisory)"),
414 );
415 policies.manifest
416 }
417 };
418 }
419 if let Some(v) = cx.opt_str(pt, "sha256") {
420 policies.sha256 = match v.as_str() {
421 "required" => ShaPolicy::Required,
422 "optional" => ShaPolicy::Optional,
423 "off" => ShaPolicy::Off,
424 _ => {
425 cx.err(
426 code::SCHEMA_FIELD,
427 format!("`policies.sha256` = {v:?} 非法(required / optional / off)"),
428 );
429 policies.sha256
430 }
431 };
432 }
433 if let Some(v) = cx.opt_int(pt, "large_asset_bytes") {
434 policies.large_asset_bytes = v.max(0) as u64;
435 }
436 if let Some(v) = cx.opt_int(pt, "deep_tree_warn") {
437 policies.deep_tree_warn = v.max(1) as usize;
438 }
439 }
440 None => cx.err(code::SCHEMA_FIELD, "`policies` 必须是表 `[policies]`"),
441 }
442 }
443
444 let mut authors = Vec::new();
446 if let Some(aot) = table.get("authors").and_then(|i| i.as_array_of_tables()) {
447 for (i, t) in aot.iter().enumerate() {
448 cx.set_rel(&format!("{rel} #authors[{i}]"));
449 cx.check_unknown(t, AUTHOR_KEYS);
450 let aid = cx.req_str(t, "id").unwrap_or_default();
451 let role = cx.req_str(t, "role").unwrap_or_default();
452 let aname = cx.opt_str(t, "name");
453 let at = cx.opt_dt(t, "at");
454 authors.push(Author {
455 id: aid,
456 name: aname,
457 role,
458 at,
459 });
460 }
461 cx.set_rel(rel);
462 } else if table.get("authors").is_some() {
463 cx.err(code::SCHEMA_FIELD, "`authors` 必须是数组表 `[[authors]]`");
464 }
465
466 let mut refs = Vec::new();
468 if let Some(aot) = table.get("refs").and_then(|i| i.as_array_of_tables()) {
469 for (i, t) in aot.iter().enumerate() {
470 cx.set_rel(&format!("{rel} #refs[{i}]"));
471 cx.check_unknown(t, REF_KEYS);
472 refs.push(RefItem {
473 id: cx.req_str(t, "id").unwrap_or_default(),
474 target: cx.req_str(t, "target").unwrap_or_default(),
475 rel: cx.req_str(t, "rel").unwrap_or_default(),
476 title: cx.opt_str(t, "title"),
477 order: cx.opt_int(t, "order"),
478 note: cx.opt_str(t, "note"),
479 });
480 }
481 cx.set_rel(rel);
482 } else if table.get("refs").is_some() {
483 cx.err(code::SCHEMA_FIELD, "`refs` 必须是数组表 `[[refs]]`");
484 }
485
486 let mut entries = Vec::new();
488 if let Some(aot) = table.get("entries").and_then(|i| i.as_array_of_tables()) {
489 for (i, t) in aot.iter().enumerate() {
490 cx.set_rel(&format!("{rel} #entries[{i}]"));
491 cx.check_unknown(t, ENTRY_KEYS);
492 let path = cx.req_str(t, "path").unwrap_or_default();
493 let role = cx.req_str(t, "role").unwrap_or_default();
494 if path.contains('/') {
495 cx.err(
496 code::SCHEMA_FIELD,
497 format!("`entries[].path` = {path:?} 必须是单段路径(不含 `/`)"),
498 );
499 }
500 let e = Entry {
501 path,
502 role,
503 id: cx.opt_str(t, "id"),
504 r#type: cx.opt_str(t, "type"),
505 title: cx.opt_str(t, "title"),
506 summary: cx.opt_str(t, "summary"),
507 order: cx.opt_int(t, "order"),
508 media_type: cx.opt_str(t, "media_type"),
509 size: cx.opt_int(t, "size"),
510 sha256: cx.opt_str(t, "sha256"),
511 count: cx.opt_int(t, "count"),
512 schema: cx.opt_str(t, "schema"),
513 optional: cx.opt_bool(t, "optional").unwrap_or(false),
514 note: cx.opt_str(t, "note"),
515 };
516 entries.push(e);
517 }
518 cx.set_rel(rel);
519 } else if table.get("entries").is_some() {
520 cx.err(code::SCHEMA_FIELD, "`entries` 必须是数组表 `[[entries]]`");
521 }
522
523 let meta = Meta {
524 doc,
525 str_version,
526 spec,
527 kind_raw,
528 kind,
529 id,
530 name,
531 r#type,
532 title,
533 summary,
534 tags,
535 revision,
536 created_at,
537 updated_at,
538 schema,
539 policies,
540 authors,
541 refs,
542 entries,
543 };
544 (meta, cx.issues)
545}
546
547pub fn canonicalize_doc(doc: &mut DocumentMut) {
555 let top_rank = |k: &str, it: &Item| -> usize {
558 match it {
559 Item::Table(_) | Item::ArrayOfTables(_) => {
560 TOP_BARE_KEYS.len()
561 + 1
562 + TABLE_ORDER
563 .iter()
564 .position(|x| *x == k)
565 .unwrap_or(TABLE_ORDER.len())
566 }
567 _ => TOP_BARE_KEYS
568 .iter()
569 .position(|x| *x == k)
570 .unwrap_or(TOP_BARE_KEYS.len()),
571 }
572 };
573 doc.as_table_mut()
574 .sort_values_by(|k1, i1, k2, i2| top_rank(k1.get(), i1).cmp(&top_rank(k2.get(), i2)));
575
576 for (key, order) in [
578 ("policies", POLICIES_KEYS),
579 ("authors", AUTHOR_KEYS),
580 ("refs", REF_KEYS),
581 ("entries", ENTRY_KEYS),
582 ] {
583 let rank = |k: &str| order.iter().position(|x| *x == k).unwrap_or(usize::MAX);
584 match doc.as_table_mut().get_mut(key) {
585 Some(Item::Table(t)) => {
586 t.sort_values_by(|k1, _, k2, _| rank(k1.get()).cmp(&rank(k2.get())));
587 }
588 Some(Item::ArrayOfTables(a)) => {
589 for t in a.iter_mut() {
590 t.sort_values_by(|k1, _, k2, _| rank(k1.get()).cmp(&rank(k2.get())));
591 }
592 }
593 _ => {}
594 }
595 }
596
597 sort_collections_in(doc);
599
600 renumber_positions_in(doc);
602}
603
604fn collection_sort_key(t: &Table) -> (i64, String) {
606 let order = t
607 .get("order")
608 .and_then(Item::as_integer)
609 .unwrap_or(i64::MAX);
610 let id = t
611 .get("path")
612 .or_else(|| t.get("id"))
613 .and_then(Item::as_str)
614 .unwrap_or("")
615 .to_string();
616 (order, id)
617}
618
619fn sort_collections_in(doc: &mut DocumentMut) {
621 for key in ["entries", "refs"] {
622 let Some(aot) = doc
623 .as_table_mut()
624 .get_mut(key)
625 .and_then(Item::as_array_of_tables_mut)
626 else {
627 continue;
628 };
629 let mut items: Vec<Table> = aot.iter().cloned().collect();
630 items.sort_by_key(collection_sort_key);
631 for (slot, table) in aot.iter_mut().zip(items) {
632 *slot = table;
633 }
634 }
635}
636
637fn renumber_positions_in(doc: &mut DocumentMut) {
643 fn walk(t: &mut Table, pos: &mut isize) {
644 *pos += 1;
645 t.set_position(Some(*pos));
646 for (_, item) in t.iter_mut() {
647 match item {
648 Item::Table(child) => walk(child, pos),
649 Item::ArrayOfTables(a) => {
650 for child in a.iter_mut() {
651 walk(child, pos);
652 }
653 }
654 _ => {}
655 }
656 }
657 }
658 let mut pos = 0isize;
659 walk(doc.as_table_mut(), &mut pos);
660}
661
662impl Meta {
665 pub fn to_json(&self) -> JValue {
668 let table = self.doc.as_table();
669 let mut map = JMap::new();
670
671 for key in TOP_BARE_KEYS {
672 if let Some(item) = table.get(key) {
673 map.insert((*key).to_string(), item_to_json(item));
674 }
675 }
676 if let Some(item) = table.get("policies") {
677 match item.as_table() {
678 Some(t) => {
679 map.insert("policies".into(), table_to_json(t, POLICIES_KEYS));
680 }
681 None => {
682 map.insert("policies".into(), item_to_json(item));
683 }
684 }
685 }
686 if let Some(aot) = table.get("authors").and_then(|i| i.as_array_of_tables()) {
687 map.insert(
688 "authors".into(),
689 JValue::Array(
690 aot.iter()
691 .map(|t| table_to_json(t, AUTHOR_KEYS))
692 .collect(),
693 ),
694 );
695 }
696 map.insert(
698 "refs".into(),
699 match table.get("refs").and_then(|i| i.as_array_of_tables()) {
700 Some(aot) => JValue::Array(
701 aot.iter()
702 .map(|t| table_to_json(t, REF_KEYS))
703 .collect(),
704 ),
705 None => JValue::Array(Vec::new()),
706 },
707 );
708 map.insert(
709 "entries".into(),
710 match table.get("entries").and_then(|i| i.as_array_of_tables()) {
711 Some(aot) => JValue::Array(
712 aot.iter()
713 .map(|t| table_to_json(t, ENTRY_KEYS))
714 .collect(),
715 ),
716 None => JValue::Array(Vec::new()),
717 },
718 );
719 if let Some(item) = table.get("ext") {
720 map.insert("ext".into(), item_to_json(item));
721 }
722 for (k, v) in table.iter() {
724 if !map.contains_key(k) {
725 map.insert(k.to_string(), item_to_json(v));
726 }
727 }
728 JValue::Object(map)
729 }
730
731 pub fn sort_collections(&mut self) {
733 sort_collections_in(&mut self.doc);
734 renumber_positions_in(&mut self.doc);
735 }
736
737 pub fn canonicalize(&mut self) {
739 canonicalize_doc(&mut self.doc);
740 }
741
742 pub fn revision_issues(&self, rel: &str) -> Vec<Issue> {
744 let mut out = Vec::new();
745 match self.revision {
746 None => {}
747 Some(r) if r < 1 => out.push(Issue::error(
748 code::REVISION_STALE,
749 rel,
750 format!("`revision` = {r},必须为 ≥ 1 的整数"),
751 )),
752 Some(_) => {}
753 }
754 if let (Some(c), Some(u)) = (self.created_at.as_deref(), self.updated_at.as_deref()) {
755 match (util::parse_rfc3339(c), util::parse_rfc3339(u)) {
756 (Some(cd), Some(ud)) if ud < cd => out.push(Issue::error(
757 code::REVISION_STALE,
758 rel,
759 format!("`updated_at`({u})早于 `created_at`({c})"),
760 )),
761 _ => {}
762 }
763 }
764 out
765 }
766}
767
768fn item_to_json(item: &Item) -> JValue {
770 match item {
771 Item::None => JValue::Null,
772 Item::Value(v) => value_to_json(v),
773 Item::Table(t) => {
774 let mut m = JMap::new();
775 for (k, v) in t.iter() {
776 m.insert(k.to_string(), item_to_json(v));
777 }
778 JValue::Object(m)
779 }
780 Item::ArrayOfTables(aot) => JValue::Array(
781 aot.iter()
782 .map(|t| {
783 let mut m = JMap::new();
784 for (k, v) in t.iter() {
785 m.insert(k.to_string(), item_to_json(v));
786 }
787 JValue::Object(m)
788 })
789 .collect(),
790 ),
791 }
792}
793
794fn value_to_json(v: &toml_edit::Value) -> JValue {
796 if let Some(s) = v.as_str() {
797 return JValue::String(s.to_string());
798 }
799 if let Some(i) = v.as_integer() {
800 return JValue::from(i);
801 }
802 if let Some(f) = v.as_float() {
803 return serde_json::Number::from_f64(f)
804 .map(JValue::Number)
805 .unwrap_or(JValue::Null);
806 }
807 if let Some(b) = v.as_bool() {
808 return JValue::Bool(b);
809 }
810 if let Some(d) = v.as_datetime() {
811 return JValue::String(d.to_string());
812 }
813 if let Some(a) = v.as_array() {
814 return JValue::Array(a.iter().map(value_to_json).collect());
815 }
816 if let Some(t) = v.as_inline_table() {
817 let mut m = JMap::new();
818 for (k, val) in t.iter() {
819 m.insert(k.to_string(), value_to_json(val));
820 }
821 return JValue::Object(m);
822 }
823 JValue::Null
824}
825
826fn table_to_json(table: &Table, order: &[&str]) -> JValue {
828 let mut m = JMap::new();
829 for k in order {
830 if let Some(v) = table.get(k) {
831 m.insert((*k).to_string(), item_to_json(v));
832 }
833 }
834 for (k, v) in table.iter() {
835 if !m.contains_key(k) {
836 m.insert(k.to_string(), item_to_json(v));
837 }
838 }
839 JValue::Object(m)
840}
841
842struct Ctx {
846 rel: String,
847 issues: Vec<Issue>,
848}
849
850impl Ctx {
851 fn set_rel(&mut self, rel: &str) {
852 self.rel = rel.to_string();
853 }
854
855 fn err(&mut self, code: &'static str, message: impl Into<String>) {
856 self.issues
857 .push(Issue::error(code, self.rel.clone(), message));
858 }
859
860 fn check_unknown(&mut self, t: &Table, allowed: &[&str]) {
861 for (k, _) in t.iter() {
862 if !allowed.contains(&k) {
863 self.err(
864 code::SCHEMA_FIELD,
865 format!("未知字段 `{k}`(扩展请放入 `[ext]`)"),
866 );
867 }
868 }
869 }
870
871 fn req_str(&mut self, t: &Table, key: &str) -> Option<String> {
872 match t.get(key) {
873 None => {
874 self.err(code::SCHEMA_FIELD, format!("缺少必填字段 `{key}`"));
875 None
876 }
877 Some(item) => match item.as_str() {
878 Some(s) => Some(s.to_string()),
879 None => {
880 self.err(code::SCHEMA_FIELD, format!("`{key}` 必须是字符串"));
881 None
882 }
883 },
884 }
885 }
886
887 fn opt_str(&mut self, t: &Table, key: &str) -> Option<String> {
888 match t.get(key) {
889 None => None,
890 Some(item) => match item.as_str() {
891 Some(s) => Some(s.to_string()),
892 None => {
893 self.err(code::SCHEMA_FIELD, format!("`{key}` 必须是字符串"));
894 None
895 }
896 },
897 }
898 }
899
900 fn opt_int(&mut self, t: &Table, key: &str) -> Option<i64> {
901 match t.get(key) {
902 None => None,
903 Some(item) => match item.as_integer() {
904 Some(i) => Some(i),
905 None => {
906 self.err(code::SCHEMA_FIELD, format!("`{key}` 必须是整数"));
907 None
908 }
909 },
910 }
911 }
912
913 fn opt_bool(&mut self, t: &Table, key: &str) -> Option<bool> {
914 match t.get(key) {
915 None => None,
916 Some(item) => match item.as_bool() {
917 Some(b) => Some(b),
918 None => {
919 self.err(code::SCHEMA_FIELD, format!("`{key}` 必须是布尔值"));
920 None
921 }
922 },
923 }
924 }
925
926 fn opt_dt(&mut self, t: &Table, key: &str) -> Option<String> {
928 let Some(item) = t.get(key) else {
929 return None;
930 };
931 if item.as_str().is_some() {
932 self.err(
933 code::PARSE,
934 format!("`{key}` 必须使用 TOML 原生 offset date-time,不得写成字符串"),
935 );
936 return None;
937 }
938 match item.as_datetime() {
939 Some(d) => {
940 if d.date.is_none() || d.time.is_none() || d.offset.is_none() {
941 self.err(
942 code::PARSE,
943 format!("`{key}` 必须是带时区偏移的 offset date-time(如 2026-09-14T10:03:11+08:00)"),
944 );
945 return None;
946 }
947 Some(d.to_string())
948 }
949 None => {
950 self.err(
951 code::PARSE,
952 format!("`{key}` 必须是 offset date-time"),
953 );
954 None
955 }
956 }
957 }
958
959 fn opt_str_array(&mut self, t: &Table, key: &str) -> Option<Vec<String>> {
960 let Some(item) = t.get(key) else {
961 return None;
962 };
963 let Some(arr) = item.as_array() else {
964 self.err(code::SCHEMA_FIELD, format!("`{key}` 必须是字符串数组"));
965 return None;
966 };
967 let mut out = Vec::new();
968 for v in arr.iter() {
969 match v.as_str() {
970 Some(s) => out.push(s.to_string()),
971 None => {
972 self.err(code::SCHEMA_FIELD, format!("`{key}` 的元素必须是字符串"));
973 }
974 }
975 }
976 Some(out)
977 }
978}