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 1510 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 1021pub(crate) mod package_scoped_path { 1022 use camino::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 crate::io::validate_safe_relative_path(&path).map_err(D::Error::custom)?; 1031 Ok(path) 1032 } 1033 1034 pub fn optional_deserialize<'de, D>(deserializer: D) -> Result<Option<Utf8PathBuf>, D::Error> 1035 where 1036 D: Deserializer<'de>, 1037 { 1038 let path = Option::<Utf8PathBuf>::deserialize(deserializer)?; 1039 if let Some(path) = &path { 1040 crate::io::validate_safe_relative_path(path).map_err(D::Error::custom)?; 1041 } 1042 Ok(path) 1043 } 1044} 1045 1046#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)] 1047pub struct Link { 1048 pub title: String, 1049 #[serde(with = "uri_serde")] 1050 pub href: Uri, 1051} 1052 1053// Note we don't use http-serde since we also want to validate the scheme and host is set. 1054mod uri_serde { 1055 use http::uri::InvalidUri; 1056 use serde::{Deserialize, Deserializer, de::Error as _}; 1057 1058 pub fn deserialize<'de, D>(deserializer: D) -> Result<http::Uri, D::Error> 1059 where 1060 D: Deserializer<'de>, 1061 { 1062 let string = String::deserialize(deserializer)?; 1063 let uri: http::Uri = string 1064 .parse() 1065 .map_err(|err: InvalidUri| D::Error::custom(err.to_string()))?; 1066 if uri.scheme().is_none() || uri.host().is_none() { 1067 return Err(D::Error::custom("uri without scheme")); 1068 } 1069 Ok(uri) 1070 } 1071 1072 pub fn deserialize_option<'de, D>(deserializer: D) -> Result<Option<http::Uri>, D::Error> 1073 where 1074 D: Deserializer<'de>, 1075 { 1076 let string: Option<String> = Option::deserialize(deserializer)?; 1077 match string { 1078 Some(s) => { 1079 let deserializer = serde::de::value::StringDeserializer::new(s); 1080 deserialize(deserializer).map(Some) 1081 } 1082 None => Ok(None), 1083 } 1084 } 1085 1086 pub fn serialize_option<S>(uri: &Option<http::Uri>, serializer: S) -> Result<S::Ok, S::Error> 1087 where 1088 S: serde::Serializer, 1089 { 1090 match uri { 1091 Some(uri) => serialize(uri, serializer), 1092 None => serializer.serialize_unit(), 1093 } 1094 } 1095 1096 pub fn serialize<S>(uri: &http::Uri, serializer: S) -> Result<S::Ok, S::Error> 1097 where 1098 S: serde::Serializer, 1099 { 1100 serializer.serialize_str(&uri.to_string()) 1101 } 1102} 1103 1104// This prefixes https as a default in the event no scheme was provided 1105mod uri_serde_default_https { 1106 use http::uri::InvalidUri; 1107 use serde::{Deserialize, Deserializer, de::Error as _}; 1108 1109 pub fn deserialize<'de, D>(deserializer: D) -> Result<http::Uri, D::Error> 1110 where 1111 D: Deserializer<'de>, 1112 { 1113 let string = String::deserialize(deserializer)?; 1114 let uri: http::Uri = string 1115 .parse() 1116 .map_err(|err: InvalidUri| D::Error::custom(err.to_string()))?; 1117 if uri.host().is_none() { 1118 return Err(D::Error::custom("uri without host")); 1119 } 1120 match uri.scheme().is_none() { 1121 true => format!("https://{string}") 1122 .parse() 1123 .map_err(|err: InvalidUri| D::Error::custom(err.to_string())), 1124 false => Ok(uri), 1125 } 1126 } 1127} 1128 1129pub(crate) mod package_name { 1130 use ecow::EcoString; 1131 use serde::Deserializer; 1132 use std::fmt; 1133 1134 pub fn deserialize<'de, D>(deserializer: D) -> Result<EcoString, D::Error> 1135 where 1136 D: Deserializer<'de>, 1137 { 1138 deserializer.deserialize_str(NameVisitor) 1139 } 1140 1141 struct NameVisitor; 1142 1143 impl<'de> serde::de::Visitor<'de> for NameVisitor { 1144 type Value = EcoString; 1145 1146 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 1147 formatter.write_str("a package name") 1148 } 1149 1150 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> 1151 where 1152 E: serde::de::Error, 1153 { 1154 if super::is_valid_package_name(value) { 1155 Ok(value.into()) 1156 } else { 1157 let error = 1158 "Package names may only contain lowercase letters, numbers, and underscores"; 1159 Err(serde::de::Error::custom(error)) 1160 } 1161 } 1162 } 1163 1164 pub fn optional_deserialize<'de, D>(deserializer: D) -> Result<Option<EcoString>, D::Error> 1165 where 1166 D: Deserializer<'de>, 1167 { 1168 deserializer.deserialize_option(OptionVisitor) 1169 } 1170 1171 struct OptionVisitor; 1172 1173 impl<'de> serde::de::Visitor<'de> for OptionVisitor { 1174 type Value = Option<EcoString>; 1175 1176 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 1177 formatter.write_str("an optional package name") 1178 } 1179 1180 fn visit_none<E>(self) -> Result<Self::Value, E> 1181 where 1182 E: serde::de::Error, 1183 { 1184 Ok(None) 1185 } 1186 1187 fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error> 1188 where 1189 D: Deserializer<'de>, 1190 { 1191 deserialize(deserializer).map(Some) 1192 } 1193 } 1194} 1195 1196pub mod map_with_package_name_keys { 1197 use ecow::EcoString; 1198 use serde::{Deserialize, Deserializer, de}; 1199 use std::collections::HashMap; 1200 1201 pub fn deserialize<'de, D, V>(deserializer: D) -> Result<HashMap<EcoString, V>, D::Error> 1202 where 1203 D: Deserializer<'de>, 1204 V: Deserialize<'de>, 1205 { 1206 struct DependenciesVisitor<V>(std::marker::PhantomData<V>); 1207 1208 impl<'de, V: Deserialize<'de>> de::Visitor<'de> for DependenciesVisitor<V> { 1209 type Value = HashMap<EcoString, V>; 1210 1211 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 1212 write!(f, "a map with valid package name keys") 1213 } 1214 1215 fn visit_map<A: de::MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> { 1216 let mut dependencies = HashMap::new(); 1217 while let Some(key) = map.next_key::<EcoString>()? { 1218 if !super::is_valid_package_name(&key) { 1219 return Err(de::Error::invalid_value( 1220 de::Unexpected::Str(&key), 1221 &"a package name containing only lowercase letter, numbers, and underscores", 1222 )); 1223 } 1224 let _: Option<_> = dependencies.insert(key, map.next_value()?); 1225 } 1226 Ok(dependencies) 1227 } 1228 } 1229 1230 deserializer.deserialize_map(DependenciesVisitor(std::marker::PhantomData)) 1231 } 1232} 1233 1234static PACKAGE_NAME_PATTERN: OnceLock<Regex> = OnceLock::new(); 1235 1236fn is_valid_package_name(name: &str) -> bool { 1237 PACKAGE_NAME_PATTERN 1238 .get_or_init(|| Regex::new("^[a-z][a-z0-9_]*$").expect("Package name regex")) 1239 .is_match(name) 1240} 1241 1242#[test] 1243fn name_with_dash() { 1244 let input = r#" 1245name = "one-two" 1246"#; 1247 1248 insta::assert_snapshot!( 1249 insta::internals::AutoName, 1250 toml::from_str::<PackageConfig>(input) 1251 .unwrap_err() 1252 .to_string() 1253 ); 1254} 1255 1256#[test] 1257fn name_with_number_start() { 1258 let input = r#" 1259name = "1" 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 docs_dot_dot_string_path() { 1271 let input = r#" 1272name = "one_two" 1273 1274[documentation] 1275pages = [{ title = "My Page", path = "../secrets.txt", source = "./path/to/my-page.md" }] 1276"#; 1277 1278 insta::assert_snapshot!( 1279 insta::internals::AutoName, 1280 toml::from_str::<PackageConfig>(input) 1281 .unwrap_err() 1282 .to_string() 1283 ); 1284} 1285 1286#[test] 1287fn docs_prefix_dot_dot_string_path() { 1288 let input = r#" 1289name = "one_two" 1290 1291[documentation] 1292pages = [{ title = "My Page", path = "something/../../secrets.txt", source = "./path/to/my-page.md" }] 1293"#; 1294 1295 insta::assert_snapshot!( 1296 insta::internals::AutoName, 1297 toml::from_str::<PackageConfig>(input) 1298 .unwrap_err() 1299 .to_string() 1300 ); 1301} 1302 1303#[cfg(windows)] 1304#[test] 1305fn docs_windows_dot_dot_string_path() { 1306 let input = r#" 1307name = "one_two" 1308 1309[documentation] 1310pages = [{ title = "My Page", path = "..\\secrets.txt", source = "./path/to/my-page.md" }] 1311"#; 1312 1313 insta::assert_snapshot!( 1314 insta::internals::AutoName, 1315 toml::from_str::<PackageConfig>(input) 1316 .unwrap_err() 1317 .to_string() 1318 ); 1319} 1320 1321#[cfg(windows)] 1322#[test] 1323fn docs_prefix_windows_dot_dot_string_path() { 1324 let input = r#" 1325name = "one_two" 1326 1327[documentation] 1328pages = [{ title = "My Page", path = "something\\..\\..\\secrets.txt", source = "./path/to/my-page.md" }] 1329"#; 1330 1331 insta::assert_snapshot!( 1332 insta::internals::AutoName, 1333 toml::from_str::<PackageConfig>(input) 1334 .unwrap_err() 1335 .to_string() 1336 ); 1337} 1338 1339#[test] 1340fn docs_dot_dot_path_buf() { 1341 let input = r#" 1342name = "one_two" 1343 1344[documentation] 1345pages = [{ title = "My Page", path = "stuff.html", source = "../secrets.txt" }] 1346"#; 1347 1348 insta::assert_snapshot!( 1349 insta::internals::AutoName, 1350 toml::from_str::<PackageConfig>(input) 1351 .unwrap_err() 1352 .to_string() 1353 ); 1354} 1355 1356#[test] 1357fn docs_prefix_dot_dot_path_buf() { 1358 let input = r#" 1359name = "one_two" 1360 1361[documentation] 1362pages = [{ title = "My Page", path = "stuff.html", source = "something/../../secrets.txt" }] 1363"#; 1364 1365 insta::assert_snapshot!( 1366 insta::internals::AutoName, 1367 toml::from_str::<PackageConfig>(input) 1368 .unwrap_err() 1369 .to_string() 1370 ); 1371} 1372 1373#[cfg(windows)] 1374#[test] 1375fn docs_windows_dot_dot_path_buf() { 1376 let input = r#" 1377name = "one_two" 1378 1379[documentation] 1380pages = [{ title = "My Page", path = "stuff.html", source = "..\\secrets.txt" }] 1381"#; 1382 1383 assert!(toml::from_str::<PackageConfig>(input).is_err()) 1384} 1385 1386#[cfg(windows)] 1387#[test] 1388fn docs_prefix_windows_dot_dot_path_buf() { 1389 let input = r#" 1390name = "one_two" 1391 1392[documentation] 1393pages = [{ title = "My Page", path = "stuff.html", source = "something\\..\\..\\secrets.txt" }] 1394"#; 1395 1396 assert!(toml::from_str::<PackageConfig>(input).is_err()) 1397} 1398 1399#[cfg(windows)] 1400#[test] 1401fn docs_absolute_source_windows() { 1402 let input = r#" 1403name = "one_two" 1404 1405[documentation] 1406pages = [{ title = "My Page", path = "stuff.html", source = "C:\\etc\\passwd" }] 1407"#; 1408 1409 assert!(toml::from_str::<PackageConfig>(input).is_err()) 1410} 1411 1412#[test] 1413fn docs_absolute_source() { 1414 let input = r#" 1415name = "one_two" 1416 1417[documentation] 1418pages = [{ title = "My Page", path = "stuff.html", source = "/etc/passwd" }] 1419"#; 1420 1421 insta::assert_snapshot!( 1422 insta::internals::AutoName, 1423 toml::from_str::<PackageConfig>(input) 1424 .unwrap_err() 1425 .to_string() 1426 ); 1427} 1428 1429#[test] 1430fn package_config_to_json() { 1431 let input = r#" 1432name = "my_project" 1433version = "1.0.0" 1434licences = ["Apache-2.0", "MIT"] 1435description = "Pretty complex config" 1436target = "erlang" 1437repository = { type = "github", user = "example", repo = "my_dep" } 1438links = [{ title = "Home page", href = "https://example.com" }] 1439internal_modules = ["my_app/internal"] 1440gleam = ">= 0.30.0" 1441 1442[dependencies] 1443gleam_stdlib = ">= 0.18.0 and < 2.0.0" 1444my_other_project = { path = "../my_other_project" } 1445 1446[dev_dependencies] 1447gleeunit = ">= 1.0.0 and < 2.0.0" 1448 1449[documentation] 1450pages = [{ title = "My Page", path = "my-page.html", source = "./path/to/my-page.md" }] 1451 1452[erlang] 1453application_start_module = "my_app/application" 1454extra_applications = ["inets", "ssl"] 1455 1456[javascript] 1457typescript_declarations = true 1458source_maps = true 1459runtime = "node" 1460 1461[javascript.deno] 1462allow_all = false 1463allow_ffi = true 1464allow_env = ["DATABASE_URL"] 1465allow_net = ["example.com:443"] 1466allow_read = ["./database.sqlite"] 1467"#; 1468 1469 let config = toml::from_str::<PackageConfig>(input).unwrap(); 1470 let json = serde_json::to_string_pretty(&config).unwrap(); 1471 let output = format!("--- GLEAM.TOML\n{input}\n\n--- EXPORTED JSON\n\n{json}"); 1472 insta::assert_snapshot!(output); 1473 1474 let roundtrip = serde_json::from_str::<PackageConfig>(&json).unwrap(); 1475 assert_eq!(config, roundtrip); 1476} 1477 1478#[test] 1479fn barebones_package_config_to_json() { 1480 let input = r#" 1481name = "my_project" 1482version = "1.0.0" 1483"#; 1484 1485 let config = toml::from_str::<PackageConfig>(input).unwrap(); 1486 let json = serde_json::to_string_pretty(&config).unwrap(); 1487 let output = format!("--- GLEAM.TOML\n{input}\n\n--- EXPORTED JSON\n\n{json}"); 1488 insta::assert_snapshot!(output); 1489} 1490 1491#[test] 1492fn dev_deps_field_name() { 1493 let toml = r#" 1494name = "wibble" 1495version = "1.0.0" 1496 1497[dev_dependencies] 1498wibble = ">= 1.0.0 and < 2.0.0" 1499"#; 1500 let hyphen_alternative = deserialise_config("gleam.toml", toml.into()).expect("valid config"); 1501 let toml = r#" 1502name = "wibble" 1503version = "1.0.0" 1504 1505[dev_dependencies] 1506wibble = ">= 1.0.0 and < 2.0.0" 1507"#; 1508 let canonical = deserialise_config("gleam.toml", toml.into()).expect("valid config"); 1509 assert_eq!(canonical, hyphen_alternative) 1510}