containerregistry_image/
descriptor.rs1use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6
7use crate::{Digest, MediaType};
8
9#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "camelCase")]
15pub struct Descriptor {
16 pub media_type: MediaType,
18
19 pub digest: Digest,
21
22 pub size: u64,
24
25 #[serde(default, skip_serializing_if = "Vec::is_empty")]
27 pub urls: Vec<String>,
28
29 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
31 pub annotations: BTreeMap<String, String>,
32
33 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub data: Option<String>,
36
37 #[serde(default, skip_serializing_if = "Option::is_none")]
39 pub platform: Option<Platform>,
40}
41
42#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
44pub struct Platform {
45 pub architecture: String,
47
48 pub os: String,
50
51 #[serde(
53 default,
54 rename = "os.version",
55 skip_serializing_if = "Option::is_none"
56 )]
57 pub os_version: Option<String>,
58
59 #[serde(default, rename = "os.features", skip_serializing_if = "Vec::is_empty")]
61 pub os_features: Vec<String>,
62
63 #[serde(default, skip_serializing_if = "Option::is_none")]
65 pub variant: Option<String>,
66
67 #[serde(default, skip_serializing_if = "Vec::is_empty")]
69 pub features: Vec<String>,
70}
71
72impl Descriptor {
73 pub fn new(media_type: MediaType, digest: Digest, size: u64) -> Self {
75 Self {
76 media_type,
77 digest,
78 size,
79 urls: Vec::new(),
80 annotations: BTreeMap::new(),
81 data: None,
82 platform: None,
83 }
84 }
85
86 pub fn with_annotation(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
88 self.annotations.insert(key.into(), value.into());
89 self
90 }
91
92 pub fn with_url(mut self, url: impl Into<String>) -> Self {
94 self.urls.push(url.into());
95 self
96 }
97
98 pub fn with_platform(mut self, platform: Platform) -> Self {
100 self.platform = Some(platform);
101 self
102 }
103}
104
105impl Platform {
106 pub fn new(architecture: impl Into<String>, os: impl Into<String>) -> Self {
108 Self {
109 architecture: architecture.into(),
110 os: os.into(),
111 os_version: None,
112 os_features: Vec::new(),
113 variant: None,
114 features: Vec::new(),
115 }
116 }
117
118 pub fn with_variant(mut self, variant: impl Into<String>) -> Self {
120 self.variant = Some(variant.into());
121 self
122 }
123
124 pub fn with_os_version(mut self, version: impl Into<String>) -> Self {
126 self.os_version = Some(version.into());
127 self
128 }
129
130 pub fn with_os_feature(mut self, feature: impl Into<String>) -> Self {
132 self.os_features.push(feature.into());
133 self
134 }
135
136 pub fn with_feature(mut self, feature: impl Into<String>) -> Self {
138 self.features.push(feature.into());
139 self
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn test_descriptor_serde() {
149 let desc = Descriptor::new(
150 MediaType::OciLayerGzip,
151 "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
152 .parse()
153 .unwrap(),
154 1234,
155 );
156
157 let json = serde_json::to_string_pretty(&desc).unwrap();
158 let parsed: Descriptor = serde_json::from_str(&json).unwrap();
159 assert_eq!(desc, parsed);
160 }
161
162 #[test]
163 fn test_descriptor_with_platform() {
164 let desc = Descriptor::new(
165 MediaType::OciManifest,
166 "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
167 .parse()
168 .unwrap(),
169 5678,
170 )
171 .with_platform(Platform::new("amd64", "linux"));
172
173 let json = serde_json::to_string(&desc).unwrap();
174 assert!(json.contains("amd64"));
175 assert!(json.contains("linux"));
176 }
177
178 #[test]
179 fn test_descriptor_with_urls() {
180 let desc = Descriptor::new(
181 MediaType::OciLayerGzip,
182 "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
183 .parse()
184 .unwrap(),
185 1234,
186 )
187 .with_url("https://example.com/blob");
188
189 let json = serde_json::to_string(&desc).unwrap();
190 assert!(json.contains("urls"));
191 assert!(json.contains("https://example.com/blob"));
192 }
193
194 #[test]
195 fn test_annotations_deterministic_order() {
196 let desc1 = Descriptor::new(
198 MediaType::OciLayerGzip,
199 "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
200 .parse()
201 .unwrap(),
202 1234,
203 )
204 .with_annotation("z-key", "value1")
205 .with_annotation("a-key", "value2");
206
207 let desc2 = Descriptor::new(
208 MediaType::OciLayerGzip,
209 "sha256:b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
210 .parse()
211 .unwrap(),
212 1234,
213 )
214 .with_annotation("a-key", "value2")
215 .with_annotation("z-key", "value1");
216
217 let json1 = serde_json::to_string(&desc1).unwrap();
218 let json2 = serde_json::to_string(&desc2).unwrap();
219
220 assert_eq!(
221 json1, json2,
222 "annotations should serialize in deterministic order"
223 );
224 }
225}