Fork of daniellemaywood.uk/gleam — Wasm codegen work
24 kB
787 lines
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(search_item_for_page(&config.name, &page.path, content))
188 }
189
190 // Generate module documentation pages
191 for module in modules {
192 let name = module.name.clone();
193 let unnest = page_unnest(&module.name);
194
195 // Read module src & create line number lookup structure
196 let source_links = SourceLinker::new(paths, config, module);
197
198 let documentation_content = module.ast.documentation.iter().join("\n");
199 let rendered_documentation =
200 render_markdown(&documentation_content, MarkdownSource::Comment);
201
202 let mut printer = Printer::new(
203 module.ast.type_info.package.clone(),
204 module.name.clone(),
205 &module.ast.names,
206 &dependencies,
207 );
208
209 let types = printer.type_definitions(&source_links, &module.ast.definitions);
210 let values = printer.value_definitions(&source_links, &module.ast.definitions);
211
212 types
213 .iter()
214 .for_each(|type_| search_items.push(search_item_for_type(&module.name, type_)));
215 values
216 .iter()
217 .for_each(|value| search_items.push(search_item_for_value(&module.name, value)));
218
219 search_items.push(search_item_for_module(module));
220
221 let page_title = format!("{} · {} · v{}", name, config.name, config.version);
222 let page_meta_description = "";
223 let path = Utf8PathBuf::from(format!("{}.html", module.name));
224
225 let template = ModuleTemplate {
226 gleam_version: COMPILER_VERSION,
227 host,
228 unnest,
229 links: &links,
230 pages: &pages,
231 documentation: rendered_documentation,
232 modules: &modules_links,
233 project_name: &config.name,
234 page_title: &page_title,
235 page_meta_description,
236 module_name: EcoString::from(&name),
237 file_path: &path.clone(),
238 project_version: &config.version.to_string(),
239 types,
240 values,
241 rendering_timestamp: &rendering_timestamp,
242 };
243
244 files.push(OutputFile {
245 path,
246 content: Content::Text(
247 template
248 .render()
249 .expect("Module documentation template rendering"),
250 ),
251 });
252 }
253
254 // Render static assets
255
256 files.push(OutputFile {
257 path: Utf8PathBuf::from("css/atom-one-light.min.css"),
258 content: Content::Text(
259 std::include_str!("../templates/docs-css/atom-one-light.min.css").to_string(),
260 ),
261 });
262
263 files.push(OutputFile {
264 path: Utf8PathBuf::from("css/atom-one-dark.min.css"),
265 content: Content::Text(
266 std::include_str!("../templates/docs-css/atom-one-dark.min.css").to_string(),
267 ),
268 });
269
270 files.push(OutputFile {
271 path: Utf8PathBuf::from("css/index.css"),
272 content: Content::Text(std::include_str!("../templates/docs-css/index.css").to_string()),
273 });
274
275 // highlightjs:
276
277 files.push(OutputFile {
278 path: Utf8PathBuf::from("js/highlight.min.js"),
279 content: Content::Text(
280 std::include_str!("../templates/docs-js/highlight.min.js").to_string(),
281 ),
282 });
283
284 files.push(OutputFile {
285 path: Utf8PathBuf::from("js/highlightjs-gleam.js"),
286 content: Content::Text(
287 std::include_str!("../templates/docs-js/highlightjs-gleam.js").to_string(),
288 ),
289 });
290
291 files.push(OutputFile {
292 path: Utf8PathBuf::from("js/highlightjs-erlang.min.js"),
293 content: Content::Text(
294 std::include_str!("../templates/docs-js/highlightjs-erlang.min.js").to_string(),
295 ),
296 });
297
298 files.push(OutputFile {
299 path: Utf8PathBuf::from("js/highlightjs-elixir.min.js"),
300 content: Content::Text(
301 std::include_str!("../templates/docs-js/highlightjs-elixir.min.js").to_string(),
302 ),
303 });
304
305 files.push(OutputFile {
306 path: Utf8PathBuf::from("js/highlightjs-javascript.min.js"),
307 content: Content::Text(
308 std::include_str!("../templates/docs-js/highlightjs-javascript.min.js").to_string(),
309 ),
310 });
311
312 files.push(OutputFile {
313 path: Utf8PathBuf::from("js/highlightjs-typescript.min.js"),
314 content: Content::Text(
315 std::include_str!("../templates/docs-js/highlightjs-typescript.min.js").to_string(),
316 ),
317 });
318
319 // lunr.min.js, search-data.json and index.js
320
321 files.push(OutputFile {
322 path: Utf8PathBuf::from("js/lunr.min.js"),
323 content: Content::Text(std::include_str!("../templates/docs-js/lunr.min.js").to_string()),
324 });
325
326 let search_data_json = serde_to_string(&SearchData {
327 items: search_items,
328 programming_language: SearchProgrammingLanguage::Gleam,
329 })
330 .expect("search index serialization");
331
332 files.push(OutputFile {
333 path: Utf8PathBuf::from("search-data.json"),
334 content: Content::Text(search_data_json.to_string()),
335 });
336
337 files.push(OutputFile {
338 path: Utf8PathBuf::from("js/index.js"),
339 content: Content::Text(std::include_str!("../templates/docs-js/index.js").to_string()),
340 });
341
342 // web fonts:
343
344 files.push(OutputFile {
345 path: Utf8PathBuf::from("fonts/karla-v23-regular-latin-ext.woff2"),
346 content: Content::Binary(
347 include_bytes!("../templates/docs-fonts/karla-v23-regular-latin-ext.woff2").to_vec(),
348 ),
349 });
350
351 files.push(OutputFile {
352 path: Utf8PathBuf::from("fonts/karla-v23-regular-latin.woff2"),
353 content: Content::Binary(
354 include_bytes!("../templates/docs-fonts/karla-v23-regular-latin.woff2").to_vec(),
355 ),
356 });
357
358 files.push(OutputFile {
359 path: Utf8PathBuf::from("fonts/karla-v23-bold-latin-ext.woff2"),
360 content: Content::Binary(
361 include_bytes!("../templates/docs-fonts/karla-v23-bold-latin-ext.woff2").to_vec(),
362 ),
363 });
364
365 files.push(OutputFile {
366 path: Utf8PathBuf::from("fonts/karla-v23-bold-latin.woff2"),
367 content: Content::Binary(
368 include_bytes!("../templates/docs-fonts/karla-v23-bold-latin.woff2").to_vec(),
369 ),
370 });
371
372 files.push(OutputFile {
373 path: Utf8PathBuf::from("fonts/ubuntu-mono-v15-regular-cyrillic-ext.woff2"),
374 content: Content::Binary(
375 include_bytes!("../templates/docs-fonts/ubuntu-mono-v15-regular-cyrillic-ext.woff2")
376 .to_vec(),
377 ),
378 });
379
380 files.push(OutputFile {
381 path: Utf8PathBuf::from("fonts/ubuntu-mono-v15-regular-cyrillic.woff2"),
382 content: Content::Binary(
383 include_bytes!("../templates/docs-fonts/ubuntu-mono-v15-regular-cyrillic.woff2")
384 .to_vec(),
385 ),
386 });
387
388 files.push(OutputFile {
389 path: Utf8PathBuf::from("fonts/ubuntu-mono-v15-regular-greek-ext.woff2"),
390 content: Content::Binary(
391 include_bytes!("../templates/docs-fonts/ubuntu-mono-v15-regular-greek-ext.woff2")
392 .to_vec(),
393 ),
394 });
395
396 files.push(OutputFile {
397 path: Utf8PathBuf::from("fonts/ubuntu-mono-v15-regular-greek.woff2"),
398 content: Content::Binary(
399 include_bytes!("../templates/docs-fonts/ubuntu-mono-v15-regular-greek.woff2").to_vec(),
400 ),
401 });
402
403 files.push(OutputFile {
404 path: Utf8PathBuf::from("fonts/ubuntu-mono-v15-regular-latin-ext.woff2"),
405 content: Content::Binary(
406 include_bytes!("../templates/docs-fonts/ubuntu-mono-v15-regular-latin-ext.woff2")
407 .to_vec(),
408 ),
409 });
410
411 files.push(OutputFile {
412 path: Utf8PathBuf::from("fonts/ubuntu-mono-v15-regular-latin.woff2"),
413 content: Content::Binary(
414 include_bytes!("../templates/docs-fonts/ubuntu-mono-v15-regular-latin.woff2").to_vec(),
415 ),
416 });
417
418 files
419}
420
421fn search_item_for_page(package: &str, path: &str, content: String) -> SearchItem {
422 SearchItem {
423 type_: SearchItemType::Page,
424 parent_title: package.to_string(),
425 title: package.to_string(),
426 content,
427 reference: path.to_string(),
428 }
429}
430
431fn search_item_for_type(module: &str, type_: &TypeDefinition<'_>) -> SearchItem {
432 let constructors = type_
433 .constructors
434 .iter()
435 .map(|constructor| {
436 let arguments = constructor
437 .arguments
438 .iter()
439 .map(|argument| format!("{}\n{}", argument.name, argument.text_documentation))
440 .join("\n");
441
442 format!(
443 "{}\n{}\n{}",
444 constructor.raw_definition, constructor.text_documentation, arguments
445 )
446 })
447 .join("\n");
448
449 SearchItem {
450 type_: SearchItemType::Type,
451 parent_title: module.to_string(),
452 title: type_.name.to_string(),
453 content: format!(
454 "{}\n{}\n{}\n{}",
455 type_.raw_definition,
456 type_.text_documentation,
457 constructors,
458 import_synonyms(module, type_.name)
459 ),
460 reference: format!("{}.html#{}", module, type_.name),
461 }
462}
463
464fn search_item_for_value(module: &str, value: &DocsValues<'_>) -> SearchItem {
465 SearchItem {
466 type_: SearchItemType::Value,
467 parent_title: module.to_string(),
468 title: value.name.to_string(),
469 content: format!(
470 "{}\n{}\n{}",
471 value.raw_definition,
472 value.text_documentation,
473 import_synonyms(module, value.name)
474 ),
475 reference: format!("{}.html#{}", module, value.name),
476 }
477}
478
479fn search_item_for_module(module: &Module) -> SearchItem {
480 SearchItem {
481 type_: SearchItemType::Module,
482 parent_title: module.name.to_string(),
483 title: module.name.to_string(),
484 content: module.ast.documentation.iter().join("\n"),
485 reference: format!("{}.html", module.name),
486 }
487}
488
489pub fn generate_json_package_interface(
490 path: Utf8PathBuf,
491 package: &Package,
492 cached_modules: &im::HashMap<EcoString, type_::ModuleInterface>,
493) -> OutputFile {
494 OutputFile {
495 path,
496 content: Content::Text(
497 serde_json::to_string(&PackageInterface::from_package(package, cached_modules))
498 .expect("JSON module interface serialisation"),
499 ),
500 }
501}
502
503pub fn generate_json_package_information(path: Utf8PathBuf, config: PackageConfig) -> OutputFile {
504 OutputFile {
505 path,
506 content: Content::Text(package_information_as_json(config)),
507 }
508}
509
510fn package_information_as_json(config: PackageConfig) -> String {
511 let info = PackageInformation {
512 package_config: config,
513 };
514 serde_json::to_string_pretty(&info).expect("JSON module information serialisation")
515}
516
517fn page_unnest(path: &str) -> String {
518 let unnest = path
519 .strip_prefix('/')
520 .unwrap_or(path)
521 .split('/')
522 .skip(1)
523 .map(|_| "..")
524 .join("/");
525 if unnest.is_empty() {
526 ".".into()
527 } else {
528 unnest
529 }
530}
531
532#[test]
533fn page_unnest_test() {
534 // Pages
535 assert_eq!(page_unnest("wibble.html"), ".");
536 assert_eq!(page_unnest("/wibble.html"), ".");
537 assert_eq!(page_unnest("/wibble/woo.html"), "..");
538 assert_eq!(page_unnest("/wibble/wobble/woo.html"), "../..");
539
540 // Modules
541 assert_eq!(page_unnest("string"), ".");
542 assert_eq!(page_unnest("gleam/string"), "..");
543 assert_eq!(page_unnest("gleam/string/inspect"), "../..");
544}
545
546fn import_synonyms(parent: &str, child: &str) -> String {
547 format!("Synonyms:\n{parent}.{child}\n{parent} {child}")
548}
549
550fn text_documentation(doc: &Option<(u32, EcoString)>) -> String {
551 let raw_text = doc
552 .as_ref()
553 .map(|(_, it)| it.to_string())
554 .unwrap_or_else(|| "".into());
555
556 // TODO: parse markdown properly and extract the text nodes
557 raw_text.replace("```gleam", "").replace("```", "")
558}
559
560fn markdown_documentation(doc: &Option<(u32, EcoString)>) -> String {
561 doc.as_ref()
562 .map(|(_, doc)| render_markdown(doc, MarkdownSource::Comment))
563 .unwrap_or_default()
564}
565
566/// An enum to represent the source of a Markdown string to render.
567enum MarkdownSource {
568 /// A Markdown string that comes from the documentation of a
569 /// definition/module. This means that each line is going to be preceded by
570 /// a whitespace.
571 Comment,
572 /// A Markdown string coming from a standalone file like a README.md.
573 Standalone,
574}
575
576fn render_markdown(text: &str, source: MarkdownSource) -> String {
577 let text = match source {
578 MarkdownSource::Standalone => text.into(),
579 // Doc comments start with "///\s", which can confuse the markdown parser
580 // and prevent tables from rendering correctly, so remove that first space.
581 MarkdownSource::Comment => text
582 .split('\n')
583 .map(|s| s.strip_prefix(' ').unwrap_or(s))
584 .join("\n"),
585 };
586
587 let mut s = String::with_capacity(text.len() * 3 / 2);
588 let p = pulldown_cmark::Parser::new_ext(&text, pulldown_cmark::Options::all());
589 pulldown_cmark::html::push_html(&mut s, p);
590 s
591}
592
593#[derive(PartialEq, Eq, PartialOrd, Ord, Clone)]
594struct Link {
595 name: String,
596 path: String,
597}
598
599#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
600struct TypeConstructor {
601 definition: String,
602 raw_definition: String,
603 documentation: String,
604 text_documentation: String,
605 arguments: Vec<TypeConstructorArg>,
606}
607
608#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
609struct TypeConstructorArg {
610 name: String,
611 doc: String,
612 text_documentation: String,
613}
614
615#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
616struct TypeDefinition<'a> {
617 name: &'a str,
618 definition: String,
619 raw_definition: String,
620 documentation: String,
621 constructors: Vec<TypeConstructor>,
622 text_documentation: String,
623 source_url: String,
624 deprecation_message: String,
625 opaque: bool,
626}
627
628#[derive(PartialEq, Eq, PartialOrd, Ord)]
629struct DocsValues<'a> {
630 name: &'a str,
631 definition: String,
632 raw_definition: String,
633 documentation: String,
634 text_documentation: String,
635 source_url: String,
636 deprecation_message: String,
637}
638
639#[derive(Template)]
640#[template(path = "documentation_page.html")]
641struct PageTemplate<'a> {
642 gleam_version: &'a str,
643 unnest: &'a str,
644 host: &'a str,
645 page_title: &'a str,
646 page_meta_description: &'a str,
647 file_path: &'a Utf8PathBuf,
648 project_name: &'a str,
649 project_version: &'a str,
650 pages: &'a [Link],
651 links: &'a [Link],
652 modules: &'a [Link],
653 content: String,
654 rendering_timestamp: &'a str,
655}
656
657#[derive(Template)]
658#[template(path = "documentation_module.html")]
659struct ModuleTemplate<'a> {
660 gleam_version: &'a str,
661 unnest: String,
662 host: &'a str,
663 page_title: &'a str,
664 page_meta_description: &'a str,
665 file_path: &'a Utf8PathBuf,
666 module_name: EcoString,
667 project_name: &'a str,
668 project_version: &'a str,
669 pages: &'a [Link],
670 links: &'a [Link],
671 modules: &'a [Link],
672 types: Vec<TypeDefinition<'a>>,
673 values: Vec<DocsValues<'a>>,
674 documentation: String,
675 rendering_timestamp: &'a str,
676}
677
678/// Search data for use by Hexdocs search, as well as the search built-in to
679/// generated documentation
680#[derive(Serialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
681struct SearchData {
682 items: Vec<SearchItem>,
683 #[serde(rename = "proglang")]
684 programming_language: SearchProgrammingLanguage,
685}
686
687/// A single item that can appear as a search result
688#[derive(Serialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
689struct SearchItem {
690 /// The type of item this is: Value, Type, Module, or other Page
691 #[serde(rename = "type")]
692 type_: SearchItemType,
693 /// The title of the module or package containing this search item
694 #[serde(rename = "parentTitle")]
695 parent_title: String,
696 /// The title of this item
697 title: String,
698 /// Markdown text which describes this item, containing documentation from
699 /// doc comments, as well as rendered definitions of types and values.
700 #[serde(rename = "doc")]
701 content: String,
702 /// The relative URL to the documentation for this search item, for example
703 /// `gleam/option.html#Option`
704 #[serde(rename = "ref")]
705 reference: String,
706}
707
708#[derive(Serialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
709#[serde(rename_all = "lowercase")]
710enum SearchItemType {
711 Value,
712 Module,
713 Page,
714 Type,
715}
716
717#[derive(Serialize, PartialEq, Eq, PartialOrd, Ord, Clone)]
718#[serde(rename_all = "lowercase")]
719enum SearchProgrammingLanguage {
720 // Elixir,
721 // Erlang,
722 Gleam,
723}
724
725#[test]
726fn package_config_to_json() {
727 let input = r#"
728name = "my_project"
729version = "1.0.0"
730licences = ["Apache-2.0", "MIT"]
731description = "Pretty complex config"
732target = "erlang"
733repository = { type = "github", user = "example", repo = "my_dep" }
734links = [{ title = "Home page", href = "https://example.com" }]
735internal_modules = ["my_app/internal"]
736gleam = ">= 0.30.0"
737
738[dependencies]
739gleam_stdlib = ">= 0.18.0 and < 2.0.0"
740my_other_project = { path = "../my_other_project" }
741
742[dev-dependencies]
743gleeunit = ">= 1.0.0 and < 2.0.0"
744
745[documentation]
746pages = [{ title = "My Page", path = "my-page.html", source = "./path/to/my-page.md" }]
747
748[erlang]
749application_start_module = "my_app/application"
750extra_applications = ["inets", "ssl"]
751
752[javascript]
753typescript_declarations = true
754runtime = "node"
755
756[javascript.deno]
757allow_all = false
758allow_ffi = true
759allow_env = ["DATABASE_URL"]
760allow_net = ["example.com:443"]
761allow_read = ["./database.sqlite"]
762"#;
763
764 let config = toml::from_str::<PackageConfig>(&input).unwrap();
765 let info = PackageInformation {
766 package_config: config.clone(),
767 };
768 let json = package_information_as_json(config);
769 let output = format!("--- GLEAM.TOML\n{input}\n\n--- EXPORTED JSON\n\n{json}");
770 insta::assert_snapshot!(output);
771
772 let roundtrip: PackageInformation = serde_json::from_str(&json).unwrap();
773 assert_eq!(info, roundtrip);
774}
775
776#[test]
777fn barebones_package_config_to_json() {
778 let input = r#"
779name = "my_project"
780version = "1.0.0"
781"#;
782
783 let config = toml::from_str::<PackageConfig>(&input).unwrap();
784 let json = package_information_as_json(config);
785 let output = format!("--- GLEAM.TOML\n{input}\n\n--- EXPORTED JSON\n\n{json}");
786 insta::assert_snapshot!(output);
787}