aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart-metadata-minifier/src/lib.rs
blob: 7d0e803cce17cd6a8095f17db9a0ef056f5a7428 (plain)
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
//! Metadata minification and expansion for Composer-compatible package
//! repositories.
//!
//! This crate is a Rust port of the PHP
//! [`composer/metadata-minifier`](https://packagist.org/packages/composer/metadata-minifier)
//! library. It operates on raw JSON values (`serde_json::Value`) so that it
//! stays independent of any particular package struct definition.
//!
//! MetadataMinifier::minify() is not ported because it is not used in Composer itself.
//! The function is mainly for package repositories.
//!
//! # Minified format
//!
//! A minified version list is a JSON array of objects where:
//!
//! * The **first** entry carries all fields in full.
//! * Each **subsequent** entry only contains the fields that *differ* from the
//!   previous expanded entry.
//! * The sentinel string `"__unset"` marks a field as *deleted*.
//!
//! A response that uses this encoding contains a top-level key
//! `"minified": "composer/2.0"`.

use serde_json::Value;

/// The sentinel value used by Composer's metadata minifier to mark a deleted
/// field.
const UNSET_SENTINEL: &str = "__unset";

/// Expand a minified version list back to full form.
///
/// Each entry in the returned list is a self-contained JSON object with all
/// fields present (i.e. no diff encoding).
///
/// If the input is empty the output is empty. If the input contains only one
/// entry it is returned as-is.
pub fn expand(versions: &[Value]) -> Vec<Value> {
    let mut expanded: Vec<Value> = Vec::with_capacity(versions.len());

    let Some((first, rest)) = versions.split_first() else {
        return expanded;
    };

    let Some(mut state) = first.as_object().cloned() else {
        expanded.push(first.clone());
        return expanded;
    };

    expanded.push(Value::Object(state.clone()));

    for diff in rest {
        let Some(diff_map) = diff.as_object() else {
            expanded.push(diff.clone());
            continue;
        };

        for (key, val) in diff_map {
            if val.as_str() == Some(UNSET_SENTINEL) {
                state.remove(key.as_str());
            } else {
                state.insert(key.clone(), val.clone());
            }
        }

        expanded.push(Value::Object(state.clone()));
    }

    expanded
}

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

    /// Mirrors the canonical Composer MetadataMinifierTest.
    #[test]
    fn expand_matches_composer_test() {
        let minified = vec![
            json!({
                "name": "foo/bar",
                "version": "2.0.0",
                "version_normalized": "2.0.0.0",
                "type": "library",
                "scripts": {"foo": ["bar"]},
                "license": ["MIT"]
            }),
            json!({
                "version": "1.2.0",
                "version_normalized": "1.2.0.0",
                "license": ["GPL"],
                "homepage": "https://example.org",
                "scripts": "__unset"
            }),
            json!({
                "version": "1.0.0",
                "version_normalized": "1.0.0.0",
                "homepage": "__unset"
            }),
        ];

        let expanded = expand(&minified);
        assert_eq!(expanded.len(), 3);

        // Version 2.0.0 — unchanged.
        assert_eq!(expanded[0]["name"], "foo/bar");
        assert_eq!(expanded[0]["version"], "2.0.0");
        assert_eq!(expanded[0]["type"], "library");
        assert_eq!(expanded[0]["scripts"], json!({"foo": ["bar"]}));
        assert_eq!(expanded[0]["license"], json!(["MIT"]));

        // Version 1.2.0 — inherits name, type; license changed; homepage
        // added; scripts removed.
        assert_eq!(expanded[1]["name"], "foo/bar");
        assert_eq!(expanded[1]["version"], "1.2.0");
        assert_eq!(expanded[1]["type"], "library");
        assert_eq!(expanded[1]["license"], json!(["GPL"]));
        assert_eq!(expanded[1]["homepage"], "https://example.org");
        assert!(expanded[1].get("scripts").is_none());

        // Version 1.0.0 — inherits from 1.2.0; homepage removed.
        assert_eq!(expanded[2]["name"], "foo/bar");
        assert_eq!(expanded[2]["version"], "1.0.0");
        assert_eq!(expanded[2]["type"], "library");
        assert_eq!(expanded[2]["license"], json!(["GPL"]));
        assert!(expanded[2].get("homepage").is_none());
        assert!(expanded[2].get("scripts").is_none());
    }

    #[test]
    fn expand_empty() {
        assert!(expand(&[]).is_empty());
    }

    #[test]
    fn expand_single_version() {
        let versions = vec![json!({"name": "a/b", "version": "1.0.0"})];
        let expanded = expand(&versions);
        assert_eq!(expanded.len(), 1);
        assert_eq!(expanded[0], versions[0]);
    }
}