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, 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}