Skip to main content

lenso_module_management/
resolver.rs

1use crate::{
2    APPLICATION_MODULE_LOCK_PROTOCOL, ApplicationModuleLock, DesiredModuleComposition,
3    DesiredModuleSelection, LockedCapabilityBinding, LockedConsoleUiArtifact, LockedModule,
4    LockedModuleReason, ManagedDeliveryKind, ModuleRootChange,
5};
6use lenso_contracts::{
7    ModuleDelivery, ModuleEligibility, ModuleEligibilityState, ModuleRelease,
8    ModuleVerificationCell, digest_json,
9};
10use schemars::JsonSchema;
11use semver::{Version, VersionReq};
12use serde::{Deserialize, Serialize};
13use std::cmp::Ordering;
14use std::collections::{BTreeMap, BTreeSet};
15
16pub const MODULE_RESOLUTION_CONFLICT_PROTOCOL: &str = "lenso.module-resolution-conflict.v1";
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
19#[serde(deny_unknown_fields)]
20pub struct ModuleResolutionCandidate {
21    pub catalog_snapshot_digest: String,
22    pub release_digest: String,
23    pub release: ModuleRelease,
24    pub eligibility: ModuleEligibility,
25    pub verification_cell: ModuleVerificationCell,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct ModuleResolutionRequest {
30    pub current_desired: DesiredModuleComposition,
31    pub current_lock: Option<ApplicationModuleLock>,
32    pub change: ModuleRootChange,
33    pub catalog_snapshot_digest: String,
34    pub trust_policy_digest: String,
35    pub resolver_version: String,
36    pub candidates: Vec<ModuleResolutionCandidate>,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct ModuleResolution {
41    pub target_desired: DesiredModuleComposition,
42    pub target_lock: ApplicationModuleLock,
43    pub removed_orphan_module_ids: Vec<String>,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
47#[serde(deny_unknown_fields)]
48pub struct ModuleResolutionConflict {
49    pub protocol: String,
50    pub code: String,
51    #[serde(default, skip_serializing_if = "Option::is_none")]
52    pub module_id: Option<String>,
53    #[serde(default, skip_serializing_if = "Vec::is_empty")]
54    pub dependency_paths: Vec<Vec<String>>,
55    #[serde(default, skip_serializing_if = "Vec::is_empty")]
56    pub constraints: Vec<String>,
57    #[serde(default, skip_serializing_if = "Vec::is_empty")]
58    pub eligible_alternatives: Vec<String>,
59}
60
61#[derive(Debug, thiserror::Error)]
62#[error("Module graph resolution failed: {conflict:?}")]
63pub struct ModuleResolutionError {
64    pub conflict: Box<ModuleResolutionConflict>,
65}
66
67#[derive(Debug, Default, Clone, Copy)]
68pub struct ModuleGraphResolver;
69
70#[derive(Debug, Clone)]
71struct RequirementConstraint {
72    requirement: VersionReq,
73    requirement_text: String,
74    capabilities: Vec<String>,
75    exact_release_digest: Option<String>,
76    delivery: Option<ManagedDeliveryKind>,
77    path: Vec<String>,
78}
79
80#[derive(Debug, Clone, Default)]
81struct SolverState {
82    constraints: BTreeMap<String, Vec<RequirementConstraint>>,
83    selections: BTreeMap<String, usize>,
84    dependencies: BTreeMap<String, BTreeSet<String>>,
85}
86
87impl ModuleGraphResolver {
88    pub fn resolve(
89        &self,
90        request: &ModuleResolutionRequest,
91    ) -> Result<ModuleResolution, ModuleResolutionError> {
92        let mut target_desired = request.current_desired.clone();
93        apply_root_change(&mut target_desired, &request.change)?;
94        normalize_desired(&mut target_desired);
95
96        let mut candidates = request.candidates.clone();
97        validate_candidates(&request.catalog_snapshot_digest, &candidates)?;
98        candidates.sort_by(|left, right| {
99            left.release
100                .module_id
101                .cmp(&right.release.module_id)
102                .then_with(|| left.release_digest.cmp(&right.release_digest))
103        });
104
105        let selected_optional = target_desired
106            .selected
107            .iter()
108            .map(|selection| {
109                (
110                    selection.module_id.clone(),
111                    selection
112                        .optional_requirements
113                        .iter()
114                        .cloned()
115                        .collect::<BTreeSet<_>>(),
116                )
117            })
118            .collect::<BTreeMap<_, _>>();
119        let direct_ids = target_desired
120            .selected
121            .iter()
122            .map(|selection| selection.module_id.clone())
123            .collect::<BTreeSet<_>>();
124        let current_locked = request
125            .current_lock
126            .as_ref()
127            .map(|lock| {
128                lock.modules
129                    .iter()
130                    .map(|module| (module.module_id.clone(), module.release_digest.clone()))
131                    .collect::<BTreeMap<_, _>>()
132            })
133            .unwrap_or_default();
134
135        let mut state = SolverState::default();
136        for selection in &target_desired.selected {
137            let requirement = parse_requirement(
138                &selection.module_id,
139                &selection.version_requirement,
140                vec![selection.module_id.clone()],
141            )?;
142            state
143                .constraints
144                .entry(selection.module_id.clone())
145                .or_default()
146                .push(RequirementConstraint {
147                    requirement,
148                    requirement_text: selection.version_requirement.clone(),
149                    capabilities: Vec::new(),
150                    exact_release_digest: selection.exact_release_digest.clone(),
151                    delivery: selection.delivery_preference,
152                    path: vec![selection.module_id.clone()],
153                });
154        }
155
156        let solved = solve(state, &candidates, &selected_optional, &current_locked)?;
157        validate_optional_selections(&target_desired, &candidates, &solved)?;
158        let desired_digest = digest_json(&target_desired)
159            .map_err(|_| conflict("desired_composition_not_canonical", None))?;
160        let target_lock = build_lock(
161            request,
162            &target_desired,
163            &desired_digest,
164            &candidates,
165            &solved,
166            &direct_ids,
167            &selected_optional,
168        );
169        let target_ids = target_lock
170            .modules
171            .iter()
172            .map(|module| module.module_id.clone())
173            .collect::<BTreeSet<_>>();
174        let mut removed_orphan_module_ids = request
175            .current_lock
176            .as_ref()
177            .into_iter()
178            .flat_map(|lock| lock.modules.iter())
179            .filter(|module| !target_ids.contains(&module.module_id))
180            .map(|module| module.module_id.clone())
181            .collect::<Vec<_>>();
182        removed_orphan_module_ids.sort();
183
184        Ok(ModuleResolution {
185            target_desired,
186            target_lock,
187            removed_orphan_module_ids,
188        })
189    }
190}
191
192fn apply_root_change(
193    desired: &mut DesiredModuleComposition,
194    change: &ModuleRootChange,
195) -> Result<(), ModuleResolutionError> {
196    let before = desired.clone();
197    match change {
198        ModuleRootChange::Install { selection } => {
199            if desired
200                .selected
201                .iter()
202                .any(|current| current.module_id == selection.module_id)
203            {
204                return Err(conflict(
205                    "module_already_selected",
206                    Some(&selection.module_id),
207                ));
208            }
209            desired.selected.push(selection.clone());
210        }
211        ModuleRootChange::Update {
212            module_id,
213            version_requirement,
214        } => find_selection_mut(desired, module_id)?
215            .version_requirement
216            .clone_from(version_requirement),
217        ModuleRootChange::Uninstall { module_id } => {
218            let old_len = desired.selected.len();
219            desired.selected.retain(|item| item.module_id != *module_id);
220            if desired.selected.len() == old_len {
221                return Err(conflict("module_not_selected", Some(module_id)));
222            }
223        }
224        ModuleRootChange::SelectOptional {
225            module_id,
226            requirement,
227            selected,
228        } => {
229            let selection = find_selection_mut(desired, module_id)?;
230            if *selected {
231                selection.optional_requirements.push(requirement.clone());
232            } else {
233                selection
234                    .optional_requirements
235                    .retain(|item| item != requirement);
236            }
237        }
238        ModuleRootChange::SwitchDelivery {
239            module_id,
240            delivery,
241        } => {
242            find_selection_mut(desired, module_id)?.delivery_preference = Some(*delivery);
243        }
244        ModuleRootChange::Restore { .. } | ModuleRootChange::Repair { .. } => {
245            return Err(conflict("change_requires_exact_lock", None));
246        }
247    }
248    if *desired != before {
249        desired.revision = desired.revision.saturating_add(1);
250    }
251    Ok(())
252}
253
254fn find_selection_mut<'a>(
255    desired: &'a mut DesiredModuleComposition,
256    module_id: &str,
257) -> Result<&'a mut DesiredModuleSelection, ModuleResolutionError> {
258    desired
259        .selected
260        .iter_mut()
261        .find(|selection| selection.module_id == module_id)
262        .ok_or_else(|| conflict("module_not_selected", Some(module_id)))
263}
264
265fn normalize_desired(desired: &mut DesiredModuleComposition) {
266    for selection in &mut desired.selected {
267        selection.optional_requirements.sort();
268        selection.optional_requirements.dedup();
269    }
270    desired
271        .selected
272        .sort_by(|left, right| left.module_id.cmp(&right.module_id));
273    desired
274        .local_overrides
275        .sort_by(|left, right| left.module_id.cmp(&right.module_id));
276}
277
278fn validate_candidates(
279    snapshot_digest: &str,
280    candidates: &[ModuleResolutionCandidate],
281) -> Result<(), ModuleResolutionError> {
282    let mut identities = BTreeSet::new();
283    for candidate in candidates {
284        let release = &candidate.release;
285        if candidate.catalog_snapshot_digest != snapshot_digest {
286            return Err(conflict(
287                "candidate_snapshot_mismatch",
288                Some(&release.module_id),
289            ));
290        }
291        if digest_json(release).ok().as_deref() != Some(candidate.release_digest.as_str()) {
292            return Err(conflict(
293                "candidate_release_digest_mismatch",
294                Some(&release.module_id),
295            ));
296        }
297        if !release.validate().is_empty() {
298            return Err(conflict(
299                "candidate_release_invalid",
300                Some(&release.module_id),
301            ));
302        }
303        if candidate.verification_cell.module_release_digest != candidate.release_digest {
304            return Err(conflict(
305                "candidate_verification_cell_mismatch",
306                Some(&release.module_id),
307            ));
308        }
309        let identity = (
310            release.module_id.clone(),
311            release.version.clone(),
312            ManagedDeliveryKind::from(&release.delivery),
313        );
314        if !identities.insert(identity) {
315            return Err(conflict(
316                "duplicate_release_identity",
317                Some(&release.module_id),
318            ));
319        }
320    }
321    Ok(())
322}
323
324#[allow(clippy::too_many_lines)]
325fn solve(
326    state: SolverState,
327    candidates: &[ModuleResolutionCandidate],
328    selected_optional: &BTreeMap<String, BTreeSet<String>>,
329    current_locked: &BTreeMap<String, String>,
330) -> Result<SolverState, ModuleResolutionError> {
331    if let Some((selected_id, _selected_index)) = state
332        .selections
333        .iter()
334        .find(|(id, index)| !candidate_matches(&candidates[**index], &state.constraints[*id]))
335    {
336        return Err(unsatisfied_conflict(
337            selected_id,
338            &state.constraints[selected_id],
339            candidates,
340        ));
341    }
342
343    let unresolved = state
344        .constraints
345        .iter()
346        .filter(|(module_id, _)| !state.selections.contains_key(*module_id))
347        .map(|(module_id, constraints)| {
348            let options = ranked_options(module_id, constraints, candidates, current_locked);
349            (module_id.clone(), options)
350        })
351        .min_by(|left, right| {
352            left.1
353                .len()
354                .cmp(&right.1.len())
355                .then_with(|| left.0.cmp(&right.0))
356        });
357    let Some((module_id, options)) = unresolved else {
358        return Ok(state);
359    };
360    if options.is_empty() {
361        return Err(unsatisfied_conflict(
362            &module_id,
363            &state.constraints[&module_id],
364            candidates,
365        ));
366    }
367
368    let mut first_error = None;
369    for candidate_index in options {
370        let candidate = &candidates[candidate_index];
371        let mut next = state.clone();
372        let mut invalid_candidate = false;
373        next.selections.insert(module_id.clone(), candidate_index);
374        let parent_path = next.constraints[&module_id]
375            .iter()
376            .map(|constraint| constraint.path.clone())
377            .min()
378            .unwrap_or_else(|| vec![module_id.clone()]);
379        for requirement in candidate
380            .release
381            .manifest
382            .requires
383            .iter()
384            .filter(|requirement| {
385                !requirement.optional
386                    || selected_optional
387                        .get(&module_id)
388                        .is_some_and(|selected| selected.contains(&requirement.module_id))
389            })
390        {
391            if path_exists(&next.dependencies, &requirement.module_id, &module_id) {
392                let mut path = parent_path.clone();
393                path.push(requirement.module_id.clone());
394                let mut error = conflict("dependency_cycle", Some(&requirement.module_id));
395                error.conflict.dependency_paths.push(path);
396                first_error.get_or_insert(error);
397                invalid_candidate = true;
398                continue;
399            }
400            next.dependencies
401                .entry(module_id.clone())
402                .or_default()
403                .insert(requirement.module_id.clone());
404            let mut path = parent_path.clone();
405            path.push(requirement.module_id.clone());
406            let parsed = match parse_requirement(
407                &requirement.module_id,
408                &requirement.version_requirement,
409                path.clone(),
410            ) {
411                Ok(parsed) => parsed,
412                Err(error) => {
413                    first_error.get_or_insert(error);
414                    invalid_candidate = true;
415                    continue;
416                }
417            };
418            next.constraints
419                .entry(requirement.module_id.clone())
420                .or_default()
421                .push(RequirementConstraint {
422                    requirement: parsed,
423                    requirement_text: requirement.version_requirement.clone(),
424                    capabilities: requirement.capabilities.clone(),
425                    exact_release_digest: None,
426                    delivery: None,
427                    path,
428                });
429        }
430        if invalid_candidate {
431            continue;
432        }
433        match solve(next, candidates, selected_optional, current_locked) {
434            Ok(solved) => return Ok(solved),
435            Err(error) => first_error.get_or_insert(error),
436        };
437    }
438    Err(first_error.unwrap_or_else(|| {
439        unsatisfied_conflict(&module_id, &state.constraints[&module_id], candidates)
440    }))
441}
442
443fn path_exists(graph: &BTreeMap<String, BTreeSet<String>>, start: &str, target: &str) -> bool {
444    if start == target {
445        return true;
446    }
447    graph.get(start).is_some_and(|children| {
448        children
449            .iter()
450            .any(|child| path_exists(graph, child, target))
451    })
452}
453
454fn ranked_options(
455    module_id: &str,
456    constraints: &[RequirementConstraint],
457    candidates: &[ModuleResolutionCandidate],
458    current_locked: &BTreeMap<String, String>,
459) -> Vec<usize> {
460    let mut options = candidates
461        .iter()
462        .enumerate()
463        .filter(|(_, candidate)| {
464            candidate.release.module_id == module_id && candidate_matches(candidate, constraints)
465        })
466        .map(|(index, _)| index)
467        .collect::<Vec<_>>();
468    options.sort_by(|left, right| {
469        compare_candidates(
470            &candidates[*left],
471            &candidates[*right],
472            current_locked.get(module_id).map(String::as_str),
473        )
474    });
475    options
476}
477
478fn compare_candidates(
479    left: &ModuleResolutionCandidate,
480    right: &ModuleResolutionCandidate,
481    current_digest: Option<&str>,
482) -> Ordering {
483    let left_current = current_digest == Some(left.release_digest.as_str());
484    let right_current = current_digest == Some(right.release_digest.as_str());
485    right_current
486        .cmp(&left_current)
487        .then_with(|| {
488            let left_linked = matches!(left.release.delivery, ModuleDelivery::Linked(_));
489            let right_linked = matches!(right.release.delivery, ModuleDelivery::Linked(_));
490            right_linked.cmp(&left_linked)
491        })
492        .then_with(|| {
493            Version::parse(&right.release.version)
494                .expect("validated release version")
495                .cmp(&Version::parse(&left.release.version).expect("validated release version"))
496        })
497        .then_with(|| left.release_digest.cmp(&right.release_digest))
498}
499
500fn candidate_matches(
501    candidate: &ModuleResolutionCandidate,
502    constraints: &[RequirementConstraint],
503) -> bool {
504    if !matches!(
505        candidate.eligibility.state,
506        ModuleEligibilityState::Eligible | ModuleEligibilityState::EligibleWithWarning
507    ) {
508        return false;
509    }
510    let Ok(version) = Version::parse(&candidate.release.version) else {
511        return false;
512    };
513    constraints.iter().all(|constraint| {
514        constraint.requirement.matches(&version)
515            && constraint
516                .exact_release_digest
517                .as_deref()
518                .is_none_or(|digest| digest == candidate.release_digest)
519            && constraint.delivery.is_none_or(|delivery| {
520                delivery == ManagedDeliveryKind::from(&candidate.release.delivery)
521            })
522            && constraint
523                .capabilities
524                .iter()
525                .all(|capability| candidate.release.manifest.capabilities.contains(capability))
526    })
527}
528
529fn parse_requirement(
530    module_id: &str,
531    requirement: &str,
532    path: Vec<String>,
533) -> Result<VersionReq, ModuleResolutionError> {
534    VersionReq::parse(requirement).map_err(|_| {
535        let mut error = conflict("invalid_version_requirement", Some(module_id));
536        error.conflict.constraints.push(requirement.to_owned());
537        error.conflict.dependency_paths.push(path);
538        error
539    })
540}
541
542fn unsatisfied_conflict(
543    module_id: &str,
544    constraints: &[RequirementConstraint],
545    candidates: &[ModuleResolutionCandidate],
546) -> ModuleResolutionError {
547    let mut error = conflict("unsatisfiable_module", Some(module_id));
548    error.conflict.dependency_paths = constraints
549        .iter()
550        .map(|constraint| constraint.path.clone())
551        .collect();
552    error.conflict.dependency_paths.sort();
553    error.conflict.dependency_paths.dedup();
554    error.conflict.constraints = constraints
555        .iter()
556        .flat_map(|constraint| {
557            let mut values = vec![constraint.requirement_text.clone()];
558            values.extend(
559                constraint
560                    .capabilities
561                    .iter()
562                    .map(|capability| format!("capability:{capability}")),
563            );
564            if let Some(digest) = &constraint.exact_release_digest {
565                values.push(format!("release:{digest}"));
566            }
567            if let Some(delivery) = constraint.delivery {
568                values.push(format!("delivery:{delivery:?}"));
569            }
570            values
571        })
572        .collect();
573    error.conflict.constraints.sort();
574    error.conflict.constraints.dedup();
575    error.conflict.eligible_alternatives = candidates
576        .iter()
577        .filter(|candidate| {
578            candidate.release.module_id == module_id
579                && matches!(
580                    candidate.eligibility.state,
581                    ModuleEligibilityState::Eligible | ModuleEligibilityState::EligibleWithWarning
582                )
583        })
584        .map(|candidate| {
585            format!(
586                "{}@{}#{:?}:{}",
587                candidate.release.module_id,
588                candidate.release.version,
589                ManagedDeliveryKind::from(&candidate.release.delivery),
590                candidate.release_digest
591            )
592        })
593        .collect();
594    error.conflict.eligible_alternatives.sort();
595    error
596}
597
598#[allow(clippy::too_many_lines)]
599fn build_lock(
600    request: &ModuleResolutionRequest,
601    desired: &DesiredModuleComposition,
602    desired_digest: &str,
603    candidates: &[ModuleResolutionCandidate],
604    solved: &SolverState,
605    direct_ids: &BTreeSet<String>,
606    selected_optional: &BTreeMap<String, BTreeSet<String>>,
607) -> ApplicationModuleLock {
608    let overrides = desired
609        .local_overrides
610        .iter()
611        .map(|item| (item.module_id.as_str(), item.content_digest.clone()))
612        .collect::<BTreeMap<_, _>>();
613    let mut modules = solved
614        .selections
615        .iter()
616        .map(|(module_id, index)| {
617            let candidate = &candidates[*index];
618            let release = &candidate.release;
619            let dependency_module_ids = release
620                .manifest
621                .requires
622                .iter()
623                .filter(|requirement| {
624                    !requirement.optional
625                        || selected_optional
626                            .get(module_id)
627                            .is_some_and(|selected| selected.contains(&requirement.module_id))
628                })
629                .map(|requirement| requirement.module_id.clone())
630                .collect::<Vec<_>>();
631            let (crate_features, migration_artifacts) = match &release.delivery {
632                ModuleDelivery::Linked(linked) => {
633                    (linked.features.clone(), linked.migrations.clone())
634                }
635                ModuleDelivery::Service(_) => (Vec::new(), Vec::new()),
636            };
637            LockedModule {
638                module_id: module_id.clone(),
639                version: release.version.clone(),
640                release_digest: candidate.release_digest.clone(),
641                manifest_digest: release.manifest_digest.clone(),
642                delivery: release.delivery.clone(),
643                reason: if direct_ids.contains(module_id) {
644                    LockedModuleReason::Direct
645                } else {
646                    LockedModuleReason::Transitive
647                },
648                dependency_module_ids,
649                crate_features,
650                migration_artifacts,
651                console_ui_artifact: release.console_ui_artifact.as_ref().map(|artifact| {
652                    LockedConsoleUiArtifact {
653                        locator: artifact.artifact.locator.clone(),
654                        digest: artifact.artifact.digest.clone(),
655                        format: artifact.format.clone(),
656                        protocol_major: artifact.protocol_major,
657                        entry: artifact.entry.clone(),
658                        entries: artifact.entries.clone(),
659                        style_assets: artifact.style_assets.clone(),
660                        manifest: artifact.manifest.clone(),
661                        requested_permissions: artifact.requested_permissions.clone(),
662                    }
663                }),
664                verification: candidate.eligibility.verification.clone(),
665                verification_cell: candidate.verification_cell.clone(),
666                lifecycle: candidate.eligibility.lifecycle.clone(),
667                local_override_digest: overrides.get(module_id.as_str()).cloned(),
668            }
669        })
670        .collect::<Vec<_>>();
671    modules.sort_by(|left, right| left.module_id.cmp(&right.module_id));
672
673    let mut capability_bindings = solved
674        .selections
675        .iter()
676        .flat_map(|(consumer, index)| {
677            candidates[*index]
678                .release
679                .manifest
680                .requires
681                .iter()
682                .filter(|requirement| {
683                    !requirement.optional
684                        || selected_optional
685                            .get(consumer)
686                            .is_some_and(|selected| selected.contains(&requirement.module_id))
687                })
688                .flat_map(move |requirement| {
689                    requirement
690                        .capabilities
691                        .iter()
692                        .map(move |capability| LockedCapabilityBinding {
693                            capability: capability.clone(),
694                            provider_module_id: requirement.module_id.clone(),
695                            consumer_module_id: consumer.clone(),
696                        })
697                })
698        })
699        .collect::<Vec<_>>();
700    capability_bindings.sort_by(|left, right| {
701        left.consumer_module_id
702            .cmp(&right.consumer_module_id)
703            .then_with(|| left.capability.cmp(&right.capability))
704            .then_with(|| left.provider_module_id.cmp(&right.provider_module_id))
705    });
706
707    ApplicationModuleLock {
708        protocol: APPLICATION_MODULE_LOCK_PROTOCOL.to_owned(),
709        application_id: desired.application_id.clone(),
710        desired_composition_digest: desired_digest.to_owned(),
711        catalog_snapshot_digest: request.catalog_snapshot_digest.clone(),
712        trust_policy_digest: request.trust_policy_digest.clone(),
713        resolver_version: request.resolver_version.clone(),
714        modules,
715        capability_bindings,
716    }
717}
718
719fn validate_optional_selections(
720    desired: &DesiredModuleComposition,
721    candidates: &[ModuleResolutionCandidate],
722    solved: &SolverState,
723) -> Result<(), ModuleResolutionError> {
724    for selection in &desired.selected {
725        let Some(candidate_index) = solved.selections.get(&selection.module_id) else {
726            continue;
727        };
728        let declared = candidates[*candidate_index]
729            .release
730            .manifest
731            .requires
732            .iter()
733            .filter(|requirement| requirement.optional)
734            .map(|requirement| requirement.module_id.as_str())
735            .collect::<BTreeSet<_>>();
736        if let Some(requirement) = selection
737            .optional_requirements
738            .iter()
739            .find(|requirement| !declared.contains(requirement.as_str()))
740        {
741            let mut error = conflict(
742                "optional_requirement_not_declared",
743                Some(&selection.module_id),
744            );
745            error.conflict.constraints.push(requirement.clone());
746            error
747                .conflict
748                .dependency_paths
749                .push(vec![selection.module_id.clone(), requirement.clone()]);
750            return Err(error);
751        }
752    }
753    Ok(())
754}
755
756fn conflict(code: &str, module_id: Option<&str>) -> ModuleResolutionError {
757    ModuleResolutionError {
758        conflict: Box::new(ModuleResolutionConflict {
759            protocol: MODULE_RESOLUTION_CONFLICT_PROTOCOL.to_owned(),
760            code: code.to_owned(),
761            module_id: module_id.map(str::to_owned),
762            dependency_paths: Vec::new(),
763            constraints: Vec::new(),
764            eligible_alternatives: Vec::new(),
765        }),
766    }
767}