Skip to main content

lenso_app_plan/authoring/
plugin_root.rs

1use std::{collections::BTreeMap, error::Error, fmt};
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use super::configuration::resolve_configuration_layers;
7use crate::{
8    AppComposition, CapabilityBinding, CapabilityCardinality, CapabilityEndpointPlan,
9    CapabilityRequirementPlan, ExecutionClassId, ExecutionLaneId, ExecutionLanePlan,
10    PluginCriticality, PluginInstancePlan, RequestAdmissionPlan, ResolvedAppPlan, RestartPolicy,
11};
12
13mod release;
14mod resolution;
15mod selection;
16pub use selection::{DependencyChoice, DependencySelection};
17
18pub use release::{PluginContract, PluginImplementation};
19use resolution::{derive_root_bindings, map_configuration_error};
20pub use resolution::{propose_plugin_root, resolve_plugin_root};
21
22fn empty_configuration() -> Value {
23    Value::Object(serde_json::Map::new())
24}
25
26fn is_empty_configuration(configuration: &Value) -> bool {
27    configuration == &empty_configuration()
28}
29
30fn default_entrypoint() -> String {
31    "default".to_owned()
32}
33
34/// Stable App-local identity of one Plugin Instance.
35#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
36#[serde(deny_unknown_fields)]
37pub struct PluginInstanceId {
38    plugin_id: String,
39    instance_key: String,
40}
41
42impl PluginInstanceId {
43    pub fn new(plugin_id: impl Into<String>, instance_key: impl Into<String>) -> Self {
44        Self {
45            plugin_id: plugin_id.into(),
46            instance_key: instance_key.into(),
47        }
48    }
49
50    pub fn plugin_id(&self) -> &str {
51        &self.plugin_id
52    }
53
54    pub fn instance_key(&self) -> &str {
55        &self.instance_key
56    }
57
58    /// Returns the unambiguous private key lowered into the current Plan schema.
59    pub fn plan_key(&self) -> String {
60        format!("{}/{}", self.plugin_id, self.instance_key)
61    }
62}
63
64impl fmt::Display for PluginInstanceId {
65    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
66        write!(formatter, "{}/{}", self.plugin_id, self.instance_key)
67    }
68}
69
70/// Generated facts for one executable Plugin Release.
71#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
72#[serde(deny_unknown_fields)]
73pub struct PluginDescriptor {
74    #[serde(default = "crate::schema::old_authoring_version")]
75    authoring_version: u32,
76    #[serde(default)]
77    runtime_profile: String,
78    plugin_id: String,
79    release_version: String,
80    root_slot: String,
81    runtime_package_id: String,
82    runtime_package_revision: String,
83    #[serde(default = "default_entrypoint")]
84    entrypoint: String,
85    #[serde(default, skip_serializing_if = "Option::is_none")]
86    configuration_schema: Option<Value>,
87    #[serde(
88        default = "empty_configuration",
89        skip_serializing_if = "is_empty_configuration"
90    )]
91    configuration_defaults: Value,
92    provided_capabilities: Vec<CapabilityEndpointPlan>,
93    required_capabilities: Vec<CapabilityRequirementPlan>,
94    execution_class: ExecutionClassId,
95    restart_policy: RestartPolicy,
96    criticality: PluginCriticality,
97}
98
99impl PluginDescriptor {
100    pub fn new(
101        plugin_id: impl Into<String>,
102        release_version: impl Into<String>,
103        root_slot: impl Into<String>,
104    ) -> Self {
105        let plugin_id = plugin_id.into();
106        let release_version = release_version.into();
107        Self {
108            authoring_version: 1,
109            runtime_profile: "lenso.native-authoring@1".to_owned(),
110            runtime_package_id: plugin_id.clone(),
111            runtime_package_revision: release_version.clone(),
112            plugin_id,
113            release_version,
114            root_slot: root_slot.into(),
115            entrypoint: default_entrypoint(),
116            configuration_schema: None,
117            configuration_defaults: empty_configuration(),
118            provided_capabilities: Vec::new(),
119            required_capabilities: Vec::new(),
120            execution_class: ExecutionClassId::native_rust(),
121            restart_policy: RestartPolicy::default(),
122            criticality: PluginCriticality::default(),
123        }
124    }
125
126    #[must_use]
127    pub fn with_runtime_package(
128        mut self,
129        package_id: impl Into<String>,
130        package_revision: impl Into<String>,
131    ) -> Self {
132        self.runtime_package_id = package_id.into();
133        self.runtime_package_revision = package_revision.into();
134        self
135    }
136
137    #[must_use]
138    pub fn with_authoring(mut self, version: u32, runtime_profile: impl Into<String>) -> Self {
139        self.authoring_version = version;
140        self.runtime_profile = runtime_profile.into();
141        self
142    }
143
144    pub const fn authoring_version(&self) -> u32 {
145        self.authoring_version
146    }
147
148    pub fn runtime_profile(&self) -> &str {
149        if self.runtime_profile.is_empty() {
150            self.execution_class.as_str()
151        } else {
152            &self.runtime_profile
153        }
154    }
155
156    #[must_use]
157    pub fn with_entrypoint(mut self, entrypoint: impl Into<String>) -> Self {
158        self.entrypoint = entrypoint.into();
159        self
160    }
161
162    #[must_use]
163    pub fn with_configuration_schema(mut self, schema: Value) -> Self {
164        self.configuration_schema = Some(schema);
165        self
166    }
167
168    #[must_use]
169    pub fn with_configuration_defaults(mut self, defaults: Value) -> Self {
170        self.configuration_defaults = defaults;
171        self
172    }
173
174    #[must_use]
175    pub fn with_capability(mut self, capability: CapabilityEndpointPlan) -> Self {
176        self.provided_capabilities.push(capability);
177        self
178    }
179
180    #[must_use]
181    pub fn with_requirement(mut self, requirement: CapabilityRequirementPlan) -> Self {
182        self.required_capabilities.push(requirement);
183        self
184    }
185
186    #[must_use]
187    pub fn with_execution_class(mut self, execution_class: ExecutionClassId) -> Self {
188        if self.authoring_version == 1
189            && self.runtime_profile == crate::schema::old_runtime_profile(&self.execution_class)
190        {
191            self.runtime_profile = crate::schema::old_runtime_profile(&execution_class);
192        }
193        self.execution_class = execution_class;
194        self
195    }
196
197    #[must_use]
198    pub fn with_restart_policy(mut self, restart_policy: RestartPolicy) -> Self {
199        self.restart_policy = restart_policy;
200        self
201    }
202
203    #[must_use]
204    pub fn with_criticality(mut self, criticality: PluginCriticality) -> Self {
205        self.criticality = criticality;
206        self
207    }
208
209    pub fn plugin_id(&self) -> &str {
210        &self.plugin_id
211    }
212
213    pub fn release_version(&self) -> &str {
214        &self.release_version
215    }
216
217    pub fn root_slot(&self) -> &str {
218        &self.root_slot
219    }
220
221    pub fn runtime_package_id(&self) -> &str {
222        &self.runtime_package_id
223    }
224
225    pub fn runtime_package_revision(&self) -> &str {
226        &self.runtime_package_revision
227    }
228
229    pub fn entrypoint(&self) -> &str {
230        &self.entrypoint
231    }
232
233    pub const fn configuration_schema(&self) -> Option<&Value> {
234        self.configuration_schema.as_ref()
235    }
236
237    pub const fn configuration_defaults(&self) -> &Value {
238        &self.configuration_defaults
239    }
240
241    pub fn provided_capabilities(&self) -> &[CapabilityEndpointPlan] {
242        &self.provided_capabilities
243    }
244
245    pub fn required_capabilities(&self) -> &[CapabilityRequirementPlan] {
246        &self.required_capabilities
247    }
248
249    pub fn execution_class(&self) -> &ExecutionClassId {
250        &self.execution_class
251    }
252
253    pub const fn restart_policy(&self) -> RestartPolicy {
254        self.restart_policy
255    }
256
257    pub const fn criticality(&self) -> PluginCriticality {
258        self.criticality
259    }
260}
261
262/// Provider cardinality owned by one Host root Slot.
263#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
264#[serde(rename_all = "snake_case")]
265pub enum HostSlotCardinality {
266    One,
267    Optional,
268    Many,
269}
270
271/// One Host-owned root attachment point.
272#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
273#[serde(deny_unknown_fields)]
274pub struct HostSlot {
275    id: String,
276    cardinality: HostSlotCardinality,
277    #[serde(default)]
278    replaceable: bool,
279    #[serde(default = "default_execution_lane")]
280    execution_lane: String,
281}
282
283fn default_execution_lane() -> String {
284    "main".to_owned()
285}
286
287impl HostSlot {
288    pub fn one(id: impl Into<String>) -> Self {
289        Self::new(id, HostSlotCardinality::One)
290    }
291
292    pub fn optional(id: impl Into<String>) -> Self {
293        Self::new(id, HostSlotCardinality::Optional)
294    }
295
296    pub fn many(id: impl Into<String>) -> Self {
297        Self::new(id, HostSlotCardinality::Many)
298    }
299
300    fn new(id: impl Into<String>, cardinality: HostSlotCardinality) -> Self {
301        Self {
302            id: id.into(),
303            cardinality,
304            replaceable: false,
305            execution_lane: default_execution_lane(),
306        }
307    }
308
309    #[must_use]
310    pub const fn replaceable(mut self) -> Self {
311        self.replaceable = true;
312        self
313    }
314
315    #[must_use]
316    pub fn with_execution_lane(mut self, execution_lane: impl Into<String>) -> Self {
317        self.execution_lane = execution_lane.into();
318        self
319    }
320
321    pub fn id(&self) -> &str {
322        &self.id
323    }
324
325    pub const fn cardinality(&self) -> HostSlotCardinality {
326        self.cardinality
327    }
328
329    pub const fn is_replaceable(&self) -> bool {
330        self.replaceable
331    }
332
333    pub fn execution_lane(&self) -> &str {
334        &self.execution_lane
335    }
336}
337
338/// One exact Plugin Release available to a Host.
339#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
340#[serde(deny_unknown_fields)]
341pub struct HostPluginRelease {
342    descriptor: PluginDescriptor,
343    #[serde(default)]
344    allow_root_override: bool,
345}
346
347impl HostPluginRelease {
348    pub const fn new(descriptor: PluginDescriptor) -> Self {
349        Self {
350            descriptor,
351            allow_root_override: false,
352        }
353    }
354
355    #[must_use]
356    pub const fn allow_root_override(mut self) -> Self {
357        self.allow_root_override = true;
358        self
359    }
360
361    pub const fn descriptor(&self) -> &PluginDescriptor {
362        &self.descriptor
363    }
364
365    pub const fn root_override_allowed(&self) -> bool {
366        self.allow_root_override
367    }
368}
369
370/// One Plugin Instance supplied by Host defaults.
371#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
372#[serde(deny_unknown_fields)]
373pub struct HostDefaultPlugin {
374    id: PluginInstanceId,
375    #[serde(default = "empty_configuration")]
376    configuration: Value,
377    #[serde(default)]
378    disableable: bool,
379}
380
381/// Host-owned configuration for an Instance that becomes active only when the
382/// App owner adds the matching Plugin Root entry.
383#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
384#[serde(deny_unknown_fields)]
385pub struct HostPluginConfiguration {
386    id: PluginInstanceId,
387    #[serde(default = "empty_configuration")]
388    configuration: Value,
389}
390
391impl HostPluginConfiguration {
392    pub fn new(
393        plugin_id: impl Into<String>,
394        instance_key: impl Into<String>,
395        configuration: Value,
396    ) -> Self {
397        Self {
398            id: PluginInstanceId::new(plugin_id, instance_key),
399            configuration,
400        }
401    }
402
403    pub const fn id(&self) -> &PluginInstanceId {
404        &self.id
405    }
406
407    pub const fn configuration(&self) -> &Value {
408        &self.configuration
409    }
410}
411
412/// One Host-private attachment from a default Plugin Instance to a provider Slot.
413///
414/// This resolves repeated Capability providers without exposing binding decisions
415/// in the user-authored Plugin Root.
416#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
417#[serde(deny_unknown_fields)]
418pub struct HostBinding {
419    #[serde(default)]
420    selection: DependencySelection,
421    #[serde(default)]
422    default_provider: Option<PluginInstanceId>,
423    #[serde(default)]
424    requirement_id: Option<String>,
425    consumer: PluginInstanceId,
426    capability_id: String,
427    #[serde(default, skip_serializing_if = "Option::is_none")]
428    provider_slot: Option<String>,
429    #[serde(default, skip_serializing_if = "Option::is_none")]
430    provider_instance: Option<PluginInstanceId>,
431    #[serde(default, skip_serializing_if = "Vec::is_empty")]
432    provider_instances: Vec<PluginInstanceId>,
433    #[serde(default, skip_serializing_if = "Option::is_none")]
434    admission: Option<RequestAdmissionPlan>,
435}
436
437impl HostBinding {
438    pub fn new(
439        consumer: PluginInstanceId,
440        capability_id: impl Into<String>,
441        provider_slot: impl Into<String>,
442    ) -> Self {
443        Self {
444            consumer,
445            selection: DependencySelection::Fixed,
446            default_provider: None,
447            requirement_id: None,
448            capability_id: capability_id.into(),
449            provider_slot: Some(provider_slot.into()),
450            provider_instance: None,
451            provider_instances: Vec::new(),
452            admission: None,
453        }
454    }
455
456    pub fn to_instance(
457        consumer: PluginInstanceId,
458        capability_id: impl Into<String>,
459        provider: PluginInstanceId,
460    ) -> Self {
461        Self {
462            consumer,
463            selection: DependencySelection::Fixed,
464            default_provider: None,
465            requirement_id: None,
466            capability_id: capability_id.into(),
467            provider_slot: None,
468            provider_instance: Some(provider),
469            provider_instances: Vec::new(),
470            admission: None,
471        }
472    }
473
474    /// Selects an exact provider set for one `many` Capability requirement.
475    pub fn to_instances(
476        consumer: PluginInstanceId,
477        capability_id: impl Into<String>,
478        providers: impl IntoIterator<Item = PluginInstanceId>,
479    ) -> Self {
480        Self {
481            consumer,
482            selection: DependencySelection::Fixed,
483            default_provider: None,
484            requirement_id: None,
485            capability_id: capability_id.into(),
486            provider_slot: None,
487            provider_instance: None,
488            provider_instances: providers.into_iter().collect(),
489            admission: None,
490        }
491    }
492
493    #[must_use]
494    pub const fn with_admission(mut self, admission: RequestAdmissionPlan) -> Self {
495        self.admission = Some(admission);
496        self
497    }
498
499    #[must_use]
500    pub fn with_requirement_id(mut self, id: impl Into<String>) -> Self {
501        self.requirement_id = Some(id.into());
502        self
503    }
504
505    /// Grants Root selection within this rule's explicit Slot or Instance set.
506    #[must_use]
507    pub fn selectable(mut self, default_provider: Option<PluginInstanceId>) -> Self {
508        self.selection = DependencySelection::Selectable;
509        self.default_provider = default_provider;
510        self
511    }
512
513    pub fn requirement_id(&self) -> std::borrow::Cow<'_, str> {
514        self.requirement_id.as_deref().map_or_else(
515            || std::borrow::Cow::Owned(format!("~{}", self.capability_id)),
516            std::borrow::Cow::Borrowed,
517        )
518    }
519
520    pub const fn consumer(&self) -> &PluginInstanceId {
521        &self.consumer
522    }
523
524    pub fn capability_id(&self) -> &str {
525        &self.capability_id
526    }
527
528    pub fn provider_slot(&self) -> Option<&str> {
529        self.provider_slot.as_deref()
530    }
531
532    pub const fn provider_instance(&self) -> Option<&PluginInstanceId> {
533        self.provider_instance.as_ref()
534    }
535
536    pub fn provider_instances(&self) -> &[PluginInstanceId] {
537        &self.provider_instances
538    }
539
540    pub const fn admission(&self) -> Option<RequestAdmissionPlan> {
541        self.admission
542    }
543}
544
545impl HostDefaultPlugin {
546    pub fn new(plugin_id: impl Into<String>, instance_key: impl Into<String>) -> Self {
547        Self {
548            id: PluginInstanceId::new(plugin_id, instance_key),
549            configuration: empty_configuration(),
550            disableable: false,
551        }
552    }
553
554    #[must_use]
555    pub fn with_configuration(mut self, configuration: Value) -> Self {
556        self.configuration = configuration;
557        self
558    }
559
560    #[must_use]
561    pub const fn disableable(mut self) -> Self {
562        self.disableable = true;
563        self
564    }
565
566    pub const fn id(&self) -> &PluginInstanceId {
567        &self.id
568    }
569
570    pub const fn configuration(&self) -> &Value {
571        &self.configuration
572    }
573
574    pub const fn is_disableable(&self) -> bool {
575        self.disableable
576    }
577}
578
579/// Immutable Host input used to resolve one App.
580#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
581#[serde(deny_unknown_fields)]
582pub struct HostCatalog {
583    #[serde(default)]
584    slots: Vec<HostSlot>,
585    #[serde(default)]
586    plugins: Vec<HostPluginRelease>,
587    #[serde(default)]
588    defaults: Vec<HostDefaultPlugin>,
589    #[serde(default)]
590    configurations: Vec<HostPluginConfiguration>,
591    #[serde(default)]
592    bindings: Vec<HostBinding>,
593    #[serde(default = "default_execution_lanes")]
594    execution_lanes: Vec<ExecutionLanePlan>,
595}
596
597fn default_execution_lanes() -> Vec<ExecutionLanePlan> {
598    vec![ExecutionLanePlan::new("main")]
599}
600
601impl HostCatalog {
602    pub fn new(
603        slots: impl IntoIterator<Item = HostSlot>,
604        plugins: impl IntoIterator<Item = HostPluginRelease>,
605        defaults: impl IntoIterator<Item = HostDefaultPlugin>,
606    ) -> Self {
607        Self {
608            slots: slots.into_iter().collect(),
609            plugins: plugins.into_iter().collect(),
610            defaults: defaults.into_iter().collect(),
611            configurations: Vec::new(),
612            bindings: Vec::new(),
613            execution_lanes: default_execution_lanes(),
614        }
615    }
616
617    #[must_use]
618    pub fn with_execution_lanes(mut self, lanes: Vec<ExecutionLanePlan>) -> Self {
619        self.execution_lanes = lanes;
620        self
621    }
622
623    #[must_use]
624    pub fn with_bindings(mut self, bindings: impl IntoIterator<Item = HostBinding>) -> Self {
625        self.bindings = bindings.into_iter().collect();
626        self
627    }
628
629    #[must_use]
630    pub fn with_configurations(
631        mut self,
632        configurations: impl IntoIterator<Item = HostPluginConfiguration>,
633    ) -> Self {
634        self.configurations = configurations.into_iter().collect();
635        self
636    }
637
638    pub fn slots(&self) -> &[HostSlot] {
639        &self.slots
640    }
641
642    pub fn plugins(&self) -> &[HostPluginRelease] {
643        &self.plugins
644    }
645
646    pub fn defaults(&self) -> &[HostDefaultPlugin] {
647        &self.defaults
648    }
649
650    pub fn configurations(&self) -> &[HostPluginConfiguration] {
651        &self.configurations
652    }
653
654    pub fn bindings(&self) -> &[HostBinding] {
655        &self.bindings
656    }
657
658    pub fn execution_lanes(&self) -> &[ExecutionLanePlan] {
659        &self.execution_lanes
660    }
661}
662
663/// Direct configuration of one Plugin Root Instance.
664#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
665#[serde(deny_unknown_fields)]
666pub struct PluginRootInstance {
667    id: PluginInstanceId,
668    #[serde(default = "empty_configuration")]
669    configuration: Value,
670}
671
672impl PluginRootInstance {
673    pub fn new(plugin_id: impl Into<String>, instance_key: impl Into<String>) -> Self {
674        Self {
675            id: PluginInstanceId::new(plugin_id, instance_key),
676            configuration: empty_configuration(),
677        }
678    }
679
680    #[must_use]
681    pub fn with_configuration(mut self, configuration: Value) -> Self {
682        self.configuration = configuration;
683        self
684    }
685
686    pub const fn id(&self) -> &PluginInstanceId {
687        &self.id
688    }
689
690    pub const fn configuration(&self) -> &Value {
691        &self.configuration
692    }
693}
694
695/// Filesystem-independent snapshot of one `plugins/` directory.
696#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
697#[serde(deny_unknown_fields)]
698pub struct PluginRootSnapshot {
699    #[serde(default)]
700    dependency_selection_adopted: bool,
701    #[serde(default)]
702    dependency_choices: Vec<DependencyChoice>,
703    #[serde(default)]
704    releases: Vec<PluginDescriptor>,
705    #[serde(default)]
706    instances: Vec<PluginRootInstance>,
707    #[serde(default)]
708    disabled: Vec<PluginInstanceId>,
709}
710
711impl PluginRootSnapshot {
712    pub fn new(
713        releases: impl IntoIterator<Item = PluginDescriptor>,
714        instances: impl IntoIterator<Item = PluginRootInstance>,
715        disabled: impl IntoIterator<Item = PluginInstanceId>,
716    ) -> Self {
717        Self {
718            dependency_selection_adopted: false,
719            dependency_choices: Vec::new(),
720            releases: releases.into_iter().collect(),
721            instances: instances.into_iter().collect(),
722            disabled: disabled.into_iter().collect(),
723        }
724    }
725
726    pub fn releases(&self) -> &[PluginDescriptor] {
727        &self.releases
728    }
729
730    /// Adopts named dependency selection with an exact persisted choice set.
731    #[must_use]
732    pub fn with_dependency_choices(mut self, choices: Vec<DependencyChoice>) -> Self {
733        self.dependency_selection_adopted = true;
734        self.dependency_choices = choices;
735        self
736    }
737
738    pub fn dependency_choices(&self) -> &[DependencyChoice] {
739        &self.dependency_choices
740    }
741
742    /// Reports whether this Root has adopted persisted named dependency choices.
743    pub const fn dependency_selection_adopted(&self) -> bool {
744        self.dependency_selection_adopted
745    }
746
747    pub fn instances(&self) -> &[PluginRootInstance] {
748        &self.instances
749    }
750
751    pub fn disabled(&self) -> &[PluginInstanceId] {
752        &self.disabled
753    }
754}
755
756/// Provenance of one enabled Plugin Instance in a resolved App.
757#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
758#[serde(rename_all = "snake_case")]
759pub enum PluginInstanceSource {
760    HostDefault,
761    HostDefaultConfiguredByRoot,
762    PluginRoot,
763}
764
765/// One enabled Plugin Instance and its private Plan key.
766#[derive(Clone, Debug, Eq, PartialEq)]
767pub struct ResolvedPluginInstance {
768    id: PluginInstanceId,
769    plan_key: String,
770    source: PluginInstanceSource,
771}
772
773impl ResolvedPluginInstance {
774    pub const fn id(&self) -> &PluginInstanceId {
775        &self.id
776    }
777
778    pub fn plan_key(&self) -> &str {
779        &self.plan_key
780    }
781
782    pub const fn source(&self) -> PluginInstanceSource {
783        self.source
784    }
785}
786
787/// Complete ready App derived from a Host Catalog and Plugin Root snapshot.
788#[derive(Clone, Debug, Eq, PartialEq)]
789pub struct ResolvedApp {
790    dependency_choices: Vec<DependencyChoice>,
791    plan: ResolvedAppPlan,
792    instances: Vec<ResolvedPluginInstance>,
793}
794
795impl ResolvedApp {
796    /// Exact selectable choices proposed by pure resolution; storage belongs to the CLI.
797    pub fn dependency_choices(&self) -> &[DependencyChoice] {
798        &self.dependency_choices
799    }
800    pub const fn plan(&self) -> &ResolvedAppPlan {
801        &self.plan
802    }
803
804    pub fn instances(&self) -> &[ResolvedPluginInstance] {
805        &self.instances
806    }
807}
808
809#[derive(Clone, Debug)]
810struct CandidateInstance<'a> {
811    id: PluginInstanceId,
812    descriptor: &'a PluginDescriptor,
813    host_configuration: Option<&'a Value>,
814    root_configuration: Option<&'a Value>,
815    source: PluginInstanceSource,
816}
817
818/// A Host Catalog and Plugin Root could not resolve one unambiguous App.
819#[derive(Clone, Debug, Eq, PartialEq)]
820pub enum PluginRootResolutionError {
821    DuplicateHostSlot(String),
822    DuplicatePluginRelease(String),
823    RootReleaseOverrideDenied(String),
824    DuplicateInstance(PluginInstanceId),
825    DuplicateDisabledMarker(PluginInstanceId),
826    UnknownPlugin(PluginInstanceId),
827    UnknownDisabledInstance(PluginInstanceId),
828    RequiredInstanceDisabled(PluginInstanceId),
829    UnknownRootSlot {
830        plugin_id: String,
831        slot: String,
832    },
833    MultipleHostDefaults {
834        slot: String,
835        instances: Vec<String>,
836    },
837    ExplicitProviderDenied {
838        slot: String,
839        instance: PluginInstanceId,
840    },
841    MissingRequiredSlot(String),
842    AmbiguousSlot {
843        slot: String,
844        instances: Vec<String>,
845    },
846    MissingCapability {
847        consumer: PluginInstanceId,
848        capability_id: String,
849        descriptor_version: String,
850    },
851    AmbiguousCapability {
852        consumer: PluginInstanceId,
853        capability_id: String,
854        candidates: Vec<PluginInstanceId>,
855    },
856    InvalidHostBinding(String),
857    InvalidHostConfiguration(String),
858    InvalidConfiguration {
859        instance: PluginInstanceId,
860        detail: String,
861    },
862    InvalidResolvedApp(String),
863}
864
865impl fmt::Display for PluginRootResolutionError {
866    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
867        match self {
868            Self::DuplicateHostSlot(slot) => write!(formatter, "duplicate Host Slot `{slot}`"),
869            Self::DuplicatePluginRelease(plugin) => {
870                write!(formatter, "duplicate Plugin Release `{plugin}`")
871            }
872            Self::RootReleaseOverrideDenied(plugin) => write!(
873                formatter,
874                "Plugin Root cannot replace Host Release `{plugin}`"
875            ),
876            Self::DuplicateInstance(instance) => {
877                write!(formatter, "duplicate Plugin Instance `{instance}`")
878            }
879            Self::DuplicateDisabledMarker(instance) => {
880                write!(formatter, "duplicate disabled marker for `{instance}`")
881            }
882            Self::UnknownPlugin(instance) => {
883                write!(
884                    formatter,
885                    "Plugin Instance `{instance}` has no exact Release"
886                )
887            }
888            Self::UnknownDisabledInstance(instance) => {
889                write!(
890                    formatter,
891                    "disabled marker refers to unknown Instance `{instance}`"
892                )
893            }
894            Self::RequiredInstanceDisabled(instance) => {
895                write!(
896                    formatter,
897                    "required Host Instance `{instance}` cannot be disabled"
898                )
899            }
900            Self::UnknownRootSlot { plugin_id, slot } => write!(
901                formatter,
902                "Plugin `{plugin_id}` offers unknown Host Slot `{slot}`"
903            ),
904            Self::MultipleHostDefaults { slot, instances } => write!(
905                formatter,
906                "Host Slot `{slot}` has multiple defaults: {}",
907                instances.join(", ")
908            ),
909            Self::ExplicitProviderDenied { slot, instance } => write!(
910                formatter,
911                "Host Slot `{slot}` does not allow `{instance}` to replace its default"
912            ),
913            Self::MissingRequiredSlot(slot) => {
914                write!(
915                    formatter,
916                    "required Host Slot `{slot}` has no enabled Plugin"
917                )
918            }
919            Self::AmbiguousSlot { slot, instances } => write!(
920                formatter,
921                "Host Slot `{slot}` has multiple explicit Plugins: {}",
922                instances.join(", ")
923            ),
924            Self::MissingCapability {
925                consumer,
926                capability_id,
927                descriptor_version,
928            } => write!(
929                formatter,
930                "Plugin Instance `{consumer}` has no provider for Capability `{capability_id}` version `{descriptor_version}`"
931            ),
932            Self::AmbiguousCapability {
933                consumer,
934                capability_id,
935                candidates,
936            } => write!(
937                formatter,
938                "Plugin Instance `{consumer}` has multiple providers for Capability `{capability_id}`: {}",
939                candidates
940                    .iter()
941                    .map(ToString::to_string)
942                    .collect::<Vec<_>>()
943                    .join(", ")
944            ),
945            Self::InvalidHostBinding(detail) => write!(formatter, "invalid Host binding: {detail}"),
946            Self::InvalidHostConfiguration(detail) => {
947                write!(formatter, "invalid Host Plugin configuration: {detail}")
948            }
949            Self::InvalidConfiguration { instance, detail } => {
950                write!(
951                    formatter,
952                    "Plugin Instance `{instance}` has invalid configuration: {detail}"
953                )
954            }
955            Self::InvalidResolvedApp(detail) => {
956                write!(formatter, "derived App is invalid: {detail}")
957            }
958        }
959    }
960}
961
962impl Error for PluginRootResolutionError {}
963
964/// Resolves one Host and one Plugin Root into a complete immutable App Plan.
965fn select_slot_candidates<'a>(
966    slots: &BTreeMap<&str, &'a HostSlot>,
967    candidates: Vec<CandidateInstance<'a>>,
968) -> Result<Vec<(CandidateInstance<'a>, &'a HostSlot)>, PluginRootResolutionError> {
969    let mut by_slot = BTreeMap::<&str, Vec<CandidateInstance<'a>>>::new();
970    for candidate in candidates {
971        by_slot
972            .entry(candidate.descriptor.root_slot())
973            .or_default()
974            .push(candidate);
975    }
976    let mut selected = Vec::new();
977    for (slot_id, slot) in slots {
978        let mut candidates = by_slot.remove(slot_id).unwrap_or_default();
979        candidates.sort_by(|left, right| left.id.cmp(&right.id));
980        if slot.cardinality == HostSlotCardinality::Many {
981            selected.extend(candidates.into_iter().map(|candidate| (candidate, *slot)));
982            continue;
983        }
984        let (defaults, explicit): (Vec<_>, Vec<_>) = candidates
985            .into_iter()
986            .partition(|candidate| candidate.source != PluginInstanceSource::PluginRoot);
987        if defaults.len() > 1 {
988            return Err(PluginRootResolutionError::MultipleHostDefaults {
989                slot: (*slot_id).to_owned(),
990                instances: defaults
991                    .iter()
992                    .map(|candidate| candidate.id.to_string())
993                    .collect(),
994            });
995        }
996        if explicit.len() > 1 {
997            return Err(PluginRootResolutionError::AmbiguousSlot {
998                slot: (*slot_id).to_owned(),
999                instances: explicit
1000                    .iter()
1001                    .map(|candidate| candidate.id.to_string())
1002                    .collect(),
1003            });
1004        }
1005        if let Some(candidate) = explicit.into_iter().next() {
1006            if !defaults.is_empty() && !slot.replaceable {
1007                return Err(PluginRootResolutionError::ExplicitProviderDenied {
1008                    slot: (*slot_id).to_owned(),
1009                    instance: candidate.id,
1010                });
1011            }
1012            selected.push((candidate, *slot));
1013        } else if let Some(candidate) = defaults.into_iter().next() {
1014            selected.push((candidate, *slot));
1015        } else if slot.cardinality == HostSlotCardinality::One {
1016            return Err(PluginRootResolutionError::MissingRequiredSlot(
1017                (*slot_id).to_owned(),
1018            ));
1019        }
1020    }
1021    Ok(selected)
1022}
1023
1024fn materialize_app(
1025    selected: Vec<(CandidateInstance<'_>, &HostSlot)>,
1026    host_bindings: &[HostBinding],
1027    lanes: &[ExecutionLanePlan],
1028    root: &PluginRootSnapshot,
1029    propose: bool,
1030) -> Result<ResolvedApp, PluginRootResolutionError> {
1031    let mut plan_instances = Vec::with_capacity(selected.len());
1032    let mut resolved_instances = Vec::with_capacity(selected.len());
1033    let mut plan_slots = BTreeMap::new();
1034    for (candidate, slot) in selected {
1035        let plan_key = candidate.id.plan_key();
1036        plan_slots.insert(plan_key.clone(), slot.id().to_owned());
1037        let overlays = candidate
1038            .host_configuration
1039            .into_iter()
1040            .chain(candidate.root_configuration)
1041            .collect::<Vec<_>>();
1042        let configuration = resolve_configuration_layers(
1043            candidate.descriptor.configuration_defaults(),
1044            &overlays,
1045            candidate.descriptor.configuration_schema(),
1046            &plan_key,
1047        )
1048        .map_err(|error| map_configuration_error(&candidate.id, error))?;
1049        let configuration = serde_json::to_string(&configuration).map_err(|error| {
1050            PluginRootResolutionError::InvalidConfiguration {
1051                instance: candidate.id.clone(),
1052                detail: error.to_string(),
1053            }
1054        })?;
1055        let descriptor = candidate.descriptor;
1056        let mut instance = PluginInstancePlan::new(&plan_key, descriptor.runtime_package_id())
1057            .with_authoring(descriptor.authoring_version(), descriptor.runtime_profile())
1058            .with_entrypoint(descriptor.entrypoint())
1059            .with_package_revision(descriptor.runtime_package_revision())
1060            .with_configuration(configuration)
1061            .with_execution_class(descriptor.execution_class().clone())
1062            .with_restart_policy(descriptor.restart_policy())
1063            .with_criticality(descriptor.criticality())
1064            .with_execution_lane(ExecutionLaneId::new(&slot.execution_lane));
1065        for capability in descriptor.provided_capabilities() {
1066            instance = instance.with_capability(capability.clone());
1067        }
1068        for requirement in descriptor.required_capabilities() {
1069            instance = instance.with_requirement(requirement.clone());
1070        }
1071        plan_instances.push(instance);
1072        resolved_instances.push(ResolvedPluginInstance {
1073            id: candidate.id,
1074            plan_key,
1075            source: candidate.source,
1076        });
1077    }
1078    let (bindings, dependency_choices) = derive_root_bindings(
1079        &plan_instances,
1080        &resolved_instances,
1081        &plan_slots,
1082        host_bindings,
1083        root,
1084        propose,
1085    )?;
1086    let composition =
1087        AppComposition::new(plan_instances, bindings).with_execution_lanes(lanes.to_vec());
1088    let plan = composition
1089        .resolve()
1090        .map_err(|error| PluginRootResolutionError::InvalidResolvedApp(error.to_string()))?;
1091    resolved_instances.sort_by(|left, right| left.id.cmp(&right.id));
1092    Ok(ResolvedApp {
1093        dependency_choices,
1094        plan,
1095        instances: resolved_instances,
1096    })
1097}