1use super::*;
2use std::collections::{BTreeMap, btree_map::Entry};
3
4use coil_storage::{StoragePlanRequest, StoragePlanner, StoragePolicy};
5
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct DeploymentArtifact {
8 logical_path: String,
9 hashed_path: String,
10 fingerprint: ContentFingerprint,
11 content_type: String,
12 byte_length: u64,
13}
14
15impl DeploymentArtifact {
16 pub fn new(
17 logical_path: impl Into<String>,
18 hashed_path: impl Into<String>,
19 fingerprint: ContentFingerprint,
20 content_type: impl Into<String>,
21 byte_length: u64,
22 ) -> Result<Self, AssetModelError> {
23 let logical_path = normalize_manifest_path("logical_path", logical_path.into())?;
24 let hashed_path = normalize_manifest_path("hashed_path", hashed_path.into())?;
25 let content_type = require_non_empty("content_type", content_type.into())?;
26
27 if logical_path == hashed_path || !hashed_path.contains(fingerprint.digest()) {
28 return Err(AssetModelError::UnhashedDeploymentArtifact {
29 logical_path,
30 hashed_path,
31 fingerprint: fingerprint.digest().to_string(),
32 });
33 }
34
35 Ok(Self {
36 logical_path,
37 hashed_path,
38 fingerprint,
39 content_type,
40 byte_length,
41 })
42 }
43
44 pub fn logical_path(&self) -> &str {
45 &self.logical_path
46 }
47
48 pub fn hashed_path(&self) -> &str {
49 &self.hashed_path
50 }
51
52 pub fn fingerprint(&self) -> &ContentFingerprint {
53 &self.fingerprint
54 }
55
56 pub fn content_type(&self) -> &str {
57 &self.content_type
58 }
59
60 pub fn byte_length(&self) -> u64 {
61 self.byte_length
62 }
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct PublishedDeploymentArtifact {
67 artifact: DeploymentArtifact,
68 delivery: AssetDeliveryPlan,
69}
70
71impl PublishedDeploymentArtifact {
72 pub fn artifact(&self) -> &DeploymentArtifact {
73 &self.artifact
74 }
75
76 pub fn delivery(&self) -> &AssetDeliveryPlan {
77 &self.delivery
78 }
79}
80
81#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct ActiveAssetManifest {
83 release_id: ReleaseId,
84 entries: BTreeMap<String, PublishedDeploymentArtifact>,
85}
86
87impl ActiveAssetManifest {
88 pub fn release_id(&self) -> &ReleaseId {
89 &self.release_id
90 }
91
92 pub fn resolve(&self, logical_path: &str) -> Option<&PublishedDeploymentArtifact> {
93 self.entries.get(logical_path)
94 }
95
96 pub fn entries(
97 &self,
98 ) -> impl ExactSizeIterator<Item = (&str, &PublishedDeploymentArtifact)> + '_ {
99 self.entries
100 .iter()
101 .map(|(path, artifact)| (path.as_str(), artifact))
102 }
103}
104
105#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct DeploymentRelease {
107 release_id: ReleaseId,
108 artifacts: Vec<DeploymentArtifact>,
109}
110
111impl DeploymentRelease {
112 pub fn new(
113 release_id: ReleaseId,
114 artifacts: impl IntoIterator<Item = DeploymentArtifact>,
115 ) -> Result<Self, AssetModelError> {
116 let artifacts = artifacts.into_iter().collect::<Vec<_>>();
117 if artifacts.is_empty() {
118 return Err(AssetModelError::EmptyField {
119 field: "deployment_artifacts",
120 });
121 }
122
123 Ok(Self {
124 release_id,
125 artifacts,
126 })
127 }
128
129 pub fn release_id(&self) -> &ReleaseId {
130 &self.release_id
131 }
132
133 pub fn artifacts(&self) -> &[DeploymentArtifact] {
134 &self.artifacts
135 }
136
137 pub fn publish(
138 &self,
139 planner: &StoragePlanner,
140 cdn_base_url: &str,
141 ) -> Result<ActiveAssetManifest, AssetModelError> {
142 let context = DeliveryContext::default().with_cdn_base_url(cdn_base_url);
143 let mut entries = BTreeMap::new();
144
145 for artifact in &self.artifacts {
146 let storage_plan = planner
147 .plan_scalable_write(
148 StoragePlanRequest::new(artifact.hashed_path())
149 .with_override(public_deployment_override()),
150 )
151 .map_err(AssetModelError::Storage)?;
152
153 if storage_plan.policy != StoragePolicy::public_asset() {
154 return Err(AssetModelError::InvalidDeploymentPolicy {
155 logical_path: artifact.logical_path().to_string(),
156 policy: storage_plan.policy,
157 });
158 }
159
160 let delivery = public_delivery_plan(
161 AssetKind::DeploymentArtifact,
162 &storage_plan,
163 None,
164 &context,
165 true,
166 )?;
167
168 match entries.entry(artifact.logical_path().to_string()) {
169 Entry::Vacant(entry) => {
170 entry.insert(PublishedDeploymentArtifact {
171 artifact: artifact.clone(),
172 delivery,
173 });
174 }
175 Entry::Occupied(_) => {
176 return Err(AssetModelError::DuplicateDeploymentArtifact {
177 release_id: self.release_id.to_string(),
178 logical_path: artifact.logical_path().to_string(),
179 });
180 }
181 }
182 }
183
184 Ok(ActiveAssetManifest {
185 release_id: self.release_id.clone(),
186 entries,
187 })
188 }
189}