1use std::collections::HashMap;
7
8#[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 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#[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#[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 pub fn is_encrypted(&self) -> bool {
90 self.prop("encryption_applied").map(|v| v == "1").unwrap_or(false)
91 }
92}
93
94#[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 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(¬e).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 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}