1#[derive(Debug, Clone, PartialEq, Eq)]
5pub struct OutputTypeDoc {
6 pub type_name: &'static str,
7 pub json_paths: &'static [&'static str],
8 pub summary: &'static str,
9 pub fields: &'static [OutputFieldDoc],
10}
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct OutputFieldDoc {
14 pub json_name: &'static str,
15 pub rust_field: &'static str,
16 pub value_shape: &'static str,
17 pub presence: &'static str,
18 pub meaning: &'static str,
19}
20
21mod dependency;
22mod document;
23mod file_evidence;
24mod file_info;
25mod license;
26mod package;
27mod summary;
28
29use dependency::{DEPENDENCY_FIELDS, RESOLVED_PACKAGE_FIELDS, TOP_LEVEL_DEPENDENCY_FIELDS};
30use document::{
31 EXTRA_DATA_FIELDS, HEADER_FIELDS, LICENSE_INDEX_PROVENANCE_FIELDS, OUTPUT_FIELDS,
32 SYSTEM_ENVIRONMENT_FIELDS,
33};
34use file_evidence::{AUTHOR_FIELDS, COPYRIGHT_FIELDS, EMAIL_FIELDS, HOLDER_FIELDS, URL_FIELDS};
35use file_info::FILE_INFO_FIELDS;
36use license::{
37 LICENSE_DETECTION_FIELDS, LICENSE_POLICY_ENTRY_FIELDS, LICENSE_REFERENCE_FIELDS,
38 LICENSE_RULE_REFERENCE_FIELDS, MATCH_FIELDS, TOP_LEVEL_LICENSE_DETECTION_FIELDS,
39};
40use package::{FILE_REFERENCE_FIELDS, PACKAGE_DATA_FIELDS, PACKAGE_FIELDS, PARTY_FIELDS};
41use summary::{
42 FACET_TALLIES_FIELDS, LICENSE_CLARITY_SCORE_FIELDS, SUMMARY_FIELDS, TALLIES_FIELDS,
43 TALLY_ENTRY_FIELDS,
44};
45
46const EMPTY_FIELDS: &[OutputFieldDoc] = &[];
47
48const DOCUMENTED_TYPES: &[OutputTypeDoc] = &[
49 OutputTypeDoc {
50 type_name: "Output",
51 json_paths: &["$"],
52 summary: "Top-level ScanCode-compatible output object.",
53 fields: OUTPUT_FIELDS,
54 },
55 OutputTypeDoc {
56 type_name: "OutputHeader",
57 json_paths: &["$.headers[]"],
58 summary: "Per-run metadata block for one scan invocation.",
59 fields: HEADER_FIELDS,
60 },
61 OutputTypeDoc {
62 type_name: "OutputExtraData",
63 json_paths: &["$.headers[].extra_data"],
64 summary: "Scanner-owned counts and provenance metadata nested under a header block.",
65 fields: EXTRA_DATA_FIELDS,
66 },
67 OutputTypeDoc {
68 type_name: "OutputSummary",
69 json_paths: &["$.summary"],
70 summary: "Optional codebase-level rollup emitted by summary/classification workflows.",
71 fields: SUMMARY_FIELDS,
72 },
73 OutputTypeDoc {
74 type_name: "OutputFileInfo",
75 json_paths: &["$.files[]"],
76 summary: "File or directory record on the main per-resource output surface.",
77 fields: FILE_INFO_FIELDS,
78 },
79 OutputTypeDoc {
80 type_name: "OutputPackage",
81 json_paths: &["$.packages[]"],
82 summary: "Assembled top-level package record on the public output contract.",
83 fields: PACKAGE_FIELDS,
84 },
85 OutputTypeDoc {
86 type_name: "OutputPackageData",
87 json_paths: &["$.files[].package_data[]"],
88 summary: "Raw parser-emitted package record attached to a specific file.",
89 fields: PACKAGE_DATA_FIELDS,
90 },
91 OutputTypeDoc {
92 type_name: "OutputDependency",
93 json_paths: &["$.files[].package_data[].dependencies[]"],
94 summary: "Raw dependency row preserved on parser-emitted package data.",
95 fields: DEPENDENCY_FIELDS,
96 },
97 OutputTypeDoc {
98 type_name: "OutputTopLevelDependency",
99 json_paths: &["$.dependencies[]"],
100 summary: "Hoisted top-level dependency record emitted after assembly.",
101 fields: TOP_LEVEL_DEPENDENCY_FIELDS,
102 },
103 OutputTypeDoc {
104 type_name: "OutputTopLevelLicenseDetection",
105 json_paths: &["$.license_detections[]"],
106 summary: "Grouped top-level license detection block across the scanned codebase.",
107 fields: TOP_LEVEL_LICENSE_DETECTION_FIELDS,
108 },
109 OutputTypeDoc {
110 type_name: "OutputTallies",
111 json_paths: &[
112 "$.tallies",
113 "$.tallies_of_key_files",
114 "$.files[].tallies",
115 "$.tallies_by_facet[].tallies",
116 ],
117 summary: "Tally block used on top-level, key-file, facet, and file-level tally surfaces.",
118 fields: TALLIES_FIELDS,
119 },
120 OutputTypeDoc {
121 type_name: "OutputFacetTallies",
122 json_paths: &["$.tallies_by_facet[]"],
123 summary: "Facet-specific tally wrapper for one user-defined facet label.",
124 fields: FACET_TALLIES_FIELDS,
125 },
126 OutputTypeDoc {
127 type_name: "OutputTallyEntry",
128 json_paths: &[
129 "$.summary.other_license_expressions[]",
130 "$.summary.other_holders[]",
131 "$.summary.other_languages[]",
132 "$.tallies.*[]",
133 ],
134 summary: "Single tally bucket entry used throughout summary and tally outputs.",
135 fields: TALLY_ENTRY_FIELDS,
136 },
137 OutputTypeDoc {
138 type_name: "OutputLicenseClarityScore",
139 json_paths: &["$.summary.license_clarity_score"],
140 summary: "Structured license-clarity scoring payload on the summary surface.",
141 fields: LICENSE_CLARITY_SCORE_FIELDS,
142 },
143 OutputTypeDoc {
144 type_name: "OutputSystemEnvironment",
145 json_paths: &["$.headers[].extra_data.system_environment"],
146 summary: "Recorded environment metadata for the scan runtime.",
147 fields: SYSTEM_ENVIRONMENT_FIELDS,
148 },
149 OutputTypeDoc {
150 type_name: "OutputLicenseIndexProvenance",
151 json_paths: &["$.headers[].extra_data.license_index_provenance"],
152 summary: "Provenance block for the effective license index used by the scan.",
153 fields: LICENSE_INDEX_PROVENANCE_FIELDS,
154 },
155 OutputTypeDoc {
156 type_name: "OutputParty",
157 json_paths: &[
158 "$.packages[].parties[]",
159 "$.files[].package_data[].parties[]",
160 "$.dependencies[].resolved_package.parties[]",
161 ],
162 summary: "Party record used on package and resolved-package surfaces.",
163 fields: PARTY_FIELDS,
164 },
165 OutputTypeDoc {
166 type_name: "OutputFileReference",
167 json_paths: &[
168 "$.files[].package_data[].file_references[]",
169 "$.dependencies[].resolved_package.file_references[]",
170 ],
171 summary: "Referenced-file record used on package-related surfaces.",
172 fields: FILE_REFERENCE_FIELDS,
173 },
174 OutputTypeDoc {
175 type_name: "OutputLicensePolicyEntry",
176 json_paths: &["$.files[].license_policy[]"],
177 summary: "Policy decoration entry attached to file-level license-policy output.",
178 fields: LICENSE_POLICY_ENTRY_FIELDS,
179 },
180 OutputTypeDoc {
181 type_name: "OutputAuthor",
182 json_paths: &["$.files[].authors[]"],
183 summary: "File-level author evidence record.",
184 fields: AUTHOR_FIELDS,
185 },
186 OutputTypeDoc {
187 type_name: "OutputCopyright",
188 json_paths: &["$.files[].copyrights[]"],
189 summary: "File-level copyright evidence record.",
190 fields: COPYRIGHT_FIELDS,
191 },
192 OutputTypeDoc {
193 type_name: "OutputEmail",
194 json_paths: &["$.files[].emails[]"],
195 summary: "File-level email evidence record.",
196 fields: EMAIL_FIELDS,
197 },
198 OutputTypeDoc {
199 type_name: "OutputHolder",
200 json_paths: &["$.files[].holders[]"],
201 summary: "File-level holder evidence record.",
202 fields: HOLDER_FIELDS,
203 },
204 OutputTypeDoc {
205 type_name: "OutputURL",
206 json_paths: &["$.files[].urls[]"],
207 summary: "File-level URL evidence record.",
208 fields: URL_FIELDS,
209 },
210 OutputTypeDoc {
211 type_name: "OutputLicenseDetection",
212 json_paths: &[
213 "$.files[].license_detections[]",
214 "$.files[].package_data[].license_detections[]",
215 "$.packages[].license_detections[]",
216 "$.dependencies[].resolved_package.license_detections[]",
217 ],
218 summary: "Grouped license detection record used on file, package_data, package, and resolved-package surfaces.",
219 fields: LICENSE_DETECTION_FIELDS,
220 },
221 OutputTypeDoc {
222 type_name: "OutputMatch",
223 json_paths: &[
224 "$.files[].license_clues[]",
225 "$.files[].license_detections[].matches[]",
226 "$.license_detections[].reference_matches[]",
227 ],
228 summary: "Match record used for clue output, grouped detections, and top-level representative references.",
229 fields: MATCH_FIELDS,
230 },
231 OutputTypeDoc {
232 type_name: "OutputLicenseReference",
233 json_paths: &["$.license_references[]"],
234 summary: "Top-level license reference record describing one emitted license key and its reference metadata.",
235 fields: LICENSE_REFERENCE_FIELDS,
236 },
237 OutputTypeDoc {
238 type_name: "OutputLicenseRuleReference",
239 json_paths: &["$.license_rule_references[]"],
240 summary: "Top-level license-rule reference record describing one emitted rule and its reference metadata.",
241 fields: LICENSE_RULE_REFERENCE_FIELDS,
242 },
243 OutputTypeDoc {
244 type_name: "OutputResolvedPackage",
245 json_paths: &[
246 "$.dependencies[].resolved_package",
247 "$.files[].package_data[].dependencies[].resolved_package",
248 ],
249 summary: "Resolved package payload nested under dependency rows.",
250 fields: RESOLVED_PACKAGE_FIELDS,
251 },
252 OutputTypeDoc {
253 type_name: "OutputFileType",
254 json_paths: &["$.files[].type"],
255 summary: "Serialized file-type enum used by file records.",
256 fields: EMPTY_FIELDS,
257 },
258 OutputTypeDoc {
259 type_name: "OutputPackageType",
260 json_paths: &["$.packages[].type", "$.files[].package_data[].type"],
261 summary: "Serialized package-type string newtype used by package-related records.",
262 fields: EMPTY_FIELDS,
263 },
264 OutputTypeDoc {
265 type_name: "OutputDatasourceId",
266 json_paths: &[
267 "$.packages[].datasource_ids[]",
268 "$.dependencies[].datasource_id",
269 "$.files[].package_data[].datasource_id",
270 ],
271 summary: "Serialized datasource-id string newtype used by package and dependency records.",
272 fields: EMPTY_FIELDS,
273 },
274];
275
276pub fn documented_output_types() -> &'static [OutputTypeDoc] {
277 DOCUMENTED_TYPES
278}
279
280#[cfg(test)]
281mod tests {
282 use super::documented_output_types;
283 use std::collections::BTreeSet;
284
285 use crate::output_schema::{
286 OutputAuthor, OutputCopyright, OutputDatasourceId, OutputDependency, OutputEmail,
287 OutputExtraData, OutputFileInfo, OutputFileReference, OutputFileType, OutputHeader,
288 OutputHolder, OutputLicenseDetection, OutputLicenseIndexProvenance,
289 OutputLicensePolicyEntry, OutputMatch, OutputPackage, OutputPackageData, OutputPackageType,
290 OutputParty, OutputSystemEnvironment, OutputTallies, OutputTallyEntry,
291 OutputTopLevelDependency, OutputTopLevelLicenseDetection, OutputURL,
292 };
293 use serde::Serialize;
294 use serde_json::{Map, Value};
295
296 fn metadata_field_names(type_name: &str) -> BTreeSet<&'static str> {
297 documented_output_types()
298 .iter()
299 .find(|ty| ty.type_name == type_name)
300 .unwrap_or_else(|| panic!("{} should be documented", type_name))
301 .fields
302 .iter()
303 .map(|field| field.json_name)
304 .collect()
305 }
306
307 fn serialized_object_keys<T: Serialize>(value: &T) -> BTreeSet<String> {
308 serde_json::to_value(value)
309 .expect("serialize")
310 .as_object()
311 .expect("object")
312 .keys()
313 .cloned()
314 .collect()
315 }
316
317 fn sample_match() -> OutputMatch {
318 OutputMatch {
319 license_expression: "mit".to_string(),
320 license_expression_spdx: "MIT".to_string(),
321 from_file: Some("src/lib.rs".to_string()),
322 start_line: 1,
323 end_line: 2,
324 matcher: Some("1-hash".to_string()),
325 score: 100.0,
326 matched_length: Some(42),
327 match_coverage: Some(100.0),
328 rule_relevance: Some(100),
329 rule_identifier: Some("mit_1.RULE".to_string()),
330 rule_url: Some("https://example.invalid/rule".to_string()),
331 matched_text: Some("Permission is hereby granted".to_string()),
332 matched_text_diagnostics: Some("diagnostics".to_string()),
333 referenced_filenames: Some(vec!["LICENSE".to_string()]),
334 }
335 }
336
337 fn sample_license_detection() -> OutputLicenseDetection {
338 OutputLicenseDetection {
339 license_expression: "mit".to_string(),
340 license_expression_spdx: "MIT".to_string(),
341 matches: vec![sample_match()],
342 detection_log: vec!["normalized".to_string()],
343 identifier: Some("det-1".to_string()),
344 }
345 }
346
347 fn sample_tally_entry() -> OutputTallyEntry {
348 OutputTallyEntry {
349 value: Some("mit".to_string()),
350 count: 1,
351 }
352 }
353
354 fn sample_tallies() -> OutputTallies {
355 OutputTallies {
356 detected_license_expression: vec![sample_tally_entry()],
357 copyrights: vec![sample_tally_entry()],
358 holders: vec![sample_tally_entry()],
359 authors: vec![sample_tally_entry()],
360 programming_language: vec![sample_tally_entry()],
361 }
362 }
363
364 fn sample_party() -> OutputParty {
365 OutputParty {
366 r#type: Some("person".to_string()),
367 role: Some("author".to_string()),
368 name: Some("Example Person".to_string()),
369 email: Some("person@example.invalid".to_string()),
370 url: Some("https://example.invalid/person".to_string()),
371 organization: Some("Example Org".to_string()),
372 organization_url: Some("https://example.invalid".to_string()),
373 timezone: Some("UTC".to_string()),
374 }
375 }
376
377 fn sample_file_reference() -> OutputFileReference {
378 OutputFileReference {
379 path: "LICENSE".to_string(),
380 size: Some(123),
381 sha1: Some("a".repeat(40)),
382 md5: Some("b".repeat(32)),
383 sha256: Some("c".repeat(64)),
384 sha512: Some("d".repeat(128)),
385 extra_data: Some(std::collections::HashMap::from_iter([(
386 "hint".to_string(),
387 Value::String("local".to_string()),
388 )])),
389 }
390 }
391
392 fn sample_license_policy_entry() -> OutputLicensePolicyEntry {
393 OutputLicensePolicyEntry {
394 license_key: "mit".to_string(),
395 label: "Allowed".to_string(),
396 color_code: "#00ff00".to_string(),
397 icon: "check".to_string(),
398 compliance_alert: None,
399 }
400 }
401
402 fn sample_author() -> OutputAuthor {
403 OutputAuthor {
404 author: "Example Author".to_string(),
405 start_line: 1,
406 end_line: 1,
407 }
408 }
409
410 fn sample_copyright() -> OutputCopyright {
411 OutputCopyright {
412 copyright: "Copyright 2026 Example".to_string(),
413 start_line: 1,
414 end_line: 1,
415 }
416 }
417
418 fn sample_email() -> OutputEmail {
419 OutputEmail {
420 email: "example@example.invalid".to_string(),
421 start_line: 1,
422 end_line: 1,
423 }
424 }
425
426 fn sample_holder() -> OutputHolder {
427 OutputHolder {
428 holder: "Example Holder".to_string(),
429 start_line: 1,
430 end_line: 1,
431 }
432 }
433
434 fn sample_url() -> OutputURL {
435 OutputURL {
436 url: "https://example.invalid".to_string(),
437 start_line: 1,
438 end_line: 1,
439 }
440 }
441
442 fn sample_package_data() -> OutputPackageData {
443 OutputPackageData {
444 package_type: Some(OutputPackageType::from(crate::models::PackageType::Cargo)),
445 namespace: Some("example".to_string()),
446 name: Some("crate-name".to_string()),
447 version: Some("1.2.3".to_string()),
448 qualifiers: Some(std::collections::HashMap::from_iter([(
449 "arch".to_string(),
450 "x86_64".to_string(),
451 )])),
452 subpath: Some("sub".to_string()),
453 primary_language: Some("Rust".to_string()),
454 description: Some("Example package data".to_string()),
455 release_date: Some("2026-05-31".to_string()),
456 parties: vec![sample_party()],
457 keywords: vec!["example".to_string()],
458 homepage_url: Some("https://example.invalid/home".to_string()),
459 download_url: Some("https://example.invalid/download".to_string()),
460 size: Some(42),
461 sha1: Some("a".repeat(40)),
462 md5: Some("b".repeat(32)),
463 sha256: Some("c".repeat(64)),
464 sha512: Some("d".repeat(128)),
465 bug_tracking_url: Some("https://example.invalid/issues".to_string()),
466 code_view_url: Some("https://example.invalid/code".to_string()),
467 vcs_url: Some("git+https://example.invalid/repo.git".to_string()),
468 copyright: Some("Copyright 2026 Example".to_string()),
469 holder: Some("Example Holder".to_string()),
470 declared_license_expression: Some("mit".to_string()),
471 declared_license_expression_spdx: Some("MIT".to_string()),
472 license_detections: vec![sample_license_detection()],
473 other_license_expression: Some("apache-2.0".to_string()),
474 other_license_expression_spdx: Some("Apache-2.0".to_string()),
475 other_license_detections: vec![sample_license_detection()],
476 extracted_license_statement: Some("MIT".to_string()),
477 notice_text: Some("notice".to_string()),
478 source_packages: vec!["pkg:cargo/source@1.0.0".to_string()],
479 file_references: vec![sample_file_reference()],
480 is_private: true,
481 is_virtual: true,
482 extra_data: Some(std::collections::HashMap::from_iter([(
483 "custom".to_string(),
484 Value::String("value".to_string()),
485 )])),
486 dependencies: vec![sample_dependency()],
487 repository_homepage_url: Some("https://example.invalid/repo-home".to_string()),
488 repository_download_url: Some("https://example.invalid/repo-download".to_string()),
489 api_data_url: Some("https://example.invalid/api".to_string()),
490 datasource_id: Some(OutputDatasourceId::from(
491 crate::models::DatasourceId::CargoToml,
492 )),
493 purl: Some("pkg:cargo/example/crate-name@1.2.3".to_string()),
494 }
495 }
496
497 fn sample_dependency() -> OutputDependency {
498 OutputDependency {
499 purl: Some("pkg:cargo/example/dep@1.0.0".to_string()),
500 extracted_requirement: Some("^1.0".to_string()),
501 scope: Some("runtime".to_string()),
502 is_runtime: Some(true),
503 is_optional: Some(false),
504 is_pinned: Some(true),
505 is_direct: Some(true),
506 resolved_package: None,
507 extra_data: Some(std::collections::HashMap::from_iter([(
508 "kind".to_string(),
509 Value::String("normal".to_string()),
510 )])),
511 }
512 }
513
514 fn sample_top_level_dependency() -> OutputTopLevelDependency {
515 OutputTopLevelDependency {
516 purl: Some("pkg:cargo/example/dep@1.0.0".to_string()),
517 extracted_requirement: Some("^1.0".to_string()),
518 scope: Some("runtime".to_string()),
519 is_runtime: Some(true),
520 is_optional: Some(false),
521 is_pinned: Some(true),
522 is_direct: Some(true),
523 resolved_package: None,
524 extra_data: Some(std::collections::HashMap::from_iter([(
525 "kind".to_string(),
526 Value::String("normal".to_string()),
527 )])),
528 dependency_uid: "dep-uid".to_string(),
529 for_package_uid: Some("pkg-uid".to_string()),
530 datafile_path: "Cargo.toml".to_string(),
531 datasource_id: OutputDatasourceId::from(crate::models::DatasourceId::CargoToml),
532 namespace: Some("example".to_string()),
533 }
534 }
535
536 fn sample_top_level_license_detection() -> OutputTopLevelLicenseDetection {
537 OutputTopLevelLicenseDetection {
538 identifier: "top-1".to_string(),
539 license_expression: "mit".to_string(),
540 license_expression_spdx: "MIT".to_string(),
541 detection_count: 1,
542 detection_log: vec!["grouped".to_string()],
543 reference_matches: vec![sample_match()],
544 }
545 }
546
547 fn sample_header() -> OutputHeader {
548 OutputHeader {
549 tool_name: "provenant".to_string(),
550 tool_version: "0.1.7".to_string(),
551 options: Map::from_iter([("--license".to_string(), Value::Bool(true))]),
552 notice: "Generated with Provenant".to_string(),
553 start_timestamp: "2026-05-31T00:00:00Z".to_string(),
554 end_timestamp: "2026-05-31T00:00:10Z".to_string(),
555 output_format_version: "3.0.0".to_string(),
556 duration: 10.0,
557 errors: vec!["none".to_string()],
558 warnings: vec!["warning".to_string()],
559 extra_data: OutputExtraData {
560 system_environment: OutputSystemEnvironment {
561 operating_system: "Linux".to_string(),
562 cpu_architecture: "x86_64".to_string(),
563 platform: "linux".to_string(),
564 platform_version: "6.0".to_string(),
565 rust_version: "1.88.0".to_string(),
566 },
567 spdx_license_list_version: "3.26".to_string(),
568 files_count: 1,
569 directories_count: 1,
570 excluded_count: 0,
571 license_index_provenance: Some(OutputLicenseIndexProvenance {
572 source: "embedded".to_string(),
573 dataset_fingerprint: "fingerprint".to_string(),
574 ignored_rules: vec!["rule-a".to_string()],
575 ignored_licenses: vec!["lic-a".to_string()],
576 ignored_rules_due_to_licenses: vec!["rule-b".to_string()],
577 added_rules: vec!["rule-c".to_string()],
578 replaced_rules: vec!["rule-d".to_string()],
579 added_licenses: vec!["lic-b".to_string()],
580 replaced_licenses: vec!["lic-c".to_string()],
581 }),
582 },
583 }
584 }
585
586 fn sample_file_info() -> OutputFileInfo {
587 OutputFileInfo {
588 name: "mod.rs".to_string(),
589 base_name: "mod".to_string(),
590 extension: ".rs".to_string(),
591 path: "src/mod.rs".to_string(),
592 file_type: OutputFileType::File,
593 mime_type: Some("text/rust".to_string()),
594 file_type_label: Some("source".to_string()),
595 size: 123,
596 date: Some("2026-05-31".to_string()),
597 sha1: Some("a".repeat(40)),
598 md5: Some("b".repeat(32)),
599 sha256: Some("c".repeat(64)),
600 sha1_git: Some("d".repeat(40)),
601 programming_language: Some("Rust".to_string()),
602 package_data: vec![sample_package_data()],
603 license_expression: Some("MIT".to_string()),
604 license_expression_spdx: Some("MIT".to_string()),
605 license_detections: vec![sample_license_detection()],
606 license_clues: vec![sample_match()],
607 percentage_of_license_text: Some(50.0),
608 copyrights: vec![sample_copyright()],
609 holders: vec![sample_holder()],
610 authors: vec![sample_author()],
611 emails: vec![sample_email()],
612 urls: vec![sample_url()],
613 for_packages: vec!["pkg-uid".to_string()],
614 scan_errors: vec!["parse warning".to_string()],
615 license_policy: Some(vec![sample_license_policy_entry()]),
616 is_generated: Some(true),
617 is_binary: Some(false),
618 is_text: Some(true),
619 is_archive: Some(false),
620 is_media: Some(false),
621 is_source: Some(true),
622 is_script: Some(false),
623 files_count: Some(1),
624 dirs_count: Some(0),
625 size_count: Some(123),
626 source_count: Some(1),
627 is_legal: true,
628 is_manifest: true,
629 is_readme: true,
630 is_top_level: true,
631 is_key_file: true,
632 is_referenced: true,
633 is_community: true,
634 facets: vec!["core".to_string()],
635 tallies: Some(sample_tallies()),
636 }
637 }
638
639 fn sample_package() -> OutputPackage {
640 OutputPackage {
641 package_type: Some(OutputPackageType::from(crate::models::PackageType::Cargo)),
642 namespace: Some("example".to_string()),
643 name: Some("crate-name".to_string()),
644 version: Some("1.2.3".to_string()),
645 qualifiers: Some(std::collections::HashMap::from_iter([(
646 "arch".to_string(),
647 "x86_64".to_string(),
648 )])),
649 subpath: Some("sub".to_string()),
650 primary_language: Some("Rust".to_string()),
651 description: Some("Example package".to_string()),
652 release_date: Some("2026-05-31".to_string()),
653 parties: vec![sample_party()],
654 keywords: vec!["example".to_string()],
655 homepage_url: Some("https://example.invalid/home".to_string()),
656 download_url: Some("https://example.invalid/download".to_string()),
657 size: Some(42),
658 sha1: Some("a".repeat(40)),
659 md5: Some("b".repeat(32)),
660 sha256: Some("c".repeat(64)),
661 sha512: Some("d".repeat(128)),
662 bug_tracking_url: Some("https://example.invalid/issues".to_string()),
663 code_view_url: Some("https://example.invalid/code".to_string()),
664 vcs_url: Some("git+https://example.invalid/repo.git".to_string()),
665 copyright: Some("Copyright 2026 Example".to_string()),
666 holder: Some("Example Holder".to_string()),
667 declared_license_expression: Some("mit".to_string()),
668 declared_license_expression_spdx: Some("MIT".to_string()),
669 license_detections: vec![sample_license_detection()],
670 other_license_expression: Some("apache-2.0".to_string()),
671 other_license_expression_spdx: Some("Apache-2.0".to_string()),
672 other_license_detections: vec![sample_license_detection()],
673 extracted_license_statement: Some("MIT".to_string()),
674 notice_text: Some("notice".to_string()),
675 source_packages: vec!["pkg:cargo/source@1.0.0".to_string()],
676 is_private: true,
677 is_virtual: true,
678 extra_data: Some(std::collections::HashMap::from_iter([(
679 "custom".to_string(),
680 Value::String("value".to_string()),
681 )])),
682 repository_homepage_url: Some("https://example.invalid/repo-home".to_string()),
683 repository_download_url: Some("https://example.invalid/repo-download".to_string()),
684 api_data_url: Some("https://example.invalid/api".to_string()),
685 purl: Some("pkg:cargo/example/crate-name@1.2.3".to_string()),
686 package_uid: "pkg-uid".to_string(),
687 datafile_paths: vec!["Cargo.toml".to_string()],
688 datasource_ids: vec![OutputDatasourceId::from(
689 crate::models::DatasourceId::CargoToml,
690 )],
691 }
692 }
693
694 fn assert_metadata_matches_serialized_keys<T: Serialize>(type_name: &str, value: &T) {
695 let documented = metadata_field_names(type_name);
696 let serialized = serialized_object_keys(value)
697 .into_iter()
698 .collect::<BTreeSet<_>>();
699 let documented_owned = documented
700 .iter()
701 .map(|s| s.to_string())
702 .collect::<BTreeSet<_>>();
703 assert_eq!(
704 documented_owned, serialized,
705 "metadata mismatch for {}",
706 type_name
707 );
708 }
709
710 #[test]
711 fn documented_type_names_are_unique() {
712 let mut seen = BTreeSet::new();
713 for ty in documented_output_types() {
714 assert!(
715 seen.insert(ty.type_name),
716 "duplicate type doc: {}",
717 ty.type_name
718 );
719 }
720 }
721
722 #[test]
723 fn documented_json_paths_are_unique() {
724 let mut seen = BTreeSet::new();
725 for ty in documented_output_types() {
726 for path in ty.json_paths {
727 assert!(seen.insert(*path), "duplicate json path doc: {}", path);
728 }
729 }
730 }
731
732 #[test]
733 fn documented_field_names_are_unique_per_type() {
734 for ty in documented_output_types() {
735 let mut seen = BTreeSet::new();
736 for field in ty.fields {
737 assert!(
738 seen.insert(field.json_name),
739 "duplicate field doc in {}: {}",
740 ty.type_name,
741 field.json_name
742 );
743 }
744 }
745 }
746
747 #[test]
748 fn output_file_info_doc_starts_with_public_serialization_order() {
749 let file_info = documented_output_types()
750 .iter()
751 .find(|ty| ty.type_name == "OutputFileInfo")
752 .expect("OutputFileInfo should be documented");
753 let fields = file_info
754 .fields
755 .iter()
756 .map(|field| field.json_name)
757 .collect::<Vec<_>>();
758
759 assert_eq!(
760 &fields[..5],
761 &["path", "type", "name", "base_name", "extension",]
762 );
763 }
764
765 #[test]
766 fn metadata_matches_serialized_keys_for_core_documented_types() {
767 assert_metadata_matches_serialized_keys("OutputHeader", &sample_header());
768 assert_metadata_matches_serialized_keys("OutputFileInfo", &sample_file_info());
769 assert_metadata_matches_serialized_keys("OutputPackage", &sample_package());
770 assert_metadata_matches_serialized_keys("OutputPackageData", &sample_package_data());
771 assert_metadata_matches_serialized_keys("OutputDependency", &sample_dependency());
772 assert_metadata_matches_serialized_keys(
773 "OutputTopLevelDependency",
774 &sample_top_level_dependency(),
775 );
776 assert_metadata_matches_serialized_keys(
777 "OutputTopLevelLicenseDetection",
778 &sample_top_level_license_detection(),
779 );
780 }
781}