1use std::path::Path;
28
29use crate::parser_warn as warn;
30use packageurl::PackageUrl;
31use serde_json::Value as JsonValue;
32use yaml_serde::Value as YamlValue;
33
34use crate::models::{
35 DatasourceId, Dependency, FileReference, PackageData, PackageType, Party, PartyType,
36};
37use crate::parsers::utils::{
38 CappedIterExt, capped_iteration_limit, read_file_to_string, truncate_field,
39};
40
41use super::PackageParser;
42use super::license_normalization::{
43 DeclaredLicenseMatchMetadata, NormalizedDeclaredLicense, build_declared_license_data,
44 combine_normalized_licenses, empty_declared_license_data, normalize_declared_license_key,
45 normalize_spdx_expression,
46};
47
48const FIELD_NAME: &str = "name";
49const FIELD_VERSION: &str = "version";
50const FIELD_ABSTRACT: &str = "abstract";
51const FIELD_DESCRIPTION: &str = "description";
52const FIELD_LICENSE: &str = "license";
53const FIELD_AUTHOR: &str = "author";
54const FIELD_RESOURCES: &str = "resources";
55const FIELD_PREREQS: &str = "prereqs";
56const FIELD_REQUIRES: &str = "requires";
57const FIELD_BUILD_REQUIRES: &str = "build_requires";
58const FIELD_TEST_REQUIRES: &str = "test_requires";
59const FIELD_CONFIGURE_REQUIRES: &str = "configure_requires";
60
61pub(crate) fn cpan_distribution_name(module: &str) -> String {
74 module.replace("::", "-")
75}
76
77pub struct CpanMetaJsonParser;
78
79impl PackageParser for CpanMetaJsonParser {
80 const PACKAGE_TYPE: PackageType = PackageType::Cpan;
81
82 fn is_match(path: &Path) -> bool {
83 path.file_name().is_some_and(|name| name == "META.json")
84 }
85
86 fn extract_packages(path: &Path) -> Vec<PackageData> {
87 let json = match read_and_parse_json(path) {
88 Ok(json) => json,
89 Err(e) => {
90 warn!("Failed to parse META.json at {:?}: {}", path, e);
91 return vec![default_package_data(DatasourceId::CpanMetaJson)];
92 }
93 };
94
95 let name = json
96 .get(FIELD_NAME)
97 .and_then(|v| v.as_str())
98 .map(|s| truncate_field(s.to_string()));
99
100 let version = extract_version_from_json(&json);
101
102 let description = json
103 .get(FIELD_ABSTRACT)
104 .and_then(|v| v.as_str())
105 .map(|s| truncate_field(s.to_string()));
106
107 let extracted_license_statement = extract_license_from_json(&json);
108 let (declared_license_expression, declared_license_expression_spdx, license_detections) =
109 normalize_cpan_declared_license(
110 json.get(FIELD_LICENSE),
111 extracted_license_statement.as_deref(),
112 );
113 let declared_license_expression = declared_license_expression.map(truncate_field);
114 let declared_license_expression_spdx = declared_license_expression_spdx.map(truncate_field);
115 let parties = extract_parties_from_json(&json);
116 let dependencies = extract_dependencies_from_json(&json);
117 let (homepage_url, vcs_url, code_view_url, bug_tracking_url) =
118 extract_resources_from_json(&json);
119
120 vec![PackageData {
121 package_type: Some(Self::PACKAGE_TYPE),
122 name,
123 version,
124 description,
125 declared_license_expression,
126 declared_license_expression_spdx,
127 license_detections,
128 extracted_license_statement,
129 parties,
130 dependencies,
131 homepage_url,
132 vcs_url,
133 code_view_url,
134 bug_tracking_url,
135 primary_language: Some("Perl".to_string()),
136 datasource_id: Some(DatasourceId::CpanMetaJson),
137 ..Default::default()
138 }]
139 }
140
141 fn metadata() -> Vec<super::metadata::ParserMetadata> {
142 vec![super::metadata::ParserMetadata {
143 description: "CPAN Perl META.json",
144 file_patterns: &["**/META.json"],
145 package_type: "cpan",
146 primary_language: "Perl",
147 documentation_url: Some("https://metacpan.org/pod/CPAN::Meta::Spec"),
148 }]
149 }
150}
151
152pub struct CpanMetaYmlParser;
156
157impl PackageParser for CpanMetaYmlParser {
158 const PACKAGE_TYPE: PackageType = PackageType::Cpan;
159
160 fn is_match(path: &Path) -> bool {
161 path.file_name().is_some_and(|name| name == "META.yml")
162 }
163
164 fn extract_packages(path: &Path) -> Vec<PackageData> {
165 let yaml = match read_and_parse_yaml(path) {
166 Ok(yaml) => yaml,
167 Err(e) => {
168 warn!("Failed to parse META.yml at {:?}: {}", path, e);
169 return vec![default_package_data(DatasourceId::CpanMetaYml)];
170 }
171 };
172
173 let name = yaml
174 .get(FIELD_NAME)
175 .and_then(|v| v.as_str())
176 .map(|s| truncate_field(s.to_string()));
177
178 let version = extract_version_from_yaml(&yaml);
179
180 let description = yaml
181 .get(FIELD_ABSTRACT)
182 .or_else(|| yaml.get(FIELD_DESCRIPTION))
183 .and_then(|v| v.as_str())
184 .map(|s| truncate_field(s.to_string()));
185
186 let extracted_license_statement = extract_license_from_yaml(&yaml);
187 let (declared_license_expression, declared_license_expression_spdx, license_detections) =
188 normalize_cpan_declared_license(
189 yaml.get(YamlValue::String(FIELD_LICENSE.to_string())),
190 extracted_license_statement.as_deref(),
191 );
192 let declared_license_expression = declared_license_expression.map(truncate_field);
193 let declared_license_expression_spdx = declared_license_expression_spdx.map(truncate_field);
194 let parties = extract_parties_from_yaml(&yaml);
195 let dependencies = extract_dependencies_from_yaml(&yaml);
196 let (homepage_url, vcs_url, bug_tracking_url) = extract_resources_from_yaml(&yaml);
197
198 vec![PackageData {
199 package_type: Some(Self::PACKAGE_TYPE),
200 name,
201 version,
202 description,
203 declared_license_expression,
204 declared_license_expression_spdx,
205 license_detections,
206 extracted_license_statement,
207 parties,
208 dependencies,
209 homepage_url,
210 vcs_url,
211 bug_tracking_url,
212 primary_language: Some("Perl".to_string()),
213 datasource_id: Some(DatasourceId::CpanMetaYml),
214 ..Default::default()
215 }]
216 }
217
218 fn metadata() -> Vec<super::metadata::ParserMetadata> {
219 vec![super::metadata::ParserMetadata {
220 description: "CPAN Perl META.yml",
221 file_patterns: &["**/META.yml"],
222 package_type: "cpan",
223 primary_language: "Perl",
224 documentation_url: Some("https://metacpan.org/pod/CPAN::Meta::Spec"),
225 }]
226 }
227}
228
229pub struct CpanManifestParser;
233
234impl PackageParser for CpanManifestParser {
235 const PACKAGE_TYPE: PackageType = PackageType::Cpan;
236
237 fn is_match(path: &Path) -> bool {
238 path.file_name().is_some_and(|name| name == "MANIFEST")
239 }
240
241 fn extract_packages(path: &Path) -> Vec<PackageData> {
242 let content = match read_file_to_string(path, None) {
243 Ok(content) => content,
244 Err(e) => {
245 warn!("Failed to read MANIFEST at {:?}: {}", path, e);
246 return vec![default_package_data(DatasourceId::CpanManifest)];
247 }
248 };
249
250 let file_references = content
251 .lines()
252 .capped("CPAN MANIFEST lines")
253 .filter(|line| !line.trim().is_empty())
254 .filter(|line| !line.trim().starts_with('#'))
255 .map(|line| {
256 let path = line.split_whitespace().next().unwrap_or(line);
257 FileReference {
258 path: truncate_field(path.to_string()),
259 size: None,
260 sha1: None,
261 md5: None,
262 sha256: None,
263 sha512: None,
264 extra_data: None,
265 }
266 })
267 .collect();
268
269 vec![PackageData {
270 package_type: Some(Self::PACKAGE_TYPE),
271 file_references,
272 primary_language: Some("Perl".to_string()),
273 datasource_id: Some(DatasourceId::CpanManifest),
274 ..Default::default()
275 }]
276 }
277
278 fn metadata() -> Vec<super::metadata::ParserMetadata> {
279 vec![super::metadata::ParserMetadata {
280 description: "CPAN Perl MANIFEST",
281 file_patterns: &["**/MANIFEST"],
282 package_type: "cpan",
283 primary_language: "Perl",
284 documentation_url: Some("https://metacpan.org/pod/Module::Manifest"),
285 }]
286 }
287}
288
289fn default_package_data(datasource_id: DatasourceId) -> PackageData {
290 PackageData {
291 package_type: Some(CpanMetaJsonParser::PACKAGE_TYPE),
292 primary_language: Some("Perl".to_string()),
293 datasource_id: Some(datasource_id),
294 ..Default::default()
295 }
296}
297
298fn read_and_parse_json(path: &Path) -> Result<serde_json::Map<String, JsonValue>, String> {
299 let content =
300 read_file_to_string(path, None).map_err(|e| format!("Failed to read file: {}", e))?;
301 let json: JsonValue =
302 serde_json::from_str(&content).map_err(|e| format!("Failed to parse JSON: {}", e))?;
303 json.as_object()
304 .cloned()
305 .ok_or_else(|| "Root JSON is not an object".to_string())
306}
307
308fn read_and_parse_yaml(path: &Path) -> Result<yaml_serde::Mapping, String> {
309 let content =
310 read_file_to_string(path, None).map_err(|e| format!("Failed to read file: {}", e))?;
311 let yaml: YamlValue =
312 yaml_serde::from_str(&content).map_err(|e| format!("Failed to parse YAML: {}", e))?;
313 yaml.as_mapping()
314 .cloned()
315 .ok_or_else(|| "Root YAML is not a mapping".to_string())
316}
317
318fn extract_version_from_json(json: &serde_json::Map<String, JsonValue>) -> Option<String> {
319 json.get(FIELD_VERSION).and_then(|v| match v {
320 JsonValue::String(s) => Some(truncate_field(s.clone())),
321 JsonValue::Number(n) => Some(truncate_field(n.to_string())),
322 _ => None,
323 })
324}
325
326fn extract_version_from_yaml(yaml: &yaml_serde::Mapping) -> Option<String> {
327 yaml.get(YamlValue::String(FIELD_VERSION.to_string()))
328 .and_then(|v| match v {
329 YamlValue::String(s) => Some(truncate_field(s.clone())),
330 YamlValue::Number(n) => Some(truncate_field(n.to_string())),
331 _ => None,
332 })
333}
334
335fn extract_license_from_json(json: &serde_json::Map<String, JsonValue>) -> Option<String> {
336 json.get(FIELD_LICENSE).and_then(|v| match v {
337 JsonValue::String(s) => Some(truncate_field(s.clone())),
338 JsonValue::Array(arr) => {
339 let limit = capped_iteration_limit(arr.len(), "CPAN JSON license array");
340 let licenses: Vec<String> = arr
341 .iter()
342 .take(limit)
343 .filter_map(|item| item.as_str().map(|s| truncate_field(s.to_string())))
344 .collect();
345 if licenses.is_empty() {
346 None
347 } else {
348 Some(truncate_field(licenses.join(" AND ")))
349 }
350 }
351 _ => None,
352 })
353}
354
355fn extract_license_from_yaml(yaml: &yaml_serde::Mapping) -> Option<String> {
356 yaml.get(YamlValue::String(FIELD_LICENSE.to_string()))
357 .and_then(|v| match v {
358 YamlValue::String(s) => Some(truncate_field(s.clone())),
359 YamlValue::Sequence(arr) => {
360 let limit = capped_iteration_limit(arr.len(), "CPAN YAML license sequence");
361 let licenses: Vec<String> = arr
362 .iter()
363 .take(limit)
364 .filter_map(|item| item.as_str().map(|s| truncate_field(s.to_string())))
365 .collect();
366 if licenses.is_empty() {
367 None
368 } else {
369 Some(truncate_field(licenses.join(" AND ")))
370 }
371 }
372 _ => None,
373 })
374}
375
376fn normalize_cpan_declared_license(
377 raw_license: Option<&impl LicenseValueAdapter>,
378 extracted_license_statement: Option<&str>,
379) -> (
380 Option<String>,
381 Option<String>,
382 Vec<crate::models::LicenseDetection>,
383) {
384 let Some(raw_license) = raw_license else {
385 return empty_declared_license_data();
386 };
387 let normalized = raw_license
388 .license_values()
389 .into_iter()
390 .map(|value| normalize_cpan_license_value(&value))
391 .collect::<Option<Vec<_>>>();
392
393 if let Some(normalized) = normalized
394 && let Some(combined) = combine_normalized_licenses(normalized, " AND ")
395 {
396 return build_declared_license_data(
397 combined,
398 DeclaredLicenseMatchMetadata::single_line(
399 extracted_license_statement.unwrap_or_default(),
400 ),
401 );
402 }
403
404 empty_declared_license_data()
405}
406
407trait LicenseValueAdapter {
408 fn license_values(&self) -> Vec<String>;
409}
410
411impl LicenseValueAdapter for JsonValue {
412 fn license_values(&self) -> Vec<String> {
413 match self {
414 JsonValue::String(value) => vec![truncate_field(value.trim().to_string())],
415 JsonValue::Array(values) => {
416 let limit = capped_iteration_limit(values.len(), "CPAN JSON license values");
417 values
418 .iter()
419 .take(limit)
420 .filter_map(|value| value.as_str())
421 .map(str::trim)
422 .filter(|value| !value.is_empty())
423 .map(|s| truncate_field(s.to_string()))
424 .collect()
425 }
426 _ => Vec::new(),
427 }
428 }
429}
430
431impl LicenseValueAdapter for YamlValue {
432 fn license_values(&self) -> Vec<String> {
433 match self {
434 YamlValue::String(value) => vec![truncate_field(value.trim().to_string())],
435 YamlValue::Sequence(values) => {
436 let limit = capped_iteration_limit(values.len(), "CPAN YAML license values");
437 values
438 .iter()
439 .take(limit)
440 .filter_map(|value| value.as_str())
441 .map(str::trim)
442 .filter(|value| !value.is_empty())
443 .map(|s| truncate_field(s.to_string()))
444 .collect()
445 }
446 _ => Vec::new(),
447 }
448 }
449}
450
451fn normalize_cpan_license_value(value: &str) -> Option<NormalizedDeclaredLicense> {
452 match value.trim() {
453 "perl_5" | "Perl_5" => Some(NormalizedDeclaredLicense::new(
454 "gpl-1.0-plus OR artistic-perl-1.0",
455 "GPL-1.0-or-later OR Artistic-1.0-Perl",
456 )),
457 "artistic_2" => Some(NormalizedDeclaredLicense::new(
458 "artistic-2.0",
459 "Artistic-2.0",
460 )),
461 "apache_2_0" => Some(NormalizedDeclaredLicense::new("apache-2.0", "Apache-2.0")),
462 other => normalize_spdx_expression(other).or_else(|| normalize_declared_license_key(other)),
463 }
464}
465
466fn extract_parties_from_json(json: &serde_json::Map<String, JsonValue>) -> Vec<Party> {
467 json.get(FIELD_AUTHOR)
468 .and_then(|v| v.as_array())
469 .map_or_else(Vec::new, |authors| {
470 let limit = capped_iteration_limit(authors.len(), "CPAN JSON authors");
471 authors
472 .iter()
473 .take(limit)
474 .filter_map(|author| {
475 author.as_str().map(|s| {
476 let (name, email) = parse_author_string(s);
477 Party {
478 r#type: Some(PartyType::Person),
479 role: Some("author".to_string()),
480 name,
481 email,
482 url: None,
483 organization: None,
484 organization_url: None,
485 timezone: None,
486 }
487 })
488 })
489 .collect()
490 })
491}
492
493fn extract_parties_from_yaml(yaml: &yaml_serde::Mapping) -> Vec<Party> {
494 yaml.get(YamlValue::String(FIELD_AUTHOR.to_string()))
495 .and_then(|v| v.as_sequence())
496 .map_or_else(Vec::new, |authors| {
497 let limit = capped_iteration_limit(authors.len(), "CPAN YAML authors");
498 authors
499 .iter()
500 .take(limit)
501 .filter_map(|author| {
502 author.as_str().map(|s| {
503 let (name, email) = parse_author_string(s);
504 Party {
505 r#type: Some(PartyType::Person),
506 role: Some("author".to_string()),
507 name,
508 email,
509 url: None,
510 organization: None,
511 organization_url: None,
512 timezone: None,
513 }
514 })
515 })
516 .collect()
517 })
518}
519
520fn parse_author_string(author_str: &str) -> (Option<String>, Option<String>) {
521 if let Some(email_start) = author_str.find('<')
522 && let Some(email_end) = author_str.find('>')
523 && email_start < email_end
524 {
525 let name = author_str[..email_start].trim();
526 let email = author_str[email_start + 1..email_end].trim();
527 return (
528 if name.is_empty() {
529 None
530 } else {
531 Some(truncate_field(name.to_string()))
532 },
533 if email.is_empty() {
534 None
535 } else {
536 Some(truncate_field(email.to_string()))
537 },
538 );
539 }
540 let trimmed = author_str.trim();
541 (
542 if trimmed.is_empty() {
543 None
544 } else {
545 Some(truncate_field(trimmed.to_string()))
546 },
547 None,
548 )
549}
550
551fn extract_resources_from_json(
552 json: &serde_json::Map<String, JsonValue>,
553) -> (
554 Option<String>,
555 Option<String>,
556 Option<String>,
557 Option<String>,
558) {
559 let resources = match json.get(FIELD_RESOURCES).and_then(|v| v.as_object()) {
560 Some(r) => r,
561 None => return (None, None, None, None),
562 };
563
564 let homepage_url = resources
565 .get("homepage")
566 .and_then(|v| v.as_str())
567 .map(|s| truncate_field(s.to_string()));
568
569 let vcs_url = resources.get("repository").and_then(|v| match v {
570 JsonValue::String(s) => Some(truncate_field(s.clone())),
571 JsonValue::Object(obj) => obj
572 .get("url")
573 .and_then(|u| u.as_str())
574 .map(|s| truncate_field(s.to_string())),
575 _ => None,
576 });
577
578 let code_view_url = resources
579 .get("repository")
580 .and_then(|v| v.as_object())
581 .and_then(|obj| {
582 obj.get("web")
583 .and_then(|u| u.as_str())
584 .map(|s| truncate_field(s.to_string()))
585 });
586
587 let bug_tracking_url = resources.get("bugtracker").and_then(|v| match v {
588 JsonValue::String(s) => Some(truncate_field(s.clone())),
589 JsonValue::Object(obj) => obj
590 .get("web")
591 .and_then(|u| u.as_str())
592 .map(|s| truncate_field(s.to_string())),
593 _ => None,
594 });
595
596 (homepage_url, vcs_url, code_view_url, bug_tracking_url)
597}
598
599fn extract_resources_from_yaml(
600 yaml: &yaml_serde::Mapping,
601) -> (Option<String>, Option<String>, Option<String>) {
602 let resources = match yaml
603 .get(YamlValue::String(FIELD_RESOURCES.to_string()))
604 .and_then(|v| v.as_mapping())
605 {
606 Some(r) => r,
607 None => return (None, None, None),
608 };
609
610 let homepage_url = resources
611 .get(YamlValue::String("homepage".to_string()))
612 .and_then(|v| v.as_str())
613 .map(|s| truncate_field(s.to_string()));
614
615 let vcs_url = resources
616 .get(YamlValue::String("repository".to_string()))
617 .and_then(|v| v.as_str())
618 .map(|s| truncate_field(s.to_string()));
619
620 let bug_tracking_url = resources
621 .get(YamlValue::String("bugtracker".to_string()))
622 .and_then(|v| v.as_str())
623 .map(|s| truncate_field(s.to_string()));
624
625 (homepage_url, vcs_url, bug_tracking_url)
626}
627
628fn extract_dependencies_from_json(json: &serde_json::Map<String, JsonValue>) -> Vec<Dependency> {
629 let mut dependencies = Vec::new();
630
631 let prereqs = match json.get(FIELD_PREREQS).and_then(|v| v.as_object()) {
632 Some(p) => p,
633 None => return dependencies,
634 };
635
636 if let Some(runtime) = prereqs.get("runtime").and_then(|v| v.as_object())
638 && let Some(requires) = runtime.get("requires").and_then(|v| v.as_object())
639 {
640 dependencies.extend(extract_dependency_group(requires, "runtime", true, false));
641 }
642
643 if let Some(build) = prereqs.get("build").and_then(|v| v.as_object())
645 && let Some(requires) = build.get("requires").and_then(|v| v.as_object())
646 {
647 dependencies.extend(extract_dependency_group(requires, "build", false, false));
648 }
649
650 if let Some(test) = prereqs.get("test").and_then(|v| v.as_object())
652 && let Some(requires) = test.get("requires").and_then(|v| v.as_object())
653 {
654 dependencies.extend(extract_dependency_group(requires, "test", false, false));
655 }
656
657 if let Some(configure) = prereqs.get("configure").and_then(|v| v.as_object())
659 && let Some(requires) = configure.get("requires").and_then(|v| v.as_object())
660 {
661 dependencies.extend(extract_dependency_group(
662 requires,
663 "configure",
664 false,
665 false,
666 ));
667 }
668
669 dependencies
670}
671
672fn extract_dependencies_from_yaml(yaml: &yaml_serde::Mapping) -> Vec<Dependency> {
673 let mut dependencies = Vec::new();
674
675 if let Some(requires) = yaml
677 .get(YamlValue::String(FIELD_REQUIRES.to_string()))
678 .and_then(|v| v.as_mapping())
679 {
680 dependencies.extend(extract_yaml_dependency_group(
681 requires, "runtime", true, false,
682 ));
683 }
684
685 if let Some(build_requires) = yaml
686 .get(YamlValue::String(FIELD_BUILD_REQUIRES.to_string()))
687 .and_then(|v| v.as_mapping())
688 {
689 dependencies.extend(extract_yaml_dependency_group(
690 build_requires,
691 "build",
692 false,
693 false,
694 ));
695 }
696
697 if let Some(test_requires) = yaml
698 .get(YamlValue::String(FIELD_TEST_REQUIRES.to_string()))
699 .and_then(|v| v.as_mapping())
700 {
701 dependencies.extend(extract_yaml_dependency_group(
702 test_requires,
703 "test",
704 false,
705 false,
706 ));
707 }
708
709 if let Some(configure_requires) = yaml
710 .get(YamlValue::String(FIELD_CONFIGURE_REQUIRES.to_string()))
711 .and_then(|v| v.as_mapping())
712 {
713 dependencies.extend(extract_yaml_dependency_group(
714 configure_requires,
715 "configure",
716 false,
717 false,
718 ));
719 }
720
721 dependencies
722}
723
724fn extract_dependency_group(
725 deps: &serde_json::Map<String, JsonValue>,
726 scope: &str,
727 is_runtime: bool,
728 is_optional: bool,
729) -> Vec<Dependency> {
730 let limit = capped_iteration_limit(deps.len(), "CPAN JSON dependency group");
731 deps.iter()
732 .take(limit)
733 .filter_map(|(name, version)| {
734 if name == "perl" {
735 return None;
736 }
737
738 let purl = PackageUrl::new("cpan", cpan_distribution_name(name))
739 .ok()
740 .map(|p| p.to_string());
741 let purl = purl.map(truncate_field);
742
743 let extracted_requirement = match version {
744 JsonValue::String(s) => Some(truncate_field(s.clone())),
745 JsonValue::Number(n) => Some(truncate_field(n.to_string())),
746 _ => None,
747 };
748
749 Some(Dependency {
750 purl,
751 extracted_requirement,
752 scope: Some(truncate_field(scope.to_string())),
753 is_runtime: Some(is_runtime),
754 is_optional: Some(is_optional),
755 is_pinned: None,
756 is_direct: Some(true),
757 resolved_package: None,
758 extra_data: None,
759 })
760 })
761 .collect()
762}
763
764fn extract_yaml_dependency_group(
765 deps: &yaml_serde::Mapping,
766 scope: &str,
767 is_runtime: bool,
768 is_optional: bool,
769) -> Vec<Dependency> {
770 let limit = capped_iteration_limit(deps.len(), "CPAN YAML dependency group");
771 deps.iter()
772 .take(limit)
773 .filter_map(|(key, value)| {
774 let name = key.as_str()?;
775
776 if name == "perl" {
777 return None;
778 }
779
780 let purl = PackageUrl::new("cpan", cpan_distribution_name(name))
781 .ok()
782 .map(|p| p.to_string());
783 let purl = purl.map(truncate_field);
784
785 let extracted_requirement = match value {
786 YamlValue::String(s) => Some(truncate_field(s.clone())),
787 YamlValue::Number(n) => Some(truncate_field(n.to_string())),
788 _ => None,
789 };
790
791 Some(Dependency {
792 purl,
793 extracted_requirement,
794 scope: Some(truncate_field(scope.to_string())),
795 is_runtime: Some(is_runtime),
796 is_optional: Some(is_optional),
797 is_pinned: None,
798 is_direct: Some(true),
799 resolved_package: None,
800 extra_data: None,
801 })
802 })
803 .collect()
804}