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
#![allow(clippy::use_self)]

use crate::package::OrganizationToResolveFor;
use anyhow::bail;
use camino::{Utf8Path, Utf8PathBuf};

use serde::{Deserialize, Serialize};
use std::path::PathBuf;

use crate::{
    package::{PackageSet, PackageVersion},
    registry::Registry,
    resolve::Resolver,
};

/// Common name between all lock files for ontologies.
pub const LOCK_FILE_NAME: &str = "Plow.lock";

/// A runtime representation of a lock file.
#[derive(Debug, Clone, Default)]
pub struct LockFile {
    // Maybe necessary later.
    _path: Option<PathBuf>,
    pub locked_dependencies: PackageSet,
}

impl LockFile {
    /// Writes the conceptual lock file to the given path.
    ///
    /// This mimics Cargo.lock and produces a similar structure, like below.
    ///
    /// ```toml
    /// [[package]]
    /// name = "@namespace/name"
    /// # A complete bare version.
    /// version = "0.2.15"
    /// # Currently left empty
    /// source = ""
    /// cksum = "24606928a235e73cdef55a0c909719cadd72fce573e5713d58cb2952d8f5794c"
    /// # Set of dependencies with corresponding versions.
    /// dependencies = [
    ///  "@x/y 0.24.0",
    ///  "@a/b 1.15.0",
    /// ]
    /// ```
    pub fn write(
        workspace_root: Option<Utf8PathBuf>,
        package_set: &[PackageInLockFile],
    ) -> Result<Utf8PathBuf, anyhow::Error> {
        let packages = PackagesInLockFile {
            version: FileVersion::V1,
            packages: package_set.to_vec(),
        };

        let serialized = toml::to_string_pretty(&packages)?;

        if let Some(workspace_root) = workspace_root {
            let lock_file_path = workspace_root.join(LOCK_FILE_NAME);
            let mut lock_file_contents = "# This file is automatically generated by the Plow package manager.\n# It is not intended for manual editing.\n\n".to_owned();
            lock_file_contents += &serialized;
            std::fs::write(&lock_file_path, lock_file_contents)?;

            return Ok(lock_file_path);
        }
        bail!("There is no workspace root to write the lock file.");
    }

    pub fn deserialize_lock_file(
        lock_file_path: &Utf8Path,
    ) -> Result<PackagesInLockFile, anyhow::Error> {
        let lock_file_contents = std::fs::read_to_string(lock_file_path)?;
        Ok(toml::from_str::<PackagesInLockFile>(&lock_file_contents)?)
    }

    pub fn previous_lock_file_exists(workspace_root: Option<Utf8PathBuf>) -> Option<Utf8PathBuf> {
        workspace_root.and_then(|workspace_root| {
            let lock_file_path_in_workspace_root = workspace_root.join(LOCK_FILE_NAME);
            if lock_file_path_in_workspace_root.exists() {
                return Some(lock_file_path_in_workspace_root);
            }
            None
        })
    }

    /// Starts locking operation, resolves dependencies and write the lock file.
    pub fn lock_with_registry(
        package_to_resolve: OrganizationToResolveFor,
        registry: &dyn Registry,
        workspace_root: Option<Utf8PathBuf>,
        respect_existing_lock_file: bool,
    ) -> Result<Self, anyhow::Error> {
        // TODO: Either this or another entry point will be expanded to support db based locks in the future.

        let (resolved_dependencies, _previously_locked_path) =
            if let Some(lock_file_path) = Self::previous_lock_file_exists(workspace_root) {
                if respect_existing_lock_file {
                    // With existing lock file input
                    let packages = Self::deserialize_lock_file(&lock_file_path)?
                        .packages
                        .iter()
                        .cloned()
                        // TODO:
                        // Currently filter the local resolutions out.
                        // Will be addressed soon.
                        .filter(|p| !p.root)
                        .collect::<Vec<_>>();

                    (
                        Into::<crate::resolve::VersionRequestResolver>::into(registry)
                            .resolve_dependencies(package_to_resolve, Some(&packages))?,
                        Some(lock_file_path),
                    )
                } else {
                    (
                        Into::<crate::resolve::VersionRequestResolver>::into(registry)
                            .resolve_dependencies(package_to_resolve, None)?,
                        None,
                    )
                }
            } else {
                // No lockfile input
                (
                    Into::<crate::resolve::VersionRequestResolver>::into(registry)
                        .resolve_dependencies(package_to_resolve, None)?,
                    None,
                )
            };

        // TODO: Do not write in this function!
        // let resolved_dependencies_with_metadata: Vec<PackageVersionWithRegistryMetadata> =
        //     resolved_dependencies
        //         .packages
        //         .iter()
        //         .map(|package_version| registry.get_package_version_metadata(package_version))
        //         .collect::<Result<Vec<_>, _>>()?;

        // if !resolved_dependencies_with_metadata.is_empty() {
        //     // Do not write a lock file if there are no dependencies.
        //     let path = Self::write(selected_file, &resolved_dependencies_with_metadata)?;
        //     return Ok(Self {
        //         _path: Some(path),
        //         locked_dependencies: resolved_dependencies,
        //     });
        // }

        // if let Some(existing_lock_file_path) = previously_locked_path {
        //     if resolved_dependencies_with_metadata.is_empty() {
        //         // If there are no dependencies, but there is an existing lock file, remove it.
        //         std::fs::remove_file(existing_lock_file_path)?;
        //     }
        // }

        // Always re-writing the lock file even if it is the same,
        // I think it is harmless since we're not interested on the creation time.
        Ok(Self {
            _path: None,
            locked_dependencies: resolved_dependencies,
        })
    }
}

#[derive(Serialize, Deserialize)]
pub enum FileVersion {
    #[serde(rename(serialize = "1", deserialize = "1"))]
    V1,
}
/// Set of packages in the form to be serialized to or to be deserialized to or from the lock file.
#[derive(Serialize, Deserialize)]
#[serde(rename(serialize = "package", deserialize = "package"))]
pub struct PackagesInLockFile {
    version: FileVersion,
    #[serde(rename(serialize = "package", deserialize = "package"))]
    packages: Vec<PackageInLockFile>,
}

impl PackagesInLockFile {
    pub fn packages(&self) -> &[PackageInLockFile] {
        &self.packages
    }
}

/// A package in the form to be serialized to or to be deserialized to or from the lock file.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct PackageInLockFile {
    pub root: bool,
    pub name: String,
    pub version: String,
    pub source: Option<String>,
    pub ontology_iri: Option<String>,
    pub cksum: Option<String>,
    pub dependencies: Vec<String>,
}

// Only one way conversion is allowed, for convenience.
#[allow(clippy::from_over_into)]
impl Into<PackageVersion> for &PackageInLockFile {
    fn into(self) -> PackageVersion {
        PackageVersion {
            package_name: self.name.clone(),
            version: self.version.clone(),
        }
    }
}

// impl From<PackageVersionWithRegistryMetadata> for PackageInLockFile {
//     fn from(package: PackageVersionWithRegistryMetadata) -> Self {
//         Self {
//             name: package.package_name,
//             version: package.version.to_string(),
//             // TODO: For example, source = "registry+https://github.com/rust-lang/crates.io-index" ??
//             source: None,
//             ontology_iri: package.ontology_iri,
//             cksum: package.cksum,
//             dependencies: package
//                 .dependencies
//                 .iter()
//                 .map(std::string::ToString::to_string)
//                 .collect_vec(),
//         }
//     }
// }