git-bug 0.2.4

A rust library for interfacing with git-bug repositories
Documentation
// git-bug-rs - A rust library for interfacing with git-bug repositories
//
// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
// SPDX-License-Identifier: GPL-3.0-or-later
//
// This file is part of git-bug-rs/git-gub.
//
// You should have received a copy of the License along with this program.
// If not, see <https://www.gnu.org/licenses/agpl.txt>.

//! Every [`Entity`][`super::Entity`] needs to be connected to a set
//! of [`Identities`][`IdentityStub`] that contributed to it.
//!
//! Considering that the [`Identity`][`Identity`] is also
//! stored as an [`Entity`][`super::Entity`], this relationship is cyclic.
//! To break the cycle, we simply attach an [`IdentityStub`] to every committed
//! [`Operation`][`super::operation::Operation`].

use serde::{Deserialize, Serialize};
use simd_json::{base::ValueAsScalar, borrowed, json_typed, lazy};

use super::id::entity_id::EntityId;
use crate::{
    entities::identity::Identity,
    replica::{
        Replica,
        entity::{id::Id, read},
    },
};

/// A identity stub is what we actually commit in the repository.
///
/// This is an almost empty [`Identity`][`Identity`], holding
/// only the [`Id`][`super::id::Id`]. When a normal Identity is serialized into
/// JSON, only the [`Id`][`super::id::Id`] is serialized. All the other data is
/// stored in git as a normal [`Entity`][`super::Entity`].
///
/// Thus, when the JSON representation is deserialized, an [`IdentityStub`] is
/// used instead, allowing us to lazy load the proper
/// [`Identity`][`Identity`] later.
// As explained in the toplevel doc comment, this Derive is only a implementation detail.
#[allow(clippy::unsafe_derive_deserialize)]
#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Deserialize, Serialize)]
pub struct IdentityStub {
    // TODO(@bpeetz): Depending on [`Identity`] here is not ideal. <2025-05-10>
    pub(crate) id: EntityId<Identity>,
}

impl IdentityStub {
    /// Access the id of this [`IdentityStub`].
    #[must_use]
    pub fn id(&self) -> EntityId<Identity> {
        self.id
    }

    /// Turn this [`IdentityStub`] into a full [`Identity`].
    ///
    /// This is a convenience wrapper around:
    /// ```no_run
    /// # use git_bug::entities::identity::Identity;
    /// # fn compile_dont_run(
    /// #     replica: git_bug::replica::Replica,
    /// #     stub: git_bug::replica::entity::identity::IdentityStub,
    /// # ) -> Result<Identity, git_bug::replica::entity::read::Error<Identity>> {
    /// replica.get::<Identity>(stub.id())
    /// # }
    /// ```
    ///
    /// # Errors
    /// When the [`replica.get`][`Replica::get`] function would error.
    pub fn resolve(self, replica: &Replica) -> Result<Identity, read::Error<Identity>> {
        replica.get(self.id)
    }
}

impl IdentityStub {
    /// Turn this [`IdentityStub`] into it's JSON value representation.
    #[must_use]
    pub fn as_value(&self) -> borrowed::Value<'_> {
        json_typed!(borrowed, {
            "id": borrowed::Value::String(self.id.as_id().to_string().into())
        })
    }

    /// Try to parse this [`IdentityStub`] from it's JSON value representation.
    ///
    /// # Errors
    /// If the value was did not actually represent an [`IdentityStub`].
    pub fn from_value(value: &lazy::Value<'_, '_, '_>) -> Result<Self, from_value::Error> {
        let base_id = value
            .get("id")
            .ok_or_else(|| from_value::Error::MissingId {
                data: value.clone().into_value().into(),
            })?;

        let base_id_str = base_id
            .as_str()
            .ok_or_else(|| from_value::Error::IdNotString {
                id: base_id.clone().into_value().into(),
            })?;

        let id =
            Id::from_hex(base_id_str.as_bytes()).map_err(|err| from_value::Error::IdParse {
                err,
                id: base_id_str.to_owned(),
            })?;

        Ok(Self {
            // Safety:
            // This is not safe at all.
            // But we have to assume that all written Id's are valid, as we cannot check them.
            id: unsafe { EntityId::from_id(id) },
        })
    }
}

#[allow(missing_docs)]
pub mod from_value {
    use simd_json::OwnedValue;

    use crate::replica::entity::id;

    #[derive(Debug, thiserror::Error)]
    pub enum Error {
        #[error("Missing Id in identity stub data: {data}")]
        MissingId { data: OwnedValue },

        #[error("The id field in the Identity stub, was not a string but: {id}")]
        IdNotString { id: OwnedValue },

        #[error("Failed to parse the id field ('{id}') in the identity stub as Id: {err}")]
        IdParse { err: id::decode::Error, id: String },
    }
}