1use std::fs;
7use std::path::Path;
8
9use anyhow::Result;
10use chrono::Local;
11
12#[derive(Debug, Clone)]
14pub struct CrateGenerationParams<'a> {
15 pub output: &'a Path,
17 pub package: &'a str,
19 pub version: &'a str,
21 pub canonical_url: &'a str,
23 pub author: &'a str,
25 pub description: &'a str,
27 pub command_invoked: &'a str,
29 pub crate_name: Option<&'a str>,
32}
33
34#[derive(Debug, Clone)]
36pub struct CrateStatistics {
37 pub num_structs: usize,
39 pub num_enums: usize,
41 pub total_types: usize,
43 pub canonical_url: String,
45}
46
47pub fn generate_crate_structure(params: CrateGenerationParams) -> Result<()> {
49 let src_dir = params.output.join("src");
51 fs::create_dir_all(&src_dir)?;
52
53 let resource_dir = src_dir.join("resources");
55 let datatypes_dir = src_dir.join("datatypes");
56 let extensions_dir = src_dir.join("extensions");
57 let primitives_dir = src_dir.join("primitives");
58 let traits_dir = src_dir.join("traits");
59 let bindings_dir = src_dir.join("bindings");
60 let profiles_dir = src_dir.join("profiles");
61
62 fs::create_dir_all(&resource_dir)?;
63 fs::create_dir_all(&datatypes_dir)?;
64 fs::create_dir_all(&extensions_dir)?;
65 fs::create_dir_all(&primitives_dir)?;
66 fs::create_dir_all(&traits_dir)?;
67 fs::create_dir_all(&bindings_dir)?;
68 fs::create_dir_all(&profiles_dir)?;
69
70 let stats = generate_crate_statistics_from_organized_dirs(
72 &resource_dir,
73 &datatypes_dir,
74 &extensions_dir,
75 &primitives_dir,
76 )?;
77
78 let cargo_toml_content = generate_cargo_toml(
80 params.package,
81 params.version,
82 params.output,
83 params.crate_name,
84 );
85 let cargo_toml_path = params.output.join("Cargo.toml");
86 fs::write(&cargo_toml_path, cargo_toml_content)?;
87
88 let lib_rs_content = generate_lib_rs_idiomatic();
90 let lib_rs_path = src_dir.join("lib.rs");
91 fs::write(&lib_rs_path, lib_rs_content)?;
92
93 let macros_content = include_str!("../macros.rs");
95 let macros_path = src_dir.join("macros.rs");
96 fs::write(¯os_path, macros_content)?;
97
98 let validation_content =
100 crate::generators::ValidationTraitGenerator::generate_validation_module();
101 let validation_path = src_dir.join("validation.rs");
102 fs::write(&validation_path, validation_content)?;
103
104 let prelude_content = generate_prelude_module();
106 let prelude_path = src_dir.join("prelude.rs");
107 fs::write(&prelude_path, prelude_content)?;
108
109 generate_module_files(
111 &resource_dir,
112 &datatypes_dir,
113 &extensions_dir,
114 &primitives_dir,
115 &traits_dir,
116 &bindings_dir,
117 &profiles_dir,
118 )?;
119
120 let readme_content = generate_readme_md(
122 params.package,
123 params.version,
124 params.canonical_url,
125 params.author,
126 params.description,
127 params.command_invoked,
128 &stats,
129 params.crate_name,
130 );
131 let readme_path = params.output.join("README.md");
132 fs::write(&readme_path, readme_content)?;
133
134 Ok(())
140}
141
142fn generate_cargo_toml(
144 package: &str,
145 version: &str,
146 _output_dir: &Path,
147 crate_name_override: Option<&str>,
148) -> String {
149 let derived = package.replace(['.', '-'], "_");
151 let crate_name = crate_name_override.unwrap_or(&derived);
152 let lib_name = crate_name.replace('-', "_");
154
155 let rh_foundation_path = if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
157 let workspace_root = Path::new(&manifest_dir).parent().and_then(|p| p.parent());
159 if let Some(root) = workspace_root {
160 let foundation_path = root.join("crates/rh-foundation");
161 if foundation_path.exists() {
162 foundation_path.display().to_string()
164 } else {
165 "../../rh/crates/rh-foundation".to_string()
167 }
168 } else {
169 "../../rh/crates/rh-foundation".to_string()
170 }
171 } else {
172 "../../rh/crates/rh-foundation".to_string()
173 };
174
175 format!(
176 r#"[package]
177name = "{crate_name}"
178version = "0.1.0"
179edition = "2021"
180description = "Generated FHIR types from {package} package version {version}"
181authors = ["FHIR Code Generator"]
182license = "MIT OR Apache-2.0"
183
184[dependencies]
185serde = {{ version = "1.0", features = ["derive"] }}
186serde_json = "1.0"
187phf = {{ version = "0.11", features = ["macros"] }}
188once_cell = "1.19"
189rh-foundation = {{ path = "{rh_foundation_path}" }}
190
191[lib]
192name = "{lib_name}"
193path = "src/lib.rs"
194"#
195 )
196}
197
198fn generate_lib_rs_idiomatic() -> String {
200 let lib_content = r#"//! Generated FHIR Rust bindings
201//!
202//! This crate contains Rust types and traits for FHIR resources and data types.
203//! It includes macros for primitive field generation and maintains FHIR compliance.
204
205// Allow clippy lint for derivable Default implementations
206//
207// TODO: Future optimization - derive Default when possible instead of manual impl
208//
209// Currently, we generate explicit Default implementations for all structs.
210// Many of these could use #[derive(Default)] instead, which would be more idiomatic.
211//
212// Pros of deriving Default:
213// - More idiomatic Rust code
214// - Less generated code (no manual impl blocks)
215// - Clearer intent (all fields use Default::default())
216//
217// Cons of current approach (manual impl):
218// - Clippy warns about 1,100+ derivable implementations
219// - More verbose generated code
220//
221// Pros of current approach:
222// - Explicit and predictable behavior
223// - Handles mixed initialization patterns consistently
224// - Simpler code generation logic
225//
226// To implement derive-based approach would require:
227// 1. Analyze all field types to ensure they implement Default
228// 2. Detect required fields with non-Default initializations (String::new(), Vec::new(), etc.)
229// 3. Add "Default" to struct derives only when ALL fields can use Default::default()
230// 4. Skip manual impl generation for those structs
231//
232#![allow(clippy::derivable_impls)]
233
234pub mod macros;
235pub mod metadata;
236pub mod primitives;
237pub mod datatypes;
238pub mod extensions;
239pub mod resources;
240pub mod profiles;
241pub mod traits;
242pub mod bindings;
243pub mod validation;
244pub mod prelude;
245
246pub use serde::{Deserialize, Serialize};
247"#;
248
249 lib_content.to_string()
250}
251pub fn generate_module_files(
253 resource_dir: &Path,
254 datatypes_dir: &Path,
255 extensions_dir: &Path,
256 primitives_dir: &Path,
257 traits_dir: &Path,
258 bindings_dir: &Path,
259 profiles_dir: &Path,
260) -> Result<()> {
261 let resource_mod_content = generate_mod_rs_for_directory(resource_dir, "FHIR resource types")?;
263 fs::write(resource_dir.join("mod.rs"), resource_mod_content)?;
264
265 let datatypes_mod_content = generate_mod_rs_for_directory(datatypes_dir, "FHIR data types")?;
267 fs::write(datatypes_dir.join("mod.rs"), datatypes_mod_content)?;
268
269 let extensions_mod_content =
271 generate_mod_rs_for_directory(extensions_dir, "FHIR extension types")?;
272 fs::write(extensions_dir.join("mod.rs"), extensions_mod_content)?;
273
274 let primitives_mod_content =
276 generate_mod_rs_for_directory(primitives_dir, "FHIR primitive types")?;
277 fs::write(primitives_dir.join("mod.rs"), primitives_mod_content)?;
278
279 let traits_mod_path = traits_dir.join("mod.rs");
281 if !traits_mod_path.exists() || fs::read_to_string(&traits_mod_path)?.len() < 500 {
282 let traits_mod_content = generate_traits_mod_rs()?;
284 fs::write(traits_mod_path, traits_mod_content)?;
285 }
286
287 let bindings_mod_content =
289 generate_mod_rs_for_directory(bindings_dir, "FHIR ValueSet bindings and enums")?;
290 fs::write(bindings_dir.join("mod.rs"), bindings_mod_content)?;
291
292 let profiles_mod_content =
294 generate_mod_rs_for_directory(profiles_dir, "FHIR profiles derived from core resources")?;
295 fs::write(profiles_dir.join("mod.rs"), profiles_mod_content)?;
296
297 Ok(())
298}
299
300fn generate_mod_rs_for_directory(dir: &Path, description: &str) -> Result<String> {
302 let mut content = String::new();
303 content.push_str(&format!("//! {description}\n\n"));
304
305 let mut rs_files = Vec::new();
307 if dir.exists() {
308 for entry in fs::read_dir(dir)? {
309 let entry = entry?;
310 let path = entry.path();
311 if path.is_file() && path.extension().is_some_and(|ext| ext == "rs") {
312 if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
313 if stem != "mod" {
314 rs_files.push(stem.to_string());
315 }
316 }
317 }
318 }
319 }
320
321 rs_files.sort();
323
324 for module_name in &rs_files {
326 content.push_str(&format!("pub mod {module_name};\n"));
327 }
328
329 Ok(content)
333}
334
335fn generate_traits_mod_rs() -> Result<String> {
337 let content = r#"//! FHIR traits for common functionality
338//!
339//! This module contains traits that define common interfaces for FHIR types.
340
341// Placeholder traits - these would be generated based on FHIR structure definitions
342
343/// Trait for types that have extensions
344pub trait HasExtensions {
345 /// Get the extensions for this type
346 fn extensions(&self) -> &[crate::datatypes::extension::Extension];
347}
348
349/// Trait for FHIR resources
350pub trait Resource {
351 /// Get the resource type name
352 fn resource_type(&self) -> &'static str;
353
354 /// Get the logical id of this resource
355 fn id(&self) -> Option<&str>;
356
357 /// Get the metadata about this resource
358 fn meta(&self) -> Option<&crate::datatypes::meta::Meta>;
359}
360
361/// Trait for domain resources (resources that can have narrative)
362pub trait DomainResource: Resource + HasExtensions {
363 /// Get the narrative text for this domain resource
364 fn narrative(&self) -> Option<&crate::datatypes::narrative::Narrative>;
365}
366"#;
367
368 Ok(content.to_string())
369}
370
371fn generate_crate_statistics_from_organized_dirs(
373 resource_dir: &Path,
374 datatypes_dir: &Path,
375 extensions_dir: &Path,
376 primitives_dir: &Path,
377) -> Result<CrateStatistics> {
378 let mut num_structs = 0;
379
380 for dir in [resource_dir, datatypes_dir, extensions_dir, primitives_dir] {
382 if dir.exists() {
383 for entry in fs::read_dir(dir)? {
384 let entry = entry?;
385 let path = entry.path();
386 if path.is_file() && path.extension().is_some_and(|ext| ext == "rs") {
387 if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
388 if stem != "mod" {
389 num_structs += 1;
390
391 if let Ok(content) = fs::read_to_string(&path) {
393 num_structs +=
394 content.matches("pub struct ").count().saturating_sub(1);
395 }
396 }
397 }
398 }
399 }
400 }
401 }
402
403 Ok(CrateStatistics {
404 num_structs,
405 num_enums: 0,
406 total_types: num_structs,
407 canonical_url: "Unknown".to_string(),
408 })
409}
410
411#[allow(clippy::too_many_arguments)]
413fn generate_readme_md(
414 package: &str,
415 version: &str,
416 canonical_url: &str,
417 author: &str,
418 description: &str,
419 command_invoked: &str,
420 stats: &CrateStatistics,
421 crate_name_override: Option<&str>,
422) -> String {
423 let derived = package.replace(['.', '-'], "_");
424 let crate_name = crate_name_override.unwrap_or(&derived).replace('-', "_");
426 let mut content = String::new();
427
428 content.push_str(&format!("# {crate_name}\n\n"));
429 content.push_str(&format!("**Generated FHIR Types for {package}**\n\n"));
430 content.push_str(&format!("This crate contains automatically generated Rust types for FHIR (Fast Healthcare Interoperability Resources) based on the `{package}` package.\n\n"));
431
432 content.push_str("## Important Notice\n\n");
433 content
434 .push_str("**This crate was automatically generated using the RH codegen CLI tool.**\n\n");
435 content.push_str(&format!(
436 "- **Generator command**:\n```bash\n{command_invoked}\n```\n\n"
437 ));
438 content.push_str(&format!("- **Generation timestamp**: {}\n\n", Local::now()));
439
440 content.push_str("## Package Information\n\n");
441
442 content.push_str(&format!("* **Package Name** {package}\n"));
443 content.push_str(&format!("* **Package Author** {author}\n"));
444 content.push_str(&format!("* **Version** {version}\n"));
445 content.push_str(&format!("* **Canonical URL** `{canonical_url}`\n\n"));
446
447 content.push_str(&format!(
448 "**Statistics: {} structs, {} enums, {} total types**\n\n",
449 stats.num_structs, stats.num_enums, stats.total_types
450 ));
451
452 content.push_str(&format!("## Description\n\n{description}"));
453
454 content.push_str("\n\n");
455
456 content.push_str("## Features\n\n");
457 content.push_str(
458 "- **Complete FHIR type definitions** - All resources, datatypes, and primitives\n",
459 );
460 content.push_str(
461 "- **Serde serialization** - Built-in JSON serialization/deserialization support\n",
462 );
463 content.push_str(
464 "- **Type metadata** - Compile-time metadata for field types and path resolution\n",
465 );
466 content.push_str(
467 "- **Idiomatic Rust** - Clean, organized module structure with proper naming conventions\n",
468 );
469 content.push_str("- **Zero-cost abstractions** - PHF (perfect hash function) maps for O(1) metadata lookups\n\n");
470
471 content.push_str("## Usage\n\n");
472 content.push_str("Add this crate to your `Cargo.toml`:\n\n");
473 content.push_str("```toml\n");
474 content.push_str("[dependencies]\n");
475 content.push_str(&format!("{crate_name} = \"0.1.0\"\n"));
476 content.push_str("```\n\n");
477
478 content.push_str("### Deserializing FHIR Resources\n\n");
479 content.push_str("```rust\n");
480 content.push_str(&format!("use {crate_name}::resources::patient::Patient;\n"));
481 content.push_str("use serde_json;\n\n");
482 content.push_str("let json_data = r#\"{\\\"resourceType\\\": \\\"Patient\\\", \\\"id\\\": \\\"example\\\"}\"#;\n");
483 content.push_str("let patient: Patient = serde_json::from_str(json_data)?;\n\n");
484 content.push_str("println!(\"Patient ID: {}\", patient.id.unwrap_or_default());\n");
485 content.push_str("```\n\n");
486
487 content.push_str("### Creating Resources Programmatically\n\n");
488 content.push_str("This crate provides two idiomatic ways to work with FHIR resources using builder traits:\n\n");
489
490 content.push_str("#### Option 1: Resource Module with Re-exported Traits (Recommended)\n\n");
491 content.push_str("Each resource module re-exports its associated traits for convenience:\n\n");
492 content.push_str("```rust\n");
493 content.push_str("// Import resource with its traits - all in one place!\n");
494 content.push_str(&format!(
495 "use {crate_name}::resources::patient::{{Patient, PatientMutators}};\n"
496 ));
497 content.push_str(&format!(
498 "use {crate_name}::prelude::*; // Gets base traits (ResourceMutators, etc.)\n"
499 ));
500 content.push_str(&format!(
501 "use {crate_name}::datatypes::human_name::HumanName;\n\n"
502 ));
503 content.push_str("// Build a patient using the builder pattern\n");
504 content.push_str("let patient = <Patient as PatientMutators>::new()\n");
505 content.push_str(" .set_id(\"patient-123\".to_string())\n");
506 content.push_str(" .set_active(true)\n");
507 content.push_str(" .add_name(HumanName {\n");
508 content.push_str(" family: Some(\"Doe\".to_string()),\n");
509 content.push_str(" given: vec![\"John\".to_string()],\n");
510 content.push_str(" ..Default::default()\n");
511 content.push_str(" })\n");
512 content.push_str(" .set_gender(Some(AdministrativeGender::Male))\n");
513 content.push_str(" .set_birth_date(\"1990-01-15\".to_string());\n");
514 content.push_str("```\n\n");
515
516 content.push_str("#### Option 2: Prelude Module\n\n");
517 content.push_str("For common base traits, use the prelude module:\n\n");
518 content.push_str("```rust\n");
519 content.push_str(&format!(
520 "use {crate_name}::prelude::*; // ValidatableResource, ResourceMutators, etc.\n"
521 ));
522 content.push_str(&format!(
523 "use {crate_name}::resources::patient::{{Patient, PatientMutators}};\n\n"
524 ));
525 content.push_str("let patient = <Patient as PatientMutators>::new()\n");
526 content.push_str(" .set_id(\"example\".to_string());\n");
527 content.push_str("```\n\n");
528 content.push_str("The prelude includes:\n");
529 content.push_str("- `ValidatableResource` - Access invariants and validation rules\n");
530 content.push_str("- `ResourceMutators` - Builder methods for all resources\n");
531 content.push_str("- `DomainResourceMutators` - Builder methods for domain resources\n\n");
532
533 content.push_str("#### Direct Struct Construction\n\n");
534 content.push_str("You can also construct resources directly:\n\n");
535 content.push_str("```rust\n");
536 content.push_str(&format!("use {crate_name}::resources::patient::Patient;\n"));
537 content.push_str(&format!(
538 "use {crate_name}::datatypes::human_name::HumanName;\n"
539 ));
540 content.push_str(&format!(
541 "use {crate_name}::bindings::administrative_gender::AdministrativeGender;\n\n"
542 ));
543 content.push_str("let patient = Patient {\n");
544 content.push_str(" id: Some(\"patient-123\".to_string()),\n");
545 content.push_str(" active: Some(true),\n");
546 content.push_str(" name: vec![HumanName {\n");
547 content.push_str(" family: Some(\"Doe\".to_string()),\n");
548 content.push_str(" given: vec![\"John\".to_string()],\n");
549 content.push_str(" ..Default::default()\n");
550 content.push_str(" }],\n");
551 content.push_str(" gender: Some(AdministrativeGender::Male),\n");
552 content.push_str(" birth_date: Some(\"1990-01-15\".to_string()),\n");
553 content.push_str(" ..Default::default()\n");
554 content.push_str("};\n");
555 content.push_str("```\n\n");
556
557 content.push_str("### Using Type Metadata\n\n");
558 content.push_str("This crate includes compile-time metadata for all FHIR types, enabling runtime type introspection and path resolution:\n\n");
559 content.push_str("```rust\n");
560 content.push_str(&format!("use {crate_name}::metadata::{{resolve_path, get_field_info, FhirFieldType, FhirPrimitiveType}};\n\n"));
561 content.push_str("// Resolve nested paths to their FHIR types\n");
562 content.push_str("if let Some(field_type) = resolve_path(\"Patient.birthDate\") {\n");
563 content.push_str(" match field_type {\n");
564 content.push_str(" FhirFieldType::Primitive(FhirPrimitiveType::Date) => {\n");
565 content.push_str(" println!(\"birthDate is a FHIR date type\");\n");
566 content.push_str(" }\n");
567 content.push_str(" _ => {}\n");
568 content.push_str(" }\n");
569 content.push_str("}\n\n");
570 content.push_str("// Resolve complex nested paths\n");
571 content.push_str("if let Some(field_type) = resolve_path(\"Patient.name.given\") {\n");
572 content.push_str(" match field_type {\n");
573 content.push_str(" FhirFieldType::Primitive(FhirPrimitiveType::String) => {\n");
574 content.push_str(" println!(\"name.given is a string array\");\n");
575 content.push_str(" }\n");
576 content.push_str(" _ => {}\n");
577 content.push_str(" }\n");
578 content.push_str("}\n\n");
579 content.push_str("// Get field information directly\n");
580 content.push_str("if let Some(field_info) = get_field_info(\"Patient\", \"active\") {\n");
581 content.push_str(" println!(\"Min cardinality: {}\", field_info.min);\n");
582 content.push_str(" println!(\"Max cardinality: {:?}\", field_info.max);\n");
583 content.push_str(" println!(\"Is choice type: {}\", field_info.is_choice_type);\n");
584 content.push_str("}\n");
585 content.push_str("```\n\n");
586
587 content.push_str("The metadata system enables:\n");
588 content.push_str("- **Path resolution** - Navigate nested paths like `Patient.name.given`\n");
589 content.push_str("- **Type introspection** - Determine field types at runtime\n");
590 content.push_str("- **Cardinality information** - Min/max occurrence constraints\n");
591 content.push_str("- **Choice type detection** - Identify polymorphic fields\n");
592 content
593 .push_str("- **Zero runtime cost** - All lookups use compile-time perfect hash maps\n\n");
594
595 content.push_str("## Structure\n\n");
596 content.push_str("This crate organizes FHIR types into logical modules:\n\n");
597 content.push_str("- **resources/** - All FHIR resources (Patient, Observation, etc.)\n");
598 content.push_str("- **profiles/** - FHIR profiles (Vitalsigns, BodyHeight, etc.)\n");
599 content.push_str(
600 "- **datatypes/** - Complex and primitive datatypes (HumanName, Address, etc.)\n",
601 );
602 content.push_str("- **bindings/** - ValueSet enumerations (AdministrativeGender, etc.)\n");
603 content.push_str("- **primitives/** - Base primitive types (DateType, DateTimeType, etc.)\n");
604 content.push_str("- **traits/** - Mutator, accessor, and existence traits for all types\n");
605 content.push_str(
606 "- **prelude.rs** - Commonly used traits (ValidatableResource, ResourceMutators, etc.)\n",
607 );
608 content.push_str("- **metadata.rs** - Type metadata and path resolution functions\n\n");
609
610 content.push_str("## Regenerating This Crate\n\n");
611 content.push_str("To regenerate this crate with updated FHIR definitions:\n\n");
612 content.push_str("```bash\n");
613 content.push_str(command_invoked);
614 content.push_str("\n```\n\n");
615
616 content.push_str("## License\n\n");
617 content.push_str("This generated crate is provided under MIT OR Apache-2.0 license.\n\n");
618
619 content.push_str("## Related Links\n\n");
620 content.push_str("- [FHIR Specification](https://hl7.org/fhir/)\n");
621 content.push_str("- [FHIR Package Registry](https://packages.fhir.org/)\n");
622 content.push_str("- [RH Project](https://github.com/reasonhealth/rh)\n\n");
623 content.push_str("---\n\n");
624 content.push_str(&format!(
625 "*Generated by RH codegen tool at {}*\n",
626 Local::now()
627 ));
628
629 content
630}
631
632fn generate_prelude_module() -> String {
634 r#"//! Prelude module - commonly used traits for convenience
635//!
636//! This module re-exports the most commonly used traits for working with
637//! FHIR resources. Import this module to avoid having to import individual
638//! traits from the `traits` module.
639//!
640//! # Example
641//!
642//! ```ignore
643//! use hl7_fhir_r4_core::prelude::*;
644//! use hl7_fhir_r4_core::resources::patient::Patient;
645//!
646//! // All mutator traits are now in scope
647//! let patient = <Patient as PatientMutators>::new()
648//! .set_id("example".to_string())
649//! .set_active(true);
650//! ```
651
652// Resource mutator traits - for building resources with method chaining
653pub use crate::traits::resource::ResourceMutators;
654pub use crate::traits::domain_resource::DomainResourceMutators;
655
656// Note: Individual resource mutator traits (PatientMutators, ObservationMutators, etc.)
657// are re-exported from their respective resource modules for convenience.
658// For example: use hl7_fhir_r4_core::resources::patient::PatientMutators;
659
660// Validation trait
661pub use crate::validation::ValidatableResource;
662"#
663 .to_string()
664}
665
666pub fn parse_package_metadata(package_json_path: &Path) -> Result<(String, String, String)> {
668 let package_json_content = fs::read_to_string(package_json_path)?;
669 let package_json: serde_json::Value = serde_json::from_str(&package_json_content)?;
670
671 let canonical = package_json
672 .get("canonical")
673 .and_then(|v| v.as_str())
674 .unwrap_or("Unknown")
675 .to_string();
676
677 let author = package_json
678 .get("author")
679 .and_then(|v| v.as_str())
680 .unwrap_or("FHIR Code Generator")
681 .to_string();
682
683 let description = package_json
684 .get("description")
685 .and_then(|v| v.as_str())
686 .unwrap_or("Generated FHIR types crate.")
687 .to_string();
688
689 Ok((canonical, author, description))
690}