this repo has no description
at wasm 786 lines 24 kB view raw
1mod printer; 2mod source_links; 3#[cfg(test)] 4mod tests; 5 6use std::{collections::HashMap, time::SystemTime}; 7 8use camino::Utf8PathBuf; 9use hexpm::version::Version; 10use printer::Printer; 11 12use crate::{ 13 build::{Module, Package}, 14 config::{DocsPage, PackageConfig}, 15 docs::source_links::SourceLinker, 16 io::{Content, FileSystemReader, OutputFile}, 17 package_interface::PackageInterface, 18 paths::ProjectPaths, 19 type_::{self}, 20 version::COMPILER_VERSION, 21}; 22use askama::Template; 23use ecow::EcoString; 24use itertools::Itertools; 25use serde::{Deserialize, Serialize}; 26use serde_json::to_string as serde_to_string; 27 28#[derive(PartialEq, Eq, Copy, Clone, Debug)] 29pub enum DocContext { 30 HexPublish, 31 Build, 32} 33 34#[derive(PartialEq, Debug, Serialize, Deserialize)] 35pub struct PackageInformation { 36 #[serde(rename = "gleam.toml")] 37 package_config: PackageConfig, 38} 39 40/// Like `ManifestPackage`, but lighter and cheaper to clone as it is all that 41/// we need for printing documentation. 42#[derive(Debug, Clone)] 43pub struct Dependency { 44 pub version: Version, 45 pub kind: DependencyKind, 46} 47 48#[derive(Debug, Clone, Copy)] 49pub enum DependencyKind { 50 Hex, 51 Path, 52 Git, 53} 54 55#[derive(Debug)] 56pub struct DocumentationConfig<'a> { 57 pub package_config: &'a PackageConfig, 58 pub dependencies: HashMap<EcoString, Dependency>, 59 pub analysed: &'a [Module], 60 pub docs_pages: &'a [DocsPage], 61 pub rendering_timestamp: SystemTime, 62 pub context: DocContext, 63} 64 65pub fn generate_html<IO: FileSystemReader>( 66 paths: &ProjectPaths, 67 config: DocumentationConfig<'_>, 68 fs: IO, 69) -> Vec<OutputFile> { 70 let DocumentationConfig { 71 package_config: config, 72 dependencies, 73 analysed, 74 docs_pages, 75 rendering_timestamp, 76 context: is_hex_publish, 77 } = config; 78 79 let modules = analysed 80 .iter() 81 .filter(|module| module.origin.is_src()) 82 .filter(|module| !config.is_internal_module(&module.name)); 83 84 let rendering_timestamp = rendering_timestamp 85 .duration_since(SystemTime::UNIX_EPOCH) 86 .expect("get current timestamp") 87 .as_secs() 88 .to_string(); 89 90 // Define user-supplied (or README) pages 91 let pages: Vec<_> = docs_pages 92 .iter() 93 .map(|page| Link { 94 name: page.title.to_string(), 95 path: page.path.to_string(), 96 }) 97 .collect(); 98 99 let doc_links = config.links.iter().map(|doc_link| Link { 100 name: doc_link.title.to_string(), 101 path: doc_link.href.to_string(), 102 }); 103 104 let repo_link = config 105 .repository 106 .as_ref() 107 .map(|r| r.url()) 108 .map(|path| Link { 109 name: "Repository".into(), 110 path, 111 }); 112 113 let host = if is_hex_publish == DocContext::HexPublish { 114 "https://hexdocs.pm" 115 } else { 116 "" 117 }; 118 119 // https://github.com/gleam-lang/gleam/issues/3020 120 let links: Vec<_> = match is_hex_publish { 121 DocContext::HexPublish => doc_links 122 .chain(repo_link) 123 .chain([Link { 124 name: "Hex".into(), 125 path: format!("https://hex.pm/packages/{0}", config.name).to_string(), 126 }]) 127 .collect(), 128 DocContext::Build => doc_links.chain(repo_link).collect(), 129 }; 130 131 let mut files = vec![]; 132 133 let mut search_items = vec![]; 134 135 let modules_links: Vec<_> = modules 136 .clone() 137 .map(|m| { 138 let path = [&m.name, ".html"].concat(); 139 Link { 140 path, 141 name: m.name.split('/').join("<wbr />/"), 142 } 143 }) 144 .sorted() 145 .collect(); 146 147 // Generate user-supplied (or README) pages 148 for page in docs_pages { 149 let content = fs.read(&page.source).unwrap_or_default(); 150 let rendered_content = render_markdown(&content, MarkdownSource::Standalone); 151 let unnest = page_unnest(&page.path); 152 153 let page_path_without_ext = page.path.split('.').next().unwrap_or(""); 154 let page_title = match page_path_without_ext { 155 // The index page, such as README, should not push it's page title 156 "index" => format!("{} · v{}", config.name, config.version), 157 // Other page title's should say so 158 _other => format!("{} · {} · v{}", page.title, config.name, config.version), 159 }; 160 let page_meta_description = match page_path_without_ext { 161 "index" => config.description.to_string().clone(), 162 _other => "".to_owned(), 163 }; 164 let path = Utf8PathBuf::from(&page.path); 165 166 let temp = PageTemplate { 167 gleam_version: COMPILER_VERSION, 168 links: &links, 169 pages: &pages, 170 modules: &modules_links, 171 project_name: &config.name, 172 page_title: &page_title, 173 page_meta_description: &page_meta_description, 174 file_path: &path.clone(), 175 project_version: &config.version.to_string(), 176 content: rendered_content, 177 rendering_timestamp: &rendering_timestamp, 178 host, 179 unnest: &unnest, 180 }; 181 182 files.push(OutputFile { 183 path, 184 content: Content::Text(temp.render().expect("Page template rendering")), 185 }); 186 187 search_items.push(SearchItem { 188 type_: SearchItemType::Page, 189 parent_title: config.name.to_string(), 190 title: config.name.to_string(), 191 content: escape_html_content(content), 192 reference: page.path.to_string(), 193 }) 194 } 195 196 // Generate module documentation pages 197 for module in modules { 198 let name = module.name.clone(); 199 let unnest = page_unnest(&module.name); 200 201 // Read module src & create line number lookup structure 202 let source_links = SourceLinker::new(paths, config, module); 203 204 let documentation_content = module.ast.documentation.iter().join("\n"); 205 let rendered_documentation = 206 render_markdown(&documentation_content.clone(), MarkdownSource::Comment); 207 208 let mut printer = Printer::new( 209 module.ast.type_info.package.clone(), 210 module.name.clone(), 211 &module.ast.names, 212 &dependencies, 213 ); 214 215 let types: Vec<TypeDefinition<'_>> = module 216 .ast 217 .definitions 218 .iter() 219 .filter_map(|definition| printer.type_definition(&source_links, definition)) 220 .sorted() 221 .collect(); 222 223 let values: Vec<DocsValues<'_>> = module 224 .ast 225 .definitions 226 .iter() 227 .filter_map(|definition| printer.value(&source_links, definition)) 228 .sorted() 229 .collect(); 230 231 types.iter().for_each(|type_| { 232 let constructors = type_ 233 .constructors 234 .iter() 235 .map(|constructor| { 236 let arguments = constructor 237 .arguments 238 .iter() 239 .map(|argument| format!("{}\n{}", argument.name, argument.doc)) 240 .join("\n"); 241 242 format!( 243 "{}\n{}\n{}", 244 constructor.definition, constructor.text_documentation, arguments 245 ) 246 }) 247 .join("\n"); 248 249 search_items.push(SearchItem { 250 type_: SearchItemType::Type, 251 parent_title: module.name.to_string(), 252 title: type_.name.to_string(), 253 content: format!( 254 "{}\n{}\n{}\n{}", 255 type_.definition, 256 type_.text_documentation, 257 constructors, 258 import_synonyms(&module.name, type_.name) 259 ), 260 reference: format!("{}.html#{}", module.name, type_.name), 261 }) 262 }); 263 values.iter().for_each(|constant| { 264 search_items.push(SearchItem { 265 type_: SearchItemType::Value, 266 parent_title: module.name.to_string(), 267 title: constant.name.to_string(), 268 content: format!( 269 "{}\n{}\n{}", 270 constant.definition, 271 constant.text_documentation, 272 import_synonyms(&module.name, constant.name) 273 ), 274 reference: format!("{}.html#{}", module.name, constant.name), 275 }) 276 }); 277 278 search_items.push(SearchItem { 279 type_: SearchItemType::Module, 280 parent_title: module.name.to_string(), 281 title: module.name.to_string(), 282 content: documentation_content, 283 reference: format!("{}.html", module.name), 284 }); 285 286 let page_title = format!("{} · {} · v{}", name, config.name, config.version); 287 let page_meta_description = ""; 288 let path = Utf8PathBuf::from(format!("{}.html", module.name)); 289 290 let template = ModuleTemplate { 291 gleam_version: COMPILER_VERSION, 292 host, 293 unnest, 294 links: &links, 295 pages: &pages, 296 documentation: rendered_documentation, 297 modules: &modules_links, 298 project_name: &config.name, 299 page_title: &page_title, 300 page_meta_description, 301 module_name: EcoString::from(&name), 302 file_path: &path.clone(), 303 project_version: &config.version.to_string(), 304 types, 305 values, 306 rendering_timestamp: &rendering_timestamp, 307 }; 308 309 files.push(OutputFile { 310 path, 311 content: Content::Text( 312 template 313 .render() 314 .expect("Module documentation template rendering"), 315 ), 316 }); 317 } 318 319 // Render static assets 320 321 files.push(OutputFile { 322 path: Utf8PathBuf::from("css/atom-one-light.min.css"), 323 content: Content::Text( 324 std::include_str!("../templates/docs-css/atom-one-light.min.css").to_string(), 325 ), 326 }); 327 328 files.push(OutputFile { 329 path: Utf8PathBuf::from("css/atom-one-dark.min.css"), 330 content: Content::Text( 331 std::include_str!("../templates/docs-css/atom-one-dark.min.css").to_string(), 332 ), 333 }); 334 335 files.push(OutputFile { 336 path: Utf8PathBuf::from("css/index.css"), 337 content: Content::Text(std::include_str!("../templates/docs-css/index.css").to_string()), 338 }); 339 340 // highlightjs: 341 342 files.push(OutputFile { 343 path: Utf8PathBuf::from("js/highlight.min.js"), 344 content: Content::Text( 345 std::include_str!("../templates/docs-js/highlight.min.js").to_string(), 346 ), 347 }); 348 349 files.push(OutputFile { 350 path: Utf8PathBuf::from("js/highlightjs-gleam.js"), 351 content: Content::Text( 352 std::include_str!("../templates/docs-js/highlightjs-gleam.js").to_string(), 353 ), 354 }); 355 356 files.push(OutputFile { 357 path: Utf8PathBuf::from("js/highlightjs-erlang.min.js"), 358 content: Content::Text( 359 std::include_str!("../templates/docs-js/highlightjs-erlang.min.js").to_string(), 360 ), 361 }); 362 363 files.push(OutputFile { 364 path: Utf8PathBuf::from("js/highlightjs-elixir.min.js"), 365 content: Content::Text( 366 std::include_str!("../templates/docs-js/highlightjs-elixir.min.js").to_string(), 367 ), 368 }); 369 370 files.push(OutputFile { 371 path: Utf8PathBuf::from("js/highlightjs-javascript.min.js"), 372 content: Content::Text( 373 std::include_str!("../templates/docs-js/highlightjs-javascript.min.js").to_string(), 374 ), 375 }); 376 377 files.push(OutputFile { 378 path: Utf8PathBuf::from("js/highlightjs-typescript.min.js"), 379 content: Content::Text( 380 std::include_str!("../templates/docs-js/highlightjs-typescript.min.js").to_string(), 381 ), 382 }); 383 384 // lunr.min.js, search_data.json and index.js 385 386 files.push(OutputFile { 387 path: Utf8PathBuf::from("js/lunr.min.js"), 388 content: Content::Text(std::include_str!("../templates/docs-js/lunr.min.js").to_string()), 389 }); 390 391 let search_data_json = serde_to_string(&SearchData { 392 items: search_items, 393 programming_language: SearchProgrammingLanguage::Gleam, 394 }) 395 .expect("search index serialization"); 396 397 files.push(OutputFile { 398 path: Utf8PathBuf::from("search_data.json"), 399 content: Content::Text(search_data_json.to_string()), 400 }); 401 402 files.push(OutputFile { 403 path: Utf8PathBuf::from("js/index.js"), 404 content: Content::Text(std::include_str!("../templates/docs-js/index.js").to_string()), 405 }); 406 407 // web fonts: 408 409 files.push(OutputFile { 410 path: Utf8PathBuf::from("fonts/karla-v23-regular-latin-ext.woff2"), 411 content: Content::Binary( 412 include_bytes!("../templates/docs-fonts/karla-v23-regular-latin-ext.woff2").to_vec(), 413 ), 414 }); 415 416 files.push(OutputFile { 417 path: Utf8PathBuf::from("fonts/karla-v23-regular-latin.woff2"), 418 content: Content::Binary( 419 include_bytes!("../templates/docs-fonts/karla-v23-regular-latin.woff2").to_vec(), 420 ), 421 }); 422 423 files.push(OutputFile { 424 path: Utf8PathBuf::from("fonts/karla-v23-bold-latin-ext.woff2"), 425 content: Content::Binary( 426 include_bytes!("../templates/docs-fonts/karla-v23-bold-latin-ext.woff2").to_vec(), 427 ), 428 }); 429 430 files.push(OutputFile { 431 path: Utf8PathBuf::from("fonts/karla-v23-bold-latin.woff2"), 432 content: Content::Binary( 433 include_bytes!("../templates/docs-fonts/karla-v23-bold-latin.woff2").to_vec(), 434 ), 435 }); 436 437 files.push(OutputFile { 438 path: Utf8PathBuf::from("fonts/ubuntu-mono-v15-regular-cyrillic-ext.woff2"), 439 content: Content::Binary( 440 include_bytes!("../templates/docs-fonts/ubuntu-mono-v15-regular-cyrillic-ext.woff2") 441 .to_vec(), 442 ), 443 }); 444 445 files.push(OutputFile { 446 path: Utf8PathBuf::from("fonts/ubuntu-mono-v15-regular-cyrillic.woff2"), 447 content: Content::Binary( 448 include_bytes!("../templates/docs-fonts/ubuntu-mono-v15-regular-cyrillic.woff2") 449 .to_vec(), 450 ), 451 }); 452 453 files.push(OutputFile { 454 path: Utf8PathBuf::from("fonts/ubuntu-mono-v15-regular-greek-ext.woff2"), 455 content: Content::Binary( 456 include_bytes!("../templates/docs-fonts/ubuntu-mono-v15-regular-greek-ext.woff2") 457 .to_vec(), 458 ), 459 }); 460 461 files.push(OutputFile { 462 path: Utf8PathBuf::from("fonts/ubuntu-mono-v15-regular-greek.woff2"), 463 content: Content::Binary( 464 include_bytes!("../templates/docs-fonts/ubuntu-mono-v15-regular-greek.woff2").to_vec(), 465 ), 466 }); 467 468 files.push(OutputFile { 469 path: Utf8PathBuf::from("fonts/ubuntu-mono-v15-regular-latin-ext.woff2"), 470 content: Content::Binary( 471 include_bytes!("../templates/docs-fonts/ubuntu-mono-v15-regular-latin-ext.woff2") 472 .to_vec(), 473 ), 474 }); 475 476 files.push(OutputFile { 477 path: Utf8PathBuf::from("fonts/ubuntu-mono-v15-regular-latin.woff2"), 478 content: Content::Binary( 479 include_bytes!("../templates/docs-fonts/ubuntu-mono-v15-regular-latin.woff2").to_vec(), 480 ), 481 }); 482 483 files 484} 485 486pub fn generate_json_package_interface( 487 path: Utf8PathBuf, 488 package: &Package, 489 cached_modules: &im::HashMap<EcoString, type_::ModuleInterface>, 490) -> OutputFile { 491 OutputFile { 492 path, 493 content: Content::Text( 494 serde_json::to_string(&PackageInterface::from_package(package, cached_modules)) 495 .expect("JSON module interface serialisation"), 496 ), 497 } 498} 499 500pub fn generate_json_package_information(path: Utf8PathBuf, config: PackageConfig) -> OutputFile { 501 OutputFile { 502 path, 503 content: Content::Text(package_information_as_json(config)), 504 } 505} 506 507fn package_information_as_json(config: PackageConfig) -> String { 508 let info = PackageInformation { 509 package_config: config, 510 }; 511 serde_json::to_string_pretty(&info).expect("JSON module information serialisation") 512} 513 514fn page_unnest(path: &str) -> String { 515 let unnest = path 516 .strip_prefix('/') 517 .unwrap_or(path) 518 .split('/') 519 .skip(1) 520 .map(|_| "..") 521 .join("/"); 522 if unnest.is_empty() { 523 ".".into() 524 } else { 525 unnest 526 } 527} 528 529#[test] 530fn page_unnest_test() { 531 // Pages 532 assert_eq!(page_unnest("wibble.html"), "."); 533 assert_eq!(page_unnest("/wibble.html"), "."); 534 assert_eq!(page_unnest("/wibble/woo.html"), ".."); 535 assert_eq!(page_unnest("/wibble/wobble/woo.html"), "../.."); 536 537 // Modules 538 assert_eq!(page_unnest("string"), "."); 539 assert_eq!(page_unnest("gleam/string"), ".."); 540 assert_eq!(page_unnest("gleam/string/inspect"), "../.."); 541} 542 543fn escape_html_content(it: String) -> String { 544 it.replace('&', "&amp;") 545 .replace('<', "&lt;") 546 .replace('>', "&gt;") 547 .replace('\"', "&quot;") 548 .replace('\'', "&#39;") 549} 550 551#[test] 552fn escape_html_content_test() { 553 assert_eq!( 554 escape_html_content("&<>\"'".to_string()), 555 "&amp;&lt;&gt;&quot;&#39;" 556 ); 557} 558 559fn import_synonyms(parent: &str, child: &str) -> String { 560 format!("Synonyms:\n{parent}.{child}\n{parent} {child}") 561} 562 563fn text_documentation(doc: &Option<(u32, EcoString)>) -> String { 564 let raw_text = doc 565 .as_ref() 566 .map(|(_, it)| it.to_string()) 567 .unwrap_or_else(|| "".into()); 568 569 // TODO: parse markdown properly and extract the text nodes 570 raw_text.replace("```gleam", "").replace("```", "") 571} 572 573fn markdown_documentation(doc: &Option<(u32, EcoString)>) -> String { 574 doc.as_ref() 575 .map(|(_, doc)| render_markdown(doc, MarkdownSource::Comment)) 576 .unwrap_or_default() 577} 578 579/// An enum to represent the source of a Markdown string to render. 580enum MarkdownSource { 581 /// A Markdown string that comes from the documentation of a 582 /// definition/module. This means that each line is going to be preceded by 583 /// a whitespace. 584 Comment, 585 /// A Markdown string coming from a standalone file like a README.md. 586 Standalone, 587} 588 589fn render_markdown(text: &str, source: MarkdownSource) -> String { 590 let text = match source { 591 MarkdownSource::Standalone => text.into(), 592 // Doc comments start with "///\s", which can confuse the markdown parser 593 // and prevent tables from rendering correctly, so remove that first space. 594 MarkdownSource::Comment => text 595 .split('\n') 596 .map(|s| s.strip_prefix(' ').unwrap_or(s)) 597 .join("\n"), 598 }; 599 600 let mut s = String::with_capacity(text.len() * 3 / 2); 601 let p = pulldown_cmark::Parser::new_ext(&text, pulldown_cmark::Options::all()); 602 pulldown_cmark::html::push_html(&mut s, p); 603 s 604} 605 606#[derive(PartialEq, Eq, PartialOrd, Ord, Clone)] 607struct Link { 608 name: String, 609 path: String, 610} 611 612#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)] 613struct TypeConstructor { 614 definition: String, 615 documentation: String, 616 text_documentation: String, 617 arguments: Vec<TypeConstructorArg>, 618} 619 620#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)] 621struct TypeConstructorArg { 622 name: String, 623 doc: String, 624} 625 626#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)] 627struct TypeDefinition<'a> { 628 name: &'a str, 629 definition: String, 630 documentation: String, 631 constructors: Vec<TypeConstructor>, 632 text_documentation: String, 633 source_url: String, 634 deprecation_message: String, 635 opaque: bool, 636} 637 638#[derive(PartialEq, Eq, PartialOrd, Ord)] 639struct DocsValues<'a> { 640 name: &'a str, 641 definition: String, 642 documentation: String, 643 text_documentation: String, 644 source_url: String, 645 deprecation_message: String, 646} 647 648#[derive(Template)] 649#[template(path = "documentation_page.html")] 650struct PageTemplate<'a> { 651 gleam_version: &'a str, 652 unnest: &'a str, 653 host: &'a str, 654 page_title: &'a str, 655 page_meta_description: &'a str, 656 file_path: &'a Utf8PathBuf, 657 project_name: &'a str, 658 project_version: &'a str, 659 pages: &'a [Link], 660 links: &'a [Link], 661 modules: &'a [Link], 662 content: String, 663 rendering_timestamp: &'a str, 664} 665 666#[derive(Template)] 667#[template(path = "documentation_module.html")] 668struct ModuleTemplate<'a> { 669 gleam_version: &'a str, 670 unnest: String, 671 host: &'a str, 672 page_title: &'a str, 673 page_meta_description: &'a str, 674 file_path: &'a Utf8PathBuf, 675 module_name: EcoString, 676 project_name: &'a str, 677 project_version: &'a str, 678 pages: &'a [Link], 679 links: &'a [Link], 680 modules: &'a [Link], 681 types: Vec<TypeDefinition<'a>>, 682 values: Vec<DocsValues<'a>>, 683 documentation: String, 684 rendering_timestamp: &'a str, 685} 686 687#[derive(Serialize, PartialEq, Eq, PartialOrd, Ord, Clone)] 688struct SearchData { 689 items: Vec<SearchItem>, 690 #[serde(rename = "proglang")] 691 programming_language: SearchProgrammingLanguage, 692} 693 694#[derive(Serialize, PartialEq, Eq, PartialOrd, Ord, Clone)] 695struct SearchItem { 696 #[serde(rename = "type")] 697 type_: SearchItemType, 698 #[serde(rename = "parentTitle")] 699 parent_title: String, 700 title: String, 701 #[serde(rename = "doc")] 702 content: String, 703 #[serde(rename = "ref")] 704 reference: String, 705} 706 707#[derive(Serialize, PartialEq, Eq, PartialOrd, Ord, Clone)] 708#[serde(rename_all = "lowercase")] 709enum SearchItemType { 710 Value, 711 Module, 712 Page, 713 Type, 714} 715 716#[derive(Serialize, PartialEq, Eq, PartialOrd, Ord, Clone)] 717#[serde(rename_all = "lowercase")] 718enum SearchProgrammingLanguage { 719 // Elixir, 720 // Erlang, 721 Gleam, 722} 723 724#[test] 725fn package_config_to_json() { 726 let input = r#" 727name = "my_project" 728version = "1.0.0" 729licences = ["Apache-2.0", "MIT"] 730description = "Pretty complex config" 731target = "erlang" 732repository = { type = "github", user = "example", repo = "my_dep" } 733links = [{ title = "Home page", href = "https://example.com" }] 734internal_modules = ["my_app/internal"] 735gleam = ">= 0.30.0" 736 737[dependencies] 738gleam_stdlib = ">= 0.18.0 and < 2.0.0" 739my_other_project = { path = "../my_other_project" } 740 741[dev-dependencies] 742gleeunit = ">= 1.0.0 and < 2.0.0" 743 744[documentation] 745pages = [{ title = "My Page", path = "my-page.html", source = "./path/to/my-page.md" }] 746 747[erlang] 748application_start_module = "my_app/application" 749extra_applications = ["inets", "ssl"] 750 751[javascript] 752typescript_declarations = true 753runtime = "node" 754 755[javascript.deno] 756allow_all = false 757allow_ffi = true 758allow_env = ["DATABASE_URL"] 759allow_net = ["example.com:443"] 760allow_read = ["./database.sqlite"] 761"#; 762 763 let config = toml::from_str::<PackageConfig>(&input).unwrap(); 764 let info = PackageInformation { 765 package_config: config.clone(), 766 }; 767 let json = package_information_as_json(config); 768 let output = format!("--- GLEAM.TOML\n{input}\n\n--- EXPORTED JSON\n\n{json}"); 769 insta::assert_snapshot!(output); 770 771 let roundtrip: PackageInformation = serde_json::from_str(&json).unwrap(); 772 assert_eq!(info, roundtrip); 773} 774 775#[test] 776fn barebones_package_config_to_json() { 777 let input = r#" 778name = "my_project" 779version = "1.0.0" 780"#; 781 782 let config = toml::from_str::<PackageConfig>(&input).unwrap(); 783 let json = package_information_as_json(config); 784 let output = format!("--- GLEAM.TOML\n{input}\n\n--- EXPORTED JSON\n\n{json}"); 785 insta::assert_snapshot!(output); 786}