Skip to main content

kcode_k1_rust_code_ktool_service/
lib.rs

1use kcode_k1_access_types::{AccessContext, Authority, GroupId, TxId as AccessTxId, UserId};
2use kcode_k1_groups::K1Groups;
3use kcode_k1_objects::K1Objects;
4use kcode_k1_rust_code_document::{RustCodeDocument, RustCodeDocumentError};
5use kcode_k1_rust_coding::{
6    K1RustCoding, RustCodingConfig, RustCodingError, RustPublishAuthorization, VerifiedCheck,
7};
8use kcode_k1_rust_package::{LibraryFamily, LibraryId, SourcePackage};
9use kcode_k1_rust_projection::{K1RustProjection, PublishOutcome};
10use kcode_k1_rust_transaction::{TransactionError, encode};
11use kcode_k1_transaction_id::TxId as RustTxId;
12use std::collections::HashMap;
13use std::error::Error;
14use std::fmt::{Debug, Display, Formatter};
15use std::path::{Path, PathBuf};
16use std::sync::{Arc, Mutex, OnceLock};
17
18const OBJECT_FILENAME: &str = "source-package.k1rust";
19const OBJECT_MEDIA_TYPE: &str = "application/vnd.kennedy.k1-rust-source-package.v1";
20
21#[derive(Debug)]
22pub enum PublicPackageError {
23    Absent,
24    Unsupported(RustCodeDocumentError),
25    Backend(String),
26}
27
28impl Display for PublicPackageError {
29    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
30        Debug::fmt(self, formatter)
31    }
32}
33
34impl Error for PublicPackageError {}
35
36#[derive(Debug)]
37pub enum RustCodeKtoolServiceError {
38    State(String),
39    CodingInitialization(String),
40    Coding(RustCodingError),
41    SourceEncoding(TransactionError),
42    Object(String),
43    CheckMismatch,
44}
45
46impl Display for RustCodeKtoolServiceError {
47    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
48        Debug::fmt(self, formatter)
49    }
50}
51
52impl Error for RustCodeKtoolServiceError {}
53
54pub struct ServiceCheck {
55    user: UserId,
56    package: SourcePackage,
57    verified: VerifiedCheck,
58}
59
60#[derive(Default)]
61struct UserCoding {
62    value: OnceLock<Result<Arc<Mutex<K1RustCoding>>, String>>,
63}
64
65pub struct RustCodeKtoolService {
66    cache_root: PathBuf,
67    config: RustCodingConfig,
68    projection: Arc<K1RustProjection>,
69    objects: Arc<K1Objects>,
70    groups: Arc<K1Groups>,
71    users: Mutex<HashMap<UserId, Arc<UserCoding>>>,
72}
73
74impl RustCodeKtoolService {
75    pub fn new(
76        cache_root: impl AsRef<Path>,
77        config: RustCodingConfig,
78        projection: Arc<K1RustProjection>,
79        objects: Arc<K1Objects>,
80        groups: Arc<K1Groups>,
81    ) -> Self {
82        Self {
83            cache_root: cache_root.as_ref().to_path_buf(),
84            config,
85            projection,
86            objects,
87            groups,
88            users: Mutex::new(HashMap::new()),
89        }
90    }
91
92    pub fn load_document(&self, id: &LibraryId) -> Result<RustCodeDocument, PublicPackageError> {
93        match self.projection.load(id) {
94            Ok(Some(package)) => RustCodeDocument::from_source_package(package)
95                .map_err(PublicPackageError::Unsupported),
96            Ok(None) => Err(PublicPackageError::Absent),
97            Err(error) => Err(PublicPackageError::Backend(error)),
98        }
99    }
100
101    pub fn identity_exists(&self, id: &LibraryId) -> Result<bool, PublicPackageError> {
102        self.projection
103            .load(id)
104            .map(|package| package.is_some())
105            .map_err(PublicPackageError::Backend)
106    }
107
108    pub fn check_fresh(
109        &self,
110        authenticated: &AccessContext,
111        document: &RustCodeDocument,
112    ) -> Result<ServiceCheck, RustCodeKtoolServiceError> {
113        let package = document.clone().into_source_package();
114        let coding = self.coding(authenticated)?;
115        let mut coding = coding
116            .lock()
117            .map_err(|_| RustCodeKtoolServiceError::State("user coding mutex poisoned".into()))?;
118        let verified = coding
119            .check_fresh(&package)
120            .map_err(RustCodeKtoolServiceError::Coding)?;
121        Ok(ServiceCheck {
122            user: authenticated.user(),
123            package,
124            verified,
125        })
126    }
127
128    pub fn current_check(
129        &self,
130        authenticated: &AccessContext,
131        document: &RustCodeDocument,
132    ) -> Result<Option<ServiceCheck>, RustCodeKtoolServiceError> {
133        let package = document.clone().into_source_package();
134        let coding = self.coding(authenticated)?;
135        let mut coding = coding
136            .lock()
137            .map_err(|_| RustCodeKtoolServiceError::State("user coding mutex poisoned".into()))?;
138        let verified = coding
139            .current_check(&package)
140            .map_err(RustCodeKtoolServiceError::Coding)?;
141        Ok(verified.map(|verified| ServiceCheck {
142            user: authenticated.user(),
143            package,
144            verified,
145        }))
146    }
147
148    pub fn publish_checked(
149        &self,
150        authenticated: &AccessContext,
151        document: &RustCodeDocument,
152        checked: ServiceCheck,
153    ) -> Result<PublishOutcome, RustCodeKtoolServiceError> {
154        let package = document.clone().into_source_package();
155        if checked.user != authenticated.user() || checked.package != package {
156            return Err(RustCodeKtoolServiceError::CheckMismatch);
157        }
158        let coding = self.coding(authenticated)?;
159        let mut coding = coding
160            .lock()
161            .map_err(|_| RustCodeKtoolServiceError::State("user coding mutex poisoned".into()))?;
162        let bytes = encode(&package).map_err(RustCodeKtoolServiceError::SourceEncoding)?;
163        self.objects
164            .save(OBJECT_FILENAME, OBJECT_MEDIA_TYPE, "", &bytes)
165            .map_err(RustCodeKtoolServiceError::Object)?;
166        let gate = PublishGate {
167            authenticated,
168            groups: &self.groups,
169        };
170        coding
171            .publish_checked(&package, &checked.verified, &gate)
172            .map_err(RustCodeKtoolServiceError::Coding)
173    }
174
175    fn coding(
176        &self,
177        authenticated: &AccessContext,
178    ) -> Result<Arc<Mutex<K1RustCoding>>, RustCodeKtoolServiceError> {
179        let slot = {
180            let mut users = self.users.lock().map_err(|_| {
181                RustCodeKtoolServiceError::State("user coding map mutex poisoned".into())
182            })?;
183            users
184                .entry(authenticated.user())
185                .or_insert_with(|| Arc::new(UserCoding::default()))
186                .clone()
187        };
188        match slot.value.get_or_init(|| {
189            let user = RustTxId::from_bytes(*authenticated.user().as_tx_id().as_bytes());
190            K1RustCoding::open(
191                &self.cache_root,
192                user,
193                self.config.clone(),
194                self.projection.clone(),
195            )
196            .map(|coding| Arc::new(Mutex::new(coding)))
197            .map_err(|error| error.to_string())
198        }) {
199            Ok(coding) => Ok(coding.clone()),
200            Err(error) => Err(RustCodeKtoolServiceError::CodingInitialization(
201                error.clone(),
202            )),
203        }
204    }
205}
206
207struct PublishGate<'a> {
208    authenticated: &'a AccessContext,
209    groups: &'a K1Groups,
210}
211
212impl RustPublishAuthorization for PublishGate<'_> {
213    fn authorize_publish(&self, family: &LibraryFamily) -> Result<bool, String> {
214        let authority_id = family.authority();
215        let authority = authority_id.transaction_id();
216        if authority.as_bytes() == self.authenticated.user().as_tx_id().as_bytes() {
217            return Ok(true);
218        }
219        let group = GroupId::new(AccessTxId::from_bytes(*authority.as_bytes()));
220        if group.sentinel().is_some()
221            || self
222                .authenticated
223                .filter()
224                .contains(Authority::Group(group))
225        {
226            return Ok(false);
227        }
228        let memberships = self
229            .groups
230            .memberships(self.authenticated.user(), self.authenticated.model())?;
231        Ok(memberships.shared_groups().contains(&group))
232    }
233}