forc_pkg/source/reg/
index_file.rs

1//! This module handles everything to do with index files.
2//!
3//! Index files are for creating set of information for identifying a published
4//! package. They are used by forc while fetching to actually convert a registry
5//! index into a IPFS CID. We also add some metadata to this index files to
6//! enable forc to do "more clever" fetching during build process. By moving
7//! dependency resolution from the time a package is fetched to the point we
8//! start fetching we are actively enabling forc to fetch packages and their
9//! dependencies in parallel.
10//!
11//! There are two main things forc needs to be able to do for index files:
12//!   1: Creation of index files from published packages
13//!   2: Calculating correct path for given package index.
14use serde::{Deserialize, Serialize};
15use std::collections::BTreeMap;
16
17#[derive(Serialize, Deserialize, Default)]
18pub struct IndexFile {
19    /// Each published instance for this specific package, keyed by their
20    /// versions. The reason we are doing this type of mapping is for use of
21    /// ease and deterministic ordering, we are effectively duplicating version
22    /// of package but keeping `PackageEntry` self contained.
23    #[serde(flatten)]
24    versions: BTreeMap<semver::Version, PackageEntry>,
25}
26
27/// A unique representation of each published package to `forc.pub`. Contains:
28///
29/// 1. The name of the package.
30/// 2. The version of the package.
31/// 3. CID of the package's source code. This is how forc actually resolves a
32///    package name, version information into actual information on how to get
33///    the package.
34/// 4. CID of the package's abi if the package is a contract.
35/// 5. Dependencies of this package. If there are other packages this package
36///    depends on, some information can be directly found in the root package
37///    to enable parallel fetching.
38#[derive(Serialize, Deserialize, Clone)]
39pub struct PackageEntry {
40    /// Name of the package.
41    /// This is the actual package name needed in forc.toml file to fetch this
42    /// package.
43    #[serde(alias = "package_name")]
44    name: String,
45    /// Version of the package.
46    /// This is the actual package version needed in forc.toml file to fetch
47    /// this package.
48    version: semver::Version,
49    /// IPFS CID of this specific package's source code. This is pinned by
50    /// forc.pub at the time of package publishing and thus will be
51    /// available all the time.
52    source_cid: String,
53    /// IPFS CID of this specific package's abi. This is pinned by
54    /// forc.pub at the time of package publishing and thus will be
55    /// available all the time if this exists in the first place, i.e the
56    /// package is a contract.
57    abi_cid: Option<String>,
58    /// Dependencies of the current package entry. Can be consumed to enable
59    /// parallel fetching by the consumers of this index, mainly forc.
60    dependencies: Vec<PackageDependencyIdentifier>,
61    /// Determines if the package should be skipped while building. Marked as
62    /// voided by the publisher for various reasons.
63    yanked: bool,
64}
65
66#[derive(Serialize, Deserialize, Clone)]
67pub struct PackageDependencyIdentifier {
68    /// Name of the dependency.
69    /// Name and version information can be used by consumer of this index
70    /// to resolve dependencies.
71    package_name: String,
72    /// Version of the dependency.
73    /// Name and version information can be used by consumer of this index
74    /// to resolve dependencies.
75    version: String,
76}
77
78impl PackageEntry {
79    pub fn new(
80        name: String,
81        version: semver::Version,
82        source_cid: String,
83        abi_cid: Option<String>,
84        dependencies: Vec<PackageDependencyIdentifier>,
85        yanked: bool,
86    ) -> Self {
87        Self {
88            name,
89            version,
90            source_cid,
91            abi_cid,
92            dependencies,
93            yanked,
94        }
95    }
96
97    /// Returns the name of this `PackageEntry`.
98    pub fn name(&self) -> &str {
99        &self.name
100    }
101
102    /// Returns the version of this `PackageEntry`.
103    pub fn version(&self) -> &semver::Version {
104        &self.version
105    }
106
107    /// Returns the source cid of this `PackageEntry`.
108    pub fn source_cid(&self) -> &str {
109        &self.source_cid
110    }
111
112    /// Returns the abi cid of this `PackageEntry`.
113    pub fn abi_cid(&self) -> Option<&str> {
114        self.abi_cid.as_deref()
115    }
116
117    /// Returns an iterator over dependencies of this package.
118    pub fn dependencies(&self) -> impl Iterator<Item = &PackageDependencyIdentifier> {
119        self.dependencies.iter()
120    }
121
122    /// Returns the `yanked` status of this package.
123    pub fn yanked(&self) -> bool {
124        self.yanked
125    }
126}
127
128impl PackageDependencyIdentifier {
129    pub fn new(package_name: String, version: String) -> Self {
130        Self {
131            package_name,
132            version,
133        }
134    }
135}
136impl IndexFile {
137    /// Returns the package entry if the specified version exists.
138    /// Otherwise returns `None`.
139    pub fn get(&self, version: &semver::Version) -> Option<&PackageEntry> {
140        self.versions.get(version)
141    }
142
143    /// Inserts a package into this `IndexFile`
144    /// NOTE: if there is a package with the same version in the index file
145    /// it will get overridden.
146    pub fn insert(&mut self, package: PackageEntry) {
147        let pkg_version = package.version().clone();
148        self.versions.insert(pkg_version, package);
149    }
150
151    /// Returns an iterator over the versions in the index file.
152    pub fn versions(&self) -> impl Iterator<Item = &semver::Version> {
153        self.versions.keys()
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn test_serialize_deserialize_empty_index() {
163        let index = IndexFile {
164            versions: BTreeMap::new(),
165        };
166
167        let serialized = serde_json::to_string(&index).unwrap();
168        assert_eq!(serialized, "{}");
169        let deserialized: IndexFile = serde_json::from_str(&serialized).unwrap();
170        assert_eq!(deserialized.versions.len(), 0);
171    }
172
173    #[test]
174    fn test_json_format() {
175        // Test parsing from a JSON
176        let json = r#"{
177        "0.0.1":{
178            "package_name":"tester",
179            "version":"0.0.1",
180            "source_cid":"QmOlderHash",
181            "abi_cid":"QmOlderAbiHash",
182            "dependencies":[],
183            "yanked": false
184        },
185        "0.0.2":{
186            "package_name":"tester",
187            "version":"0.0.2",
188            "source_cid":"QmExampleHash",
189            "abi_cid":"QmExampleAbiHash",
190            "dependencies":[],
191            "yanked": false
192        }
193    }"#;
194
195        let deserialized: IndexFile = serde_json::from_str(json).unwrap();
196
197        assert_eq!(deserialized.versions.len(), 2);
198        assert!(deserialized
199            .versions
200            .contains_key(&semver::Version::new(0, 0, 1)));
201        assert!(deserialized
202            .versions
203            .contains_key(&semver::Version::new(0, 0, 2)));
204
205        let v011 = &deserialized.versions[&semver::Version::new(0, 0, 1)];
206        assert_eq!(v011.source_cid, "QmOlderHash");
207        assert_eq!(v011.abi_cid, Some("QmOlderAbiHash".to_string()));
208        assert_eq!(v011.dependencies.len(), 0);
209
210        let v012 = &deserialized.versions[&semver::Version::new(0, 0, 2)];
211        assert_eq!(v012.source_cid, "QmExampleHash");
212        assert_eq!(v012.abi_cid, Some("QmExampleAbiHash".to_string()));
213        assert_eq!(v012.dependencies.len(), 0);
214    }
215
216    #[test]
217    fn test_add_new_package_entry_and_parse_back() {
218        let json = r#"{
219        "1.0.0": {
220            "name": "existing-package",
221            "version": "1.0.0",
222            "source_cid": "QmExistingHash",
223            "abi_cid": "QmExistingAbiHash",
224            "dependencies": [
225                {
226                    "package_name": "dep1",
227                    "version": "^0.5.0"
228                }
229            ],
230            "yanked": false
231        }
232    }"#;
233
234        let mut index_file: IndexFile = serde_json::from_str(json).unwrap();
235
236        assert_eq!(index_file.versions.len(), 1);
237        assert!(index_file
238            .versions
239            .contains_key(&semver::Version::new(1, 0, 0)));
240
241        let dependencies = vec![
242            PackageDependencyIdentifier::new("new-dep1".to_string(), "^1.0.0".to_string()),
243            PackageDependencyIdentifier::new("new-dep2".to_string(), "=0.9.0".to_string()),
244        ];
245
246        let yanked = false;
247
248        let new_package = PackageEntry::new(
249            "new-package".to_string(),
250            semver::Version::new(2, 1, 0),
251            "QmNewPackageHash".to_string(),
252            Some("QmNewPackageAbiHash".to_string()),
253            dependencies,
254            yanked,
255        );
256
257        index_file.insert(new_package);
258
259        assert_eq!(index_file.versions.len(), 2);
260        assert!(index_file
261            .versions
262            .contains_key(&semver::Version::new(1, 0, 0)));
263        assert!(index_file
264            .versions
265            .contains_key(&semver::Version::new(2, 1, 0)));
266
267        let updated_json = serde_json::to_string_pretty(&index_file).unwrap();
268        let reparsed_index: IndexFile = serde_json::from_str(&updated_json).unwrap();
269
270        assert_eq!(reparsed_index.versions.len(), 2);
271        assert!(reparsed_index
272            .versions
273            .contains_key(&semver::Version::new(1, 0, 0)));
274        assert!(reparsed_index
275            .versions
276            .contains_key(&semver::Version::new(2, 1, 0)));
277
278        let new_pkg = reparsed_index.get(&semver::Version::new(2, 1, 0)).unwrap();
279        assert_eq!(new_pkg.name(), "new-package");
280        assert_eq!(new_pkg.version(), &semver::Version::new(2, 1, 0));
281        assert_eq!(new_pkg.source_cid(), "QmNewPackageHash");
282        assert_eq!(new_pkg.abi_cid(), Some("QmNewPackageAbiHash"));
283
284        let deps: Vec<_> = new_pkg.dependencies().collect();
285        assert_eq!(deps.len(), 2);
286        assert_eq!(deps[0].package_name, "new-dep1");
287        assert_eq!(deps[0].version, "^1.0.0");
288        assert_eq!(deps[1].package_name, "new-dep2");
289        assert_eq!(deps[1].version, "=0.9.0");
290
291        let orig_pkg = reparsed_index.get(&semver::Version::new(1, 0, 0)).unwrap();
292        assert_eq!(orig_pkg.name(), "existing-package");
293        assert_eq!(orig_pkg.source_cid(), "QmExistingHash");
294    }
295
296    #[test]
297    fn test_json_with_dependencies() {
298        // Test parsing a JSON with dependencies
299        let json = r#"{
300            "1.0.0": {
301                "package_name": "main-package",
302                "version": "1.0.0",
303                "source_cid": "QmMainHash",
304                "abi_cid": null,
305                "dependencies": [
306                    {
307                        "package_name": "dep-package",
308                        "version": "^0.5.0"
309                    },
310                    {
311                        "package_name": "another-dep",
312                        "version": "=0.9.1"
313                    },
314                    {
315                        "package_name": "third-dep",
316                        "version": "0.2.0"
317                    }
318                ],
319                "yanked": false
320            }
321        }"#;
322
323        let deserialized: IndexFile = serde_json::from_str(json).unwrap();
324
325        // Verify main package
326        assert_eq!(deserialized.versions.len(), 1);
327        assert!(deserialized
328            .versions
329            .contains_key(&semver::Version::new(1, 0, 0)));
330
331        let main_pkg = &deserialized.versions[&semver::Version::new(1, 0, 0)];
332        assert_eq!(main_pkg.name, "main-package");
333        assert_eq!(main_pkg.source_cid, "QmMainHash");
334        assert_eq!(main_pkg.abi_cid, None);
335        assert!(!main_pkg.yanked);
336
337        // Verify dependencies
338        assert_eq!(main_pkg.dependencies.len(), 3);
339
340        // Check first dependency
341        let dep1 = &main_pkg.dependencies[0];
342        assert_eq!(dep1.package_name, "dep-package");
343        assert_eq!(dep1.version, "^0.5.0");
344
345        // Check second dependency
346        let dep2 = &main_pkg.dependencies[1];
347        assert_eq!(dep2.package_name, "another-dep");
348        assert_eq!(dep2.version, "=0.9.1");
349
350        // Check third dependency
351        let dep3 = &main_pkg.dependencies[2];
352        assert_eq!(dep3.package_name, "third-dep");
353        assert_eq!(dep3.version, "0.2.0");
354
355        // Test round-trip serialization
356        let serialized = serde_json::to_string_pretty(&deserialized).unwrap();
357        println!("Re-serialized JSON: {}", serialized);
358
359        // Deserialize again to ensure it's valid
360        let re_deserialized: IndexFile = serde_json::from_str(&serialized).unwrap();
361        assert_eq!(re_deserialized.versions.len(), 1);
362
363        // Verify the structure is preserved
364        let main_pkg2 = &re_deserialized.versions[&semver::Version::new(1, 0, 0)];
365        assert_eq!(main_pkg2.dependencies.len(), 3);
366    }
367
368    #[test]
369    fn test_json_with_missing_optional_fields() {
370        // Test parsing a JSON where some optional fields are missing
371        let json = r#"{
372            "0.5.0": {
373                "package_name": "minimal-package",
374                "version": "0.5.0",
375                "source_cid": "QmMinimalHash",
376                "dependencies": [],
377                "yanked": false
378            }
379        }"#;
380
381        let deserialized: IndexFile = serde_json::from_str(json).unwrap();
382
383        assert_eq!(deserialized.versions.len(), 1);
384        let pkg = &deserialized.versions[&semver::Version::new(0, 5, 0)];
385        assert_eq!(pkg.name, "minimal-package");
386        assert_eq!(pkg.source_cid, "QmMinimalHash");
387        assert_eq!(pkg.abi_cid, None);
388        assert_eq!(pkg.dependencies.len(), 0);
389    }
390}