Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
L
Lorenz Leutgeb
rust/msrv: 1.85.0 → 1.88.0
CI — 0 passed, 1 failed
z6Mki2ag...Tyuy failure
9 days ago 9ea040ccd05e2c7f8b964d99cc1f65233f43271c History
heartwood crates radicle src git canonical symbolic.rs
//! Symbolic references, which link neither to nor from protected references.
//! The prototypical example is `HEAD → refs/heads/main`.

use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use thiserror::Error;

use crate::git::fmt::Qualified;
use crate::git::fmt::RefStr;
use crate::git::fmt::RefString;

use super::protect::Unprotected;

/// A type alias for a [`RefString`] that has yet to be validated into a
/// a symbolic reference name.
pub type RawName = RefString;

/// A type alias for a [`RefString`] that has yet to be validated into a
/// symbolic reference target.
pub type RawTarget = RefString;

/// The target of a symbolic reference.
///
/// A target is either:
/// - [`Direct`](Target::Direct): a concrete qualified reference
///   (e.g. `refs/heads/main`).
/// - [`Symbolic`](Target::Symbolic): another symbolic reference name
///   (e.g. `MAIN`) that must itself resolve through the chain.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Target {
    /// A concrete qualified reference — the end of a chain.
    Direct(Direct),
    /// Another symbolic reference name — an intermediate link.
    Symbolic(Symbolic),
}

impl AsRef<RefStr> for Target {
    fn as_ref(&self) -> &RefStr {
        match self {
            Target::Direct(direct) => direct.0.as_ref(),
            Target::Symbolic(symbolic) => symbolic.0.as_ref(),
        }
    }
}

/// A concrete qualified reference — the end of a chain.
// `Unprotected` has `super` visibility, so `Direct` is used to allow `Target`
// to be `pub`.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Direct(Unprotected<Qualified<'static>>);

impl Direct {
    fn to_ref_string(&self) -> Unprotected<RefString> {
        self.0.to_ref_string()
    }
}

impl PartialEq<RefString> for Direct {
    fn eq(&self, other: &RefString) -> bool {
        **self.0.as_ref() == **other
    }
}

impl AsRef<Qualified<'static>> for Direct {
    fn as_ref(&self) -> &Qualified<'static> {
        self.0.as_ref()
    }
}

/// A concrete qualified reference — the end of a chain.
// `Unprotected` has `super` visibility, so `Symbolic` is used to allow `Target`
// to be `pub`.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Symbolic(Unprotected<RefString>);

impl AsRef<RefString> for Symbolic {
    fn as_ref(&self) -> &RefString {
        self.0.as_ref()
    }
}

impl Target {
    /// Returns the underlying reference as a `&RefStr`.
    pub fn as_refstr(&self) -> &RefStr {
        match self {
            Target::Direct(q) => q.as_ref().as_ref(),
            Target::Symbolic(s) => s.as_ref().as_ref(),
        }
    }

    fn direct(d: Unprotected<Qualified<'static>>) -> Self {
        Self::Direct(Direct(d))
    }

    fn symbolic(s: Unprotected<RefString>) -> Self {
        Self::Symbolic(Symbolic(s))
    }

    /// Classify an [`Unprotected<RefString>`] as either
    /// [`Direct`](Target::Direct) or [`Symbolic`](Target::Symbolic)
    /// based on whether it is [`Qualified`].
    ///
    /// The [`Unprotected`] proof is preserved in the resulting variant.
    fn classify(s: Unprotected<RefString>) -> Self {
        match Qualified::from_refstr(s.as_ref()) {
            // Safety: the Qualified is derived from an Unprotected string,
            // so it is also unprotected.
            Some(q) => Target::direct(
                Unprotected::new(q.to_owned())
                    .expect("qualified derived from unprotected is unprotected"),
            ),
            None => Target::symbolic(s),
        }
    }
}

impl Serialize for Target {
    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        serializer.serialize_str(self.as_refstr().as_str())
    }
}

impl std::fmt::Display for Target {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.as_refstr().as_str())
    }
}

