1use super::*;
2use coil_auth::{DefaultSubject, DefaultTuple, DefaultTupleUpdate, Entity, Relation};
3use coil_storage::{
4 SingleNodeEscapeHatchPlanner, StoragePlan, StoragePlanRequest, StoragePlanner,
5 StoragePolicyOverride,
6};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub struct ManagedAssetRevision {
10 id: RevisionId,
11 storage_plan: StoragePlan,
12 content_type: String,
13 byte_length: u64,
14 fingerprint: ContentFingerprint,
15}
16
17impl ManagedAssetRevision {
18 pub fn plan(
19 id: RevisionId,
20 planner: &StoragePlanner,
21 logical_path: impl Into<String>,
22 override_policy: Option<StoragePolicyOverride>,
23 content_type: impl Into<String>,
24 byte_length: u64,
25 fingerprint: ContentFingerprint,
26 ) -> Result<Self, AssetModelError> {
27 let mut request = StoragePlanRequest::new(logical_path);
28 if let Some(override_policy) = override_policy {
29 request = request.with_override(override_policy);
30 }
31
32 let storage_plan = planner
33 .plan_scalable_write(request)
34 .map_err(AssetModelError::Storage)?;
35 Self::new(id, storage_plan, content_type, byte_length, fingerprint)
36 }
37
38 pub fn plan_with_single_node_escape_hatch(
39 id: RevisionId,
40 planner: &SingleNodeEscapeHatchPlanner,
41 logical_path: impl Into<String>,
42 override_policy: Option<StoragePolicyOverride>,
43 content_type: impl Into<String>,
44 byte_length: u64,
45 fingerprint: ContentFingerprint,
46 ) -> Result<Self, AssetModelError> {
47 let mut request = StoragePlanRequest::new(logical_path);
48 if let Some(override_policy) = override_policy {
49 request = request.with_override(override_policy);
50 }
51
52 let storage_plan = planner
53 .plan_write(request)
54 .map_err(AssetModelError::Storage)?;
55 Self::new(id, storage_plan, content_type, byte_length, fingerprint)
56 }
57
58 pub fn new(
59 id: RevisionId,
60 storage_plan: StoragePlan,
61 content_type: impl Into<String>,
62 byte_length: u64,
63 fingerprint: ContentFingerprint,
64 ) -> Result<Self, AssetModelError> {
65 Ok(Self {
66 id,
67 storage_plan,
68 content_type: require_non_empty("content_type", content_type.into())?,
69 byte_length,
70 fingerprint,
71 })
72 }
73
74 pub fn id(&self) -> &RevisionId {
75 &self.id
76 }
77
78 pub fn storage_plan(&self) -> &StoragePlan {
79 &self.storage_plan
80 }
81
82 pub fn content_type(&self) -> &str {
83 &self.content_type
84 }
85
86 pub fn byte_length(&self) -> u64 {
87 self.byte_length
88 }
89
90 pub fn fingerprint(&self) -> &ContentFingerprint {
91 &self.fingerprint
92 }
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum PublicationStatus {
97 Draft,
98 Published,
99 Unpublished,
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub enum PublicationTransition {
104 PublishCurrent,
105 Unpublish,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq)]
109pub struct PublicationState {
110 status: PublicationStatus,
111 live_revision: Option<ManagedAssetRevision>,
112}
113
114impl PublicationState {
115 pub fn status(&self) -> PublicationStatus {
116 self.status
117 }
118
119 pub fn is_published(&self) -> bool {
120 self.status == PublicationStatus::Published
121 }
122
123 pub fn live_revision(&self) -> Option<&ManagedAssetRevision> {
124 self.live_revision.as_ref()
125 }
126}
127
128#[derive(Debug, Clone, PartialEq, Eq)]
129pub struct ManagedAsset {
130 id: AssetId,
131 display_name: String,
132 current_revision: ManagedAssetRevision,
133 publication: PublicationState,
134}
135
136impl ManagedAsset {
137 pub fn new(
138 id: AssetId,
139 display_name: impl Into<String>,
140 initial_revision: ManagedAssetRevision,
141 ) -> Result<Self, AssetModelError> {
142 Ok(Self {
143 id,
144 display_name: require_non_empty("display_name", display_name.into())?,
145 current_revision: initial_revision,
146 publication: PublicationState {
147 status: PublicationStatus::Draft,
148 live_revision: None,
149 },
150 })
151 }
152
153 pub fn id(&self) -> &AssetId {
154 &self.id
155 }
156
157 pub fn display_name(&self) -> &str {
158 &self.display_name
159 }
160
161 pub fn current_revision(&self) -> &ManagedAssetRevision {
162 &self.current_revision
163 }
164
165 pub fn publication(&self) -> &PublicationState {
166 &self.publication
167 }
168
169 pub fn auth_entity(&self) -> Entity {
170 Entity::asset(self.id.to_string())
171 }
172
173 pub fn has_pending_changes(&self) -> bool {
174 match self.publication.live_revision() {
175 Some(live_revision) => live_revision.id() != self.current_revision.id(),
176 None => true,
177 }
178 }
179
180 pub fn publish_current(&mut self) {
181 self.publication.live_revision = Some(self.current_revision.clone());
182 self.publication.status = PublicationStatus::Published;
183 }
184
185 pub fn apply_publication_transition(
186 &mut self,
187 transition: PublicationTransition,
188 ) -> Result<(), AssetModelError> {
189 match transition {
190 PublicationTransition::PublishCurrent => {
191 self.publish_current();
192 Ok(())
193 }
194 PublicationTransition::Unpublish => self.unpublish(),
195 }
196 }
197
198 pub fn replace_current_revision(&mut self, revision: ManagedAssetRevision) {
199 self.current_revision = revision;
200 }
201
202 pub fn unpublish(&mut self) -> Result<(), AssetModelError> {
203 if self.publication.live_revision.is_none() {
204 return Err(AssetModelError::CannotUnpublishWithoutLiveRevision {
205 asset_id: self.id.to_string(),
206 });
207 }
208
209 self.publication.live_revision = None;
210 self.publication.status = PublicationStatus::Unpublished;
211 Ok(())
212 }
213
214 pub fn plan_public_delivery(
215 &self,
216 context: &DeliveryContext<'_>,
217 ) -> Result<AssetDeliveryPlan, AssetModelError> {
218 if !self.publication.is_published() {
219 return Err(AssetModelError::NotPublished {
220 asset_id: self.id.to_string(),
221 });
222 }
223
224 let live_revision = self.publication.live_revision().ok_or_else(|| {
225 AssetModelError::MissingLiveRevision {
226 asset_id: self.id.to_string(),
227 }
228 })?;
229
230 public_delivery_plan(
231 AssetKind::ManagedAsset,
232 live_revision.storage_plan(),
233 Some(live_revision.id().clone()),
234 context,
235 false,
236 )
237 .map_err(|error| match error {
238 AssetModelError::PublicDeliveryRequiresPublicCdn { .. } => {
239 AssetModelError::PublicDeliveryRequiresPublicCdn {
240 asset_id: self.id.to_string(),
241 delivery_mode: live_revision.storage_plan().policy.delivery_mode,
242 }
243 }
244 other => other,
245 })
246 }
247
248 pub fn plan_authorized_delivery(
249 &self,
250 context: &DeliveryContext<'_>,
251 ) -> Result<AssetDeliveryPlan, AssetModelError> {
252 authorized_delivery_plan(
253 AssetKind::ManagedAsset,
254 self.current_revision.storage_plan(),
255 Some(self.current_revision.id().clone()),
256 context,
257 )
258 }
259
260 pub fn auth_updates(&self) -> Vec<DefaultTupleUpdate> {
261 let public_tuple = DefaultTuple::new(
262 self.auth_entity(),
263 Relation::ReadPublic,
264 DefaultSubject::entity(Entity::any_user()),
265 );
266
267 if self.publication.is_published()
268 && self
269 .publication
270 .live_revision()
271 .is_some_and(|revision| revision.storage_plan().public_delivery_eligible())
272 {
273 vec![DefaultTupleUpdate::Write(public_tuple)]
274 } else {
275 vec![DefaultTupleUpdate::Delete(public_tuple)]
276 }
277 }
278}