Skip to main content

f00_format/
json.rs

1use f00_core::Entry;
2use serde::Serialize;
3
4use crate::perms::format_permissions;
5
6#[derive(Serialize)]
7struct JsonEntry<'a> {
8    name: &'a str,
9    path: String,
10    /// Absolute path when canonicalization succeeds; otherwise omitted.
11    #[serde(skip_serializing_if = "Option::is_none")]
12    absolute_path: Option<String>,
13    kind: &'static str,
14    size: u64,
15    /// Permission bits as octal string (e.g. `"644"`), matching prior schema.
16    mode: String,
17    /// Same as `mode` — explicit name for machine consumers.
18    mode_octal: String,
19    /// `ls -l` style permission string (e.g. `"-rw-r--r--"`).
20    permissions: String,
21    readonly: bool,
22    #[serde(skip_serializing_if = "Option::is_none")]
23    modified: Option<String>,
24    #[serde(skip_serializing_if = "Option::is_none")]
25    accessed: Option<String>,
26    #[serde(skip_serializing_if = "Option::is_none")]
27    changed: Option<String>,
28    #[serde(skip_serializing_if = "Option::is_none")]
29    created: Option<String>,
30    inode: u64,
31    nlink: u64,
32    /// Allocated blocks in 512-byte units (GNU `ls -s` style) when known.
33    blocks: u64,
34    uid: u32,
35    gid: u32,
36    #[serde(skip_serializing_if = "str::is_empty")]
37    owner: &'a str,
38    #[serde(skip_serializing_if = "str::is_empty")]
39    group: &'a str,
40    #[serde(skip_serializing_if = "str::is_empty")]
41    author: &'a str,
42    #[serde(skip_serializing_if = "Option::is_none")]
43    symlink_target: Option<String>,
44    #[serde(skip_serializing_if = "str::is_empty")]
45    context: &'a str,
46    #[serde(skip_serializing_if = "Option::is_none")]
47    extension: Option<&'a str>,
48    git_status: &'static str,
49    depth: usize,
50}
51
52fn rfc3339(dt: Option<chrono::DateTime<chrono::Local>>) -> Option<String> {
53    dt.map(|d| d.to_rfc3339())
54}
55
56/// Serialize entries (skipping directory headers) as pretty JSON.
57pub fn format_json(entries: &[Entry]) -> Result<String, serde_json::Error> {
58    let items: Vec<JsonEntry<'_>> = entries
59        .iter()
60        .filter(|e| !e.is_dir_header)
61        .map(|e| {
62            let mode = format!("{:o}", e.mode);
63            let absolute_path = std::fs::canonicalize(&e.path)
64                .ok()
65                .map(|p| p.display().to_string());
66            JsonEntry {
67                name: &e.name,
68                path: e.path.display().to_string(),
69                absolute_path,
70                kind: e.kind.as_str(),
71                size: e.size,
72                mode: mode.clone(),
73                mode_octal: mode,
74                permissions: format_permissions(e),
75                readonly: e.readonly,
76                modified: rfc3339(e.modified_datetime()),
77                accessed: rfc3339(e.accessed_datetime()),
78                changed: rfc3339(e.changed_datetime()),
79                created: rfc3339(e.created_datetime()),
80                inode: e.inode,
81                nlink: e.nlink,
82                blocks: e.blocks,
83                uid: e.uid,
84                gid: e.gid,
85                owner: e.owner.as_str(),
86                group: e.group.as_str(),
87                author: e.author.as_str(),
88                symlink_target: e.symlink_target.as_ref().map(|p| p.display().to_string()),
89                context: e.context.as_str(),
90                extension: e.extension(),
91                git_status: e.git_status.as_str(),
92                depth: e.depth,
93            }
94        })
95        .collect();
96
97    serde_json::to_string_pretty(&items)
98}