Skip to main content

recoco_core/setup/
components.rs

1// ReCoco is a Rust-only fork of CocoIndex, by [CocoIndex](https://CocoIndex)
2// Original code from CocoIndex is copyrighted by CocoIndex
3// SPDX-FileCopyrightText: 2025-2026 CocoIndex (upstream)
4// SPDX-FileContributor: CocoIndex Contributors
5//
6// All modifications from the upstream for ReCoco are copyrighted by Knitli Inc.
7// SPDX-FileCopyrightText: 2026 Knitli Inc. (ReCoco)
8// SPDX-FileContributor: Adam Poulemanos <adam@knit.li>
9//
10// Both the upstream CocoIndex code and the ReCoco modifications are licensed under the Apache-2.0 License.
11// SPDX-License-Identifier: Apache-2.0
12
13use super::{CombinedState, ResourceSetupChange, SetupChangeType, StateChange};
14use crate::prelude::*;
15
16pub trait State<Key>: Debug + Send + Sync {
17    fn key(&self) -> Key;
18}
19
20#[async_trait]
21pub trait SetupOperator: 'static + Send + Sync {
22    type Key: Debug + Hash + Eq + Clone + Send + Sync;
23    type State: State<Self::Key>;
24    type SetupState: Send + Sync + IntoIterator<Item = Self::State>;
25    type Context: Sync;
26
27    fn describe_key(&self, key: &Self::Key) -> String;
28
29    fn describe_state(&self, state: &Self::State) -> String;
30
31    fn is_up_to_date(&self, current: &Self::State, desired: &Self::State) -> bool;
32
33    async fn create(&self, state: &Self::State, context: &Self::Context) -> Result<()>;
34
35    async fn delete(&self, key: &Self::Key, context: &Self::Context) -> Result<()>;
36
37    async fn update(&self, state: &Self::State, context: &Self::Context) -> Result<()> {
38        self.delete(&state.key(), context).await?;
39        self.create(state, context).await
40    }
41}
42
43#[derive(Debug)]
44struct CompositeStateUpsert<S> {
45    state: S,
46    already_exists: bool,
47}
48#[derive(Debug)]
49pub struct SetupChange<D: SetupOperator> {
50    desc: D,
51    keys_to_delete: IndexSet<D::Key>,
52    states_to_upsert: Vec<CompositeStateUpsert<D::State>>,
53}
54
55impl<D: SetupOperator> SetupChange<D> {
56    pub fn create(
57        desc: D,
58        desired: Option<D::SetupState>,
59        existing: CombinedState<D::SetupState>,
60    ) -> Result<Self> {
61        let existing_component_states = CombinedState {
62            current: existing.current.map(|s| {
63                s.into_iter()
64                    .map(|s| (s.key(), s))
65                    .collect::<IndexMap<_, _>>()
66            }),
67            staging: existing
68                .staging
69                .into_iter()
70                .map(|s| match s {
71                    StateChange::Delete => StateChange::Delete,
72                    StateChange::Upsert(s) => {
73                        StateChange::Upsert(s.into_iter().map(|s| (s.key(), s)).collect())
74                    }
75                })
76                .collect(),
77            legacy_state_key: existing.legacy_state_key,
78        };
79        let mut keys_to_delete = IndexSet::new();
80        let mut states_to_upsert = vec![];
81
82        // Collect all existing component keys
83        for c in existing_component_states.possible_versions() {
84            keys_to_delete.extend(c.keys().cloned());
85        }
86
87        if let Some(desired_state) = desired {
88            for desired_comp_state in desired_state {
89                let key = desired_comp_state.key();
90
91                // Remove keys that should be kept from deletion list
92                keys_to_delete.shift_remove(&key);
93
94                // Add components that need to be updated
95                let is_up_to_date = existing_component_states.always_exists()
96                    && existing_component_states.possible_versions().all(|v| {
97                        v.get(&key)
98                            .is_some_and(|s| desc.is_up_to_date(s, &desired_comp_state))
99                    });
100                if !is_up_to_date {
101                    let already_exists = existing_component_states
102                        .possible_versions()
103                        .any(|v| v.contains_key(&key));
104                    states_to_upsert.push(CompositeStateUpsert {
105                        state: desired_comp_state,
106                        already_exists,
107                    });
108                }
109            }
110        }
111
112        Ok(Self {
113            desc,
114            keys_to_delete,
115            states_to_upsert,
116        })
117    }
118}
119
120impl<D: SetupOperator + Send + Sync> ResourceSetupChange for SetupChange<D> {
121    fn describe_changes(&self) -> Vec<setup::ChangeDescription> {
122        let mut result = vec![];
123
124        for key in &self.keys_to_delete {
125            result.push(setup::ChangeDescription::Action(format!(
126                "Delete {}",
127                self.desc.describe_key(key)
128            )));
129        }
130
131        for state in &self.states_to_upsert {
132            result.push(setup::ChangeDescription::Action(format!(
133                "{} {}",
134                if state.already_exists {
135                    "Update"
136                } else {
137                    "Create"
138                },
139                self.desc.describe_state(&state.state)
140            )));
141        }
142
143        result
144    }
145
146    fn change_type(&self) -> SetupChangeType {
147        if self.keys_to_delete.is_empty() && self.states_to_upsert.is_empty() {
148            SetupChangeType::NoChange
149        } else if self.keys_to_delete.is_empty() {
150            SetupChangeType::Create
151        } else if self.states_to_upsert.is_empty() {
152            SetupChangeType::Delete
153        } else {
154            SetupChangeType::Update
155        }
156    }
157}
158
159pub async fn apply_component_changes<D: SetupOperator>(
160    changes: Vec<&SetupChange<D>>,
161    context: &D::Context,
162) -> Result<()> {
163    // First delete components that need to be removed
164    for change in changes.iter() {
165        for key in &change.keys_to_delete {
166            change.desc.delete(key, context).await?;
167        }
168    }
169
170    // Then upsert components that need to be updated
171    for change in changes.iter() {
172        for state in &change.states_to_upsert {
173            if state.already_exists {
174                change.desc.update(&state.state, context).await?;
175            } else {
176                change.desc.create(&state.state, context).await?;
177            }
178        }
179    }
180
181    Ok(())
182}
183
184impl<A: ResourceSetupChange, B: ResourceSetupChange> ResourceSetupChange for (A, B) {
185    fn describe_changes(&self) -> Vec<setup::ChangeDescription> {
186        let mut result = vec![];
187        result.extend(self.0.describe_changes());
188        result.extend(self.1.describe_changes());
189        result
190    }
191
192    fn change_type(&self) -> SetupChangeType {
193        match (self.0.change_type(), self.1.change_type()) {
194            (SetupChangeType::Invalid, _) | (_, SetupChangeType::Invalid) => {
195                SetupChangeType::Invalid
196            }
197            (SetupChangeType::NoChange, b) => b,
198            (a, _) => a,
199        }
200    }
201}