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_detections: vec![sample_license_detection()],
604 license_clues: vec![sample_match()],
605 percentage_of_license_text: Some(50.0),
606 copyrights: vec![sample_copyright()],
607 holders: vec![sample_holder()],
608 authors: vec![sample_author()],
609 emails: vec![sample_email()],
610 urls: vec![sample_url()],
611 for_packages: vec!["pkg-uid".to_string()],
612 scan_errors: vec!["parse warning".to_string()],
613 license_policy: Some(vec![sample_license_policy_entry()]),
614 is_generated: Some(true),
615 is_binary: Some(false),
616 is_text: Some(true),
617 is_archive: Some(false),
618 is_media: Some(false),
619 is_source: Some(true),
620 is_script: Some(false),
621 files_count: Some(1),
622 dirs_count: Some(0),
623 size_count: Some(123),
624 source_count: Some(1),
625 is_legal: true,
626 is_manifest: true,
627 is_readme: true,
628 is_top_level: true,
629 is_key_file: true,
630 is_referenced: true,
631 is_community: true,
632 facets: vec!["core".to_string()],
633 tallies: Some(sample_tallies()),
634 }
635 }
636
637 fn sample_package() -> OutputPackage {
638 OutputPackage {
639 package_type: Some(OutputPackageType::from(crate::models::PackageType::Cargo)),
640 namespace: Some("example".to_string()),
641 name: Some("crate-name".to_string()),
642 version: Some("1.2.3".to_string()),
643 qualifiers: Some(std::collections::HashMap::from_iter([(
644 "arch".to_string(),
645 "x86_64".to_string(),
646 )])),
647 subpath: Some("sub".to_string()),
648 primary_language: Some("Rust".to_string()),
649 description: Some("Example package".to_string()),
650 release_date: Some("2026-05-31".to_string()),
651 parties: vec![sample_party()],
652 keywords: vec!["example".to_string()],
653 homepage_url: Some("https://example.invalid/home".to_string()),
654 download_url: Some("https://example.invalid/download".to_string()),
655 size: Some(42),
656 sha1: Some("a".repeat(40)),
657 md5: Some("b".repeat(32)),
658 sha256: Some("c".repeat(64)),
659 sha512: Some("d".repeat(128)),
660 bug_tracking_url: Some("https://example.invalid/issues".to_string()),
661 code_view_url: Some("https://example.invalid/code".to_string()),
662 vcs_url: Some("git+https://example.invalid/repo.git".to_string()),
663 copyright: Some("Copyright 2026 Example".to_string()),
664 holder: Some("Example Holder".to_string()),
665 declared_license_expression: Some("mit".to_string()),
666 declared_license_expression_spdx: Some("MIT".to_string()),
667 license_detections: vec![sample_license_detection()],
668 other_license_expression: Some("apache-2.0".to_string()),
669 other_license_expression_spdx: Some("Apache-2.0".to_string()),
670 other_license_detections: vec![sample_license_detection()],
671 extracted_license_statement: Some("MIT".to_string()),
672 notice_text: Some("notice".to_string()),
673 source_packages: vec!["pkg:cargo/source@1.0.0".to_string()],
674 is_private: true,
675 is_virtual: true,
676 extra_data: Some(std::collections::HashMap::from_iter([(
677 "custom".to_string(),
678 Value::String("value".to_string()),
679 )])),
680 repository_homepage_url: Some("https://example.invalid/repo-home".to_string()),
681 repository_download_url: Some("https://example.invalid/repo-download".to_string()),
682 api_data_url: Some("https://example.invalid/api".to_string()),
683 purl: Some("pkg:cargo/example/crate-name@1.2.3".to_string()),
684 package_uid: "pkg-uid".to_string(),
685 datafile_paths: vec!["Cargo.toml".to_string()],
686 datasource_ids: vec![OutputDatasourceId::from(
687 crate::models::DatasourceId::CargoToml,
688 )],
689 }
690 }
691
692 fn assert_metadata_matches_serialized_keys<T: Serialize>(type_name: &str, value: &T) {
693 let documented = metadata_field_names(type_name);
694 let serialized = serialized_object_keys(value)
695 .into_iter()
696 .collect::<BTreeSet<_>>();
697 let documented_owned = documented
698 .iter()
699 .map(|s| s.to_string())
700 .collect::<BTreeSet<_>>();
701 assert_eq!(
702 documented_owned, serialized,
703 "metadata mismatch for {}",
704 type_name
705 );
706 }
707
708 #[test]
709 fn documented_type_names_are_unique() {
710 let mut seen = BTreeSet::new();
711 for ty in documented_output_types() {
712 assert!(
713 seen.insert(ty.type_name),
714 "duplicate type doc: {}",
715 ty.type_name
716 );
717 }
718 }
719
720 #[test]
721 fn documented_json_paths_are_unique() {
722 let mut seen = BTreeSet::new();
723 for ty in documented_output_types() {
724 for path in ty.json_paths {
725 assert!(seen.insert(*path), "duplicate json path doc: {}", path);
726 }
727 }
728 }
729
730 #[test]
731 fn documented_field_names_are_unique_per_type() {
732 for ty in documented_output_types() {
733 let mut seen = BTreeSet::new();
734 for field in ty.fields {
735 assert!(
736 seen.insert(field.json_name),
737 "duplicate field doc in {}: {}",
738 ty.type_name,
739 field.json_name
740 );
741 }
742 }
743 }
744
745 #[test]
746 fn output_file_info_doc_starts_with_public_serialization_order() {
747 let file_info = documented_output_types()
748 .iter()
749 .find(|ty| ty.type_name == "OutputFileInfo")
750 .expect("OutputFileInfo should be documented");
751 let fields = file_info
752 .fields
753 .iter()
754 .map(|field| field.json_name)
755 .collect::<Vec<_>>();
756
757 assert_eq!(
758 &fields[..5],
759 &["path", "type", "name", "base_name", "extension",]
760 );
761 }
762
763 #[test]
764 fn metadata_matches_serialized_keys_for_core_documented_types() {
765 assert_metadata_matches_serialized_keys("OutputHeader", &sample_header());
766 assert_metadata_matches_serialized_keys("OutputFileInfo", &sample_file_info());
767 assert_metadata_matches_serialized_keys("OutputPackage", &sample_package());
768 assert_metadata_matches_serialized_keys("OutputPackageData", &sample_package_data());
769 assert_metadata_matches_serialized_keys("OutputDependency", &sample_dependency());
770 assert_metadata_matches_serialized_keys(
771 "OutputTopLevelDependency",
772 &sample_top_level_dependency(),
773 );
774 assert_metadata_matches_serialized_keys(
775 "OutputTopLevelLicenseDetection",
776 &sample_top_level_license_detection(),
777 );
778 }
779}