Fork of daniellemaywood.uk/gleam — Wasm codegen work
6.9 kB
243 lines
1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2023 The Gleam contributors
3
4use std::fmt;
5use std::str::FromStr;
6
7use crate::Error;
8use crate::error::Result;
9use crate::io::make_relative;
10use camino::{Utf8Path, Utf8PathBuf};
11use ecow::EcoString;
12use hexpm::version::Range;
13use serde::Deserialize;
14use serde::de::{self, Deserializer, MapAccess, Visitor};
15use serde::ser::{Serialize, SerializeMap, Serializer};
16
17#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
18#[serde(untagged, remote = "Self", deny_unknown_fields)]
19pub enum Requirement {
20 Hex {
21 #[serde(deserialize_with = "deserialise_range")]
22 version: Range,
23 },
24
25 Path {
26 path: Utf8PathBuf,
27 },
28
29 Git {
30 git: EcoString,
31 #[serde(rename = "ref")]
32 ref_: EcoString,
33 #[serde(default)]
34 path: Option<Utf8PathBuf>,
35 },
36}
37
38impl Requirement {
39 pub fn hex(range: &str) -> Result<Requirement> {
40 Ok(Requirement::Hex {
41 version: Range::new(range.to_string()).map_err(|e| Error::InvalidVersionFormat {
42 input: range.to_string(),
43 error: e.to_string(),
44 })?,
45 })
46 }
47
48 pub fn path(path: &str) -> Requirement {
49 Requirement::Path { path: path.into() }
50 }
51
52 pub fn git(url: &str, ref_: &str) -> Requirement {
53 Requirement::Git {
54 git: url.into(),
55 ref_: ref_.into(),
56 path: None,
57 }
58 }
59
60 pub fn git_with_path(url: &str, ref_: &str, path: &str) -> Requirement {
61 Requirement::Git {
62 git: url.into(),
63 ref_: ref_.into(),
64 path: Some(path.into()),
65 }
66 }
67
68 pub fn to_toml(&self, root_path: &Utf8Path) -> String {
69 match self {
70 Requirement::Hex { version: range } => {
71 format!(r#"{{ version = "{range}" }}"#)
72 }
73 Requirement::Path { path } => {
74 format!(
75 r#"{{ path = "{}" }}"#,
76 make_relative(root_path, path).as_str().replace('\\', "/")
77 )
78 }
79 Requirement::Git {
80 git: url,
81 ref_,
82 path,
83 } => match path {
84 Some(path) => {
85 let path = path.as_str().replace('\\', "/");
86 format!(r#"{{ git = "{url}", ref = "{ref_}", path = "{path}" }}"#)
87 }
88 None => format!(r#"{{ git = "{url}", ref = "{ref_}" }}"#),
89 },
90 }
91 }
92}
93
94// Serialization
95
96impl Serialize for Requirement {
97 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
98 where
99 S: Serializer,
100 {
101 let mut map = serializer.serialize_map(Some(1))?;
102 match self {
103 Requirement::Hex { version: range } => map.serialize_entry("version", range)?,
104 Requirement::Path { path } => map.serialize_entry("path", path)?,
105 Requirement::Git {
106 git: url,
107 ref_,
108 path,
109 } => {
110 map.serialize_entry("git", url)?;
111 map.serialize_entry("ref", ref_)?;
112 if let Some(path) = path {
113 map.serialize_entry("path", path)?;
114 }
115 }
116 }
117 map.end()
118 }
119}
120
121// Deserialization
122
123fn deserialise_range<'de, D>(deserializer: D) -> Result<Range, D::Error>
124where
125 D: Deserializer<'de>,
126{
127 let version = String::deserialize(deserializer)?;
128 Range::new(version).map_err(de::Error::custom)
129}
130
131#[derive(Debug, Copy, Clone)]
132pub struct Void;
133
134impl FromStr for Requirement {
135 type Err = Error;
136
137 fn from_str(s: &str) -> Result<Self, Self::Err> {
138 Requirement::hex(s)
139 }
140}
141
142struct RequirementVisitor;
143
144impl<'de> Visitor<'de> for RequirementVisitor {
145 type Value = Requirement;
146
147 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
148 formatter.write_str("string or map")
149 }
150
151 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
152 where
153 E: de::Error,
154 {
155 match value.parse::<Requirement>() {
156 Ok(value) => Ok(value),
157 Err(error) => Err(de::Error::custom(error)),
158 }
159 }
160
161 fn visit_map<M>(self, visitor: M) -> Result<Self::Value, M::Error>
162 where
163 M: MapAccess<'de>,
164 {
165 Requirement::deserialize(de::value::MapAccessDeserializer::new(visitor))
166 }
167}
168
169impl<'de> Deserialize<'de> for Requirement {
170 fn deserialize<D>(deserializer: D) -> Result<Requirement, D::Error>
171 where
172 D: Deserializer<'de>,
173 {
174 let requirement = deserializer.deserialize_any(RequirementVisitor)?;
175 if let Requirement::Git {
176 path: Some(path), ..
177 } = &requirement
178 {
179 if path.as_str().is_empty() {
180 return Err(de::Error::custom("git dependency path must not be empty"));
181 }
182 crate::io::validate_safe_relative_path(path).map_err(de::Error::custom)?;
183 }
184 Ok(requirement)
185 }
186}
187
188#[cfg(test)]
189mod tests {
190
191 use super::*;
192 use std::collections::HashMap;
193
194 #[test]
195 fn read_requirement() {
196 let toml = r#"
197 short = "~> 0.5"
198 hex = { version = "~> 1.0.0" }
199 local = { path = "/path/to/package" }
200 github = { git = "https://github.com/gleam-lang/otp.git", ref = "4d34935" }
201 "#;
202 let deps: HashMap<String, Requirement> = toml::from_str(toml).unwrap();
203 assert_eq!(deps["short"], Requirement::hex("~> 0.5").unwrap());
204 assert_eq!(deps["hex"], Requirement::hex("~> 1.0.0").unwrap());
205 assert_eq!(deps["local"], Requirement::path("/path/to/package"));
206 assert_eq!(
207 deps["github"],
208 Requirement::git("https://github.com/gleam-lang/otp.git", "4d34935")
209 );
210 }
211
212 #[test]
213 fn read_wrong_version() {
214 let toml = r#"
215 short = ">= 2.0 and < 3.0.0"
216 "#;
217
218 let error =
219 toml::from_str::<HashMap<String, Requirement>>(toml).expect_err("invalid version");
220 insta::assert_snapshot!(error.to_string());
221 }
222
223 #[test]
224 fn read_git_requirement_with_escaping_path() {
225 let toml = r#"
226 monorepo = { git = "https://github.com/gleam-lang/gleam.git", ref = "main", path = "../escape" }
227 "#;
228
229 let error =
230 toml::from_str::<HashMap<String, Requirement>>(toml).expect_err("escaping path");
231 insta::assert_snapshot!(error.to_string());
232 }
233
234 #[test]
235 fn read_git_requirement_with_empty_path() {
236 let toml = r#"
237 monorepo = { git = "https://github.com/gleam-lang/gleam.git", ref = "main", path = "" }
238 "#;
239
240 let error = toml::from_str::<HashMap<String, Requirement>>(toml).expect_err("empty path");
241 insta::assert_snapshot!(error.to_string());
242 }
243}