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 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}