1use serde::{de::DeserializeOwned, Deserialize, Serialize};
3
4#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
20pub struct Revision(Option<i64>);
21
22impl Revision {
23 pub fn as_i64(&self) -> Option<i64> {
24 self.0
25 }
26}
27
28impl std::fmt::Display for Revision {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 match self.0 {
31 Some(n) => write!(f, "{}", n),
32 None => write!(f, ""),
33 }
34 }
35}
36
37impl AsRef<Option<i64>> for Revision {
38 fn as_ref(&self) -> &Option<i64> {
39 &self.0
40 }
41}
42
43impl Revision {
44 pub const HEAD: Revision = Revision(Some(-1));
46 pub const INIT: Revision = Revision(Some(1));
48 pub const DEFAULT: Revision = Revision(None);
50
51 pub fn from(i: i64) -> Self {
53 Revision(Some(i))
54 }
55}
56
57#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
59#[serde(rename_all = "camelCase")]
60pub struct Author {
61 pub name: String,
63 pub email: String,
65}
66
67#[derive(Debug, Serialize, Deserialize)]
71#[serde(rename_all = "camelCase")]
72pub struct Project {
73 pub name: String,
75 pub creator: Author,
77 pub url: Option<String>,
79 pub created_at: Option<String>,
81}
82
83#[derive(Debug, Serialize, Deserialize)]
85#[serde(rename_all = "camelCase")]
86pub struct Repository {
87 pub name: String,
89 pub creator: Author,
91 pub head_revision: Revision,
93 pub url: Option<String>,
95 pub created_at: Option<String>,
97}
98
99#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
101#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
102#[serde(tag = "type", content = "content")]
103pub enum EntryContent {
104 Json(serde_json::Value),
106 Text(String),
108 Directory,
110}
111
112#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
114#[serde(rename_all = "camelCase")]
115pub struct Entry {
116 pub path: String,
118 #[serde(flatten)]
120 pub content: EntryContent,
121 pub revision: Revision,
123 pub url: String,
125 pub modified_at: Option<String>,
127}
128
129impl Entry {
130 pub fn entry_type(&self) -> EntryType {
131 match self.content {
132 EntryContent::Json(_) => EntryType::Json,
133 EntryContent::Text(_) => EntryType::Text,
134 EntryContent::Directory => EntryType::Directory,
135 }
136 }
137}
138
139#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
141#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
142pub enum EntryType {
143 Json,
145 Text,
147 Directory,
149}
150
151#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
154#[serde(rename_all = "camelCase")]
155pub struct ListEntry {
156 pub path: String,
157 pub r#type: EntryType,
158}
159
160#[derive(Debug, PartialEq, Eq)]
162pub enum QueryType {
163 Identity,
164 IdentityJson,
165 IdentityText,
166 JsonPath(Vec<String>),
167}
168
169#[derive(Debug)]
171pub struct Query {
172 pub(crate) path: String,
173 pub(crate) r#type: QueryType,
174}
175
176impl Query {
177 fn normalize_path(path: &str) -> String {
178 if path.starts_with('/') {
179 path.to_owned()
180 } else {
181 format!("/{}", path)
182 }
183 }
184
185 pub fn identity(path: &str) -> Option<Self> {
188 if path.is_empty() {
189 return None;
190 }
191 Some(Query {
192 path: Self::normalize_path(path),
193 r#type: QueryType::Identity,
194 })
195 }
196
197 pub fn of_text(path: &str) -> Option<Self> {
200 if path.is_empty() {
201 return None;
202 }
203 Some(Query {
204 path: Self::normalize_path(path),
205 r#type: QueryType::IdentityText,
206 })
207 }
208
209 pub fn of_json(path: &str) -> Option<Self> {
212 if path.is_empty() {
213 return None;
214 }
215 Some(Query {
216 path: Self::normalize_path(path),
217 r#type: QueryType::IdentityJson,
218 })
219 }
220
221 pub fn of_json_path(path: &str, exprs: Vec<String>) -> Option<Self> {
227 if !path.to_lowercase().ends_with("json") {
228 return None;
229 }
230 if exprs.iter().any(|expr| expr.is_empty()) {
231 return None;
232 }
233 Some(Query {
234 path: Self::normalize_path(path),
235 r#type: QueryType::JsonPath(exprs),
236 })
237 }
238}
239
240#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
242#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
243#[serde(tag = "markup", content = "detail")]
244pub enum CommitDetail {
245 Markdown(String),
247 Plaintext(String),
249}
250
251#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
253#[serde(rename_all = "camelCase")]
254pub struct CommitMessage {
255 pub summary: String,
257 #[serde(flatten, skip_serializing_if = "Option::is_none")]
258 pub detail: Option<CommitDetail>,
260}
261
262impl CommitMessage {
263 pub fn only_summary(summary: &str) -> Self {
264 CommitMessage {
265 summary: summary.to_owned(),
266 detail: None,
267 }
268 }
269}
270
271#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
273#[serde(rename_all = "camelCase")]
274pub struct PushResult {
275 pub revision: Revision,
277 pub pushed_at: Option<String>,
279}
280
281#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
283#[serde(rename_all = "camelCase")]
284pub struct Commit {
285 pub revision: Revision,
287 pub author: Author,
289 pub commit_message: CommitMessage,
291 pub pushed_at: Option<String>,
293}
294
295#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
297#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
298#[serde(tag = "type", content = "content")]
299pub enum ChangeContent {
300 UpsertJson(serde_json::Value),
302
303 UpsertText(String),
305
306 Remove,
308
309 Rename(String),
311
312 ApplyJsonPatch(serde_json::Value),
315
316 ApplyTextPatch(String),
319}
320
321#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
323#[serde(rename_all = "camelCase")]
324pub struct Change {
325 pub path: String,
327 #[serde(flatten)]
329 pub content: ChangeContent,
330}
331
332#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
335#[serde(rename_all = "camelCase")]
336pub struct WatchFileResult {
337 pub revision: Revision,
339 pub entry: Entry,
341}
342
343#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
346#[serde(rename_all = "camelCase")]
347pub struct WatchRepoResult {
348 pub revision: Revision,
350}
351
352pub(crate) trait Watchable: DeserializeOwned + Send {
355 fn revision(&self) -> Revision;
356}
357
358impl Watchable for WatchFileResult {
359 fn revision(&self) -> Revision {
360 self.revision
361 }
362}
363
364impl Watchable for WatchRepoResult {
365 fn revision(&self) -> Revision {
366 self.revision
367 }
368}
369
370#[cfg(test)]
371mod test {
372 use super::*;
373
374 #[test]
375 fn test_query_identity() {
376 let query = Query::identity("/a.json").unwrap();
377
378 assert_eq!(query.path, "/a.json");
379 assert_eq!(query.r#type, QueryType::Identity);
380 }
381
382 #[test]
383 fn test_query_identity_auto_fix_path() {
384 let query = Query::identity("a.json").unwrap();
385
386 assert_eq!(query.path, "/a.json");
387 assert_eq!(query.r#type, QueryType::Identity);
388 }
389
390 #[test]
391 fn test_query_reject_empty_path() {
392 let query = Query::identity("");
393
394 assert!(query.is_none());
395 }
396}