Skip to main content

provenant/parsers/
pylock_toml.rs

1use std::collections::{HashMap, HashSet, VecDeque};
2use std::path::Path;
3
4use crate::parser_warn as warn;
5use packageurl::PackageUrl;
6use regex::Regex;
7use serde_json::{Map as JsonMap, Value as JsonValue};
8use toml::Value as TomlValue;
9use toml::map::Map as TomlMap;
10
11use crate::models::{DatasourceId, Dependency, PackageData, PackageType, ResolvedPackage};
12use crate::parsers::python::read_toml_file;
13
14use super::PackageParser;
15
16const FIELD_LOCK_VERSION: &str = "lock-version";
17const FIELD_CREATED_BY: &str = "created-by";
18const SUPPORTED_LOCK_VERSION: &str = "1.0";
19const FIELD_REQUIRES_PYTHON: &str = "requires-python";
20const FIELD_ENVIRONMENTS: &str = "environments";
21const FIELD_EXTRAS: &str = "extras";
22const FIELD_DEPENDENCY_GROUPS: &str = "dependency-groups";
23const FIELD_DEFAULT_GROUPS: &str = "default-groups";
24const FIELD_PACKAGES: &str = "packages";
25const FIELD_NAME: &str = "name";
26const FIELD_VERSION: &str = "version";
27const FIELD_MARKER: &str = "marker";
28const FIELD_DEPENDENCIES: &str = "dependencies";
29const FIELD_INDEX: &str = "index";
30const FIELD_VCS: &str = "vcs";
31const FIELD_DIRECTORY: &str = "directory";
32const FIELD_ARCHIVE: &str = "archive";
33const FIELD_SDIST: &str = "sdist";
34const FIELD_WHEELS: &str = "wheels";
35const FIELD_HASHES: &str = "hashes";
36const FIELD_TOOL: &str = "tool";
37const FIELD_ATTESTATION_IDENTITIES: &str = "attestation-identities";
38
39pub struct PylockTomlParser;
40
41#[derive(Clone, Debug, Default)]
42struct MarkerClassification {
43    is_runtime: bool,
44    is_optional: bool,
45    scope: Option<String>,
46}
47
48struct DependencyAnalysisContext<'a> {
49    package_tables: &'a [&'a TomlMap<String, TomlValue>],
50    dependency_indices: &'a [Vec<usize>],
51    incoming_counts: &'a [usize],
52    root_classifications: &'a [MarkerClassification],
53    runtime_reachable: &'a HashSet<usize>,
54    optional_reachable: &'a HashSet<usize>,
55    scope_sets: &'a HashMap<String, HashSet<usize>>,
56}
57
58impl PackageParser for PylockTomlParser {
59    const PACKAGE_TYPE: PackageType = PackageType::Pypi;
60
61    fn is_match(path: &Path) -> bool {
62        let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else {
63            return false;
64        };
65
66        file_name == "pylock.toml"
67            || file_name
68                .strip_prefix("pylock.")
69                .and_then(|suffix| suffix.strip_suffix(".toml"))
70                .is_some_and(|middle| !middle.is_empty() && !middle.contains('.'))
71    }
72
73    fn extract_packages(path: &Path) -> Vec<PackageData> {
74        let toml_content = match read_toml_file(path) {
75            Ok(content) => content,
76            Err(e) => {
77                warn!("Failed to read pylock.toml at {:?}: {}", path, e);
78                return vec![default_package_data()];
79            }
80        };
81
82        vec![parse_pylock_toml(&toml_content)]
83    }
84}
85
86fn parse_pylock_toml(toml_content: &TomlValue) -> PackageData {
87    let lock_version = toml_content
88        .get(FIELD_LOCK_VERSION)
89        .and_then(TomlValue::as_str);
90    if lock_version != Some(SUPPORTED_LOCK_VERSION) {
91        warn!(
92            "Invalid pylock.toml: missing or unsupported lock-version {:?}",
93            lock_version
94        );
95        return default_package_data();
96    }
97
98    let created_by = toml_content
99        .get(FIELD_CREATED_BY)
100        .and_then(TomlValue::as_str);
101    if created_by.is_none() {
102        warn!("Invalid pylock.toml: missing required created-by field");
103        return default_package_data();
104    }
105
106    let Some(package_values) = toml_content
107        .get(FIELD_PACKAGES)
108        .and_then(TomlValue::as_array)
109    else {
110        warn!("Invalid pylock.toml: missing required packages array");
111        return default_package_data();
112    };
113
114    let package_tables: Vec<&TomlMap<String, TomlValue>> = package_values
115        .iter()
116        .filter_map(TomlValue::as_table)
117        .collect();
118    if package_tables.is_empty() {
119        warn!("Invalid pylock.toml: packages array does not contain package tables");
120        return default_package_data();
121    }
122
123    let dependency_indices = build_dependency_indices(&package_tables);
124    let incoming_counts = build_incoming_counts(package_tables.len(), &dependency_indices);
125    let default_groups = extract_string_set(toml_content, FIELD_DEFAULT_GROUPS);
126
127    let root_classifications: Vec<MarkerClassification> = package_tables
128        .iter()
129        .enumerate()
130        .map(|(index, table)| {
131            if incoming_counts[index] == 0 {
132                classify_marker(
133                    table.get(FIELD_MARKER).and_then(TomlValue::as_str),
134                    &default_groups,
135                )
136            } else {
137                MarkerClassification::default()
138            }
139        })
140        .collect();
141
142    let runtime_roots: Vec<usize> = root_classifications
143        .iter()
144        .enumerate()
145        .filter_map(|(index, info)| {
146            (incoming_counts[index] == 0 && info.is_runtime).then_some(index)
147        })
148        .collect();
149    let optional_roots: Vec<usize> = root_classifications
150        .iter()
151        .enumerate()
152        .filter_map(|(index, info)| {
153            (incoming_counts[index] == 0 && info.is_optional).then_some(index)
154        })
155        .collect();
156
157    let runtime_reachable = collect_reachable_indices(&dependency_indices, &runtime_roots);
158    let optional_reachable = collect_reachable_indices(&dependency_indices, &optional_roots);
159
160    let mut scope_sets: HashMap<String, HashSet<usize>> = HashMap::new();
161    for (index, info) in root_classifications.iter().enumerate() {
162        if incoming_counts[index] != 0 {
163            continue;
164        }
165
166        if let Some(scope) = info.scope.as_ref() {
167            scope_sets.insert(
168                scope.clone(),
169                collect_reachable_indices(&dependency_indices, &[index]),
170            );
171        }
172    }
173
174    let analysis = DependencyAnalysisContext {
175        package_tables: &package_tables,
176        dependency_indices: &dependency_indices,
177        incoming_counts: &incoming_counts,
178        root_classifications: &root_classifications,
179        runtime_reachable: &runtime_reachable,
180        optional_reachable: &optional_reachable,
181        scope_sets: &scope_sets,
182    };
183
184    let mut package_data = default_package_data();
185    package_data.extra_data = build_lock_extra_data(toml_content);
186    package_data.dependencies = package_tables
187        .iter()
188        .enumerate()
189        .filter_map(|(index, package_table)| {
190            build_top_level_dependency(index, package_table, &analysis)
191        })
192        .collect();
193
194    package_data
195}
196
197fn build_top_level_dependency(
198    index: usize,
199    package_table: &TomlMap<String, TomlValue>,
200    analysis: &DependencyAnalysisContext<'_>,
201) -> Option<Dependency> {
202    let name = normalized_package_name(package_table)?;
203    let version = package_version(package_table);
204    let direct = analysis
205        .incoming_counts
206        .get(index)
207        .copied()
208        .unwrap_or_default()
209        == 0;
210
211    let (is_runtime, is_optional, scope) = if direct {
212        let classification = analysis
213            .root_classifications
214            .get(index)
215            .cloned()
216            .unwrap_or_default();
217        (
218            classification.is_runtime,
219            classification.is_optional,
220            classification.scope,
221        )
222    } else {
223        let is_runtime = analysis.runtime_reachable.contains(&index);
224        let is_optional = !is_runtime && analysis.optional_reachable.contains(&index);
225        let scope = scope_for_index(analysis.scope_sets, index);
226        (is_runtime, is_optional, scope)
227    };
228
229    Some(Dependency {
230        purl: create_pypi_purl(&name, version.as_deref()),
231        extracted_requirement: None,
232        scope,
233        is_runtime: Some(is_runtime),
234        is_optional: Some(is_optional),
235        is_pinned: Some(is_package_pinned(package_table)),
236        is_direct: Some(direct),
237        resolved_package: Some(Box::new(build_resolved_package(
238            package_table,
239            analysis.package_tables,
240            analysis
241                .dependency_indices
242                .get(index)
243                .map(Vec::as_slice)
244                .unwrap_or(&[]),
245        ))),
246        extra_data: build_package_extra_data(package_table),
247    })
248}
249
250fn build_resolved_package(
251    package_table: &TomlMap<String, TomlValue>,
252    package_tables: &[&TomlMap<String, TomlValue>],
253    dependency_indices: &[usize],
254) -> ResolvedPackage {
255    let name = normalized_package_name(package_table).unwrap_or_default();
256    let version = package_version(package_table).unwrap_or_default();
257    let (_, repository_download_url, api_data_url, purl) = build_pypi_urls(
258        Some(&name),
259        (!version.is_empty()).then_some(version.as_str()),
260    );
261    let repository_homepage_url = Some(format!("https://pypi.org/project/{}", name));
262    let (download_url, sha256, sha512, md5) = extract_artifact_metadata(package_table);
263
264    ResolvedPackage {
265        primary_language: Some("Python".to_string()),
266        download_url,
267        sha1: None,
268        sha256,
269        sha512,
270        md5,
271        is_virtual: false,
272        extra_data: build_package_extra_data(package_table),
273        dependencies: dependency_indices
274            .iter()
275            .filter_map(|child_index| package_tables.get(*child_index))
276            .filter_map(|child| build_resolved_dependency(child))
277            .collect(),
278        repository_homepage_url,
279        repository_download_url,
280        api_data_url,
281        datasource_id: Some(DatasourceId::PypiPylockToml),
282        purl,
283        ..ResolvedPackage::new(PylockTomlParser::PACKAGE_TYPE, String::new(), name, version)
284    }
285}
286
287fn build_resolved_dependency(package_table: &TomlMap<String, TomlValue>) -> Option<Dependency> {
288    let name = normalized_package_name(package_table)?;
289    let version = package_version(package_table);
290
291    Some(Dependency {
292        purl: create_pypi_purl(&name, version.as_deref()),
293        extracted_requirement: None,
294        scope: None,
295        is_runtime: None,
296        is_optional: None,
297        is_pinned: Some(is_package_pinned(package_table)),
298        is_direct: Some(true),
299        resolved_package: None,
300        extra_data: build_package_extra_data(package_table),
301    })
302}
303
304fn build_dependency_indices(package_tables: &[&TomlMap<String, TomlValue>]) -> Vec<Vec<usize>> {
305    package_tables
306        .iter()
307        .map(|package_table| {
308            package_table
309                .get(FIELD_DEPENDENCIES)
310                .and_then(TomlValue::as_array)
311                .into_iter()
312                .flatten()
313                .filter_map(TomlValue::as_table)
314                .flat_map(|reference| {
315                    resolve_dependency_reference_indices(package_tables, reference)
316                })
317                .collect()
318        })
319        .collect()
320}
321
322fn resolve_dependency_reference_indices(
323    package_tables: &[&TomlMap<String, TomlValue>],
324    reference: &TomlMap<String, TomlValue>,
325) -> Vec<usize> {
326    let matches: Vec<usize> = package_tables
327        .iter()
328        .enumerate()
329        .filter_map(|(index, package_table)| {
330            package_reference_matches(package_table, reference).then_some(index)
331        })
332        .collect();
333
334    if matches.len() == 1 {
335        matches
336    } else {
337        Vec::new()
338    }
339}
340
341fn package_reference_matches(
342    package_table: &TomlMap<String, TomlValue>,
343    reference: &TomlMap<String, TomlValue>,
344) -> bool {
345    reference.iter().all(|(key, ref_value)| {
346        package_table
347            .get(key)
348            .is_some_and(|pkg_value| toml_values_match(pkg_value, ref_value))
349    })
350}
351
352fn toml_values_match(left: &TomlValue, right: &TomlValue) -> bool {
353    match (left, right) {
354        (TomlValue::String(left), TomlValue::String(right)) => left == right,
355        (TomlValue::Integer(left), TomlValue::Integer(right)) => left == right,
356        (TomlValue::Float(left), TomlValue::Float(right)) => left == right,
357        (TomlValue::Boolean(left), TomlValue::Boolean(right)) => left == right,
358        (TomlValue::Datetime(left), TomlValue::Datetime(right)) => left == right,
359        (TomlValue::Array(left), TomlValue::Array(right)) => {
360            left.len() == right.len()
361                && left
362                    .iter()
363                    .zip(right.iter())
364                    .all(|(left, right)| toml_values_match(left, right))
365        }
366        (TomlValue::Table(left), TomlValue::Table(right)) => {
367            right.iter().all(|(key, right_value)| {
368                left.get(key)
369                    .is_some_and(|left_value| toml_values_match(left_value, right_value))
370            })
371        }
372        _ => false,
373    }
374}
375
376fn build_incoming_counts(package_count: usize, dependency_indices: &[Vec<usize>]) -> Vec<usize> {
377    let mut incoming = vec![0; package_count];
378    for dependency_list in dependency_indices {
379        for &child_index in dependency_list {
380            if let Some(count) = incoming.get_mut(child_index) {
381                *count += 1;
382            }
383        }
384    }
385    incoming
386}
387
388fn collect_reachable_indices(dependency_indices: &[Vec<usize>], roots: &[usize]) -> HashSet<usize> {
389    let mut visited = HashSet::new();
390    let mut queue: VecDeque<usize> = roots.iter().copied().collect();
391
392    while let Some(index) = queue.pop_front() {
393        if !visited.insert(index) {
394            continue;
395        }
396
397        for &child_index in dependency_indices.get(index).into_iter().flatten() {
398            queue.push_back(child_index);
399        }
400    }
401
402    visited
403}
404
405fn classify_marker(marker: Option<&str>, default_groups: &HashSet<String>) -> MarkerClassification {
406    let Some(marker) = marker else {
407        return MarkerClassification {
408            is_runtime: true,
409            is_optional: false,
410            scope: None,
411        };
412    };
413
414    let extras = extract_marker_memberships(marker, "extras");
415    if !extras.is_empty() {
416        return MarkerClassification {
417            is_runtime: false,
418            is_optional: true,
419            scope: single_scope(extras),
420        };
421    }
422
423    let groups = extract_marker_memberships(marker, "dependency_groups");
424    let non_default_groups: Vec<String> = groups
425        .into_iter()
426        .filter(|group| !default_groups.contains(group))
427        .collect();
428    if !non_default_groups.is_empty() {
429        return MarkerClassification {
430            is_runtime: false,
431            is_optional: false,
432            scope: single_scope(non_default_groups),
433        };
434    }
435
436    MarkerClassification {
437        is_runtime: true,
438        is_optional: false,
439        scope: None,
440    }
441}
442
443fn extract_marker_memberships(marker: &str, variable_name: &str) -> Vec<String> {
444    let pattern = format!(
445        r#"['\"]([^'\"]+)['\"]\s+in\s+{}\b"#,
446        regex::escape(variable_name)
447    );
448    let Ok(regex) = Regex::new(&pattern) else {
449        return Vec::new();
450    };
451
452    let mut memberships: Vec<String> = regex
453        .captures_iter(marker)
454        .filter_map(|captures| {
455            captures
456                .get(1)
457                .map(|value| value.as_str().trim().to_string())
458        })
459        .filter(|value| !value.is_empty())
460        .collect();
461    memberships.sort();
462    memberships.dedup();
463    memberships
464}
465
466fn single_scope(values: Vec<String>) -> Option<String> {
467    (values.len() == 1).then(|| values[0].clone())
468}
469
470fn scope_for_index(scope_sets: &HashMap<String, HashSet<usize>>, index: usize) -> Option<String> {
471    let matches: Vec<String> = scope_sets
472        .iter()
473        .filter_map(|(scope, indices)| indices.contains(&index).then_some(scope.clone()))
474        .collect();
475    single_scope(matches)
476}
477
478fn normalized_package_name(package_table: &TomlMap<String, TomlValue>) -> Option<String> {
479    package_table
480        .get(FIELD_NAME)
481        .and_then(TomlValue::as_str)
482        .map(|value| value.trim().to_ascii_lowercase())
483}
484
485fn package_version(package_table: &TomlMap<String, TomlValue>) -> Option<String> {
486    package_table
487        .get(FIELD_VERSION)
488        .and_then(TomlValue::as_str)
489        .map(|value| value.to_string())
490}
491
492fn is_package_pinned(package_table: &TomlMap<String, TomlValue>) -> bool {
493    package_table.contains_key(FIELD_VERSION)
494        || package_table
495            .get(FIELD_VCS)
496            .and_then(TomlValue::as_table)
497            .is_some_and(|table| table.contains_key("commit-id"))
498        || has_hashes(package_table.get(FIELD_ARCHIVE))
499        || has_hashes(package_table.get(FIELD_SDIST))
500        || package_table
501            .get(FIELD_WHEELS)
502            .and_then(TomlValue::as_array)
503            .into_iter()
504            .flatten()
505            .filter_map(TomlValue::as_table)
506            .any(|wheel| wheel.contains_key(FIELD_HASHES))
507}
508
509fn has_hashes(value: Option<&TomlValue>) -> bool {
510    value
511        .and_then(TomlValue::as_table)
512        .is_some_and(|table| table.contains_key(FIELD_HASHES))
513}
514
515fn build_lock_extra_data(toml_content: &TomlValue) -> Option<HashMap<String, JsonValue>> {
516    let mut extra_data = HashMap::new();
517
518    for (source_key, target_key) in [
519        (FIELD_LOCK_VERSION, "lock_version"),
520        (FIELD_CREATED_BY, "created_by"),
521        (FIELD_REQUIRES_PYTHON, "requires_python"),
522        (FIELD_ENVIRONMENTS, FIELD_ENVIRONMENTS),
523        (FIELD_EXTRAS, FIELD_EXTRAS),
524        (FIELD_DEPENDENCY_GROUPS, FIELD_DEPENDENCY_GROUPS),
525        (FIELD_DEFAULT_GROUPS, FIELD_DEFAULT_GROUPS),
526    ] {
527        if let Some(value) = toml_content.get(source_key) {
528            extra_data.insert(target_key.to_string(), toml_value_to_json(value));
529        }
530    }
531
532    if let Some(tool) = toml_content.get(FIELD_TOOL) {
533        extra_data.insert(FIELD_TOOL.to_string(), toml_value_to_json(tool));
534    }
535
536    (!extra_data.is_empty()).then_some(extra_data)
537}
538
539fn build_package_extra_data(
540    package_table: &TomlMap<String, TomlValue>,
541) -> Option<HashMap<String, JsonValue>> {
542    let mut extra_data = HashMap::new();
543
544    for key in [
545        FIELD_MARKER,
546        FIELD_REQUIRES_PYTHON,
547        FIELD_INDEX,
548        FIELD_VCS,
549        FIELD_DIRECTORY,
550        FIELD_ARCHIVE,
551        FIELD_SDIST,
552        FIELD_WHEELS,
553        FIELD_TOOL,
554        FIELD_ATTESTATION_IDENTITIES,
555    ] {
556        if let Some(value) = package_table.get(key) {
557            extra_data.insert(key.to_string(), toml_value_to_json(value));
558        }
559    }
560
561    (!extra_data.is_empty()).then_some(extra_data)
562}
563
564fn extract_artifact_metadata(
565    package_table: &TomlMap<String, TomlValue>,
566) -> (
567    Option<String>,
568    Option<String>,
569    Option<String>,
570    Option<String>,
571) {
572    if let Some(archive_table) = package_table
573        .get(FIELD_ARCHIVE)
574        .and_then(TomlValue::as_table)
575    {
576        return (
577            archive_table
578                .get("url")
579                .and_then(TomlValue::as_str)
580                .map(|value| value.to_string())
581                .or_else(|| {
582                    archive_table
583                        .get("path")
584                        .and_then(TomlValue::as_str)
585                        .map(|value| value.to_string())
586                }),
587            extract_hash_by_name(archive_table, "sha256"),
588            extract_hash_by_name(archive_table, "sha512"),
589            extract_hash_by_name(archive_table, "md5"),
590        );
591    }
592
593    if let Some(sdist_table) = package_table.get(FIELD_SDIST).and_then(TomlValue::as_table) {
594        return (
595            sdist_table
596                .get("url")
597                .and_then(TomlValue::as_str)
598                .map(|value| value.to_string())
599                .or_else(|| {
600                    sdist_table
601                        .get("path")
602                        .and_then(TomlValue::as_str)
603                        .map(|value| value.to_string())
604                }),
605            extract_hash_by_name(sdist_table, "sha256"),
606            extract_hash_by_name(sdist_table, "sha512"),
607            extract_hash_by_name(sdist_table, "md5"),
608        );
609    }
610
611    let wheel_table = package_table
612        .get(FIELD_WHEELS)
613        .and_then(TomlValue::as_array)
614        .and_then(|wheels| wheels.first())
615        .and_then(TomlValue::as_table);
616
617    (
618        wheel_table
619            .and_then(|table| table.get("url"))
620            .and_then(TomlValue::as_str)
621            .map(|value| value.to_string())
622            .or_else(|| {
623                wheel_table
624                    .and_then(|table| table.get("path"))
625                    .and_then(TomlValue::as_str)
626                    .map(|value| value.to_string())
627            }),
628        wheel_table.and_then(|table| extract_hash_by_name(table, "sha256")),
629        wheel_table.and_then(|table| extract_hash_by_name(table, "sha512")),
630        wheel_table.and_then(|table| extract_hash_by_name(table, "md5")),
631    )
632}
633
634fn extract_hash_by_name(table: &TomlMap<String, TomlValue>, name: &str) -> Option<String> {
635    table
636        .get(FIELD_HASHES)
637        .and_then(TomlValue::as_table)
638        .and_then(|hashes| hashes.get(name))
639        .and_then(TomlValue::as_str)
640        .map(|value| value.to_string())
641}
642
643fn extract_string_set(toml_content: &TomlValue, key: &str) -> HashSet<String> {
644    toml_content
645        .get(key)
646        .and_then(TomlValue::as_array)
647        .into_iter()
648        .flatten()
649        .filter_map(TomlValue::as_str)
650        .map(|value| value.to_string())
651        .collect()
652}
653
654fn build_pypi_urls(
655    name: Option<&str>,
656    version: Option<&str>,
657) -> (
658    Option<String>,
659    Option<String>,
660    Option<String>,
661    Option<String>,
662) {
663    let repository_homepage_url = name.map(|value| format!("https://pypi.org/project/{}", value));
664    let repository_download_url = name.and_then(|value| {
665        version.map(|ver| {
666            format!(
667                "https://pypi.org/packages/source/{}/{}/{}-{}.tar.gz",
668                &value[..1.min(value.len())],
669                value,
670                value,
671                ver
672            )
673        })
674    });
675    let api_data_url = name.map(|value| {
676        if let Some(ver) = version {
677            format!("https://pypi.org/pypi/{}/{}/json", value, ver)
678        } else {
679            format!("https://pypi.org/pypi/{}/json", value)
680        }
681    });
682    let purl = name.and_then(|value| create_pypi_purl(value, version));
683
684    (
685        repository_homepage_url,
686        repository_download_url,
687        api_data_url,
688        purl,
689    )
690}
691
692fn create_pypi_purl(name: &str, version: Option<&str>) -> Option<String> {
693    if let Ok(mut purl) = PackageUrl::new(PylockTomlParser::PACKAGE_TYPE.as_str(), name) {
694        if let Some(version) = version
695            && purl.with_version(version).is_err()
696        {
697            return None;
698        }
699        return Some(purl.to_string());
700    }
701
702    let mut purl = format!("pkg:pypi/{}", name);
703    if let Some(version) = version
704        && !version.is_empty()
705    {
706        purl.push('@');
707        purl.push_str(version);
708    }
709    Some(purl)
710}
711
712fn toml_value_to_json(value: &TomlValue) -> JsonValue {
713    match value {
714        TomlValue::String(value) => JsonValue::String(value.clone()),
715        TomlValue::Integer(value) => JsonValue::String(value.to_string()),
716        TomlValue::Float(value) => JsonValue::String(value.to_string()),
717        TomlValue::Boolean(value) => JsonValue::Bool(*value),
718        TomlValue::Datetime(value) => JsonValue::String(value.to_string()),
719        TomlValue::Array(values) => {
720            JsonValue::Array(values.iter().map(toml_value_to_json).collect())
721        }
722        TomlValue::Table(values) => JsonValue::Object(
723            values
724                .iter()
725                .map(|(key, value)| (key.clone(), toml_value_to_json(value)))
726                .collect::<JsonMap<String, JsonValue>>(),
727        ),
728    }
729}
730
731fn default_package_data() -> PackageData {
732    PackageData {
733        package_type: Some(PylockTomlParser::PACKAGE_TYPE),
734        primary_language: Some("Python".to_string()),
735        datasource_id: Some(DatasourceId::PypiPylockToml),
736        ..Default::default()
737    }
738}
739
740crate::register_parser!(
741    "pylock.toml lockfile",
742    &["**/pylock.toml", "**/pylock.*.toml"],
743    "pypi",
744    "Python",
745    Some("https://packaging.python.org/en/latest/specifications/pylock-toml/"),
746);