Fork of daniellemaywood.uk/gleam — Wasm codegen work
2

Configure Feed

Select the types of activity you want to include in your feed.

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