Skip to content

Commit f6d227c

Browse files
committed
Add app & locale details requests
1 parent 0da29d6 commit f6d227c

File tree

6 files changed

+264
-0
lines changed

6 files changed

+264
-0
lines changed

Sources/API/APIProvider+Apps.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,35 @@ public extension APIProvider {
3232
selector: selector
3333
))
3434
}
35+
36+
/// Fetches app metadata.
37+
///
38+
/// - Parameters:
39+
/// - adamId: Your unique App Store app identifier.
40+
///
41+
/// - Returns: An object of type `AppDetails`.
42+
///
43+
/// - Throws: An error of type `APIError`.
44+
func appDetails(adamId: Int) async throws -> Response<AppDetails> {
45+
try await provider.requestDataModel(from: AppDetailsRequest(adamId: adamId))
46+
}
47+
48+
/// Fetches the localized default product page for an app.
49+
///
50+
/// - Parameters:
51+
/// - adamId: Your unique App Store app identifier.
52+
/// - parameters: Detailed app asset details of a device.
53+
///
54+
/// - Returns: An object of type `AppLocaleDetails`.
55+
///
56+
/// - Throws: An error of type `APIError`.
57+
func appLocaleDetails(
58+
adamId: Int,
59+
parameters: AppLocaleDetailsParameters? = nil
60+
) async throws -> Response<AppLocaleDetails> {
61+
try await provider.requestDataModel(from: AppLocaleDetailsRequest(
62+
adamId: adamId,
63+
parameters: parameters
64+
))
65+
}
3566
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import Foundation
2+
3+
/// The media detail object.
4+
public struct AppDetails: Codable, Hashable, Sendable, Identifiable {
5+
/// A unique ID.
6+
public let id: String
7+
/// Your unique App Store app identifier.
8+
public let adamId: Int
9+
/// The name of an app.
10+
public let appName: String
11+
/// The name of the app author.
12+
public let artistName: String
13+
/// Countries or regions where the app is available.
14+
public let availableStorefronts: [String]
15+
/// Device classes supported.
16+
public let deviceClasses: [DeviceClass]
17+
/// The URL of the image asset.
18+
public let iconPictureUrl: URL
19+
/// Indicates if the app is a pre-order.
20+
public let isPreOrder: Bool
21+
/// The genre of the app.
22+
public let primaryGenre: String
23+
/// The secondary genre of the app.
24+
public let secondaryGenre: String
25+
/// The primary language of the app.
26+
public let primaryLanguage: String
27+
28+
public init(
29+
id: String,
30+
adamId: Int,
31+
appName: String,
32+
artistName: String,
33+
availableStorefronts: [String],
34+
deviceClasses: [DeviceClass],
35+
iconPictureUrl: URL,
36+
isPreOrder: Bool,
37+
primaryGenre: String,
38+
secondaryGenre: String,
39+
primaryLanguage: String
40+
) {
41+
self.id = id
42+
self.adamId = adamId
43+
self.appName = appName
44+
self.artistName = artistName
45+
self.availableStorefronts = availableStorefronts
46+
self.deviceClasses = deviceClasses
47+
self.iconPictureUrl = iconPictureUrl
48+
self.isPreOrder = isPreOrder
49+
self.primaryGenre = primaryGenre
50+
self.secondaryGenre = secondaryGenre
51+
self.primaryLanguage = primaryLanguage
52+
}
53+
54+
public init(from decoder: any Decoder) throws {
55+
let container = try decoder.container(keyedBy: CodingKeys.self)
56+
self.id = try container.decode(String.self, forKey: .id)
57+
self.adamId = try container.decode(Int.self, forKey: .adamId)
58+
self.appName = try container.decode(String.self, forKey: .appName)
59+
self.artistName = try container.decode(String.self, forKey: .artistName)
60+
self.availableStorefronts = try container.decode([String].self, forKey: .availableStorefronts)
61+
self.deviceClasses = try container.decode([DeviceClass].self, forKey: .deviceClasses)
62+
self.iconPictureUrl = try container.decode(URL.self, forKey: .iconPictureUrl)
63+
self.primaryGenre = try container.decode(String.self, forKey: .primaryGenre)
64+
self.secondaryGenre = try container.decode(String.self, forKey: .secondaryGenre)
65+
self.primaryLanguage = try container.decode(String.self, forKey: .primaryLanguage)
66+
self.isPreOrder = try {
67+
if let isPreOrder = try? container.decode(Bool.self, forKey: .isPreOrder) {
68+
return isPreOrder
69+
} else if let isPreOrderString = try? container.decode(String.self, forKey: .isPreOrder),
70+
let isPreOrder = Bool(isPreOrderString) {
71+
return isPreOrder
72+
} else {
73+
throw DecodingError.dataCorrupted(DecodingError.Context(
74+
codingPath: [CodingKeys.isPreOrder],
75+
debugDescription: "Data corrupted"
76+
))
77+
}
78+
}()
79+
}
80+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import Foundation
2+
3+
/// The media locale detail object.
4+
public struct AppLocaleDetails: Codable, Hashable, Sendable {
5+
public struct AppPreviewDeviceWithAssets: Codable, Hashable, Sendable {
6+
public let appPreviewDeviceFallBackDevices: [String]?
7+
public let screenshots: [Asset]?
8+
public let appPreviews: [Asset]?
9+
10+
public init(
11+
appPreviewDeviceFallBackDevices: [String]? = nil,
12+
screenshots: [Asset]? = nil,
13+
appPreviews: [Asset]? = nil
14+
) {
15+
self.appPreviewDeviceFallBackDevices = appPreviewDeviceFallBackDevices
16+
self.screenshots = screenshots
17+
self.appPreviews = appPreviews
18+
}
19+
}
20+
21+
public struct Asset: Codable, Hashable, Sendable, Identifiable {
22+
public let assetGenId: String
23+
public let assetToken: String
24+
public let assetUrl: URL
25+
public let appPreviewDevice: String
26+
public let sortPosition: Int
27+
public let sourceHeight: Int
28+
public let sourceWidth: Int
29+
public let orientation: MediaAssetOrientation
30+
public let assetType: MediaAssetType
31+
public let checksum: String
32+
public let pictureUrl: URL?
33+
public let videoUrl: URL?
34+
public let assetDuplicationType: String?
35+
36+
public var id: String { assetGenId }
37+
38+
public init(
39+
assetGenId: String,
40+
assetToken: String,
41+
assetUrl: URL,
42+
appPreviewDevice: String,
43+
sortPosition: Int,
44+
sourceHeight: Int,
45+
sourceWidth: Int,
46+
orientation: MediaAssetOrientation,
47+
assetType: MediaAssetType,
48+
checksum: String,
49+
pictureUrl: URL?,
50+
videoUrl: URL?,
51+
assetDuplicationType: String?
52+
) {
53+
self.assetGenId = assetGenId
54+
self.assetToken = assetToken
55+
self.assetUrl = assetUrl
56+
self.appPreviewDevice = appPreviewDevice
57+
self.sortPosition = sortPosition
58+
self.sourceHeight = sourceHeight
59+
self.sourceWidth = sourceWidth
60+
self.orientation = orientation
61+
self.assetType = assetType
62+
self.checksum = checksum
63+
self.pictureUrl = pictureUrl
64+
self.videoUrl = videoUrl
65+
self.assetDuplicationType = assetDuplicationType
66+
}
67+
}
68+
69+
/// The name of an app.
70+
public let appName: String
71+
/// The subtitle for the app.
72+
public let subTitle: String
73+
/// An abbreviated description of the app.
74+
public let shortDescription: String
75+
/// The app language.
76+
public let language: String
77+
/// The language of the primary locale for the app.
78+
public let isPrimaryLocale: Bool
79+
/// The device display name, fallback device display names, and associated assets.
80+
public let appPreviewDeviceWithAssets: [String: AppPreviewDeviceWithAssets]?
81+
82+
public init(
83+
appName: String,
84+
subTitle: String,
85+
shortDescription: String,
86+
language: String,
87+
isPrimaryLocale: Bool,
88+
appPreviewDeviceWithAssets: [String: AppPreviewDeviceWithAssets]? = nil
89+
) {
90+
self.appName = appName
91+
self.subTitle = subTitle
92+
self.shortDescription = shortDescription
93+
self.language = language
94+
self.isPrimaryLocale = isPrimaryLocale
95+
self.appPreviewDeviceWithAssets = appPreviewDeviceWithAssets
96+
}
97+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
struct AppDetailsRequest: RequestType {
2+
let path: String
3+
let method = HTTPMethod.get
4+
5+
init(adamId: Int) {
6+
path = "/api/v5/apps/\(adamId)"
7+
}
8+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
public struct AppLocaleDetailsParameters: Hashable, Encodable, Sendable {
2+
/// Detailed app asset details of a device. Use `true` for expanded values in the API response.
3+
public let expand: Bool
4+
5+
public init(expand: Bool) {
6+
self.expand = expand
7+
}
8+
}
9+
10+
struct AppLocaleDetailsRequest: RequestType {
11+
let path: String
12+
let method = HTTPMethod.get
13+
let query: RequestQuery?
14+
15+
init(adamId: Int, parameters: AppLocaleDetailsParameters?) {
16+
path = "/api/v5/apps/\(adamId)/locale-details"
17+
query = RequestQuery(parameters)
18+
}
19+
}

Tests/Snapshots/AppsRequestsTests.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,33 @@ final class AppsRequestsTests: SnapshotTestCase {
6868
"""
6969
}
7070
}
71+
72+
func testDetailsRequest() async throws {
73+
try await assertRequest(AppDetailsRequest(adamId: 12345)) {
74+
"""
75+
GET https://api.searchads.apple.com/api/v5/apps/12345
76+
Accept-Encoding: gzip;q=1.0, compress;q=0.5
77+
Accept-Language: en-US;q=1.0
78+
Authorization: Bearer token
79+
Host: api.searchads.apple.com
80+
X-Ap-Context: orgId=12345
81+
"""
82+
}
83+
}
84+
85+
func testLocaleDetailsRequest() async throws {
86+
try await assertRequest(AppLocaleDetailsRequest(
87+
adamId: 12345,
88+
parameters: AppLocaleDetailsParameters(expand: true)
89+
)) {
90+
"""
91+
GET https://api.searchads.apple.com/api/v5/apps/12345/locale-details?expand=true
92+
Accept-Encoding: gzip;q=1.0, compress;q=0.5
93+
Accept-Language: en-US;q=1.0
94+
Authorization: Bearer token
95+
Host: api.searchads.apple.com
96+
X-Ap-Context: orgId=12345
97+
"""
98+
}
99+
}
71100
}

0 commit comments

Comments
 (0)