1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
//! Data models of CentralDogma
use serde::{de::DeserializeOwned, Deserialize, Serialize};

/// A revision number of a [`Commit`].
///
/// A revision number is an integer which refers to a specific point of repository history.
/// When a repository is created, it starts with an initial commit whose revision is 1.
/// As new commits are added, each commit gets its own revision number,
/// monotonically increasing from the previous commit's revision. i.e. 1, 2, 3, ...
///
/// A revision number can also be represented as a negative integer.
/// When a revision number is negative, we start from -1 which refers to the latest commit in repository history,
/// which is often called 'HEAD' of the repository.
/// A smaller revision number refers to the older commit.
/// e.g. -2 refers to the commit before the latest commit, and so on.
///
/// A revision with a negative integer is called 'relative revision'.
/// By contrast, a revision with a positive integer is called 'absolute revision'.
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
pub struct Revision(Option<i64>);

impl Revision {
    pub fn as_i64(&self) -> Option<i64> {
        self.0
    }
}

impl std::fmt::Display for Revision {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self.0 {
            Some(n) => write!(f, "{}", n),
            None => write!(f, ""),
        }
    }
}

impl AsRef<Option<i64>> for Revision {
    fn as_ref(&self) -> &Option<i64> {
        &self.0
    }
}

impl Revision {
    /// Revision `-1`, also known as `HEAD`.
    pub const HEAD: Revision = Revision(Some(-1));
    /// Revision `1`, also known as `INIT`.
    pub const INIT: Revision = Revision(Some(1));
    /// Omitted revision, behavior is decided on server side, usually [`HEAD`]
    pub const DEFAULT: Revision = Revision(None);

    /// Create a new instance with the specified revision number.
    pub fn from(i: i64) -> Self {
        Revision(Some(i))
    }
}

/// Creator of a project or repository or commit
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Author {
    /// Name of this author.
    pub name: String,
    /// Email of this author.
    pub email: String,
}

/// A top-level element in Central Dogma storage model.
/// A project has "dogma" and "meta" repositories by default which contain project configuration
/// files accessible by administrators and project owners respectively.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Project {
    /// Name of this project.
    pub name: String,
    /// The author who initially created this project.
    pub creator: Author,
    /// Url of this project
    pub url: Option<String>,
    /// When the project was created
    pub created_at: Option<String>,
}

/// Repository information
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Repository {
    /// Name of this repository.
    pub name: String,
    /// The author who initially created this repository.
    pub creator: Author,
    /// Head [`Revision`] of the repository.
    pub head_revision: Revision,
    /// Url of this repository.
    pub url: Option<String>,
    /// When the repository was created.
    pub created_at: Option<String>,
}

/// The content of an [`Entry`]
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[serde(tag = "type", content = "content")]
pub enum EntryContent {
    /// Content as a JSON Value.
    Json(serde_json::Value),
    /// Content as a String.
    Text(String),
    /// This Entry is a directory.
    Directory,
}

/// A file or a directory in a repository.
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Entry {
    /// Path of this entry.
    pub path: String,
    /// Content of this entry.
    #[serde(flatten)]
    pub content: EntryContent,
    /// Revision of this entry.
    pub revision: Revision,
    /// Url of this entry.
    pub url: String,
    /// When this entry was last modified.
    pub modified_at: Option<String>,
}

impl Entry {
    pub fn entry_type(&self) -> EntryType {
        match self.content {
            EntryContent::Json(_) => EntryType::Json,
            EntryContent::Text(_) => EntryType::Text,
            EntryContent::Directory => EntryType::Directory,
        }
    }
}

/// The type of a [`ListEntry`]
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum EntryType {
    /// A UTF-8 encoded JSON file.
    Json,
    /// A UTF-8 encoded text file.
    Text,
    /// A directory.
    Directory,
}

/// A metadata of a file or a directory in a repository.
/// ListEntry has no content.
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ListEntry {
    pub path: String,
    pub r#type: EntryType,
}

/// Type of a [`Query`]
#[derive(Debug, PartialEq, Eq)]
pub enum QueryType {
    Identity,
    IdentityJson,
    IdentityText,
    JsonPath(Vec<String>),
}

/// A Query on a file
#[derive(Debug)]
pub struct Query {
    pub(crate) path: String,
    pub(crate) r#type: QueryType,
}

impl Query {
    fn normalize_path(path: &str) -> String {
        if path.starts_with('/') {
            path.to_owned()
        } else {
            format!("/{}", path)
        }
    }

    /// Returns a newly-created [`Query`] that retrieves the content as it is.
    /// Returns `None` if path is empty
    pub fn identity(path: &str) -> Option<Self> {
        if path.is_empty() {
            return None;
        }
        Some(Query {
            path: Self::normalize_path(path),
            r#type: QueryType::Identity,
        })
    }

