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}