Skip to main content

jasper_core/
model.rs

1//! Joplin 条目的数据模型。
2//!
3//! 字段集来源:joplin/packages/lib/services/database/types.ts
4//! type_ 枚举来源:joplin/packages/lib/BaseModel.ts:12-29
5
6use std::collections::HashMap;
7
8/// Joplin 的 type_ 枚举(只读客户端关心的子集)。
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum ItemType {
11    Note = 1,
12    Folder = 2,
13    Resource = 4,
14    Tag = 5,
15    NoteTag = 6,
16    /// 其它类型(Revision=13、MasterKey=9 等),只读 MVP 跳过。
17    Other,
18}
19
20impl ItemType {
21    pub fn from_i64(v: i64) -> Self {
22        match v {
23            1 => ItemType::Note,
24            2 => ItemType::Folder,
25            4 => ItemType::Resource,
26            5 => ItemType::Tag,
27            6 => ItemType::NoteTag,
28            _ => ItemType::Other,
29        }
30    }
31}
32
33/// markup_language:决定笔记正文如何渲染。
34/// 来源:joplin/packages/renderer/types.ts:3-7
35/// serde 下序列化为整数 1|2(与 Joplin 元数据一致,插件 ABI 依赖此形状)。
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
38#[cfg_attr(feature = "serde", serde(into = "i64", from = "i64"))]
39pub enum MarkupLanguage {
40    Markdown = 1,
41    Html = 2,
42}
43
44impl MarkupLanguage {
45    pub fn from_i64(v: i64) -> Self {
46        match v {
47            2 => MarkupLanguage::Html,
48            _ => MarkupLanguage::Markdown,
49        }
50    }
51}
52
53impl From<i64> for MarkupLanguage {
54    fn from(v: i64) -> Self {
55        MarkupLanguage::from_i64(v)
56    }
57}
58
59impl From<MarkupLanguage> for i64 {
60    fn from(v: MarkupLanguage) -> Self {
61        v as i64
62    }
63}
64
65/// 解析后的原始条目:标题/正文 + 所有元数据键值对(均为字符串)。
66/// 后续按 type_ 转成下面的强类型结构。
67#[derive(Debug, Clone)]
68pub struct RawItem {
69    pub type_: i64,
70    pub title: Option<String>,
71    pub body: Option<String>,
72    pub props: HashMap<String, String>,
73}
74
75impl RawItem {
76    pub fn item_type(&self) -> ItemType {
77        ItemType::from_i64(self.type_)
78    }
79
80    pub fn id(&self) -> Option<&str> {
81        self.props.get("id").map(|s| s.as_str())
82    }
83
84    pub fn prop(&self, key: &str) -> Option<&str> {
85        self.props.get(key).map(|s| s.as_str())
86    }
87
88    /// 是否为加密条目(明文场景应跳过)。来源:encryption_applied 字段。
89    pub fn is_encrypted(&self) -> bool {
90        self.prop("encryption_applied").map(|v| v == "1").unwrap_or(false)
91    }
92}
93
94// 下列 cfg_attr(serde) 让插件 ABI(spec §6.5 数据形状)与宿主共用同一套类型;
95// 规范形状之外的字段标 serde(default),规范形状的 JSON 也能反序列化。
96#[derive(Debug, Clone)]
97#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
98pub struct Note {
99    pub id: String,
100    pub parent_id: String,
101    pub title: String,
102    pub body: String,
103    pub created_time: i64,
104    pub updated_time: i64,
105    pub markup_language: MarkupLanguage,
106    pub is_todo: bool,
107    pub todo_completed: bool,
108    #[cfg_attr(feature = "serde", serde(default))]
109    pub is_conflict: bool,
110    #[cfg_attr(feature = "serde", serde(default))]
111    pub source_url: String,
112    #[cfg_attr(feature = "serde", serde(default))]
113    pub order: i64,
114}
115
116#[derive(Debug, Clone)]
117#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
118pub struct Folder {
119    pub id: String,
120    pub parent_id: String,
121    pub title: String,
122    pub created_time: i64,
123    pub updated_time: i64,
124    #[cfg_attr(feature = "serde", serde(default))]
125    pub icon: String,
126}
127
128#[derive(Debug, Clone)]
129#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
130pub struct Resource {
131    pub id: String,
132    pub title: String,
133    pub mime: String,
134    pub file_extension: String,
135    pub size: i64,
136    pub updated_time: i64,
137}
138
139#[derive(Debug, Clone)]
140#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
141pub struct Tag {
142    pub id: String,
143    pub parent_id: String,
144    pub title: String,
145}
146
147#[derive(Debug, Clone)]
148#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
149pub struct NoteTag {
150    pub id: String,
151    pub note_id: String,
152    pub tag_id: String,
153}
154
155#[cfg(all(test, feature = "serde"))]
156mod serde_tests {
157    use super::*;
158
159    #[test]
160    fn markup_language_serializes_as_int() {
161        assert_eq!(serde_json::to_string(&MarkupLanguage::Markdown).unwrap(), "1");
162        assert_eq!(serde_json::to_string(&MarkupLanguage::Html).unwrap(), "2");
163        assert_eq!(serde_json::from_str::<MarkupLanguage>("2").unwrap(), MarkupLanguage::Html);
164        // 未知值回落 Markdown(与 from_i64 一致)
165        assert_eq!(serde_json::from_str::<MarkupLanguage>("99").unwrap(), MarkupLanguage::Markdown);
166    }
167
168    #[test]
169    fn note_round_trips_and_accepts_spec_shape() {
170        let note = Note {
171            id: "a".repeat(32),
172            parent_id: "b".repeat(32),
173            title: "标题".into(),
174            body: "正文".into(),
175            created_time: 1,
176            updated_time: 2,
177            markup_language: MarkupLanguage::Html,
178            is_todo: true,
179            todo_completed: false,
180            is_conflict: false,
181            source_url: String::new(),
182            order: 7,
183        };
184        let json = serde_json::to_string(&note).unwrap();
185        assert!(json.contains("\"markup_language\":2"));
186        let back: Note = serde_json::from_str(&json).unwrap();
187        assert_eq!(back.title, note.title);
188        assert_eq!(back.order, 7);
189
190        // 规范 §6.5 的 Note 形状(无 is_conflict/order)也能反序列化,缺省补零值
191        let spec = r#"{"id":"x","parent_id":"","title":"t","body":"b",
192            "markup_language":1,"created_time":0,"updated_time":0,
193            "is_todo":false,"todo_completed":false,"source_url":""}"#;
194        let n: Note = serde_json::from_str(spec).unwrap();
195        assert!(!n.is_conflict);
196        assert_eq!(n.order, 0);
197    }
198
199    #[test]
200    fn folder_accepts_missing_icon() {
201        let f: Folder = serde_json::from_str(
202            r#"{"id":"x","parent_id":"","title":"t","created_time":0,"updated_time":0}"#,
203        )
204        .unwrap();
205        assert_eq!(f.icon, "");
206    }
207}