/// Names of symbolic references are unprotected references.
/// Requiring [`Unprotected`] makes symbolic references that link *from*
/// protected references unrepresentable.
pub(super) type Name = Unprotected<RefString>;

impl std::cmp::Ord for Name {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        self.as_ref().cmp(other.as_ref())
    }
}

impl std::cmp::PartialOrd for Name {
    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
        Some(self.cmp(other))
    }
}

/// Maintains an acyclic set of symbolic references.
/// Note that dangling references are not detected.
///
/// Internally, targets are stored as [`Target`], which distinguishes
/// direct (qualified) targets from symbolic (intermediate) ones. This
/// means resolution and cycle-checking can pattern-match on the variant
/// rather than re-parsing the string.
///
/// # Deserialization Order
///
/// Deserialization validates entries in iteration order via
/// [`TryFrom<IndexMap>`]. This means deserialization only succeeds if
/// symbolic reference whose target is another symbolic
/// reference appears *after* that target in the JSON.
/// For example,
/// `{"MAIN": "refs/heads/master", "HEAD": "MAIN"}` is valid, but
/// `{"HEAD": "MAIN", "MAIN": "refs/heads/master"}` is not.
///
/// To ensure that serialization and deserialization are inverses,
/// we must ensure that insertion order and iteration order are the same.
/// We do this by using [`IndexMap`] internally, which preserves this property,
/// and enabling the feature `preserve_order` of the `serde_json` crate.
///
/// Normally, JSON objects are unordered (see [RFC 8259, Sec. 4]).
/// Any compatible (de-)serializer must preserve key order.
///
/// [RFC 8259, Sec. 4]: <https://datatracker.ietf.org/doc/html/rfc8259#section-4>
#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
#[serde(try_from = "IndexMap<Name, Unprotected<RefString>>")]
pub struct SymbolicRefs(IndexMap<Name, Target>);

/// Read-only access.
impl SymbolicRefs {
    /// Returns an iterator over all contained symbolic references, as pairs
    /// of their name and [`Target`].
    pub fn iter(&self) -> impl Iterator<Item = (&RawName, &Target)> {
        self.0.iter().map(|(name, target)| (name.as_ref(), target))
    }

    /// Returns an iterator over all contained symbolic references that
    /// resolve to a direct (qualified) target. The yielded target is the
    /// final [`Qualified`] reference after chasing through any intermediate
    /// symbolic references.
    pub fn iter_resolved(&self) -> impl Iterator<Item = (&RawName, &Qualified<'static>)> {
        self.0.keys().filter_map(|name| {
            self.resolve(name.as_ref())
                .map(|target| (name.as_ref(), target))
        })
    }

    /// Resolve a name through the chain of symbolic references until a
    /// [`Target::Direct`] target is reached. Returns `None` if the
    /// name is not in the map or if the chain dangles (ends at a
    /// [`Target::Symbolic`] whose name is not a key).
    fn resolve(&self, name: &RefString) -> Option<&Qualified<'static>> {
        let mut current = self.0.get(name)?;
        loop {
            match current {
                Target::Direct(q) => return Some(q.as_ref()),
                Target::Symbolic(s) => match self.0.get(s.as_ref()) {
                    Some(next) => current = next,
                    None => return None,
                },
            }
        }
    }

    /// Returns `true` if the set of symbolic references is empty.
    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }
}

/// Utilities for handling of `HEAD`.
impl SymbolicRefs {
    /// Construct [`SymbolicRefs`] for the single symbolic reference `HEAD`
    /// targeting `/refs/heads/<branch_name>`.
    // This exists to encapsulate [`super::protect::Unprotected`].
    pub fn head(branch_name: &RefString) -> Self {
        let mut result = Self::default();
        result
            .try_insert_unprotected(
                Unprotected::head().to_owned(),
                Unprotected::branch(branch_name).to_ref_string(),
            )
            .expect("not creating cycle");
        result
    }

    /// Convenience method to get the resolved target of the `HEAD` reference.
    /// Returns the final [`Qualified`] reference after chasing the chain.
    /// See also [`SymbolicRefs::head`].
    pub fn resolve_head(&self) -> Option<&Qualified<'static>> {
        self.resolve(Unprotected::head().as_ref())
    }
}

