kcode_k1_rust_code_ktool_service/
lib.rs1use 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, SourcePackage};
9use kcode_k1_rust_projection::{K1RustProjection, PublishOutcome};
10use kcode_k1_rust_transaction::{RustSourceTransaction, TransactionError, encode};
11use kcode_k1_rust_worktree::{OpenedSource, SourceSelector, publication_source};
12use kcode_k1_transaction_id::TxId as RustTxId;
13use semver::Version;
14use std::collections::HashMap;
15use std::error::Error;
16use std::fmt::{Debug, Display, Formatter};
17use std::path::{Path, PathBuf};
18use std::sync::{Arc, Mutex, OnceLock};
19
20const OBJECT_FILENAME: &str = "rust-publication.k1rust";
21const OBJECT_MEDIA_TYPE: &str = "application/vnd.kennedy.k1-rust-source-transaction.v1";
22
23#[derive(Clone, Debug, Eq, PartialEq)]
24pub struct OpenedDocument {
25 selector: SourceSelector,
26 document: RustCodeDocument,
27}
28
29impl OpenedDocument {
30 pub fn selector(&self) -> &SourceSelector {
31 &self.selector
32 }
33
34 pub fn document(&self) -> &RustCodeDocument {
35 &self.document
36 }
37
38 fn from_source(source: OpenedSource) -> Result<Self, RustCodeKtoolServiceError> {
39 let selector = source.selector().clone();
40 let document = RustCodeDocument::from_source_package(source.into_source())
41 .map_err(RustCodeKtoolServiceError::Unsupported)?;
42 Ok(Self { selector, document })
43 }
44}
45
46#[derive(Debug)]
47pub enum RustCodeKtoolServiceError {
48 State(String),
49 Authorization(String),
50 AccessDenied,
51 LibraryExists,
52 LibraryAbsent,
53 NoPublishedVersion,
54 WorktreeChanged,
55 VersionUsed,
56 Unsupported(RustCodeDocumentError),
57 CodingInitialization(String),
58 Coding(RustCodingError),
59 SourceEncoding(TransactionError),
60 Object(String),
61 CheckMismatch,
62 Projection(String),
63}
64
65impl Display for RustCodeKtoolServiceError {
66 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
67 Debug::fmt(self, formatter)
68 }
69}
70
71impl Error for RustCodeKtoolServiceError {}
72
73pub struct ServiceCheck {
74 user: UserId,
75 package: SourcePackage,
76 verified: VerifiedCheck,
77}
78
79#[derive(Default)]
80struct UserCoding {
81 value: OnceLock<Result<Arc<Mutex<K1RustCoding>>, String>>,
82}
83
84pub struct RustCodeKtoolService {
85 cache_root: PathBuf,
86 config: RustCodingConfig,
87 projection: Arc<K1RustProjection>,
88 objects: Arc<K1Objects>,
89 groups: Arc<K1Groups>,
90 users: Mutex<HashMap<UserId, Arc<UserCoding>>>,
91}
92
93impl RustCodeKtoolService {
94 pub fn new(
95 cache_root: impl AsRef<Path>,
96 config: RustCodingConfig,
97 projection: Arc<K1RustProjection>,
98 objects: Arc<K1Objects>,
99 groups: Arc<K1Groups>,
100 ) -> Self {
101 Self {
102 cache_root: cache_root.as_ref().to_path_buf(),
103 config,
104 projection,
105 objects,
106 groups,
107 users: Mutex::new(HashMap::new()),
108 }
109 }
110
111 pub fn create(
112 &self,
113 authenticated: &AccessContext,
114 family: LibraryFamily,
115 ) -> Result<OpenedDocument, RustCodeKtoolServiceError> {
116 self.authorize(authenticated, &family)?;
117 if self
118 .projection
119 .family_exists(&family)
120 .map_err(RustCodeKtoolServiceError::Projection)?
121 {
122 return Err(RustCodeKtoolServiceError::LibraryExists);
123 }
124 let identity = kcode_k1_rust_package::LibraryId::new(family, Version::new(0, 0, 0))
125 .map_err(|error| RustCodeKtoolServiceError::State(error.to_string()))?;
126 let document = RustCodeDocument::approved_default(identity)
127 .map_err(RustCodeKtoolServiceError::Unsupported)?;
128 let worktree = self
129 .projection
130 .create(&document.clone().into_source_package())
131 .map_err(map_write_error)?;
132 OpenedDocument::from_source(OpenedSource::unpublished(&worktree))
133 }
134
135 pub fn open(
136 &self,
137 authenticated: &AccessContext,
138 family: &LibraryFamily,
139 selector: Option<&SourceSelector>,
140 ) -> Result<OpenedDocument, RustCodeKtoolServiceError> {
141 if !matches!(selector, Some(SourceSelector::Published(_))) {
142 self.authorize(authenticated, family)?;
143 }
144 let source = self
145 .projection
146 .open_source(family, selector)
147 .map_err(RustCodeKtoolServiceError::Projection)?
148 .ok_or(RustCodeKtoolServiceError::LibraryAbsent)?;
149 OpenedDocument::from_source(source)
150 }
151
152 pub fn latest_published(
153 &self,
154 family: &LibraryFamily,
155 ) -> Result<OpenedDocument, RustCodeKtoolServiceError> {
156 let source = self
157 .projection
158 .latest_published(family)
159 .map_err(RustCodeKtoolServiceError::Projection)?
160 .ok_or_else(|| {
161 if self.projection.family_exists(family).unwrap_or(false) {
162 RustCodeKtoolServiceError::NoPublishedVersion
163 } else {
164 RustCodeKtoolServiceError::LibraryAbsent
165 }
166 })?;
167 OpenedDocument::from_source(source)
168 }
169
170 pub fn overwrite(
171 &self,
172 authenticated: &AccessContext,
173 opened: &OpenedDocument,
174 next: RustCodeDocument,
175 ) -> Result<OpenedDocument, RustCodeKtoolServiceError> {
176 let family = opened.document.identity().family();
177 self.authorize(authenticated, family)?;
178 if next.identity() != opened.document.identity() {
179 return Err(RustCodeKtoolServiceError::WorktreeChanged);
180 }
181 let source = next.into_source_package();
182 let worktree = match opened.selector() {
183 SourceSelector::Published(version) => {
184 let published = self
185 .projection
186 .open_source(family, Some(&SourceSelector::Published(version.clone())))
187 .map_err(RustCodeKtoolServiceError::Projection)?
188 .ok_or(RustCodeKtoolServiceError::WorktreeChanged)?;
189 if published.source() != &opened.document.clone().into_source_package() {
190 return Err(RustCodeKtoolServiceError::WorktreeChanged);
191 }
192 self.projection.fork(&source).map_err(map_write_error)?
193 }
194 SourceSelector::Unpublished(id) => {
195 let current = self
196 .projection
197 .load_unpublished(family, *id)
198 .map_err(RustCodeKtoolServiceError::Projection)?
199 .ok_or(RustCodeKtoolServiceError::WorktreeChanged)?;
200 if current.source() != &opened.document.clone().into_source_package() {
201 return Err(RustCodeKtoolServiceError::WorktreeChanged);
202 }
203 self.projection
204 .overwrite(*id, current.revision(), &source)
205 .map_err(map_write_error)?
206 }
207 };
208 OpenedDocument::from_source(OpenedSource::unpublished(&worktree))
209 }
210
211 pub fn versioned(
212 &self,
213 document: &RustCodeDocument,
214 version: Version,
215 ) -> Result<RustCodeDocument, RustCodeKtoolServiceError> {
216 let package = publication_source(&document.clone().into_source_package(), version)
217 .map_err(|error| RustCodeKtoolServiceError::State(error.to_string()))?;
218 RustCodeDocument::from_source_package(package)
219 .map_err(RustCodeKtoolServiceError::Unsupported)
220 }
221
222 pub fn check_fresh(
223 &self,
224 authenticated: &AccessContext,
225 document: &RustCodeDocument,
226 ) -> Result<ServiceCheck, RustCodeKtoolServiceError> {
227 let package = document.clone().into_source_package();
228 let coding = self.coding(authenticated)?;
229 let mut coding = coding
230 .lock()
231 .map_err(|_| RustCodeKtoolServiceError::State("user coding mutex poisoned".into()))?;
232 let verified = coding
233 .check_fresh(&package)
234 .map_err(RustCodeKtoolServiceError::Coding)?;
235 Ok(ServiceCheck {
236 user: authenticated.user(),
237 package,
238 verified,
239 })
240 }
241
242 pub fn current_check(
243 &self,
244 authenticated: &AccessContext,
245 document: &RustCodeDocument,
246 ) -> Result<Option<ServiceCheck>, RustCodeKtoolServiceError> {
247 let package = document.clone().into_source_package();
248 let coding = self.coding(authenticated)?;
249 let mut coding = coding
250 .lock()
251 .map_err(|_| RustCodeKtoolServiceError::State("user coding mutex poisoned".into()))?;
252 let verified = coding
253 .current_check(&package)
254 .map_err(RustCodeKtoolServiceError::Coding)?;
255 Ok(verified.map(|verified| ServiceCheck {
256 user: authenticated.user(),
257 package,
258 verified,
259 }))
260 }
261
262 pub fn publish_checked(
263 &self,
264 authenticated: &AccessContext,
265 document: &RustCodeDocument,
266 checked: ServiceCheck,
267 ) -> Result<PublishOutcome, RustCodeKtoolServiceError> {
268 let package = document.clone().into_source_package();
269 if checked.user != authenticated.user() || checked.package != package {
270 return Err(RustCodeKtoolServiceError::CheckMismatch);
271 }
272 if self
273 .projection
274 .version_exists(package.id())
275 .map_err(RustCodeKtoolServiceError::Projection)?
276 {
277 return Err(RustCodeKtoolServiceError::VersionUsed);
278 }
279 let coding = self.coding(authenticated)?;
280 let mut coding = coding
281 .lock()
282 .map_err(|_| RustCodeKtoolServiceError::State("user coding mutex poisoned".into()))?;
283 let bytes = encode(&RustSourceTransaction::Publish(package.clone()))
284 .map_err(RustCodeKtoolServiceError::SourceEncoding)?;
285 self.objects
286 .save(OBJECT_FILENAME, OBJECT_MEDIA_TYPE, "", &bytes)
287 .map_err(RustCodeKtoolServiceError::Object)?;
288 let gate = AuthorityGate {
289 authenticated,
290 groups: &self.groups,
291 };
292 coding
293 .publish_checked(&package, &checked.verified, &gate)
294 .map_err(RustCodeKtoolServiceError::Coding)
295 }
296
297 fn authorize(
298 &self,
299 authenticated: &AccessContext,
300 family: &LibraryFamily,
301 ) -> Result<(), RustCodeKtoolServiceError> {
302 match (AuthorityGate {
303 authenticated,
304 groups: &self.groups,
305 })
306 .authorize_publish(family)
307 {
308 Ok(true) => Ok(()),
309 Ok(false) => Err(RustCodeKtoolServiceError::AccessDenied),
310 Err(error) => Err(RustCodeKtoolServiceError::Authorization(error)),
311 }
312 }
313
314 fn coding(
315 &self,
316 authenticated: &AccessContext,
317 ) -> Result<Arc<Mutex<K1RustCoding>>, RustCodeKtoolServiceError> {
318 let slot = {
319 let mut users = self.users.lock().map_err(|_| {
320 RustCodeKtoolServiceError::State("user coding map mutex poisoned".into())
321 })?;
322 users
323 .entry(authenticated.user())
324 .or_insert_with(|| Arc::new(UserCoding::default()))
325 .clone()
326 };
327 match slot.value.get_or_init(|| {
328 let user = RustTxId::from_bytes(*authenticated.user().as_tx_id().as_bytes());
329 K1RustCoding::open(
330 &self.cache_root,
331 user,
332 self.config.clone(),
333 self.projection.clone(),
334 )
335 .map(|coding| Arc::new(Mutex::new(coding)))
336 .map_err(|error| error.to_string())
337 }) {
338 Ok(coding) => Ok(coding.clone()),
339 Err(error) => Err(RustCodeKtoolServiceError::CodingInitialization(
340 error.clone(),
341 )),
342 }
343 }
344}
345
346fn map_write_error(error: String) -> RustCodeKtoolServiceError {
347 if error == "unpublished version changed" || error == "source transaction was rejected" {
348 RustCodeKtoolServiceError::WorktreeChanged
349 } else if error == "library family already exists" {
350 RustCodeKtoolServiceError::LibraryExists
351 } else {
352 RustCodeKtoolServiceError::Projection(error)
353 }
354}
355
356struct AuthorityGate<'a> {
357 authenticated: &'a AccessContext,
358 groups: &'a K1Groups,
359}
360
361impl RustPublishAuthorization for AuthorityGate<'_> {
362 fn authorize_publish(&self, family: &LibraryFamily) -> Result<bool, String> {
363 let authority = family.authority();
364 let authority = authority.transaction_id();
365 if authority.as_bytes() == self.authenticated.user().as_tx_id().as_bytes() {
366 return Ok(true);
367 }
368 let group = GroupId::new(AccessTxId::from_bytes(*authority.as_bytes()));
369 if group.sentinel().is_some()
370 || self
371 .authenticated
372 .filter()
373 .contains(Authority::Group(group))
374 {
375 return Ok(false);
376 }
377 let memberships = self
378 .groups
379 .memberships(self.authenticated.user(), self.authenticated.model())?;
380 Ok(memberships.shared_groups().contains(&group))
381 }
382}