    /// Returns a newly-created [`Query`] that retrieves the textual content as it is.
    /// Returns `None` if path is empty
    pub fn of_text(path: &str) -> Option<Self> {
        if path.is_empty() {
            return None;
        }
        Some(Query {
            path: Self::normalize_path(path),
            r#type: QueryType::IdentityText,
        })
    }

    /// Returns a newly-created [`Query`] that retrieves the JSON content as it is.
    /// Returns `None` if path is empty
    pub fn of_json(path: &str) -> Option<Self> {
        if path.is_empty() {
            return None;
        }
        Some(Query {
            path: Self::normalize_path(path),
            r#type: QueryType::IdentityJson,
        })
    }

    /// Returns a newly-created [`Query`] that applies a series of
    /// [JSON path expressions](https://github.com/json-path/JsonPath/blob/master/README.md)
    /// to the content.
    /// Returns `None` if path is empty or does not end with `.json`.
    /// Returns `None` if any of the path expression provided is empty.
    pub fn of_json_path(path: &str, exprs: Vec<String>) -> Option<Self> {
        if !path.to_lowercase().ends_with("json") {
            return None;
        }
        if exprs.iter().any(|expr| expr.is_empty()) {
            return None;
        }
        Some(Query {
            path: Self::normalize_path(path),
            r#type: QueryType::JsonPath(exprs),
        })
    }
}

/// Typed content of a [`CommitMessage`]
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[serde(tag = "markup", content = "detail")]
pub enum CommitDetail {
    /// Commit details as markdown
    Markdown(String),
    /// Commit details as plaintext
    Plaintext(String),
}

/// Description of a [`Commit`]
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct CommitMessage {
    /// Summary of this commit message
    pub summary: String,
    #[serde(flatten, skip_serializing_if = "Option::is_none")]
    /// Detailed description of this commit message
    pub detail: Option<CommitDetail>,
}

impl CommitMessage {
    pub fn only_summary(summary: &str) -> Self {
        CommitMessage {
            summary: summary.to_owned(),
            detail: None,
        }
    }
}

/// Result of a [push](trait@crate::ContentService#tymethod.push) operation.
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct PushResult {
    /// Revision of this commit.
    pub revision: Revision,
    /// When this commit was pushed.
    pub pushed_at: Option<String>,
}

/// A set of Changes and its metadata.
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Commit {
    /// Revision of this commit.
    pub revision: Revision,
    /// Author of this commit.
    pub author: Author,
    /// Description of this commit.
    pub commit_message: CommitMessage,
    /// When this commit was pushed.
    pub pushed_at: Option<String>,
}

/// Typed content of a [`Change`].
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
#[serde(tag = "type", content = "content")]
pub enum ChangeContent {
    /// Adds a new JSON file or replaces an existing file with the provided json.
    UpsertJson(serde_json::Value),

    /// Adds a new text file or replaces an existing file with the provided content.
    UpsertText(String),

    /// Removes an existing file.
    Remove,

    /// Renames an existsing file to this provided path.
    Rename(String),

    /// Applies a JSON patch to a JSON file with the provided JSON patch object,
    /// as defined in [RFC 6902](https://tools.ietf.org/html/rfc6902).
    ApplyJsonPatch(serde_json::Value),

    /// Applies a textual patch to a text file with the provided
    /// [unified format](https://en.wikipedia.org/wiki/Diff_utility#Unified_format) string.
    ApplyTextPatch(String),
}

/// A modification of an individual [`Entry`]
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Change {
    /// Path of the file change.
    pub path: String,
    /// Content of the file change.
    #[serde(flatten)]
    pub content: ChangeContent,
}

/// A change result from a
/// [watch_file](trait@crate::WatchService#tymethod.watch_file_stream) operation.
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WatchFileResult {
    /// Revision of the change.
    pub revision: Revision,
    /// Content of the change.
    pub entry: Entry,
}

/// A change result from a
/// [watch_repo](trait@crate::WatchService#tymethod.watch_repo_stream) operation.
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct WatchRepoResult {
    /// Revision of the change.
    pub revision: Revision,
}

/// A resource that is watchable
/// Currently supported [`WatchFileResult`] and [`WatchRepoResult`]
pub(crate) trait Watchable: DeserializeOwned + Send {
    fn revision(&self) -> Revision;
}

impl Watchable for WatchFileResult {
    fn revision(&self) -> Revision {
        self.revision
    }
}

impl Watchable for WatchRepoResult {
    fn revision(&self) -> Revision {
        self.revision
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_query_identity() {
        let query = Query::identity("/a.json").unwrap();

        assert_eq!(query.path, "/a.json");
        assert_eq!(query.r#type, QueryType::Identity);
    }

    #[test]
    fn test_query_identity_auto_fix_path() {
        let query = Query::identity("a.json").unwrap();

        assert_eq!(query.path, "/a.json");
        assert_eq!(query.r#type, QueryType::Identity);
    }

    #[test]
    fn test_query_reject_empty_path() {
        let query = Query::identity("");

        assert!(query.is_none());
    }
}