git_bug/replica/entity/identity/
mod.rs

1// git-bug-rs - A rust library for interfacing with git-bug repositories
2//
3// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
4// SPDX-License-Identifier: GPL-3.0-or-later
5//
6// This file is part of git-bug-rs/git-gub.
7//
8// You should have received a copy of the License along with this program.
9// If not, see <https://www.gnu.org/licenses/agpl.txt>.
10
11//! Every [`Entity`][`super::Entity`] needs to be connected to a set
12//! of [`Identities`][`IdentityStub`] that contributed to it.
13//!
14//! Considering that the [`Identity`][`Identity`] is also
15//! stored as an [`Entity`][`super::Entity`], this relationship is cyclic.
16//! To break the cycle, we simply attach an [`IdentityStub`] to every committed
17//! [`Operation`][`super::operation::Operation`].
18
19use serde::{Deserialize, Serialize};
20use simd_json::{base::ValueAsScalar, borrowed, json_typed, lazy};
21
22use super::id::entity_id::EntityId;
23use crate::{
24    entities::identity::Identity,
25    replica::{
26        Replica,
27        entity::{id::Id, read},
28    },
29};
30
31/// A identity stub is what we actually commit in the repository.
32///
33/// This is an almost empty [`Identity`][`Identity`], holding
34/// only the [`Id`][`super::id::Id`]. When a normal Identity is serialized into
35/// JSON, only the [`Id`][`super::id::Id`] is serialized. All the other data is
36/// stored in git as a normal [`Entity`][`super::Entity`].
37///
38/// Thus, when the JSON representation is deserialized, an [`IdentityStub`] is
39/// used instead, allowing us to lazy load the proper
40/// [`Identity`][`Identity`] later.
41// As explained in the toplevel doc comment, this Derive is only a implementation detail.
42#[allow(clippy::unsafe_derive_deserialize)]
43#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Deserialize, Serialize)]
44pub struct IdentityStub {
45    // TODO(@bpeetz): Depending on [`Identity`] here is not ideal. <2025-05-10>
46    pub(crate) id: EntityId<Identity>,
47}
48
49impl IdentityStub {
50    /// Access the id of this [`IdentityStub`].
51    #[must_use]
52    pub fn id(&self) -> EntityId<Identity> {
53        self.id
54    }
55
56    /// Turn this [`IdentityStub`] into a full [`Identity`].
57    ///
58    /// This is a convenience wrapper around:
59    /// ```no_run
60    /// # use git_bug::entities::identity::Identity;
61    /// # fn compile_dont_run(
62    /// #     replica: git_bug::replica::Replica,
63    /// #     stub: git_bug::replica::entity::identity::IdentityStub,
64    /// # ) -> Result<Identity, git_bug::replica::entity::read::Error<Identity>> {
65    /// replica.get::<Identity>(stub.id())
66    /// # }
67    /// ```
68    ///
69    /// # Errors
70    /// When the [`replica.get`][`Replica::get`] function would error.
71    pub fn resolve(self, replica: &Replica) -> Result<Identity, read::Error<Identity>> {
72        replica.get(self.id)
73    }
74}
75
76impl IdentityStub {
77    /// Turn this [`IdentityStub`] into it's JSON value representation.
78    #[must_use]
79    pub fn as_value(&self) -> borrowed::Value<'_> {
80        json_typed!(borrowed, {
81            "id": borrowed::Value::String(self.id.as_id().to_string().into())
82        })
83    }
84
85    /// Try to parse this [`IdentityStub`] from it's JSON value representation.
86    ///
87    /// # Errors
88    /// If the value was did not actually represent an [`IdentityStub`].
89    pub fn from_value(value: &lazy::Value<'_, '_, '_>) -> Result<Self, from_value::Error> {
90        let base_id = value
91            .get("id")
92            .ok_or_else(|| from_value::Error::MissingId {
93                data: value.clone().into_value().into(),
94            })?;
95
96        let base_id_str = base_id
97            .as_str()
98            .ok_or_else(|| from_value::Error::IdNotString {
99                id: base_id.clone().into_value().into(),
100            })?;
101
102        let id =
103            Id::from_hex(base_id_str.as_bytes()).map_err(|err| from_value::Error::IdParse {
104                err,
105                id: base_id_str.to_owned(),
106            })?;
107
108        Ok(Self {
109            // Safety:
110            // This is not safe at all.
111            // But we have to assume that all written Id's are valid, as we cannot check them.
112            id: unsafe { EntityId::from_id(id) },
113        })
114    }
115}
116
117#[allow(missing_docs)]
118pub mod from_value {
119    use simd_json::OwnedValue;
120
121    use crate::replica::entity::id;
122
123    #[derive(Debug, thiserror::Error)]
124    pub enum Error {
125        #[error("Missing Id in identity stub data: {data}")]
126        MissingId { data: OwnedValue },
127
128        #[error("The id field in the Identity stub, was not a string but: {id}")]
129        IdNotString { id: OwnedValue },
130
131        #[error("Failed to parse the id field ('{id}') in the identity stub as Id: {err}")]
132        IdParse { err: id::decode::Error, id: String },
133    }
134}