Fork of daniellemaywood.uk/gleam — Wasm codegen work
46 kB
1521 lines
1mod stale_package_remover;
2use crate::error::{FileIoAction, FileKind};
3use crate::io::FileSystemReader;
4use crate::io::ordered_map;
5use crate::manifest::Manifest;
6use crate::requirement::Requirement;
7use crate::version::COMPILER_VERSION;
8use crate::{Error, Result};
9use camino::{Utf8Path, Utf8PathBuf};
10use ecow::EcoString;
11use globset::{Glob, GlobSetBuilder};
12use hexpm::version::{self, LowestVersion, Version};
13use http::Uri;
14use serde::ser::SerializeSeq;
15use serde::{Deserialize, Serialize};
16use std::collections::HashMap;
17use std::fmt::{self};
18use std::marker::PhantomData;
19
20#[cfg(test)]
21use crate::manifest::ManifestPackage;
22
23use crate::build::{Mode, Runtime, Target};
24
25fn default_version() -> Version {
26 Version::parse("0.1.0").expect("default version")
27}
28
29fn erlang_target() -> Target {
30 Target::Erlang
31}
32
33fn default_javascript_runtime() -> Runtime {
34 Runtime::NodeJs
35}
36
37#[derive(Clone, Debug, PartialEq, Eq)]
38pub struct SpdxLicense {
39 pub licence: String,
40}
41
42impl ToString for SpdxLicense {
43 fn to_string(&self) -> String {
44 String::from(&self.licence)
45 }
46}
47
48impl<'de> Deserialize<'de> for SpdxLicense {
49 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
50 where
51 D: serde::Deserializer<'de>,
52 {
53 deserializer.deserialize_str(SpdxLicenseVisitor)
54 }
55}
56
57struct SpdxLicenseVisitor;
58
59impl<'de> serde::de::Visitor<'de> for SpdxLicenseVisitor {
60 type Value = SpdxLicense;
61
62 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
63 formatter.write_str("a SPDX License ID")
64 }
65
66 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
67 where
68 E: serde::de::Error,
69 {
70 match spdx::license_id(value) {
71 None => Err(serde::de::Error::custom(format!(
72 "{value} is not a known SPDX License identifier"
73 ))),
74 Some(_) => Ok(SpdxLicense {
75 licence: value.to_string(),
76 }),
77 }
78 }
79}
80
81impl Serialize for SpdxLicense {
82 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
83 where
84 S: serde::Serializer,
85 {
86 serializer.serialize_str(&self.licence)
87 }
88}
89
90impl AsRef<str> for SpdxLicense {
91 fn as_ref(&self) -> &str {
92 self.licence.as_str()
93 }
94}
95
96#[derive(Debug, PartialEq, Clone)]
97pub struct GleamVersion(version::Range);
98impl From<version::Range> for GleamVersion {
99 fn from(range: version::Range) -> Self {
100 Self(range)
101 }
102}
103
104impl From<GleamVersion> for version::Range {
105 fn from(gleam_version: GleamVersion) -> Self {
106 gleam_version.0
107 }
108}
109
110impl From<GleamVersion> for pubgrub::Range<Version> {
111 fn from(gleam_version: GleamVersion) -> Self {
112 gleam_version.0.into()
113 }
114}
115
116impl GleamVersion {
117 pub fn from_pubgrub(range: pubgrub::Range<Version>) -> Self {
118 let range: version::Range = range.into();
119 range.into()
120 }
121
122 pub fn as_pubgrub(&self) -> &pubgrub::Range<Version> {
123 self.0.to_pubgrub()
124 }
125
126 pub fn new(spec: String) -> Result<GleamVersion> {
127 let hex =
128 version::Range::new(spec.to_string()).map_err(|e| Error::InvalidVersionFormat {
129 input: spec,
130 error: e.to_string(),
131 })?;
132 Ok(hex.into())
133 }
134
135 pub fn lowest_version(&self) -> Option<Version> {
136 self.as_pubgrub().lowest_version()
137 }
138
139 pub fn hex(&self) -> &version::Range {
140 &self.0
141 }
142}
143
144#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
145pub struct PackageConfig {
146 #[serde(deserialize_with = "package_name::deserialize")]
147 pub name: EcoString,
148 #[serde(default = "default_version")]
149 pub version: Version,
150
151 #[serde(
152 default,
153 rename = "gleam",
154 deserialize_with = "deserialise_gleam_version",
155 serialize_with = "serialise_gleam_version"
156 )]
157 pub gleam_version: Option<GleamVersion>,
158 #[serde(default, alias = "licenses")]
159 pub licences: Vec<SpdxLicense>,
160 #[serde(default)]
161 pub description: EcoString,
162 #[serde(default, alias = "docs")]
163 pub documentation: Docs,
164 #[serde(
165 default,
166 serialize_with = "ordered_map",
167 deserialize_with = "map_with_package_name_keys ::deserialize"
168 )]
169 pub dependencies: HashMap<EcoString, Requirement>,
170 #[serde(
171 default,
172 alias = "dev-dependencies",
173 serialize_with = "ordered_map",
174 deserialize_with = "map_with_package_name_keys ::deserialize"
175 )]
176 pub dev_dependencies: HashMap<EcoString, Requirement>,
177 #[serde(default)]
178 pub repository: Option<Repository>,
179 #[serde(default)]
180 pub links: Vec<Link>,
181 #[serde(default)]
182 pub erlang: ErlangConfig,
183 #[serde(default)]
184 pub javascript: JavaScriptConfig,
185 #[serde(default = "erlang_target")]
186 pub target: Target,
187 #[serde(default)]
188 pub internal_modules: Option<Vec<Glob>>,
189}
190
191pub fn serialise_gleam_version<S>(
192 gleam_gersion: &Option<GleamVersion>,
193 serializer: S,
194) -> Result<S::Ok, S::Error>
195where
196 S: serde::Serializer,
197{
198 match gleam_gersion {
199 Some(version) => serializer.serialize_str(&version.hex().to_string()),
200 None => serializer.serialize_none(),
201 }
202}
203
204pub fn deserialise_gleam_version<'de, D>(deserialiser: D) -> Result<Option<GleamVersion>, D::Error>
205where
206 D: serde::Deserializer<'de>,
207{
208 match Deserialize::deserialize(deserialiser)? {
209 Some(range_string) => {
210 let hex = version::Range::new(range_string).map_err(serde::de::Error::custom)?;
211 Ok(Some(hex.into()))
212 }
213 None => Ok(None),
214 }
215}
216
217impl PackageConfig {
218 pub fn dependencies_for(&self, mode: Mode) -> Result<HashMap<EcoString, Requirement>> {
219 match mode {
220 Mode::Dev | Mode::Lsp => self.all_direct_dependencies(),
221 Mode::Prod => Ok(self.dependencies.clone()),
222 }
223 }
224
225 // Return all the dependencies listed in the configuration, that is, all the
226 // direct dependencies, both in the `dependencies` and `dev_dependencies`.
227 pub fn all_direct_dependencies(&self) -> Result<HashMap<EcoString, Requirement>> {
228 let mut deps =
229 HashMap::with_capacity(self.dependencies.len() + self.dev_dependencies.len());
230 for (name, requirement) in self.dependencies.iter().chain(&self.dev_dependencies) {
231 let already_inserted = deps.insert(name.clone(), requirement.clone()).is_some();
232 if already_inserted {
233 return Err(Error::DuplicateDependency(name.clone()));
234 }
235 }
236 Ok(deps)
237 }
238
239 pub fn read<FS: FileSystemReader, P: AsRef<Utf8Path>>(
240 path: P,
241 fs: &FS,
242 ) -> Result<PackageConfig, Error> {
243 let toml = fs.read(path.as_ref())?;
244 deserialise_config(path, toml)
245 }
246
247 /// Get the locked packages for the current config and a given (optional)
248 /// manifest of previously locked packages.
249 ///
250 /// If a package is removed or the specified required version range for it
251 /// changes then it is not considered locked. This also goes for any child
252 /// packages of the package which have no other parents.
253 ///
254 /// This function should be used each time resolution is performed so that
255 /// outdated deps are removed from the manifest and not locked to the
256 /// previously selected versions.
257 ///
258 pub fn locked(&self, manifest: Option<&Manifest>) -> Result<HashMap<EcoString, Version>> {
259 match manifest {
260 None => Ok(HashMap::new()),
261 Some(manifest) => {
262 let requirements = self.all_direct_dependencies()?;
263 let fresh_and_locked = stale_package_remover::StalePackageRemover::fresh_and_locked(
264 &requirements,
265 manifest,
266 );
267 Ok(fresh_and_locked)
268 }
269 }
270 }
271
272 /// Determines whether the given module should be hidden in the docs or not
273 ///
274 /// The developer can specify a list of glob patterns in the gleam.toml file
275 /// to determine modules that should not be shown in the package's documentation
276 pub fn is_internal_module(&self, module: &str) -> bool {
277 let package = &self.name;
278 match &self.internal_modules {
279 Some(globs) => {
280 let mut builder = GlobSetBuilder::new();
281 for glob in globs {
282 _ = builder.add(glob.clone());
283 }
284 builder.build()
285 }
286
287 // If no patterns were specified in the config then we use a default value
288 None => GlobSetBuilder::new()
289 .add(Glob::new(&format!("{package}/internal")).expect("internal module glob"))
290 .add(Glob::new(&format!("{package}/internal/*")).expect("internal module glob"))
291 .build(),
292 }
293 .expect("internal module globs")
294 .is_match(module)
295 }
296
297 // Checks to see if the gleam version specified in the config is compatible
298 // with the current compiler version
299 pub fn check_gleam_compatibility(&self) -> Result<(), Error> {
300 if let Some(version) = &self.gleam_version {
301 let range = version.as_pubgrub();
302 let compiler_version =
303 Version::parse(COMPILER_VERSION).expect("Parse compiler semantic version");
304
305 // We ignore the pre-release and build metadata when checking compatibility
306 let mut version_without_pre = compiler_version.clone();
307 version_without_pre.pre = vec![];
308 version_without_pre.build = None;
309 if !range.contains(&version_without_pre) {
310 return Err(Error::IncompatibleCompilerVersion {
311 package: self.name.to_string(),
312 required_version: range.to_string(),
313 gleam_version: COMPILER_VERSION.to_string(),
314 });
315 }
316 }
317 Ok(())
318 }
319
320 pub fn tag_for_version(&self, version: &Version) -> String {
321 let prefix = match self.repository.as_ref() {
322 Some(
323 Repository::GitHub { tag_prefix, .. }
324 | Repository::GitLab { tag_prefix, .. }
325 | Repository::BitBucket { tag_prefix, .. }
326 | Repository::Codeberg { tag_prefix, .. }
327 | Repository::SourceHut { tag_prefix, .. }
328 | Repository::Gitea { tag_prefix, .. }
329 | Repository::Forgejo { tag_prefix, .. }
330 | Repository::Tangled { tag_prefix, .. },
331 ) => tag_prefix.as_ref(),
332
333 Some(Repository::Custom { .. }) | None => None,
334 };
335
336 match prefix {
337 Some(prefix) => format!("{prefix}v{version}"),
338 None => format!("v{version}"),
339 }
340 }
341}
342
343fn deserialise_config<P: AsRef<Utf8Path>>(
344 path: P,
345 toml: String,
346) -> std::result::Result<PackageConfig, Error> {
347 let config: PackageConfig = toml::from_str(&toml).map_err(|e| Error::FileIo {
348 action: FileIoAction::Parse,
349 kind: FileKind::File,
350 path: path.as_ref().to_path_buf(),
351 err: Some(e.to_string()),
352 })?;
353 Ok(config)
354}
355
356// https://github.com/gleam-lang/gleam/issues/4867
357#[test]
358fn deny_extra_deps_properties() {
359 let toml = r#"
360name = "wibble"
361version = "1.0.0"
362
363[dependencies]
364aide_generator = { git = "git@github.com:crowdhailer/aide.git", ref = "f559c5bc", extra = "idk what this is" }
365"#;
366 let error = deserialise_config("gleam.toml", toml.into())
367 .expect_err("should fail to deserialise because of additional path");
368
369 insta::assert_snapshot!(insta::internals::AutoName, error.pretty_string());
370}
371
372#[test]
373fn invalid_dependency_name() {
374 let toml = r#"
375name = "wibble"
376version = "1.0.0"
377
378[dependencies]
379Hello = "> 1.0.0"
380"#;
381 let error = deserialise_config("gleam.toml", toml.into())
382 .expect_err("should fail to deserialise because invalid name");
383 insta::assert_snapshot!(insta::internals::AutoName, error.pretty_string());
384}
385
386#[test]
387fn invalid_dev_dependency_name() {
388 let toml = r#"
389name = "wibble"
390version = "1.0.0"
391
392[dev_dependencies]
393"yes." = "> 1.0.0"
394"#;
395 let error = deserialise_config("gleam.toml", toml.into())
396 .expect_err("should fail to deserialise because invalid name");
397 insta::assert_snapshot!(insta::internals::AutoName, error.pretty_string());
398}
399
400#[test]
401fn locked_no_manifest() {
402 let mut config = PackageConfig::default();
403 config.dependencies = [
404 ("prod1".into(), Requirement::hex("~> 1.0").unwrap()),
405 ("prod2".into(), Requirement::hex("~> 2.0").unwrap()),
406 ]
407 .into();
408 config.dev_dependencies = [
409 ("dev1".into(), Requirement::hex("~> 1.0").unwrap()),
410 ("dev2".into(), Requirement::hex("~> 2.0").unwrap()),
411 ]
412 .into();
413 assert_eq!(config.locked(None).unwrap(), [].into());
414}
415
416#[test]
417fn locked_no_changes() {
418 let mut config = PackageConfig::default();
419 config.dependencies = [
420 ("prod1".into(), Requirement::hex("~> 1.0").unwrap()),
421 ("prod2".into(), Requirement::hex("~> 2.0").unwrap()),
422 ]
423 .into();
424 config.dev_dependencies = [
425 ("dev1".into(), Requirement::hex("~> 1.0").unwrap()),
426 ("dev2".into(), Requirement::hex("~> 2.0").unwrap()),
427 ]
428 .into();
429 let manifest = Manifest {
430 requirements: config.all_direct_dependencies().unwrap(),
431 packages: vec![
432 manifest_package("prod1", "1.1.0", &[]),
433 manifest_package("prod2", "1.2.0", &[]),
434 manifest_package("dev1", "1.1.0", &[]),
435 manifest_package("dev2", "1.2.0", &[]),
436 ],
437 };
438 assert_eq!(
439 config.locked(Some(&manifest)).unwrap(),
440 [
441 locked_version("prod1", "1.1.0"),
442 locked_version("prod2", "1.2.0"),
443 locked_version("dev1", "1.1.0"),
444 locked_version("dev2", "1.2.0"),
445 ]
446 .into()
447 );
448}
449
450#[test]
451fn locked_some_removed() {
452 let mut config = PackageConfig::default();
453 config.dependencies = [("prod1".into(), Requirement::hex("~> 1.0").unwrap())].into();
454 config.dev_dependencies = [("dev2".into(), Requirement::hex("~> 2.0").unwrap())].into();
455 let manifest = Manifest {
456 requirements: config.all_direct_dependencies().unwrap(),
457 packages: vec![
458 manifest_package("prod1", "1.1.0", &[]),
459 manifest_package("prod2", "1.2.0", &[]), // Not in config
460 manifest_package("dev1", "1.1.0", &[]), // Not in config
461 manifest_package("dev2", "1.2.0", &[]),
462 ],
463 };
464 assert_eq!(
465 config.locked(Some(&manifest)).unwrap(),
466 [
467 // prod2 removed
468 // dev1 removed
469 locked_version("prod1", "1.1.0"),
470 locked_version("dev2", "1.2.0"),
471 ]
472 .into()
473 );
474}
475
476#[test]
477fn locked_some_changed() {
478 let mut config = PackageConfig::default();
479 config.dependencies = [
480 ("prod1".into(), Requirement::hex("~> 3.0").unwrap()), // Does not match manifest
481 ("prod2".into(), Requirement::hex("~> 2.0").unwrap()),
482 ]
483 .into();
484 config.dev_dependencies = [
485 ("dev1".into(), Requirement::hex("~> 3.0").unwrap()), // Does not match manifest
486 ("dev2".into(), Requirement::hex("~> 2.0").unwrap()),
487 ]
488 .into();
489 let manifest = Manifest {
490 requirements: [
491 ("prod1".into(), Requirement::hex("~> 1.0").unwrap()),
492 ("prod2".into(), Requirement::hex("~> 2.0").unwrap()),
493 ("dev1".into(), Requirement::hex("~> 1.0").unwrap()),
494 ("dev2".into(), Requirement::hex("~> 2.0").unwrap()),
495 ]
496 .into(),
497 packages: vec![
498 manifest_package("prod1", "1.1.0", &[]),
499 manifest_package("prod2", "1.2.0", &[]),
500 manifest_package("dev1", "1.1.0", &[]),
501 manifest_package("dev2", "1.2.0", &[]),
502 ],
503 };
504 assert_eq!(
505 config.locked(Some(&manifest)).unwrap(),
506 [
507 // prod1 removed
508 // dev1 removed
509 locked_version("prod2", "1.2.0"),
510 locked_version("dev2", "1.2.0"),
511 ]
512 .into()
513 );
514}
515
516#[test]
517fn locked_nested_are_removed_too() {
518 let mut config = PackageConfig::default();
519 config.dependencies = [
520 ("1".into(), Requirement::hex("~> 2.0").unwrap()), // Does not match manifest
521 ("2".into(), Requirement::hex("~> 1.0").unwrap()),
522 ]
523 .into();
524 config.dev_dependencies = [].into();
525 let manifest = Manifest {
526 requirements: [
527 ("1".into(), Requirement::hex("~> 1.0").unwrap()),
528 ("2".into(), Requirement::hex("~> 1.0").unwrap()),
529 ]
530 .into(),
531 packages: vec![
532 manifest_package("1", "1.1.0", &["1.1", "1.2"]),
533 manifest_package("1.1", "1.1.0", &["1.1.1", "1.1.2"]),
534 manifest_package("1.1.1", "1.1.0", &["shared"]),
535 manifest_package("1.1.2", "1.1.0", &[]),
536 manifest_package("1.2", "1.1.0", &["1.2.1", "1.2.2"]),
537 manifest_package("1.2.1", "1.1.0", &[]),
538 manifest_package("1.2.2", "1.1.0", &[]),
539 manifest_package("2", "2.1.0", &["2.1", "2.2"]),
540 manifest_package("2.1", "2.1.0", &["2.1.1", "2.1.2"]),
541 manifest_package("2.1.1", "2.1.0", &[]),
542 manifest_package("2.1.2", "2.1.0", &[]),
543 manifest_package("2.2", "2.1.0", &["2.2.1", "2.2.2", "shared"]),
544 manifest_package("2.2.1", "2.1.0", &[]),
545 manifest_package("2.2.2", "2.1.0", &[]),
546 manifest_package("shared", "2.1.0", &[]),
547 ],
548 };
549 assert_eq!(
550 config.locked(Some(&manifest)).unwrap(),
551 [
552 // 1* removed
553 locked_version("2", "2.1.0"),
554 locked_version("2.1", "2.1.0"),
555 locked_version("2.1.1", "2.1.0"),
556 locked_version("2.1.2", "2.1.0"),
557 locked_version("2.2", "2.1.0"),
558 locked_version("2.2.1", "2.1.0"),
559 locked_version("2.2.2", "2.1.0"),
560 locked_version("shared", "2.1.0"),
561 ]
562 .into()
563 );
564}
565
566// https://github.com/gleam-lang/gleam/issues/1754
567#[test]
568fn locked_unlock_new() {
569 let mut config = PackageConfig::default();
570 config.dependencies = [
571 ("1".into(), Requirement::hex("~> 1.0").unwrap()),
572 ("2".into(), Requirement::hex("~> 1.0").unwrap()),
573 ("3".into(), Requirement::hex("~> 3.0").unwrap()), // Does not match manifest
574 ]
575 .into();
576 config.dev_dependencies = [].into();
577 let manifest = Manifest {
578 requirements: [
579 ("1".into(), Requirement::hex("~> 1.0").unwrap()),
580 ("2".into(), Requirement::hex("~> 1.0").unwrap()),
581 ]
582 .into(),
583 packages: vec![
584 manifest_package("1", "1.1.0", &["3"]),
585 manifest_package("2", "1.1.0", &["3"]),
586 manifest_package("3", "1.1.0", &[]),
587 ],
588 };
589 assert_eq!(
590 config.locked(Some(&manifest)).unwrap(),
591 [locked_version("1", "1.1.0"), locked_version("2", "1.1.0"),].into()
592 )
593}
594
595#[test]
596fn default_internal_modules() {
597 // When no internal modules are specified then we default to
598 // `["$package/internal", "$package/internal/*"]`
599 let mut config = PackageConfig::default();
600 config.name = "my_package".into();
601 config.internal_modules = None;
602
603 assert!(config.is_internal_module("my_package/internal"));
604 assert!(config.is_internal_module("my_package/internal/wibble"));
605 assert!(config.is_internal_module("my_package/internal/wibble/wobble"));
606 assert!(!config.is_internal_module("my_package/internallll"));
607 assert!(!config.is_internal_module("my_package/other"));
608 assert!(!config.is_internal_module("my_package/other/wibble"));
609 assert!(!config.is_internal_module("other/internal"));
610}
611
612#[test]
613fn no_internal_modules() {
614 // When no internal modules are specified then we default to
615 // `["$package/internal", "$package/internal/*"]`
616 let mut config = PackageConfig::default();
617 config.name = "my_package".into();
618 config.internal_modules = Some(vec![]);
619
620 assert!(!config.is_internal_module("my_package/internal"));
621 assert!(!config.is_internal_module("my_package/internal/wibble"));
622 assert!(!config.is_internal_module("my_package/internal/wibble/wobble"));
623 assert!(!config.is_internal_module("my_package/internallll"));
624 assert!(!config.is_internal_module("my_package/other"));
625 assert!(!config.is_internal_module("my_package/other/wibble"));
626 assert!(!config.is_internal_module("other/internal"));
627}
628
629#[test]
630fn hidden_a_directory_from_docs() {
631 let mut config = PackageConfig::default();
632 config.internal_modules = Some(vec![Glob::new("package/internal/*").expect("")]);
633
634 let mod1 = "package/internal";
635 let mod2 = "package/internal/module";
636
637 assert_eq!(config.is_internal_module(mod1), false);
638 assert_eq!(config.is_internal_module(mod2), true);
639}
640
641#[test]
642fn hidden_two_directories_from_docs() {
643 let mut config = PackageConfig::default();
644 config.internal_modules = Some(vec![
645 Glob::new("package/internal1/*").expect(""),
646 Glob::new("package/internal2/*").expect(""),
647 ]);
648
649 let mod1 = "package/internal1";
650 let mod2 = "package/internal1/module";
651 let mod3 = "package/internal2";
652 let mod4 = "package/internal2/module";
653
654 assert_eq!(config.is_internal_module(mod1), false);
655 assert_eq!(config.is_internal_module(mod2), true);
656 assert_eq!(config.is_internal_module(mod3), false);
657 assert_eq!(config.is_internal_module(mod4), true);
658}
659
660#[test]
661fn hidden_a_directory_and_a_file_from_docs() {
662 let mut config = PackageConfig::default();
663 config.internal_modules = Some(vec![
664 Glob::new("package/internal1/*").expect(""),
665 Glob::new("package/module").expect(""),
666 ]);
667
668 let mod1 = "package/internal1";
669 let mod2 = "package/internal1/module";
670 let mod3 = "package/module";
671 let mod4 = "package/module/inner";
672
673 assert_eq!(config.is_internal_module(mod1), false);
674 assert_eq!(config.is_internal_module(mod2), true);
675 assert_eq!(config.is_internal_module(mod3), true);
676 assert_eq!(config.is_internal_module(mod4), false);
677}
678
679#[test]
680fn hidden_a_file_in_all_directories_from_docs() {
681 let mut config = PackageConfig::default();
682 config.internal_modules = Some(vec![Glob::new("package/*/module1").expect("")]);
683
684 let mod1 = "package/internal1/module1";
685 let mod2 = "package/internal2/module1";
686 let mod3 = "package/internal2/module2";
687 let mod4 = "package/module";
688
689 assert_eq!(config.is_internal_module(mod1), true);
690 assert_eq!(config.is_internal_module(mod2), true);
691 assert_eq!(config.is_internal_module(mod3), false);
692 assert_eq!(config.is_internal_module(mod4), false);
693}
694
695#[cfg(test)]
696fn manifest_package(
697 name: &'static str,
698 version: &'static str,
699 requirements: &'static [&'static str],
700) -> ManifestPackage {
701 use crate::manifest::Base16Checksum;
702
703 ManifestPackage {
704 name: name.into(),
705 version: Version::parse(version).unwrap(),
706 build_tools: vec![],
707 otp_app: None,
708 requirements: requirements
709 .iter()
710 .map(|requirement| (*requirement).into())
711 .collect(),
712 source: crate::manifest::ManifestPackageSource::Hex {
713 outer_checksum: Base16Checksum(vec![]),
714 },
715 }
716}
717
718#[cfg(test)]
719fn locked_version(name: &'static str, version: &'static str) -> (EcoString, Version) {
720 (name.into(), Version::parse(version).unwrap())
721}
722
723impl Default for PackageConfig {
724 fn default() -> Self {
725 Self {
726 name: Default::default(),
727 version: default_version(),
728 gleam_version: Default::default(),
729 description: Default::default(),
730 documentation: Default::default(),
731 dependencies: Default::default(),
732 erlang: Default::default(),
733 javascript: Default::default(),
734 repository: Default::default(),
735 dev_dependencies: Default::default(),
736 licences: Default::default(),
737 links: Default::default(),
738 internal_modules: Default::default(),
739 target: Target::Erlang,
740 }
741 }
742}
743
744#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Default, Clone)]
745pub struct ErlangConfig {
746 /// An module that can be set in the `.app` file as the entrypoint for a stateful application
747 /// that defines a singleton supervision tree.
748 /// Erlang syntax.
749 #[serde(default)]
750 pub application_start_module: Option<EcoString>,
751 /// The argument for the start module start function. If not set then `[]` is used as the
752 /// default argument.
753 /// Erlang syntax.
754 #[serde(default)]
755 pub application_start_argument: Option<EcoString>,
756 #[serde(default)]
757 pub extra_applications: Vec<EcoString>,
758}
759
760#[derive(Deserialize, Serialize, Debug, PartialEq, Default, Clone)]
761pub struct JavaScriptConfig {
762 #[serde(default)]
763 pub typescript_declarations: bool,
764 #[serde(default)]
765 pub source_maps: bool,
766 #[serde(default = "default_javascript_runtime")]
767 pub runtime: Runtime,
768 #[serde(default, rename = "deno")]
769 pub deno: DenoConfig,
770}
771
772#[derive(Deserialize, Debug, PartialEq, Eq, Clone)]
773pub enum DenoFlag {
774 AllowAll,
775 Allow(Vec<String>),
776}
777
778impl Default for DenoFlag {
779 fn default() -> Self {
780 Self::Allow(Vec::new())
781 }
782}
783
784impl Serialize for DenoFlag {
785 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
786 where
787 S: serde::Serializer,
788 {
789 match self {
790 DenoFlag::AllowAll => serializer.serialize_bool(true),
791 DenoFlag::Allow(items) => {
792 let mut seq = serializer.serialize_seq(Some(items.len()))?;
793 for e in items {
794 seq.serialize_element(e)?;
795 }
796 seq.end()
797 }
798 }
799 }
800}
801
802fn bool_or_seq_string_to_deno_flag<'de, D>(deserializer: D) -> Result<DenoFlag, D::Error>
803where
804 D: serde::Deserializer<'de>,
805{
806 struct StringOrVec(PhantomData<Vec<String>>);
807
808 impl<'de> serde::de::Visitor<'de> for StringOrVec {
809 type Value = DenoFlag;
810
811 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
812 formatter.write_str("bool or list of strings")
813 }
814
815 fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
816 where
817 E: serde::de::Error,
818 {
819 if value {
820 Ok(DenoFlag::AllowAll)
821 } else {
822 Ok(DenoFlag::default())
823 }
824 }
825
826 fn visit_seq<S>(self, visitor: S) -> Result<Self::Value, S::Error>
827 where
828 S: serde::de::SeqAccess<'de>,
829 {
830 let allow: Vec<String> =
831 Deserialize::deserialize(serde::de::value::SeqAccessDeserializer::new(visitor))
832 .unwrap_or_default();
833
834 Ok(DenoFlag::Allow(allow))
835 }
836 }
837
838 deserializer.deserialize_any(StringOrVec(PhantomData))
839}
840
841#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Default, Clone)]
842pub struct DenoConfig {
843 #[serde(default, deserialize_with = "bool_or_seq_string_to_deno_flag")]
844 pub allow_env: DenoFlag,
845 #[serde(default)]
846 pub allow_sys: bool,
847 #[serde(default)]
848 pub allow_hrtime: bool,
849 #[serde(default, deserialize_with = "bool_or_seq_string_to_deno_flag")]
850 pub allow_net: DenoFlag,
851 #[serde(default)]
852 pub allow_ffi: bool,
853 #[serde(default, deserialize_with = "bool_or_seq_string_to_deno_flag")]
854 pub allow_read: DenoFlag,
855 #[serde(default, deserialize_with = "bool_or_seq_string_to_deno_flag")]
856 pub allow_run: DenoFlag,
857 #[serde(default, deserialize_with = "bool_or_seq_string_to_deno_flag")]
858 pub allow_write: DenoFlag,
859 #[serde(default)]
860 pub allow_all: bool,
861 #[serde(default)]
862 pub unstable: bool,
863 #[serde(
864 default,
865 serialize_with = "uri_serde::serialize_option",
866 deserialize_with = "uri_serde::deserialize_option"
867 )]
868 pub location: Option<Uri>,
869}
870
871#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)]
872#[serde(tag = "type")]
873pub enum Repository {
874 #[serde(rename = "github")]
875 GitHub {
876 user: String,
877 repo: String,
878 path: Option<String>,
879 #[serde(alias = "tag-prefix")]
880 tag_prefix: Option<String>,
881 },
882 #[serde(rename = "gitlab")]
883 GitLab {
884 user: String,
885 repo: String,
886 path: Option<String>,
887 #[serde(alias = "tag-prefix")]
888 tag_prefix: Option<String>,
889 },
890 #[serde(rename = "bitbucket")]
891 BitBucket {
892 user: String,
893 repo: String,
894 path: Option<String>,
895 #[serde(alias = "tag-prefix")]
896 tag_prefix: Option<String>,
897 },
898 #[serde(rename = "codeberg")]
899 Codeberg {
900 user: String,
901 repo: String,
902 path: Option<String>,
903 #[serde(alias = "tag-prefix")]
904 tag_prefix: Option<String>,
905 },
906 #[serde(rename = "gitea")]
907 Gitea {
908 user: String,
909 repo: String,
910 path: Option<String>,
911 #[serde(alias = "tag-prefix")]
912 tag_prefix: Option<String>,
913 #[serde(
914 serialize_with = "uri_serde::serialize",
915 deserialize_with = "uri_serde_default_https::deserialize"
916 )]
917 host: Uri,
918 },
919 #[serde(rename = "forgejo")]
920 Forgejo {
921 user: String,
922 repo: String,
923 path: Option<String>,
924 #[serde(alias = "tag-prefix")]
925 tag_prefix: Option<String>,
926 #[serde(
927 serialize_with = "uri_serde::serialize",
928 deserialize_with = "uri_serde_default_https::deserialize"
929 )]
930 host: Uri,
931 },
932 #[serde(rename = "sourcehut")]
933 SourceHut {
934 user: String,
935 repo: String,
936 path: Option<String>,
937 #[serde(alias = "tag-prefix")]
938 tag_prefix: Option<String>,
939 },
940 #[serde(rename = "tangled")]
941 Tangled {
942 user: String,
943 repo: String,
944 path: Option<String>,
945 #[serde(alias = "tag-prefix")]
946 tag_prefix: Option<String>,
947 },
948 #[serde(rename = "custom")]
949 Custom {
950 url: String,
951 #[serde(alias = "tag-prefix")]
952 tag_prefix: Option<String>,
953 },
954}
955
956impl Repository {
957 pub fn url(&self) -> String {
958 match self {
959 Repository::GitHub { repo, user, .. } => {
960 format!("https://github.com/{user}/{repo}")
961 }
962 Repository::GitLab { repo, user, .. } => {
963 format!("https://gitlab.com/{user}/{repo}")
964 }
965 Repository::BitBucket { repo, user, .. } => {
966 format!("https://bitbucket.com/{user}/{repo}")
967 }
968 Repository::Codeberg { repo, user, .. } => {
969 format!("https://codeberg.org/{user}/{repo}")
970 }
971 Repository::SourceHut { repo, user, .. } => {
972 format!("https://git.sr.ht/~{user}/{repo}")
973 }
974 Repository::Tangled { repo, user, .. } => {
975 format!("https://tangled.sh/{user}/{repo}")
976 }
977 Repository::Gitea {
978 repo, user, host, ..
979 }
980 | Repository::Forgejo {
981 repo, user, host, ..
982 } => {
983 let string_host = host.to_string();
984 let cleaned_host = string_host.trim_end_matches('/');
985 format!("{cleaned_host}/{user}/{repo}")
986 }
987 Repository::Custom { url, .. } => url.clone(),
988 }
989 }
990
991 pub fn path(&self) -> Option<&String> {
992 match self {
993 Repository::GitHub { path, .. }
994 | Repository::GitLab { path, .. }
995 | Repository::BitBucket { path, .. }
996 | Repository::Codeberg { path, .. }
997 | Repository::SourceHut { path, .. }
998 | Repository::Tangled { path, .. }
999 | Repository::Gitea { path, .. }
1000 | Repository::Forgejo { path, .. } => path.as_ref(),
1001
1002 Repository::Custom { .. } => None,
1003 }
1004 }
1005}
1006
1007#[derive(Deserialize, Serialize, Default, Debug, PartialEq, Eq, Clone)]
1008pub struct Docs {
1009 #[serde(default)]
1010 pub pages: Vec<DocsPage>,
1011}
1012
1013#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)]
1014pub struct DocsPage {
1015 pub title: String,
1016 #[serde(default, deserialize_with = "non_ascending_path_string::deserialize")]
1017 pub path: String,
1018 #[serde(default, deserialize_with = "non_ascending_path_buf::deserialize")]
1019 pub source: Utf8PathBuf,
1020}
1021
1022mod non_ascending_path_string {
1023 use serde::{Deserialize, Deserializer, de::Error as _};
1024
1025 pub fn deserialize<'de, D>(deserializer: D) -> Result<String, D::Error>
1026 where
1027 D: Deserializer<'de>,
1028 {
1029 let path = String::deserialize(deserializer)?.replace('\\', "/");
1030 if path.starts_with("../") || path.contains("/../") {
1031 return Err(D::Error::custom("paths must not contain .. segments"));
1032 }
1033 Ok(path)
1034 }
1035}
1036
1037mod non_ascending_path_buf {
1038 use camino::{Utf8Component, Utf8PathBuf};
1039 use serde::{Deserialize, Deserializer, de::Error as _};
1040
1041 pub fn deserialize<'de, D>(deserializer: D) -> Result<Utf8PathBuf, D::Error>
1042 where
1043 D: Deserializer<'de>,
1044 {
1045 let path = Utf8PathBuf::deserialize(deserializer)?;
1046 if path.is_absolute() || (cfg!(windows) && path.starts_with("\\")) {
1047 return Err(D::Error::custom("paths must be relative"));
1048 }
1049 for component in path.components() {
1050 if component == Utf8Component::ParentDir {
1051 return Err(D::Error::custom("paths must not contain .. segments"));
1052 }
1053 }
1054 Ok(path)
1055 }
1056}
1057
1058#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)]
1059pub struct Link {
1060 pub title: String,
1061 #[serde(with = "uri_serde")]
1062 pub href: Uri,
1063}
1064
1065// Note we don't use http-serde since we also want to validate the scheme and host is set.
1066mod uri_serde {
1067 use http::uri::InvalidUri;
1068 use serde::{Deserialize, Deserializer, de::Error as _};
1069
1070 pub fn deserialize<'de, D>(deserializer: D) -> Result<http::Uri, D::Error>
1071 where
1072 D: Deserializer<'de>,
1073 {
1074 let string = String::deserialize(deserializer)?;
1075 let uri: http::Uri = string
1076 .parse()
1077 .map_err(|err: InvalidUri| D::Error::custom(err.to_string()))?;
1078 if uri.scheme().is_none() || uri.host().is_none() {
1079 return Err(D::Error::custom("uri without scheme"));
1080 }
1081 Ok(uri)
1082 }
1083
1084 pub fn deserialize_option<'de, D>(deserializer: D) -> Result<Option<http::Uri>, D::Error>
1085 where
1086 D: Deserializer<'de>,
1087 {
1088 let string: Option<String> = Option::deserialize(deserializer)?;
1089 match string {
1090 Some(s) => {
1091 let deserializer = serde::de::value::StringDeserializer::new(s);
1092 deserialize(deserializer).map(Some)
1093 }
1094 None => Ok(None),
1095 }
1096 }
1097
1098 pub fn serialize_option<S>(uri: &Option<http::Uri>, serializer: S) -> Result<S::Ok, S::Error>
1099 where
1100 S: serde::Serializer,
1101 {
1102 match uri {
1103 Some(uri) => serialize(uri, serializer),
1104 None => serializer.serialize_unit(),
1105 }
1106 }
1107
1108 pub fn serialize<S>(uri: &http::Uri, serializer: S) -> Result<S::Ok, S::Error>
1109 where
1110 S: serde::Serializer,
1111 {
1112 serializer.serialize_str(&uri.to_string())
1113 }
1114}
1115
1116// This prefixes https as a default in the event no scheme was provided
1117mod uri_serde_default_https {
1118 use http::uri::InvalidUri;
1119 use serde::{Deserialize, Deserializer, de::Error as _};
1120
1121 pub fn deserialize<'de, D>(deserializer: D) -> Result<http::Uri, D::Error>
1122 where
1123 D: Deserializer<'de>,
1124 {
1125 let string = String::deserialize(deserializer)?;
1126 let uri: http::Uri = string
1127 .parse()
1128 .map_err(|err: InvalidUri| D::Error::custom(err.to_string()))?;
1129 if uri.host().is_none() {
1130 return Err(D::Error::custom("uri without host"));
1131 }
1132 match uri.scheme().is_none() {
1133 true => format!("https://{string}")
1134 .parse()
1135 .map_err(|err: InvalidUri| D::Error::custom(err.to_string())),
1136 false => Ok(uri),
1137 }
1138 }
1139}
1140
1141pub(crate) mod package_name {
1142 use ecow::EcoString;
1143 use serde::Deserializer;
1144 use std::fmt;
1145
1146 pub fn deserialize<'de, D>(deserializer: D) -> Result<EcoString, D::Error>
1147 where
1148 D: Deserializer<'de>,
1149 {
1150 deserializer.deserialize_str(NameVisitor)
1151 }
1152
1153 struct NameVisitor;
1154
1155 impl<'de> serde::de::Visitor<'de> for NameVisitor {
1156 type Value = EcoString;
1157
1158 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1159 formatter.write_str("a package name")
1160 }
1161
1162 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
1163 where
1164 E: serde::de::Error,
1165 {
1166 if super::is_valid_package_name(value) {
1167 Ok(value.into())
1168 } else {
1169 let error =
1170 "Package names may only contain lowercase letters, numbers, and underscores";
1171 Err(serde::de::Error::custom(error))
1172 }
1173 }
1174 }
1175
1176 pub fn optional_deserialize<'de, D>(deserializer: D) -> Result<Option<EcoString>, D::Error>
1177 where
1178 D: Deserializer<'de>,
1179 {
1180 deserializer.deserialize_option(OptionVisitor)
1181 }
1182
1183 struct OptionVisitor;
1184
1185 impl<'de> serde::de::Visitor<'de> for OptionVisitor {
1186 type Value = Option<EcoString>;
1187
1188 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1189 formatter.write_str("an optional package name")
1190 }
1191
1192 fn visit_none<E>(self) -> Result<Self::Value, E>
1193 where
1194 E: serde::de::Error,
1195 {
1196 Ok(None)
1197 }
1198
1199 fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
1200 where
1201 D: Deserializer<'de>,
1202 {
1203 deserialize(deserializer).map(Some)
1204 }
1205 }
1206}
1207
1208pub mod map_with_package_name_keys {
1209 use ecow::EcoString;
1210 use serde::{Deserialize, Deserializer, de};
1211 use std::collections::HashMap;
1212
1213 pub fn deserialize<'de, D, V>(deserializer: D) -> Result<HashMap<EcoString, V>, D::Error>
1214 where
1215 D: Deserializer<'de>,
1216 V: Deserialize<'de>,
1217 {
1218 struct DependenciesVisitor<V>(std::marker::PhantomData<V>);
1219
1220 impl<'de, V: Deserialize<'de>> de::Visitor<'de> for DependenciesVisitor<V> {
1221 type Value = HashMap<EcoString, V>;
1222
1223 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1224 write!(f, "a map with valid package name keys")
1225 }
1226
1227 fn visit_map<A: de::MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
1228 let mut dependencies = HashMap::new();
1229 while let Some(key) = map.next_key::<EcoString>()? {
1230 if !super::is_valid_package_name(&key) {
1231 return Err(de::Error::invalid_value(
1232 de::Unexpected::Str(&key),
1233 &"a package name containing only lowercase letter, numbers, and underscores",
1234 ));
1235 }
1236 let _: Option<_> = dependencies.insert(key, map.next_value()?);
1237 }
1238 Ok(dependencies)
1239 }
1240 }
1241
1242 deserializer.deserialize_map(DependenciesVisitor(std::marker::PhantomData))
1243 }
1244}
1245
1246fn is_valid_package_name(name: &str) -> bool {
1247 use regex::Regex;
1248 use std::sync::OnceLock;
1249 static PACKAGE_NAME_PATTERN: OnceLock<Regex> = OnceLock::new();
1250 PACKAGE_NAME_PATTERN
1251 .get_or_init(|| Regex::new("^[a-z][a-z0-9_]*$").expect("Package name regex"))
1252 .is_match(name)
1253}
1254
1255#[test]
1256fn name_with_dash() {
1257 let input = r#"
1258name = "one-two"
1259"#;
1260
1261 insta::assert_snapshot!(
1262 insta::internals::AutoName,
1263 toml::from_str::<PackageConfig>(input)
1264 .unwrap_err()
1265 .to_string()
1266 );
1267}
1268
1269#[test]
1270fn name_with_number_start() {
1271 let input = r#"
1272name = "1"
1273"#;
1274 insta::assert_snapshot!(
1275 insta::internals::AutoName,
1276 toml::from_str::<PackageConfig>(input)
1277 .unwrap_err()
1278 .to_string(),
1279 )
1280}
1281
1282#[test]
1283fn docs_dot_dot_string_path() {
1284 let input = r#"
1285name = "one_two"
1286
1287[documentation]
1288pages = [{ title = "My Page", path = "../secrets.txt", source = "./path/to/my-page.md" }]
1289"#;
1290
1291 insta::assert_snapshot!(
1292 insta::internals::AutoName,
1293 toml::from_str::<PackageConfig>(input)
1294 .unwrap_err()
1295 .to_string()
1296 );
1297}
1298
1299#[test]
1300fn docs_prefix_dot_dot_string_path() {
1301 let input = r#"
1302name = "one_two"
1303
1304[documentation]
1305pages = [{ title = "My Page", path = "something/../../secrets.txt", source = "./path/to/my-page.md" }]
1306"#;
1307
1308 insta::assert_snapshot!(
1309 insta::internals::AutoName,
1310 toml::from_str::<PackageConfig>(input)
1311 .unwrap_err()
1312 .to_string()
1313 );
1314}
1315
1316#[test]
1317fn docs_windows_dot_dot_string_path() {
1318 let input = r#"
1319name = "one_two"
1320
1321[documentation]
1322pages = [{ title = "My Page", path = "..\\secrets.txt", source = "./path/to/my-page.md" }]
1323"#;
1324
1325 insta::assert_snapshot!(
1326 insta::internals::AutoName,
1327 toml::from_str::<PackageConfig>(input)
1328 .unwrap_err()
1329 .to_string()
1330 );
1331}
1332
1333#[test]
1334fn docs_prefix_windows_dot_dot_string_path() {
1335 let input = r#"
1336name = "one_two"
1337
1338[documentation]
1339pages = [{ title = "My Page", path = "something\\..\\..\\secrets.txt", source = "./path/to/my-page.md" }]
1340"#;
1341
1342 insta::assert_snapshot!(
1343 insta::internals::AutoName,
1344 toml::from_str::<PackageConfig>(input)
1345 .unwrap_err()
1346 .to_string()
1347 );
1348}
1349
1350#[test]
1351fn docs_dot_dot_path_buf() {
1352 let input = r#"
1353name = "one_two"
1354
1355[documentation]
1356pages = [{ title = "My Page", path = "stuff.html", source = "../secrets.txt" }]
1357"#;
1358
1359 insta::assert_snapshot!(
1360 insta::internals::AutoName,
1361 toml::from_str::<PackageConfig>(input)
1362 .unwrap_err()
1363 .to_string()
1364 );
1365}
1366
1367#[test]
1368fn docs_prefix_dot_dot_path_buf() {
1369 let input = r#"
1370name = "one_two"
1371
1372[documentation]
1373pages = [{ title = "My Page", path = "stuff.html", source = "something/../../secrets.txt" }]
1374"#;
1375
1376 insta::assert_snapshot!(
1377 insta::internals::AutoName,
1378 toml::from_str::<PackageConfig>(input)
1379 .unwrap_err()
1380 .to_string()
1381 );
1382}
1383
1384#[cfg(windows)]
1385#[test]
1386fn docs_windows_dot_dot_path_buf() {
1387 let input = r#"
1388name = "one_two"
1389
1390[documentation]
1391pages = [{ title = "My Page", path = "stuff.html", source = "..\\secrets.txt" }]
1392"#;
1393
1394 assert!(toml::from_str::<PackageConfig>(input).is_err())
1395}
1396
1397#[cfg(windows)]
1398#[test]
1399fn docs_prefix_windows_dot_dot_path_buf() {
1400 let input = r#"
1401name = "one_two"
1402
1403[documentation]
1404pages = [{ title = "My Page", path = "stuff.html", source = "something\\..\\..\\secrets.txt" }]
1405"#;
1406
1407 assert!(toml::from_str::<PackageConfig>(input).is_err())
1408}
1409
1410#[cfg(windows)]
1411#[test]
1412fn docs_absolute_source_windows() {
1413 let input = r#"
1414name = "one_two"
1415
1416[documentation]
1417pages = [{ title = "My Page", path = "stuff.html", source = "C:\\etc\\passwd" }]
1418"#;
1419
1420 assert!(toml::from_str::<PackageConfig>(input).is_err())
1421}
1422
1423#[test]
1424fn docs_absolute_source() {
1425 let input = r#"
1426name = "one_two"
1427
1428[documentation]
1429pages = [{ title = "My Page", path = "stuff.html", source = "/etc/passwd" }]
1430"#;
1431
1432 insta::assert_snapshot!(
1433 insta::internals::AutoName,
1434 toml::from_str::<PackageConfig>(input)
1435 .unwrap_err()
1436 .to_string()
1437 );
1438}
1439
1440#[test]
1441fn package_config_to_json() {
1442 let input = r#"
1443name = "my_project"
1444version = "1.0.0"
1445licences = ["Apache-2.0", "MIT"]
1446description = "Pretty complex config"
1447target = "erlang"
1448repository = { type = "github", user = "example", repo = "my_dep" }
1449links = [{ title = "Home page", href = "https://example.com" }]
1450internal_modules = ["my_app/internal"]
1451gleam = ">= 0.30.0"
1452
1453[dependencies]
1454gleam_stdlib = ">= 0.18.0 and < 2.0.0"
1455my_other_project = { path = "../my_other_project" }
1456
1457[dev_dependencies]
1458gleeunit = ">= 1.0.0 and < 2.0.0"
1459
1460[documentation]
1461pages = [{ title = "My Page", path = "my-page.html", source = "./path/to/my-page.md" }]
1462
1463[erlang]
1464application_start_module = "my_app/application"
1465extra_applications = ["inets", "ssl"]
1466
1467[javascript]
1468typescript_declarations = true
1469source_maps = true
1470runtime = "node"
1471
1472[javascript.deno]
1473allow_all = false
1474allow_ffi = true
1475allow_env = ["DATABASE_URL"]
1476allow_net = ["example.com:443"]
1477allow_read = ["./database.sqlite"]
1478"#;
1479
1480 let config = toml::from_str::<PackageConfig>(input).unwrap();
1481 let json = serde_json::to_string_pretty(&config).unwrap();
1482 let output = format!("--- GLEAM.TOML\n{input}\n\n--- EXPORTED JSON\n\n{json}");
1483 insta::assert_snapshot!(output);
1484
1485 let roundtrip = serde_json::from_str::<PackageConfig>(&json).unwrap();
1486 assert_eq!(config, roundtrip);
1487}
1488
1489#[test]
1490fn barebones_package_config_to_json() {
1491 let input = r#"
1492name = "my_project"
1493version = "1.0.0"
1494"#;
1495
1496 let config = toml::from_str::<PackageConfig>(input).unwrap();
1497 let json = serde_json::to_string_pretty(&config).unwrap();
1498 let output = format!("--- GLEAM.TOML\n{input}\n\n--- EXPORTED JSON\n\n{json}");
1499 insta::assert_snapshot!(output);
1500}
1501
1502#[test]
1503fn dev_deps_field_name() {
1504 let toml = r#"
1505name = "wibble"
1506version = "1.0.0"
1507
1508[dev_dependencies]
1509wibble = ">= 1.0.0 and < 2.0.0"
1510"#;
1511 let hyphen_alternative = deserialise_config("gleam.toml", toml.into()).expect("valid config");
1512 let toml = r#"
1513name = "wibble"
1514version = "1.0.0"
1515
1516[dev_dependencies]
1517wibble = ">= 1.0.0 and < 2.0.0"
1518"#;
1519 let canonical = deserialise_config("gleam.toml", toml.into()).expect("valid config");
1520 assert_eq!(canonical, hyphen_alternative)
1521}