9
use reqwest::Method;
10
use serde::{Deserialize, Serialize};
11
use smbcloud_ascapi_core::jsonapi::{
12
- CreateBody, CreateData, Document, Resource, ResourceId, ToOne, UpdateRelationshipsBody,
13
- UpdateRelationshipsData,
12
+ CreateBody, CreateData, Document, Resource, ResourceId, ToOne, UpdateBody, UpdateData,
13
+ UpdateRelationshipsBody, UpdateRelationshipsData,
14
};
15
use smbcloud_ascapi_core::Client;
16
use smbcloud_ascapi_core::Result;
68
pub app: ToOne,
69
}
70
71
+/// Editable attributes for a `PATCH /v1/appStoreVersions/{id}`. App Store
72
+/// Connect has no "rename a version" action: changing `version_string` on
73
+/// the platform's editable version (e.g. a staged `1.0.9` to `1.1.0`) is a
74
+/// plain attribute update. Only the fields set here are sent, so passing
75
+/// just `version_string` leaves `copyright` untouched. The version must be
76
+/// in an editable state (`PREPARE_FOR_SUBMISSION`, `DEVELOPER_REJECTED`).
77
+#[derive(Debug, Clone, Default, Serialize)]
78
+#[serde(rename_all = "camelCase")]
79
+pub struct AppStoreVersionUpdateFields {
80
+ #[serde(skip_serializing_if = "Option::is_none")]
81
+ pub version_string: Option<String>,
82
+ #[serde(skip_serializing_if = "Option::is_none")]
83
+ pub copyright: Option<String>,
84
+}
85
+
86
/// Per-platform version records, the unit the store reviews.
87
///
88
/// An extension trait rather than inherent methods, because `Client`
108
attributes: AppStoreVersionCreateAttributes,
109
) -> Result<AppStoreVersion>;
110
111
+ /// `PATCH /v1/appStoreVersions/{id}` with attributes — e.g. change the
112
+ /// version string of the platform's editable version. Apple exposes no
113
+ /// separate rename action; this is how `1.0.9` becomes `1.1.0`.
114
+ async fn update_app_store_version(
115
+ &self,
116
+ id: &str,
117
+ fields: AppStoreVersionUpdateFields,
118
+ ) -> Result<AppStoreVersion>;
119
+
120
/// `DELETE /v1/appStoreVersions/{id}` — only allowed while the version
121
/// hasn't been submitted for review.
122
async fn delete_app_store_version(&self, id: &str) -> Result<()>;
182
Ok(doc.data)
183
}
184
185
+ async fn update_app_store_version(
186
+ &self,
187
+ id: &str,
188
+ fields: AppStoreVersionUpdateFields,
189
+ ) -> Result<AppStoreVersion> {
190
+ let path = format!("/v1/appStoreVersions/{id}");
191
+ let body = UpdateBody {
192
+ data: UpdateData {
193
+ resource_type: RESOURCE_TYPE,
194
+ id: id.to_string(),
195
+ attributes: fields,
196
+ },
197
+ };
198
+ let doc: Document<AppStoreVersionAttributes> =
199
+ self.request(Method::PATCH, &path, &[], Some(&body)).await?;
200
+ Ok(doc.data)
201
+ }
202
+
203
async fn delete_app_store_version(&self, id: &str) -> Result<()> {
204
let path = format!("/v1/appStoreVersions/{id}");
205
self.request_no_content::<()>(Method::DELETE, &path, &[], None)
240
pub struct AppStoreVersionBuildRelationship {
241
pub build: ToOne,
242
}
243
+
244
+#[cfg(test)]
245
+mod tests {
246
+ use super::*;
247
+ use serde_json::json;
248
+
249
+ /// `PATCH` responses are deserialized as a whole
250
+ /// [`AppStoreVersionAttributes`], so every attribute except `platform`
251
+ /// has to stay optional — otherwise a response that carries only the
252
+ /// platform (as a rename does) fails to parse after App Store Connect
253
+ /// has already applied the update. Making one of these required turns
254
+ /// this into a failure.
255
+ #[test]
256
+ fn a_version_response_omitting_every_optional_attribute_still_parses() {
257
+ let doc: Document<AppStoreVersionAttributes> = serde_json::from_value(json!({
258
+ "data": {
259
+ "id": "b1f9f3a2-6f9a-4a4e-8a4f-3f5b0a9c0d11",
260
+ "type": "appStoreVersions",
261
+ "attributes": { "platform": "IOS" }
262
+ }
263
+ }))
264
+ .unwrap();
265
+
266
+ assert_eq!(doc.data.attributes.platform, Platform::Ios);
267
+ assert!(doc.data.attributes.version_string.is_none());
268
+ assert!(doc.data.attributes.copyright.is_none());
269
+ }
270
+
271
+ /// The other half of that contract: a response that does carry the
272
+ /// attributes round-trips, so `versionString` is read back from the
273
+ /// rename request's own reply.
274
+ #[test]
275
+ fn a_version_response_reads_back_the_renamed_string() {
276
+ let doc: Document<AppStoreVersionAttributes> = serde_json::from_value(json!({
277
+ "data": {
278
+ "id": "b1f9f3a2-6f9a-4a4e-8a4f-3f5b0a9c0d11",
279
+ "type": "appStoreVersions",
280
+ "attributes": {
281
+ "platform": "MAC_OS",
282
+ "versionString": "1.1.0",
283
+ "appVersionState": "PREPARE_FOR_SUBMISSION",
284
+ "copyright": "2026 SplitFire AB"
285
+ }
286
+ }
287
+ }))
288
+ .unwrap();
289
+
290
+ assert_eq!(doc.data.attributes.platform, Platform::MacOs);
291
+ assert_eq!(doc.data.attributes.version_string.as_deref(), Some("1.1.0"));
292
+ assert_eq!(
293
+ doc.data.attributes.app_version_state.as_deref(),
294
+ Some("PREPARE_FOR_SUBMISSION")
295
+ );
296
+ }
297
+}