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 }
399 }
400
401 fn sample_author() -> OutputAuthor {
402 OutputAuthor {
403 author: "Example Author".to_string(),
404 start_line: 1,
405 end_line: 1,
406 }
407 }
408
409 fn sample_copyright() -> OutputCopyright {
410 OutputCopyright {
411 copyright: "Copyright 2026 Example".to_string(),
412 start_line: 1,
413 end_line: 1,
414 }
415 }
416
417 fn sample_email() -> OutputEmail {
418 OutputEmail {
419 email: "example@example.invalid".to_string(),
420 start_line: 1,
421 end_line: 1,
422 }
423 }
424
425 fn sample_holder() -> OutputHolder {
426 OutputHolder {
427 holder: "Example Holder".to_string(),
428 start_line: 1,
429 end_line: 1,
430 }
431 }
432
433 fn sample_url() -> OutputURL {
434 OutputURL {
435 url: "https://example.invalid".to_string(),
436 start_line: 1,
437 end_line: 1,
438 }
439 }
440
441 fn sample_package_data() -> OutputPackageData {
442 OutputPackageData {
443 package_type: Some(OutputPackageType::from(crate::models::PackageType::Cargo)),
444 namespace: Some("example".to_string()),
445 name: Some("crate-name".to_string()),
446 version: Some("1.2.3".to_string()),
447 qualifiers: Some(std::collections::HashMap::from_iter([(
448 "arch".to_string(),
449 "x86_64".to_string(),
450 )])),
451 subpath: Some("sub".to_string()),
452 primary_language: Some("Rust".to_string()),
453 description: Some("Example package data".to_string()),
454 release_date: Some("2026-05-31".to_string()),
455 parties: vec![sample_party()],
456 keywords: vec!["example".to_string()],
457 homepage_url: Some("https://example.invalid/home".to_string()),
458 download_url: Some("https://example.invalid/download".to_string()),
459 size: Some(42),
460 sha1: Some("a".repeat(40)),
461 md5: Some("b".repeat(32)),
462 sha256: Some("c".repeat(64)),
463 sha512: Some("d".repeat(128)),
464 bug_tracking_url: Some("https://example.invalid/issues".to_string()),
465 code_view_url: Some("https://example.invalid/code".to_string()),
466 vcs_url: Some("git+https://example.invalid/repo.git".to_string()),
467 copyright: Some("Copyright 2026 Example".to_string()),
468 holder: Some("Example Holder".to_string()),
469 declared_license_expression: Some("mit".to_string()),
470 declared_license_expression_spdx: Some("MIT".to_string()),
471 license_detections: vec![sample_license_detection()],
472 other_license_expression: Some("apache-2.0".to_string()),
473 other_license_expression_spdx: Some("Apache-2.0".to_string()),
474 other_license_detections: vec![sample_license_detection()],
475 extracted_license_statement: Some("MIT".to_string()),
476 notice_text: Some("notice".to_string()),
477 source_packages: vec!["pkg:cargo/source@1.0.0".to_string()],
478 file_references: vec![sample_file_reference()],
479 is_private: true,
480 is_virtual: true,
481 extra_data: Some(std::collections::HashMap::from_iter([(
482 "custom".to_string(),
483 Value::String("value".to_string()),
484 )])),
485 dependencies: vec![sample_dependency()],
486 repository_homepage_url: Some("https://example.invalid/repo-home".to_string()),
487 repository_download_url: Some("https://example.invalid/repo-download".to_string()),
488 api_data_url: Some("https://example.invalid/api".to_string()),
489 datasource_id: Some(OutputDatasourceId::from(
490 crate::models::DatasourceId::CargoToml,
491 )),
492 purl: Some("pkg:cargo/example/crate-name@1.2.3".to_string()),
493 }
494 }
495
496 fn sample_dependency() -> OutputDependency {
497 OutputDependency {
498 purl: Some("pkg:cargo/example/dep@1.0.0".to_string()),
499 extracted_requirement: Some("^1.0".to_string()),
500 scope: Some("runtime".to_string()),
501 is_runtime: Some(true),
502 is_optional: Some(false),
503 is_pinned: Some(true),
504 is_direct: Some(true),
505 resolved_package: None,
506 extra_data: Some(std::collections::HashMap::from_iter([(
507 "kind".to_string(),
508 Value::String("normal".to_string()),
509 )])),
510 }
511 }
512
513 fn sample_top_level_dependency() -> OutputTopLevelDependency {
514 OutputTopLevelDependency {
515 purl: Some("pkg:cargo/example/dep@1.0.0".to_string()),
516 extracted_requirement: Some("^1.0".to_string()),
517 scope: Some("runtime".to_string()),
518 is_runtime: Some(true),
519 is_optional: Some(false),
520 is_pinned: Some(true),
521 is_direct: Some(true),
522 resolved_package: None,
523 extra_data: Some(std::collections::HashMap::from_iter([(
524 "kind".to_string(),
525 Value::String("normal".to_string()),
526 )])),
527 dependency_uid: "dep-uid".to_string(),
528 for_package_uid: Some("pkg-uid".to_string()),
529 datafile_path: "Cargo.toml".to_string(),
530 datasource_id: OutputDatasourceId::from(crate::models::DatasourceId::CargoToml),
531 namespace: Some("example".to_string()),
532 }
533 }
534
535 fn sample_top_level_license_detection() -> OutputTopLevelLicenseDetection {
536 OutputTopLevelLicenseDetection {
537 identifier: "top-1".to_string(),
538 license_expression: "mit".to_string(),
539 license_expression_spdx: "MIT".to_string(),
540 detection_count: 1,
541 detection_log: vec!["grouped".to_string()],
542 reference_matches: vec![sample_match()],
543 }
544 }
545
546 fn sample_header() -> OutputHeader {
547 OutputHeader {
548 tool_name: "provenant".to_string(),
549 tool_version: "0.1.7".to_string(),
550 options: Map::from_iter([("--license".to_string(), Value::Bool(true))]),
551 notice: "Generated with Provenant".to_string(),
552 start_timestamp: "2026-05-31T00:00:00Z".to_string(),
553 end_timestamp: "2026-05-31T00:00:10Z".to_string(),
554 output_format_version: "3.0.0".to_string(),
555 duration: 10.0,
556 errors: vec!["none".to_string()],
557 warnings: vec!["warning".to_string()],
558 extra_data: OutputExtraData {
559 system_environment: OutputSystemEnvironment {
560 operating_system: "Linux".to_string(),
561 cpu_architecture: "x86_64".to_string(),
562 platform: "linux".to_string(),
563 platform_version: "6.0".to_string(),
564 rust_version: "1.88.0".to_string(),
565 },
566 spdx_license_list_version: "3.26".to_string(),
567 files_count: 1,
568 directories_count: 1,
569 excluded_count: 0,
570 license_index_provenance: Some(OutputLicenseIndexProvenance {
571 source: "embedded".to_string(),
572 dataset_fingerprint: "fingerprint".to_string(),
573 ignored_rules: vec!["rule-a".to_string()],
574 ignored_licenses: vec!["lic-a".to_string()],
575 ignored_rules_due_to_licenses: vec!["rule-b".to_string()],
576 added_rules: vec!["rule-c".to_string()],
577 replaced_rules: vec!["rule-d".to_string()],
578 added_licenses: vec!["lic-b".to_string()],
579 replaced_licenses: vec!["lic-c".to_string()],
580 }),
581 },
582 }
583 }
584
585 fn sample_file_info() -> OutputFileInfo {
586 OutputFileInfo {
587 name: "mod.rs".to_string(),
588 base_name: "mod".to_string(),
589 extension: ".rs".to_string(),
590 path: "src/mod.rs".to_string(),
591 file_type: OutputFileType::File,
592 mime_type: Some("text/rust".to_string()),
593 file_type_label: Some("source".to_string()),
594 size: 123,
595 date: Some("2026-05-31".to_string()),
596 sha1: Some("a".repeat(40)),
597 md5: Some("b".repeat(32)),
598 sha256: Some("c".repeat(64)),
599 sha1_git: Some("d".repeat(40)),
600 programming_language: Some("Rust".to_string()),
601 package_data: vec![sample_package_data()],
602 license_expression: Some("MIT".to_string()),
603 license_expression_spdx: Some("MIT".to_string()),
604 license_detections: vec![sample_license_detection()],
605 license_clues: vec![sample_match()],
606 percentage_of_license_text: Some(50.0),
607 copyrights: vec![sample_copyright()],
608 holders: vec![sample_holder()],
609 authors: vec![sample_author()],
610 emails: vec![sample_email()],
611 urls: vec![sample_url()],
612 for_packages: vec!["pkg-uid".to_string()],
613 scan_errors: vec!["parse warning".to_string()],
614 license_policy: Some(vec![sample_license_policy_entry()]),
615 is_generated: Some(true),
616 is_binary: Some(false),
617 is_text: Some(true),
618 is_archive: Some(false),
619 is_media: Some(false),
620 is_source: Some(true),
621 is_script: Some(false),
622 files_count: Some(1),
623 dirs_count: Some(0),
624 size_count: Some(123),
625 source_count: Some(1),
626 is_legal: true,
627 is_manifest: true,
628 is_readme: true,
629 is_top_level: true,
630 is_key_file: true,
631 is_referenced: true,
632 is_community: true,
633 facets: vec!["core".to_string()],
634 tallies: Some(sample_tallies()),
635 }
636 }
637
638 fn sample_package() -> OutputPackage {
639 OutputPackage {
640 package_type: Some(OutputPackageType::from(crate::models::PackageType::Cargo)),
641 namespace: Some("example".to_string()),
642 name: Some("crate-name".to_string()),
643 version: Some("1.2.3".to_string()),
644 qualifiers: Some(std::collections::HashMap::from_iter([(
645 "arch".to_string(),
646 "x86_64".to_string(),
647 )])),
648 subpath: Some("sub".to_string()),
649 primary_language: Some("Rust".to_string()),
650 description: Some("Example package".to_string()),
651 release_date: Some("2026-05-31".to_string()),
652 parties: vec![sample_party()],
653 keywords: vec!["example".to_string()],
654 homepage_url: Some("https://example.invalid/home".to_string()),
655 download_url: Some("https://example.invalid/download".to_string()),
656 size: Some(42),
657 sha1: Some("a".repeat(40)),
658 md5: Some("b".repeat(32)),
659 sha256: Some("c".repeat(64)),
660 sha512: Some("d".repeat(128)),
661 bug_tracking_url: Some("https://example.invalid/issues".to_string()),
662 code_view_url: Some("https://example.invalid/code".to_string()),
663 vcs_url: Some("git+https://example.invalid/repo.git".to_string()),
664 copyright: Some("Copyright 2026 Example".to_string()),
665 holder: Some("Example Holder".to_string()),
666 declared_license_expression: Some("mit".to_string()),
667 declared_license_expression_spdx: Some("MIT".to_string()),
668 license_detections: vec![sample_license_detection()],
669 other_license_expression: Some("apache-2.0".to_string()),
670 other_license_expression_spdx: Some("Apache-2.0".to_string()),
671 other_license_detections: vec![sample_license_detection()],
672 extracted_license_statement: Some("MIT".to_string()),
673 notice_text: Some("notice".to_string()),
674 source_packages: vec!["pkg:cargo/source@1.0.0".to_string()],
675 is_private: true,
676 is_virtual: true,
677 extra_data: Some(std::collections::HashMap::from_iter([(
678 "custom".to_string(),
679 Value::String("value".to_string()),
680 )])),
681 repository_homepage_url: Some("https://example.invalid/repo-home".to_string()),
682 repository_download_url: Some("https://example.invalid/repo-download".to_string()),
683 api_data_url: Some("https://example.invalid/api".to_string()),
684 purl: Some("pkg:cargo/example/crate-name@1.2.3".to_string()),
685 package_uid: "pkg-uid".to_string(),
686 datafile_paths: vec!["Cargo.toml".to_string()],
687 datasource_ids: vec![OutputDatasourceId::from(
688 crate::models::DatasourceId::CargoToml,
689 )],
690 }
691 }
692
693 fn assert_metadata_matches_serialized_keys<T: Serialize>(type_name: &str, value: &T) {
694 let documented = metadata_field_names(type_name);
695 let serialized = serialized_object_keys(value)
696 .into_iter()
697 .collect::<BTreeSet<_>>();
698 let documented_owned = documented
699 .iter()
700 .map(|s| s.to_string())
701 .collect::<BTreeSet<_>>();
702 assert_eq!(
703 documented_owned, serialized,
704 "metadata mismatch for {}",
705 type_name
706 );
707 }
708
709 #[test]
710 fn documented_type_names_are_unique() {
711 let mut seen = BTreeSet::new();
712 for ty in documented_output_types() {
713 assert!(
714 seen.insert(ty.type_name),
715 "duplicate type doc: {}",
716 ty.type_name
717 );
718 }
719 }
720
721 #[test]
722 fn documented_json_paths_are_unique() {
723 let mut seen = BTreeSet::new();
724 for ty in documented_output_types() {
725 for path in ty.json_paths {
726 assert!(seen.insert(*path), "duplicate json path doc: {}", path);
727 }
728 }
729 }
730
731 #[test]
732 fn documented_field_names_are_unique_per_type() {
733 for ty in documented_output_types() {
734 let mut seen = BTreeSet::new();
735 for field in ty.fields {
736 assert!(
737 seen.insert(field.json_name),
738 "duplicate field doc in {}: {}",
739 ty.type_name,
740 field.json_name
741 );
742 }
743 }
744 }
745
746 #[test]
747 fn output_file_info_doc_starts_with_public_serialization_order() {
748 let file_info = documented_output_types()
749 .iter()
750 .find(|ty| ty.type_name == "OutputFileInfo")
751 .expect("OutputFileInfo should be documented");
752 let fields = file_info
753 .fields
754 .iter()
755 .map(|field| field.json_name)
756 .collect::<Vec<_>>();
757
758 assert_eq!(
759 &fields[..5],
760 &["path", "type", "name", "base_name", "extension",]
761 );
762 }
763
764 #[test]
765 fn metadata_matches_serialized_keys_for_core_documented_types() {
766 assert_metadata_matches_serialized_keys("OutputHeader", &sample_header());
767 assert_metadata_matches_serialized_keys("OutputFileInfo", &sample_file_info());
768 assert_metadata_matches_serialized_keys("OutputPackage", &sample_package());
769 assert_metadata_matches_serialized_keys("OutputPackageData", &sample_package_data());
770 assert_metadata_matches_serialized_keys("OutputDependency", &sample_dependency());
771 assert_metadata_matches_serialized_keys(
772 "OutputTopLevelDependency",
773 &sample_top_level_dependency(),
774 );
775 assert_metadata_matches_serialized_keys(
776 "OutputTopLevelLicenseDetection",
777 &sample_top_level_license_detection(),
778 );
779 }
780}