1use std::collections::{HashMap, HashSet};
28
29use indexmap::IndexMap;
30
31use super::Profile;
32use super::model::{Envelope, ModuleHeader, Node, Role, TreeModel, TypeNodeRef, ValueNodeRef};
33use super::paths::{
34 MANIFEST, NodeFileKind, Root, module_dir, module_dir_prefix, module_manifest_path,
35 node_file_path, package_dir, to_physical,
36};
37use super::stems::stem_for;
38use super::v4_model::{V4, V4Extra};
39use crate::ir::v4::access::{Access, AccessControlled};
40use crate::ir::v4::distribution::{Distribution, EntryPoints};
41use crate::ir::v4::module::{ModuleDefinition, ModuleSpecification};
42use crate::ir::v4::package::{PackageDefinition, PackageSpecification};
43use crate::ir::v4::tree_files::DistributionKind;
44use crate::ir::v4::{DocumentMeta, FormatVersion, IRFile, LinkedMetadataCarrier};
45use crate::ir::{Diagnostic, DiagnosticCode, DiagnosticStage};
46use crate::naming::{ModuleName, Name, PackageName};
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub struct TreePolicy {
56 pub profile: Profile,
57 pub path_budget: u32,
58}
59
60struct Stem<'a, T> {
62 name: Name,
63 value: &'a T,
64 stem: String,
65 truncated: bool,
66}
67
68#[derive(Debug, Clone, PartialEq)]
75pub struct ManifestHeader {
76 pub format_version: FormatVersion,
77 pub distribution: DistributionKind,
78 pub package: PackageName,
79 pub dependencies: Vec<PackageName>,
81 pub entry_points: EntryPoints,
83 pub metadata: Option<Box<DocumentMeta>>,
85}
86
87pub fn write_manifest_header(
92 header: &ManifestHeader,
93 policy: &TreePolicy,
94) -> Result<(String, String), Diagnostic> {
95 let envelope = Envelope {
96 kind: header.distribution,
97 package: header.package.clone(),
98 path_budget: policy.path_budget,
99 dependencies: header.dependencies.clone(),
100 extra: V4Extra {
101 format_version: header.format_version.clone(),
102 entry_points: header.entry_points.clone(),
103 metadata: header.metadata.clone(),
104 },
105 };
106 let value = V4::encode_manifest(&envelope)?;
107 Ok((MANIFEST.to_owned(), policy.profile.write(&value)))
108}
109
110pub fn write_manifest(file: &IRFile, policy: &TreePolicy) -> Result<(String, String), Diagnostic> {
112 write_manifest_header(&manifest_header(file), policy)
113}
114
115fn manifest_header(file: &IRFile) -> ManifestHeader {
117 ManifestHeader {
118 format_version: file.format_version.clone(),
119 distribution: kind_of(&file.distribution),
120 package: file.distribution.package_name().clone(),
121 dependencies: dependency_names(&file.distribution),
122 entry_points: entry_points_of(&file.distribution),
123 metadata: file.metadata.clone(),
124 }
125}
126
127pub fn write_definition_module(
132 root: Root,
133 package: &PackageName,
134 module_name: &str,
135 module: &AccessControlled<ModuleDefinition>,
136 format_version: &FormatVersion,
137 policy: &TreePolicy,
138) -> Result<Vec<(String, String)>, Diagnostic> {
139 refuse_old_version_metadata(module, format_version)?;
140 let path = module_path(root, package, module_name)?;
141 let definition = &module.value;
142 let header = ModuleHeader {
143 path: path.into_path(),
144 public: module.access != Access::Private,
145 doc: definition.doc.clone(),
146 };
147 write_module_with::<V4>(
148 root,
149 package,
150 &header,
151 Role::Definitions,
152 &nodes(&definition.types, Node::Def),
153 &nodes(&definition.values, Node::Def),
154 policy,
155 format_version,
156 &|doc| policy.profile.write(doc),
157 )
158}
159
160pub fn write_specification_module(
167 root: Root,
168 package: &PackageName,
169 module_name: &str,
170 module: &ModuleSpecification,
171 format_version: &FormatVersion,
172 policy: &TreePolicy,
173) -> Result<Vec<(String, String)>, Diagnostic> {
174 refuse_old_version_metadata(module, format_version)?;
175 let path = module_path(root, package, module_name)?;
176 let dir = module_dir(root, package, path.as_path());
177
178 if !module.annotations.is_empty() {
179 return Err(Diagnostic::new(
180 DiagnosticCode::InvalidDistributionShape,
181 DiagnosticStage::Semantic,
182 module_manifest_path(root, &dir),
183 "module annotations cannot be written to a document tree",
184 ));
185 }
186
187 let header = ModuleHeader {
188 path: path.into_path(),
189 public: true,
190 doc: module.doc.clone(),
191 };
192 write_module_with::<V4>(
193 root,
194 package,
195 &header,
196 Role::Specifications,
197 &nodes(&module.types, Node::Spec),
198 &nodes(&module.values, Node::Spec),
199 policy,
200 format_version,
201 &|doc| policy.profile.write(doc),
202 )
203}
204
205fn nodes<'a, T, N>(entries: &'a IndexMap<String, T>, node: fn(&'a T) -> N) -> IndexMap<String, N> {
208 entries
209 .iter()
210 .map(|(key, value)| (key.clone(), node(value)))
211 .collect()
212}
213
214pub fn write_tree(file: &IRFile, policy: &TreePolicy) -> Result<Vec<(String, String)>, Diagnostic> {
225 if file.format_version != FormatVersion::String("4.1.0".to_owned())
226 && file.has_linked_metadata()
227 {
228 return Err(Diagnostic::new(
229 DiagnosticCode::InvalidDistributionShape,
230 DiagnosticStage::Semantic,
231 "",
232 "linked metadata requires formatVersion 4.1.0",
233 ));
234 }
235 let mut out = Files::default();
236 out.set(write_manifest(file, policy)?);
237 let format_version = &file.format_version;
238
239 match &file.distribution {
240 Distribution::Library(content) => {
241 write_definition_modules(
242 Root::Pkg,
243 &content.package_name,
244 &content.def,
245 format_version,
246 policy,
247 &mut out,
248 )?;
249 for (key, specification) in &content.dependencies {
250 let package = dependency_name(key)?;
251 write_specification_modules(
252 Root::Deps,
253 &package,
254 specification,
255 format_version,
256 policy,
257 &mut out,
258 )?;
259 }
260 }
261 Distribution::Specs(content) => {
262 write_specification_modules(
263 Root::Pkg,
264 &content.package_name,
265 &content.spec,
266 format_version,
267 policy,
268 &mut out,
269 )?;
270 for (key, specification) in &content.dependencies {
271 let package = dependency_name(key)?;
272 write_specification_modules(
273 Root::Deps,
274 &package,
275 specification,
276 format_version,
277 policy,
278 &mut out,
279 )?;
280 }
281 }
282 Distribution::Application(content) => {
283 write_definition_modules(
284 Root::Pkg,
285 &content.package_name,
286 &content.def,
287 format_version,
288 policy,
289 &mut out,
290 )?;
291 for (key, definition) in &content.dependencies {
292 let package = dependency_name(key)?;
293 write_definition_modules(
294 Root::Deps,
295 &package,
296 definition,
297 format_version,
298 policy,
299 &mut out,
300 )?;
301 }
302 }
303 }
304
305 Ok(out.into_vec())
306}
307
308fn refuse_old_version_metadata<T: LinkedMetadataCarrier>(
309 value: &T,
310 version: &FormatVersion,
311) -> Result<(), Diagnostic> {
312 if *version != FormatVersion::String("4.1.0".to_owned()) && value.contains_linked_metadata() {
313 return Err(Diagnostic::new(
314 DiagnosticCode::InvalidDistributionShape,
315 DiagnosticStage::Semantic,
316 "",
317 "linked metadata requires formatVersion 4.1.0",
318 ));
319 }
320 Ok(())
321}
322
323#[derive(Default)]
332pub(crate) struct Files {
333 entries: Vec<(String, String)>,
334 positions: HashMap<String, usize>,
335}
336
337impl Files {
338 pub(crate) fn set(&mut self, (path, text): (String, String)) {
339 match self.positions.get(&path) {
340 Some(&at) => self.entries[at].1 = text,
341 None => {
342 self.positions.insert(path.clone(), self.entries.len());
343 self.entries.push((path, text));
344 }
345 }
346 }
347
348 pub(crate) fn into_vec(self) -> Vec<(String, String)> {
349 self.entries
350 }
351}
352
353fn write_definition_modules(
358 root: Root,
359 package: &PackageName,
360 definition: &PackageDefinition,
361 format_version: &FormatVersion,
362 policy: &TreePolicy,
363 out: &mut Files,
364) -> Result<(), Diagnostic> {
365 for (name, module) in &definition.modules {
366 for file in write_definition_module(root, package, name, module, format_version, policy)? {
367 out.set(file);
368 }
369 }
370 Ok(())
371}
372
373fn write_specification_modules(
374 root: Root,
375 package: &PackageName,
376 specification: &PackageSpecification,
377 format_version: &FormatVersion,
378 policy: &TreePolicy,
379 out: &mut Files,
380) -> Result<(), Diagnostic> {
381 for (name, module) in &specification.modules {
382 for file in write_specification_module(root, package, name, module, format_version, policy)?
383 {
384 out.set(file);
385 }
386 }
387 Ok(())
388}
389
390#[allow(clippy::too_many_arguments)]
402pub(crate) fn write_module_with<M: TreeModel>(
403 root: Root,
404 package: &PackageName,
405 header: &ModuleHeader,
406 role: Role,
407 types: &IndexMap<String, TypeNodeRef<'_, M>>,
408 values: &IndexMap<String, ValueNodeRef<'_, M>>,
409 policy: &TreePolicy,
410 version: &M::Version,
411 render: &dyn Fn(&M::Doc) -> String,
412) -> Result<Vec<(String, String)>, Diagnostic> {
413 let dir = module_dir(root, package, &header.path);
414 fits(root, &dir, policy)?;
415 let type_stems = stems_for(types, root, &dir, NodeFileKind::Type, policy)?;
416 let value_stems = stems_for(values, root, &dir, NodeFileKind::Value, policy)?;
417
418 let type_names: Vec<Name> = type_stems.iter().map(|s| s.name.clone()).collect();
419 let value_names: Vec<Name> = value_stems.iter().map(|s| s.name.clone()).collect();
420 let file_names: Vec<(Name, String)> = truncated(&type_stems)
421 .chain(truncated(&value_stems))
422 .collect();
423
424 let mut out = Vec::with_capacity(1 + type_stems.len() + value_stems.len());
425 let manifest_path = module_manifest_path(root, &dir);
426 let manifest = M::encode_module(
427 version,
428 header,
429 role,
430 (&type_names, &value_names),
431 &file_names,
432 )
433 .map_err(|diagnostic| in_file(&manifest_path, diagnostic))?;
434 out.push((manifest_path, render(&manifest)));
435
436 for stem in &type_stems {
437 let path = node_file_path(root, &dir, &stem.stem, NodeFileKind::Type);
438 let file = M::encode_type_file(version, &stem.name, stem.value)
439 .map_err(|diagnostic| in_file(&path, diagnostic))?;
440 out.push((path, render(&file)));
441 }
442
443 for stem in &value_stems {
444 let path = node_file_path(root, &dir, &stem.stem, NodeFileKind::Value);
445 let file = M::encode_value_file(version, &stem.name, stem.value)
446 .map_err(|diagnostic| in_file(&path, diagnostic))?;
447 out.push((path, render(&file)));
448 }
449
450 Ok(out)
451}
452
453fn truncated<'a, T>(stems: &'a [Stem<'a, T>]) -> impl Iterator<Item = (Name, String)> + 'a {
455 stems
456 .iter()
457 .filter(|stem| stem.truncated)
458 .map(|stem| (stem.name.clone(), stem.stem.clone()))
459}
460
461fn stems_for<'a, T>(
468 items: &'a IndexMap<String, T>,
469 root: Root,
470 dir: &str,
471 kind: NodeFileKind,
472 policy: &TreePolicy,
473) -> Result<Vec<Stem<'a, T>>, Diagnostic> {
474 let prefix = module_dir_prefix(root, dir);
475 let suffix = format!(".{}{}", kind.as_str(), policy.profile.extension());
476 let mut seen: HashSet<String> = HashSet::with_capacity(items.len());
477 let mut out = Vec::with_capacity(items.len());
478
479 for (key, value) in items {
480 let name = entry_name(key, root, dir)?;
481 let chosen = stem_for(&name, &prefix, &suffix, policy.path_budget)?;
482 if seen.contains(&chosen.stem) {
483 return Err(Diagnostic::new(
484 DiagnosticCode::InvalidDistributionShape,
485 DiagnosticStage::Semantic,
486 format!("{prefix}{}{suffix}", chosen.stem),
487 format!(
488 "two {} names share the file stem \"{}\"",
489 kind.as_str(),
490 chosen.stem
491 ),
492 ));
493 }
494 seen.insert(chosen.stem.clone());
495 out.push(Stem {
496 name,
497 value,
498 stem: chosen.stem,
499 truncated: chosen.truncated,
500 });
501 }
502
503 Ok(out)
504}
505
506fn fits(root: Root, dir: &str, policy: &TreePolicy) -> Result<(), Diagnostic> {
509 let physical = to_physical(&module_manifest_path(root, dir), policy.profile);
510 if physical.chars().count() > policy.path_budget as usize {
511 return Err(Diagnostic::new(
512 DiagnosticCode::InvalidDistributionShape,
513 DiagnosticStage::Semantic,
514 physical.clone(),
515 format!("path budget {} cannot fit {physical}", policy.path_budget),
516 ));
517 }
518 Ok(())
519}
520
521fn module_path(
531 root: Root,
532 package: &PackageName,
533 module_name: &str,
534) -> Result<ModuleName, Diagnostic> {
535 ModuleName::from_canonical_string(module_name).map_err(|message| {
536 let dir = format!("{}/{module_name}", package_dir(root, package));
537 Diagnostic::new(
538 DiagnosticCode::InvalidPath,
539 DiagnosticStage::Semantic,
540 module_manifest_path(root, &dir),
541 message,
542 )
543 })
544}
545
546fn entry_name(key: &str, root: Root, dir: &str) -> Result<Name, Diagnostic> {
548 Name::from_canonical_string(key).map_err(|message| {
549 Diagnostic::new(
550 DiagnosticCode::InvalidName,
551 DiagnosticStage::Semantic,
552 module_manifest_path(root, dir),
553 message,
554 )
555 })
556}
557
558fn dependency_name(key: &str) -> Result<PackageName, Diagnostic> {
561 PackageName::from_canonical_string(key).map_err(|message| {
562 Diagnostic::new(
563 DiagnosticCode::InvalidPath,
564 DiagnosticStage::Semantic,
565 MANIFEST,
566 message,
567 )
568 })
569}
570
571fn kind_of(distribution: &Distribution) -> DistributionKind {
576 match distribution {
577 Distribution::Library(_) => DistributionKind::Library,
578 Distribution::Specs(_) => DistributionKind::Specs,
579 Distribution::Application(_) => DistributionKind::Application,
580 }
581}
582
583fn dependency_names(distribution: &Distribution) -> Vec<PackageName> {
589 let keys: Vec<&String> = match distribution {
590 Distribution::Library(content) => content.dependencies.keys().collect(),
591 Distribution::Specs(content) => content.dependencies.keys().collect(),
592 Distribution::Application(content) => content.dependencies.keys().collect(),
593 };
594 keys.into_iter()
595 .map(|key| PackageName::parse(key))
596 .collect()
597}
598
599fn entry_points_of(distribution: &Distribution) -> EntryPoints {
601 match distribution {
602 Distribution::Application(content) => content.entry_points.clone(),
603 Distribution::Library(_) | Distribution::Specs(_) => EntryPoints::new(),
604 }
605}
606
607fn in_file(path: &str, diagnostic: Diagnostic) -> Diagnostic {
614 let cursor = if diagnostic.cursor.is_empty() {
615 path.to_owned()
616 } else {
617 format!("{path}#{}", diagnostic.cursor)
618 };
619 Diagnostic {
620 cursor,
621 ..diagnostic
622 }
623}