#[derive(Debug, Error)]
pub enum InsertionError {
    #[error("inserting symbolic reference '{name} → {target}' would create a cycle")]
    Cyclic { name: RawName, target: RawName },

    #[error(
        "inserting symbolic reference '{name} → {target}' would result in a symbolic reference targeting an unqualified reference"
    )]
    TargetNotQualified { name: RawName, target: RawName },
}

/// Mutability.
impl SymbolicRefs {
    /// Insert a symbolic reference from the given `name` to the given `target`.
    ///
    /// Internally, this classifies the `target` into the [`Target`] and
    /// delegates the insertion.
    pub(super) fn try_insert_unprotected(
        &mut self,
        name: Name,
        target: Unprotected<RefString>,
    ) -> Result<(), InsertionError> {
        self.insert(name, Target::classify(target))
    }

    /// Check whether `name` is reachable from `start` by chasing through
    /// the map. Used to detect cycles before insertion.
    ///
    /// Unlike [`resolve`](SymbolicRefs::resolve), this chases through
    /// *all* entries regardless of whether the target is [`Direct`](Target::Direct)
    /// or [`Symbolic`](Target::Symbolic), since a qualified ref string can
    /// also be a key in the map and thus part of a cycle.
    fn is_reachable_from(&self, start: &RefString, name: &Name) -> bool {
        let name = name.as_ref();
        if start == name {
            return true;
        }
        let mut current: &RefStr = start;
        loop {
            match self.0.get(current) {
                None => return false,
                Some(target) => {
                    let next = target.as_refstr();
                    if *next == **name {
                        return true;
                    }
                    current = next;
                }
            }
        }
    }

    /// Try to insert a symbolic reference.
    /// Errors if `name` or `target` is protected (see [`super::protect`]) or would
    /// cause infinite recursion (e.g. `A → B` and `B → A`).
    ///
    /// # Panics
    ///
    /// If `name` or `target` is not unprotected.
    #[cfg(test)]
    fn try_insert(&mut self, name: RawName, target: RawTarget) -> Result<(), InsertionError> {
        self.try_insert_unprotected(
            Unprotected::new(name).expect("name is unprotected"),
            Unprotected::new(target).expect("target is unprotected"),
        )
    }

    /// Consume `other` by iteratively inserting into self.
    pub fn combine(&mut self, other: SymbolicRefs) -> Result<(), InsertionError> {
        for (name, target) in other.0 {
            self.insert(name, target)?;
        }
        Ok(())
    }

    /// Insert a symbolic reference from the given `name` to the given `target`.
    ///
    /// The targets in the map can change their classification from
    /// [`Target::Direct`] to [`Target::Symbolic`], if the new insertion of the
    /// `name` or `target` matches an existing key or existing entries.
    ///
    /// # Errors
    ///
    /// The `target` reference must either:
    /// - be a direct [`Qualified`] reference, or
    /// - resolve to a direct [`Qualified`] reference, if it is a keyed entry in [`SymbolicRefs`].
    ///
    /// If neither of these is satisfied then an
    /// [`InsertionError::TargetNotQualified`] error is returned.
    ///
    /// If the `name` and `target` end up in a cycle, e.g., `a → b → a`, then an
    /// [`InsertionError::Cyclic`] error is returned.
    fn insert(&mut self, name: Name, mut target: Target) -> Result<(), InsertionError> {
        let target_str = match &target {
            Target::Direct(q) => q.as_ref().to_ref_string(),
            Target::Symbolic(s) => s.as_ref().clone(),
        };

        if self.is_reachable_from(&target_str, &name) {
            return Err(InsertionError::Cyclic {
                name: name.into_inner(),
                target: target_str,
            });
        }

        // A [`Target::Direct`] whose string is already a key is actually
        // an intermediate link — downgrade to [`Target::Symbolic`].
        if let Target::Direct(q) = &target
            && self.0.contains_key(&q.as_ref().to_ref_string())
        {
            target = Target::symbolic(q.to_ref_string());
        }

        if let Target::Symbolic(s) = &target
            && self.resolve(s.as_ref()).is_none()
        {
            return Err(InsertionError::TargetNotQualified {
                name: name.into_inner(),
                target: target_str,
            });
        }

        self.reclassify_targets(&name);
        self.0.insert(name, target);
        Ok(())
    }

