|
| 1 | +//===----------------------------------------------------------------------===// |
| 2 | +// |
| 3 | +// This source file is part of the Soto for AWS open source project |
| 4 | +// |
| 5 | +// Copyright (c) 2026 the Soto project authors |
| 6 | +// Licensed under Apache License v2.0 |
| 7 | +// |
| 8 | +// See LICENSE.txt for license information |
| 9 | +// See CONTRIBUTORS.txt for the list of Soto project authors |
| 10 | +// |
| 11 | +// SPDX-License-Identifier: Apache-2.0 |
| 12 | +// |
| 13 | +//===----------------------------------------------------------------------===// |
| 14 | + |
| 15 | +import Foundation |
| 16 | + |
| 17 | +#if os(Linux) |
| 18 | +import FoundationNetworking |
| 19 | +#endif |
| 20 | + |
| 21 | +@available(macOS 13, *) |
| 22 | +struct Downloader { |
| 23 | + let directory: GitHubTree |
| 24 | + let ghAPI: GitHubAPI |
| 25 | + |
| 26 | + static func setup( |
| 27 | + ghToken: String? |
| 28 | + ) async throws(DownloaderError) -> Downloader { |
| 29 | + let ghAPI = GitHubAPI(ghToken: ghToken) |
| 30 | + /// Get git commit hash from soto and download models using that hash |
| 31 | + let hash = try await getModelHash(path: "/soto-project/soto/refs/heads/main/.aws-model-hash", ghAPI: ghAPI) |
| 32 | + let directory = try await getAPIModelsAWS(hash: hash, ghAPI: ghAPI) |
| 33 | + return .init(directory: directory, ghAPI: ghAPI) |
| 34 | + } |
| 35 | + |
| 36 | + /// Get current git hash for models used by Soto |
| 37 | + static func getModelHash(path: String, ghAPI: GitHubAPI) async throws(DownloaderError) -> String { |
| 38 | + let file = try await ghAPI.downloadRawFile(path: path) |
| 39 | + return String(decoding: file, as: UTF8.self).filter { !$0.isNewline && !$0.isWhitespace } |
| 40 | + } |
| 41 | + |
| 42 | + /// Get api-models-aws tree |
| 43 | + static func getAPIModelsAWS(hash: String, ghAPI: GitHubAPI) async throws(DownloaderError) -> GitHubTree { |
| 44 | + // get https://github.com/aws/api-models-aws.git tree |
| 45 | + let data = try await ghAPI.callGitHubAPI(path: "/repos/aws/api-models-aws/git/trees/\(hash)?recursive=1") |
| 46 | + do { |
| 47 | + return try JSONDecoder().decode(GitHubTree.self, from: data) |
| 48 | + } catch { |
| 49 | + throw .failedToDecode("GitHub trees response", error) |
| 50 | + } |
| 51 | + } |
| 52 | + |
| 53 | + /// Get GitHub resource, using URL from tree output |
| 54 | + private func getGitHubResource(url: URL) async throws(DownloaderError) -> String { |
| 55 | + do { |
| 56 | + let data = try await self.ghAPI.callGitHubAPI(url: url) |
| 57 | + let resource: GitHubResource |
| 58 | + do { |
| 59 | + resource = try JSONDecoder().decode(GitHubResource.self, from: data) |
| 60 | + } catch { |
| 61 | + throw .failedToDecode("GitHub trees response", error) |
| 62 | + } |
| 63 | + let encodedContent = resource.content.filter { !$0.isNewline && !$0.isWhitespace } |
| 64 | + guard let content = Data(base64Encoded: encodedContent) else { |
| 65 | + throw .failedToBase64Decode(url.absoluteString) |
| 66 | + } |
| 67 | + return String(decoding: content, as: UTF8.self) |
| 68 | + } |
| 69 | + } |
| 70 | + |
| 71 | + func writeServiceFile(_ service: String, to folder: String) async throws(DownloaderError) { |
| 72 | + guard let entry = directory.tree.first(where: { $0.path.hasPrefix("models/\(service)/") && $0.path.hasSuffix(".json") }) else { |
| 73 | + throw .failedToFindService(service) |
| 74 | + } |
| 75 | + let fileContents = try await getGitHubResource(url: entry.url) |
| 76 | + let target = URL(fileURLWithPath: folder).appending(path: "\(service).json") |
| 77 | + do { |
| 78 | + try FileManager.default.createDirectory(atPath: folder, withIntermediateDirectories: true) |
| 79 | + print("Writing: \(target.relativePath)") |
| 80 | + try fileContents.write(to: target, atomically: true, encoding: .utf8) |
| 81 | + } catch { |
| 82 | + throw .failedToWriteFile(target.absoluteString) |
| 83 | + } |
| 84 | + } |
| 85 | + |
| 86 | + func writeEndpoints(to folder: String) async throws(DownloaderError) { |
| 87 | + let endpoints = try await self.ghAPI.downloadRawFile( |
| 88 | + path: |
| 89 | + "/aws/aws-sdk-go-v2/refs/heads/main/codegen/smithy-aws-go-codegen/src/main/resources/software/amazon/smithy/aws/go/codegen/endpoints.json" |
| 90 | + ) |
| 91 | + let target = URL(fileURLWithPath: folder).appending(path: "endpoints.json") |
| 92 | + do { |
| 93 | + print("Writing: \(target.relativePath)") |
| 94 | + try endpoints.write(to: target) |
| 95 | + } catch { |
| 96 | + throw .failedToWriteFile(target.absoluteString) |
| 97 | + } |
| 98 | + } |
| 99 | + |
| 100 | + func loadConfig(path: String) async throws(DownloaderError) -> ConfigFile { |
| 101 | + let file: Data |
| 102 | + do { |
| 103 | + file = try Data(contentsOf: URL(fileURLWithPath: path)) |
| 104 | + } catch { |
| 105 | + throw .failedToLoad(path) |
| 106 | + } |
| 107 | + do { |
| 108 | + return try JSONDecoder().decode(ConfigFile.self, from: file) |
| 109 | + } catch { |
| 110 | + throw .failedToDecode(path, error) |
| 111 | + } |
| 112 | + } |
| 113 | +} |
0 commit comments