Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

feat(swift): cts client #2610

Merged
merged 12 commits into from
Jan 30, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Foundation

public protocol Credentials {
/// ApplicationID to target. Is passed as a HTTP header.
var applicationID: String { get }
var appId: String { get }

/**
* APIKey for a given ApplicationID. Is passed as a HTTP header.
Expand Down
Fluf22 marked this conversation as resolved.
Show resolved Hide resolved

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ public enum AlgoliaError: Error, LocalizedError {
case noReachableHosts(intermediateErrors: [Error])
case missingData
case decodingFailure(Error)
case runtimeError(String)
case invalidCredentials(String)
case invalidArgument(String, String)

public var errorDescription: String? {
switch self {
Expand All @@ -27,6 +30,12 @@ public enum AlgoliaError: Error, LocalizedError {
return "Missing response data"
case .decodingFailure:
return "Response decoding failed"
case let .runtimeError(error):
return "\(error)"
case let .invalidCredentials(credential):
return "`\(credential)` is missing."
case let .invalidArgument(argument, operationId):
return "Parameter `\(argument)` is required when calling `\(operationId)`."
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,13 @@ open class Transporter {
request = request.set(\.timeoutInterval, to: timeout)

for (key, value) in configuration.defaultHeaders ?? [:] {
request.setValue(value, forHTTPHeaderField: key.lowercased())
request.setValue(value, forHTTPHeaderField: key.capitalized)
}
request.setValue(
UserAgentController.httpHeaderValue, forHTTPHeaderField: "User-Agent".lowercased()
UserAgentController.httpHeaderValue, forHTTPHeaderField: "User-Agent".capitalized
)
for (key, value) in headers {
request.setValue(value, forHTTPHeaderField: key.lowercased())
request.setValue(value, forHTTPHeaderField: key.capitalized)
}

if callType == CallType.write {
Expand Down Expand Up @@ -126,16 +126,4 @@ open class Transporter {

throw AlgoliaError.noReachableHosts(intermediateErrors: intermediateErrors)
}

private func buildHeaders(with requestHeaders: [String: String]) -> [String: String] {
var httpHeaders: [String: String] = [:]
for (key, value) in configuration.defaultHeaders ?? [:] {
httpHeaders.updateValue(value, forKey: key)
}
httpHeaders.updateValue(UserAgentController.httpHeaderValue, forKey: "User-Agent")
for (key, value) in requestHeaders {
httpHeaders.updateValue(value, forKey: key)
}
return httpHeaders
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public enum UserAgentController {
}

public internal(set) static var extensions: [UserAgentExtending] = [
UserAgent.operatingSystem, UserAgent.library,
UserAgent.library, UserAgent.operatingSystem,
]

public static var httpHeaderValue: String {
Expand Down
1 change: 1 addition & 0 deletions config/generation.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -149,4 +149,5 @@ export const patterns = [
'clients/algoliasearch-client-swift/Sources/Core/Helpers/Version.swift',

'tests/output/swift/Package.swift',
'!tests/output/swift/Utils/**',
];
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,20 @@ public void run(Map<String, CodegenModel> models, Map<String, CodegenOperation>
}
paramsType.enhanceParameters(step.parameters, stepOut, ope);

// Swift is strongly-typed and compiled language,
// it can't have nil object when something is expected
if (language.equals("swift")) {
@SuppressWarnings("unchecked")
Fluf22 marked this conversation as resolved.
Show resolved Hide resolved
var isNotTestable =
step.type != null &&
step.type.equals("method") &&
((List<Map<String, Object>>) stepOut.getOrDefault("parametersWithDataType", new ArrayList<>())).stream()
.anyMatch(item -> (boolean) item.getOrDefault("isNullObject", false));
if (isNotTestable) {
continue;
}
}

if (step.expected.type != null) {
switch (step.expected.type) {
case "userAgent":
Expand Down
2 changes: 1 addition & 1 deletion playground/swift/playground/playground/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Task {
.init(firstname: "Warren", lastname: "Speach", followers: 42, company: "Norwalk Crmc")
]

let client = SearchClient(applicationID: applicationID, apiKey: apiKey)
let client = SearchClient(appId: applicationID, apiKey: apiKey)

for contact in contacts {
let saveObjRes = try await client.saveObject(indexName: "contacts", body: contact)
Expand Down
3 changes: 3 additions & 0 deletions scripts/cts/runCts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ async function runCtsOne(language: string): Promise<void> {
case 'scala':
await run('sbt test', { cwd, language });
break;
case 'swift':
await run('rm -rf .build && swift test -q --parallel', { cwd, language });
Fluf22 marked this conversation as resolved.
Show resolved Hide resolved
break;
default:
spinner.warn(`skipping unknown language '${language}' to run the CTS`);
return;
Expand Down
14 changes: 11 additions & 3 deletions templates/swift/Version.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,21 @@ public struct Version {
let components = version.components(separatedBy: ".")

guard components.count >= 3 else {
fatalError("version is not formatted correctly")
fatalError("version is not formatted correctly")
}

self.major = Int(components[0]) ?? 0
self.minor = Int(components[1]) ?? 0
self.patch = Int(components[2]) ?? 0
self.prereleaseIdentifier = components.count == 4 ? components[3] : nil

if components.count == 4 {
let prereleaseComponents = components[2].components(separatedBy: "-")

self.patch = Int(prereleaseComponents[0]) ?? 0
self.prereleaseIdentifier = "\(prereleaseComponents[1]).\(components[3])"
} else {
self.patch = Int(components[2]) ?? 0
self.prereleaseIdentifier = nil
}
}

}
Expand Down
21 changes: 17 additions & 4 deletions templates/swift/api.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ typealias Client = {{classname}}
private var configuration: Configuration
private var transporter: Transporter

var applicationID: String {
self.configuration.applicationID
var appId: String {
self.configuration.appId
}

public init(configuration: Configuration, transporter: Transporter) {
Expand All @@ -28,8 +28,8 @@ typealias Client = {{classname}}
self.init(configuration: configuration, transporter: Transporter(configuration: configuration))
}

public convenience init(applicationID: String, apiKey: String{{#hasRegionalHost}}, region: Region{{#fallbackToAliasHost}}?{{/fallbackToAliasHost}}{{/hasRegionalHost}}) throws {
self.init(configuration: try Configuration(applicationID: applicationID, apiKey: apiKey{{#hasRegionalHost}}, region: region{{/hasRegionalHost}}))
public convenience init(appId: String, apiKey: String{{#hasRegionalHost}}, region: Region{{#fallbackToAliasHost}}?{{/fallbackToAliasHost}}{{/hasRegionalHost}}) throws {
self.init(configuration: try Configuration(appId: appId, apiKey: apiKey{{#hasRegionalHost}}, region: region{{/hasRegionalHost}}))
}

{{#operation}}
Expand Down Expand Up @@ -105,6 +105,19 @@ typealias Client = {{classname}}
{{/isDeprecated}}
{{^returnType}}@discardableResult{{/returnType}}
{{#vendorExtensions}}{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}open{{/nonPublicApi}} func {{operationId}}WithHTTPInfo({{#allParams}}{{paramName}}: {{#isEnum}}{{#isContainer}}[{{enumName}}_{{operationId}}]{{/isContainer}}{{^isContainer}}{{enumName}}_{{operationId}}{{/isContainer}}{{/isEnum}}{{^isEnum}}{{#lambda.to-codable}}{{{dataType}}}{{/lambda.to-codable}}{{/isEnum}}{{^required}}? = nil{{/required}}, {{/allParams}}requestOptions userRequestOptions: RequestOptions? = nil) async throws -> Response<{{{returnType}}}{{#returnType}}{{#isResponseOptional}}?{{/isResponseOptional}}{{/returnType}}{{^returnType}}AnyCodable{{/returnType}}> {
{{#pathParams}}{{#isString}}{{#required}}guard !{{{paramName}}}.isEmpty else {
throw AlgoliaError.invalidArgument("{{{paramName}}}", "{{{operationId}}}")
}

{{/required}}{{/isString}}{{/pathParams}}{{#queryParams}}{{#isString}}{{#required}}guard !{{{paramName}}}.isEmpty else {
throw AlgoliaError.invalidArgument("{{{paramName}}}", "{{{operationId}}}")
}

{{/required}}{{/isString}}{{/queryParams}}{{#bodyParam}}{{#isFreeFormObject}}{{#required}}guard !{{{paramName}}}.isEmpty else {
throw AlgoliaError.invalidArgument("{{{paramName}}}", "{{{operationId}}}")
}

{{/required}}{{/isFreeFormObject}}{{/bodyParam}}
{{^pathParams}}let{{/pathParams}}{{#pathParams}}{{#-first}}var{{/-first}}{{/pathParams}} resourcePath = "{{{path}}}"{{#pathParams}}
let {{paramName}}PreEscape = "\({{#isEnum}}{{paramName}}{{#isContainer}}{{{dataType}}}{{/isContainer}}{{^isContainer}}.rawValue{{/isContainer}}{{/isEnum}}{{^isEnum}}APIHelper.mapValueToPathItem({{paramName}}){{/isEnum}})"
let {{paramName}}PostEscape = {{paramName}}PreEscape.addingPercentEncoding(withAllowedCharacters: .{{#x-is-custom-request}}urlPathAllowed{{/x-is-custom-request}}{{^x-is-custom-request}}urlPathAlgoliaAllowed{{/x-is-custom-request}}) ?? ""
Expand Down
41 changes: 30 additions & 11 deletions templates/swift/client_configuration.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import Core
import AnyCodable
#endif

typealias {{#lambda.client-to-name}}{{{client}}}{{/lambda.client-to-name}}ClientConfiguration = Configuration

public struct Configuration: Core.Configuration, Credentials {
{{#hasRegionalHost}}

Expand All @@ -14,7 +16,7 @@ public struct Configuration: Core.Configuration, Credentials {
]
{{/hasRegionalHost}}

public let applicationID: String
public let appId: String
public let apiKey: String
public var writeTimeout: TimeInterval
public var readTimeout: TimeInterval
Expand All @@ -24,33 +26,41 @@ public struct Configuration: Core.Configuration, Credentials {

public let batchSize: Int{{/isSearchClient}}

init(applicationID: String,
init(appId: String,
apiKey: String,{{#hasRegionalHost}}
region: Region{{#fallbackToAliasHost}}? = nil{{/fallbackToAliasHost}},{{/hasRegionalHost}}
writeTimeout: TimeInterval = DefaultConfiguration.default.writeTimeout,
readTimeout: TimeInterval = DefaultConfiguration.default.readTimeout,
logLevel: LogLevel = DefaultConfiguration.default.logLevel,
defaultHeaders: [String: String]? = DefaultConfiguration.default.defaultHeaders{{#isSearchClient}},
batchSize: Int = 1000{{/isSearchClient}}) throws {
self.applicationID = applicationID
guard !appId.isEmpty else {
throw AlgoliaError.invalidCredentials("appId")
}

guard !apiKey.isEmpty else {
throw AlgoliaError.invalidCredentials("apiKey")
}

self.appId = appId
self.apiKey = apiKey
self.writeTimeout = writeTimeout
self.readTimeout = readTimeout
self.logLevel = logLevel
self.defaultHeaders = [
"X-Algolia-Application-Id": applicationID,
"X-Algolia-Application-Id": appId,
"X-Algolia-API-Key": apiKey,
"Content-Type": "application/json"
].merging(defaultHeaders ?? [:]) { (_, new) in new }{{#isSearchClient}}

self.batchSize = batchSize{{/isSearchClient}}

{{^hasRegionalHost}}
{{^uniqueHost}}{{^hasRegionalHost}}
func buildHost(_ components: (suffix: String, callType: RetryableHost.CallTypeSupport)) throws
-> RetryableHost
{
guard let url = URL(string: "https://\(applicationID)\(components.suffix)") else {
throw GenericError(description: "Malformed URL")
guard let url = URL(string: "https://\(appId)\(components.suffix)") else {
throw AlgoliaError.runtimeError("Malformed URL")
}

return RetryableHost(url: url, callType: components.callType)
Expand All @@ -68,10 +78,19 @@ public struct Configuration: Core.Configuration, Credentials {
].map(buildHost).shuffled()

self.hosts = hosts + commonHosts
{{/hasRegionalHost}}
{{/hasRegionalHost}}{{/uniqueHost}}
{{#uniqueHost}}
guard let url = URL(string: "https://{{{.}}}") else {
throw AlgoliaError.runtimeError("Malformed URL")
}

self.hosts = [
.init(url: url)
]
{{/uniqueHost}}
{{#hasRegionalHost}}
guard {{#fallbackToAliasHost}}region == nil || {{/fallbackToAliasHost}}authorizedRegions.contains(region{{#fallbackToAliasHost}}!{{/fallbackToAliasHost}}) else {
throw GenericError(description:
throw AlgoliaError.runtimeError(
"`region` {{^fallbackToAliasHost}}is required and {{/fallbackToAliasHost}}must be one of the following: \(authorizedRegions.map { $0.rawValue }.joined(separator: ", "))"
)
}
Expand All @@ -80,7 +99,7 @@ public struct Configuration: Core.Configuration, Credentials {
if let region = region {
{{/fallbackToAliasHost}}
guard let url = URL(string: "https://{{{regionalHost}}}".replacingOccurrences(of: "{region}", with: region.rawValue)) else {
throw GenericError(description: "Malformed URL")
throw AlgoliaError.runtimeError("Malformed URL")
}

self.hosts = [
Expand All @@ -89,7 +108,7 @@ public struct Configuration: Core.Configuration, Credentials {
{{#fallbackToAliasHost}}
} else {
guard let url = URL(string: "https://{{{hostWithFallback}}}") else {
throw GenericError(description: "Malformed URL")
throw AlgoliaError.runtimeError("Malformed URL")
}

self.hosts = [
Expand Down
21 changes: 19 additions & 2 deletions templates/swift/tests/Package.mustache
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,32 @@ let package = Package(
name: "AlgoliaSearchClientTests",
dependencies: [
.package(url: "https://github.com/Flight-School/AnyCodable", .upToNextMajor(from: "0.6.1")),
.package(url: "https://github.com/SwiftyJSON/SwiftyJSON.git", from: "5.0.0"),
.package(path: "../../../clients/algoliasearch-client-swift"),
],
targets: [
.target(
name: "Utils",
dependencies: [
.product(name: "AnyCodable", package: "AnyCodable")
],
path: "Tests/Utils"
),
.testTarget(
name: "client",
dependencies: [
.product(name: "AnyCodable", package: "AnyCodable"),
.target(name: "Utils"),{{#packageList}}
.product(
name: "{{.}}",
package: "algoliasearch-client-swift"
),{{/packageList}}
]
),
.testTarget(
name: "requests",
dependencies: [
.product(name: "AnyCodable", package: "AnyCodable"),
.product(name: "SwiftyJSON", package: "SwiftyJSON"),{{#packageList}}
.target(name: "Utils"),{{#packageList}}
.product(
name: "{{.}}",
package: "algoliasearch-client-swift"
Expand Down
3 changes: 3 additions & 0 deletions templates/swift/tests/client/createClient.mustache
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
let configuration: {{import}}.Configuration = try {{import}}.Configuration(appId: "{{parametersWithDataTypeMap.appId.value}}", apiKey: "{{parametersWithDataTypeMap.apiKey.value}}"{{#hasRegionalHost}}, region: {{#parametersWithDataTypeMap.region}}Region(rawValue: "{{parametersWithDataTypeMap.region.value}}"){{/parametersWithDataTypeMap.region}}{{^parametersWithDataTypeMap.region}}nil{{/parametersWithDataTypeMap.region}}{{/hasRegionalHost}})
let transporter: Transporter = Transporter(configuration: configuration, requestBuilder: EchoRequestBuilder())
{{^autoCreateClient}}let client = {{/autoCreateClient}}{{client}}(configuration: configuration, transporter: transporter)
5 changes: 5 additions & 0 deletions templates/swift/tests/client/method.mustache
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
let response = try await client{{#path}}.{{.}}WithHTTPInfo{{/path}}(
{{#parametersWithDataType}}{{> tests/generateParams }}{{^-last}},{{/-last}}
{{/parametersWithDataType}})
let responseBodyData = try XCTUnwrap(response.bodyData)
let echoResponse = try CodableHelper.jsonDecoder.decode(EchoResponse.self, from: responseBodyData)
Loading
Loading