    /// When a new key is inserted, any existing [`Target::Direct`] whose
    /// qualified string matches the new key is no longer terminal — it is
    /// now an intermediate link and must be reclassified as [`Target::Symbolic`].
    fn reclassify_targets(&mut self, new_key: &Name) {
        let key = new_key.as_ref();
        for existing in self.0.values_mut() {
            if let Target::Direct(q) = existing
                && q == key
            {
                *existing = Target::symbolic(q.to_ref_string());
            }
        }
    }
}

impl TryFrom<IndexMap<Name, Unprotected<RefString>>> for SymbolicRefs {
    type Error = InsertionError;

    fn try_from(map: IndexMap<Name, Unprotected<RefString>>) -> Result<Self, Self::Error> {
        map.into_iter()
            .try_fold(Self::default(), |mut result, (name, target)| {
                result.try_insert_unprotected(name, target)?;
                Ok(result)
            })
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
    use crate::assert_matches;
    use crate::git::fmt::refname;

    use super::*;

    #[test]
    fn infinite_single() {
        let mut symbolic = SymbolicRefs::default();

        assert_matches!(
            symbolic.try_insert(refname!("a"), refname!("a")),
            Err(InsertionError::Cyclic { .. })
        );

        assert!(symbolic.is_empty());
    }

    #[test]
    fn infinite_multi() {
        let mut symbolic = SymbolicRefs::default();

        assert_matches!(
            symbolic.try_insert(refname!("a"), refname!("refs/heads/b")),
            Ok(())
        );

        assert_matches!(
            symbolic.try_insert(refname!("refs/heads/b"), refname!("refs/heads/c")),
            Ok(())
        );

        assert_matches!(
            symbolic.try_insert(refname!("refs/heads/c"), refname!("a")),
            Err(InsertionError::Cyclic { .. })
        );
    }

    #[test]
    fn deserialize_valid() {
        assert_matches!(
            serde_json::from_value::<SymbolicRefs>(serde_json::json!({
                "refs/heads/a": "refs/heads/b",
            })),
            Ok(_)
        );
    }

    #[test]
    fn deserialize_order() {
        assert_matches!(
            serde_json::from_value::<SymbolicRefs>(serde_json::json!({
                "MAIN": "refs/heads/master",
                "HEAD": "MAIN",
            })),
            Ok(_)
        );

        assert_matches!(
            serde_json::from_value::<SymbolicRefs>(serde_json::json!({
                "HEAD": "MAIN",
                "MAIN": "refs/heads/master",
            })),
            Err(_)
        );
    }

    #[test]
    fn deserialize_infinite() {
        assert_matches!(
            serde_json::from_value::<SymbolicRefs>(serde_json::json!({
                "refs/heads/a": "refs/heads/a",
            })),
            Err(_)
        );

        assert_matches!(
            serde_json::from_value::<SymbolicRefs>(serde_json::json!({
                "refs/heads/a": "refs/heads/b",
                "refs/heads/b": "refs/heads/c",
                "refs/heads/c": "refs/heads/a",
            })),
            Err(_)
        );

        assert_matches!(
            serde_json::from_value::<SymbolicRefs>(serde_json::json!({
                "HEAD": "b",
            })),
            Err(_)
        );
    }

    /// Verifies that resolution works correctly for chains with 2 links
    /// (even-length), e.g. `HEAD → MAIN → refs/heads/master`.
    #[test]
    fn resolve_two_hop_chain() {
        let symrefs = serde_json::from_value::<SymbolicRefs>(serde_json::json!({
            "MAIN": "refs/heads/master",
            "HEAD": "MAIN",
        }))
        .unwrap();

        // HEAD → MAIN → refs/heads/master should resolve to refs/heads/master
        assert_eq!(
            symrefs.resolve_head().map(|r| r.as_str()),
            Some("refs/heads/master"),
        );
    }

    /// Motivates why we cannot simply delegate to [`BTreeMap::extend`]
    /// for combining [`SymbolicRefs`].
    #[test]
    fn infinite_extend() {
        let mut a = SymbolicRefs::default();
        assert_matches!(
            a.try_insert(refname!("refs/heads/a"), refname!("refs/heads/b")),
            Ok(())
        );

        let mut b = SymbolicRefs::default();
        assert_matches!(
            b.try_insert(refname!("refs/heads/b"), refname!("refs/heads/a")),
            Ok(())
        );

        assert_matches!(a.combine(b), Err(InsertionError::Cyclic { .. }));
    }

    /// Verifies that direct targets are stored as [`Target::Direct`].
    #[test]
    fn target_classification() {
        let symrefs = serde_json::from_value::<SymbolicRefs>(serde_json::json!({
            "HEAD": "refs/heads/main",
        }))
        .unwrap();

        let (_, target) = symrefs.iter().next().unwrap();
        assert_matches!(target, Target::Direct(_));
    }

    /// Verifies that symbolic targets are stored as [`Target::Symbolic`].
    #[test]
    fn target_classification_symbolic() {
        let symrefs = serde_json::from_value::<SymbolicRefs>(serde_json::json!({
            "MAIN": "refs/heads/master",
            "HEAD": "MAIN",
        }))
        .unwrap();

        let head_entry = symrefs
            .iter()
            .find_map(|(name, target)| (name.as_str() == "HEAD").then_some(target))
            .unwrap();
        assert_matches!(head_entry, Target::Symbolic(_));

        let main_entry = symrefs
            .iter()
            .find_map(|(name, target)| (name.as_str() == "MAIN").then_some(target))
            .unwrap();
        assert_matches!(main_entry, Target::Direct(_));
    }

    /// Verifies that an existing direct target can become a symbolic target
    /// during a new insertion.
    #[test]
    fn target_reclassification() {
        let mut symrefs = SymbolicRefs::default();
        symrefs
            .try_insert(refname!("HEAD"), refname!("refs/heads/main"))
            .unwrap();
        symrefs
            .try_insert(refname!("refs/heads/main"), refname!("refs/heads/master"))
            .unwrap();
        let main = symrefs
            .iter()
            .find_map(|(_, target)| {
                (target.as_refstr().as_str() == "refs/heads/main").then_some(target)
            })
            .unwrap();
        assert_matches!(main, Target::Symbolic(_));
    }

    /// Verifies that an existing direct target can become a symbolic target
    /// during a new insertion.
    #[test]
    fn target_reclassification_commutative() {
        let mut symrefs = SymbolicRefs::default();
        symrefs
            .try_insert(refname!("refs/heads/main"), refname!("refs/heads/master"))
            .unwrap();
        symrefs
            .try_insert(refname!("HEAD"), refname!("refs/heads/main"))
            .unwrap();
        let main = symrefs
            .iter()
            .find_map(|(_, target)| {
                (target.as_refstr().as_str() == "refs/heads/main").then_some(target)
            })
            .unwrap();
        assert_matches!(main, Target::Symbolic(_));
    }

    #[test]
    fn reclassification_reverse_chain() {
        let mut symrefs = SymbolicRefs::default();

        // Build the chain in reverse: terminal first, origin last.
        for (name, target) in [
            (refname!("refs/heads/c"), refname!("refs/heads/d")),
            (refname!("refs/heads/b"), refname!("refs/heads/c")),
            (refname!("refs/heads/a"), refname!("refs/heads/b")),
        ] {
            symrefs.try_insert(name, target).unwrap();
        }

        // Only refs/heads/d (the terminal) should be Direct.
        // refs/heads/b and refs/heads/c are both keys AND targets — Symbolic.
        for (_, target) in symrefs.iter() {
            match target.as_refstr().as_str() {
                "refs/heads/d" => assert_matches!(target, Target::Direct(_)),
                other => {
                    assert_matches!(target, Target::Symbolic(_), "expected Symbolic for {other}")
                }
            }
        }

        // Resolution should still work through the full chain.
        assert_eq!(
            symrefs
                .resolve(&refname!("refs/heads/a"))
                .map(|q| q.as_str()),
            Some("refs/heads/d"),
        );
    }

    #[test]
    fn reclassification_diamond() {
        let mut symrefs = SymbolicRefs::default();
        symrefs
            .try_insert(refname!("HEAD"), refname!("refs/heads/main"))
            .unwrap();
        symrefs
            .try_insert(refname!("DEFAULT"), refname!("refs/heads/main"))
            .unwrap();

        // Both targets are Direct — refs/heads/main is not a key yet.
        // Now make it a key:
        symrefs
            .try_insert(refname!("refs/heads/main"), refname!("refs/heads/master"))
            .unwrap();

        // Both HEAD and DEFAULT's targets should now be Symbolic.
        let targets_for_main: Vec<_> = symrefs
            .iter()
            .filter(|(_, t)| t.as_refstr().as_str() == "refs/heads/main")
            .collect();
        assert_eq!(targets_for_main.len(), 2);
        for (name, target) in targets_for_main {
            assert_matches!(
                target,
                Target::Symbolic(_),
                "expected Symbolic for {name}'s target"
            );
        }
    }

    #[test]
    fn reclassification_order_invariant() {
        // Order A: HEAD first, then the chain link.
        let a = {
            let mut s = SymbolicRefs::default();
            s.try_insert(refname!("HEAD"), refname!("refs/heads/main"))
                .unwrap();
            s.try_insert(refname!("refs/heads/main"), refname!("refs/heads/master"))
                .unwrap();
            s
        };

        // Order B: chain link first, then HEAD.
        let b = {
            let mut s = SymbolicRefs::default();
            s.try_insert(refname!("refs/heads/main"), refname!("refs/heads/master"))
                .unwrap();
            s.try_insert(refname!("HEAD"), refname!("refs/heads/main"))
                .unwrap();
            s
        };

        // Both should resolve HEAD to the same place.
        assert_eq!(a.resolve_head(), b.resolve_head());

        // Both should have the same classification for the refs/heads/main target.
        let classify_a = a
            .iter()
            .find(|(_, t)| t.as_refstr().as_str() == "refs/heads/main")
            .unwrap()
            .1;
        let classify_b = b
            .iter()
            .find(|(_, t)| t.as_refstr().as_str() == "refs/heads/main")
            .unwrap()
            .1;
        assert_matches!(classify_a, Target::Symbolic(_));
        assert_matches!(classify_b, Target::Symbolic(_));
    }

    #[test]
    fn reclassification_combine() {
        // A has HEAD → refs/heads/main (Direct)
        let mut a = SymbolicRefs::head(&refname!("main"));

        // B has refs/heads/main → refs/heads/master (Direct)
        let mut b = SymbolicRefs::default();
        b.try_insert(refname!("refs/heads/main"), refname!("refs/heads/master"))
            .unwrap();

        a.combine(b).unwrap();

        // After combine, HEAD's target refs/heads/main should be Symbolic.
        let main_target = a
            .iter()
            .find(|(_, t)| t.as_refstr().as_str() == "refs/heads/main")
            .unwrap()
            .1;
        assert_matches!(main_target, Target::Symbolic(_));
        assert_eq!(
            a.resolve_head().map(|q| q.as_str()),
            Some("refs/heads/master")
        );
    }

    #[test]
    fn reclassification_combine_reverse() {
        // B has refs/heads/main → refs/heads/master (Direct)
        let mut b = SymbolicRefs::default();
        b.try_insert(refname!("refs/heads/main"), refname!("refs/heads/master"))
            .unwrap();

        // A has HEAD → refs/heads/main (Direct)
        let a = SymbolicRefs::head(&refname!("main"));

        b.combine(a).unwrap();

        // HEAD's target refs/heads/main IS a key — should be Symbolic.
        let main_target = b
            .iter()
            .find_map(|(_, t)| (t.as_refstr().as_str() == "refs/heads/main").then_some(t))
            .unwrap();
        assert_matches!(main_target, Target::Symbolic(_));
        assert_eq!(
            b.resolve_head().map(|q| q.as_str()),
            Some("refs/heads/master")
        );
    }
}