Skip to content

Commit e6ef8ee

Browse files
authored
[Swift6][Client] Make Swift 6 generator thread safe (#23191)
* [Swift6][Client] Make Swift 6 generator thread safe * [Swift6][Client] Make Swift 6 generator thread safe * [Swift6][Client] Make Swift 6 generator thread safe * [Swift6][Client] Make Swift 6 generator thread safe * [Swift6][Client] Make Swift 6 generator thread safe * [Swift6][Client] Make Swift 6 generator thread safe
1 parent be5fb5d commit e6ef8ee

94 files changed

Lines changed: 3026 additions & 1146 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/Swift6ClientCodegen.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,9 @@ public void processOpts() {
673673
infrastructureFolder,
674674
"OpenAPIDateWithoutTime.swift"));
675675
}
676+
supportingFiles.add(new SupportingFile("OpenAPIMutex.mustache",
677+
infrastructureFolder,
678+
"OpenAPIMutex.swift"));
676679
supportingFiles.add(new SupportingFile("APIs.mustache",
677680
infrastructureFolder,
678681
"APIs.swift"));

modules/openapi-generator/src/main/resources/swift6/APIs.mustache

Lines changed: 168 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -16,34 +16,116 @@ import Alamofire{{/useAlamofire}}
1616
{{/swiftUseApiNamespace}}
1717

1818
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}open{{/nonPublicApi}} class {{projectName}}APIConfiguration: @unchecked Sendable {
19-
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var basePath: String{{#useVapor}}
20-
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var customHeaders: HTTPHeaders
21-
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var apiClient: Vapor.Client?
22-
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var apiWrapper: @Sendable (inout Vapor.ClientRequest) throws -> ()
23-
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var contentConfiguration: ContentConfiguration{{/useVapor}}{{^useVapor}}
24-
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var customHeaders: [String: String]
25-
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var credential: URLCredential?
26-
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var requestBuilderFactory: RequestBuilderFactory
27-
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var apiResponseQueue: DispatchQueue
28-
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var codableHelper: CodableHelper
29-
30-
/// Configures the range of HTTP status codes that will result in a successful response
19+
20+
// MARK: - Private state
21+
22+
private struct State {
23+
var basePath: String{{#useVapor}}
24+
var customHeaders: HTTPHeaders
25+
var apiClient: Vapor.Client?
26+
var apiWrapper: @Sendable (inout Vapor.ClientRequest) throws -> ()
27+
var contentConfiguration: ContentConfiguration{{/useVapor}}{{^useVapor}}
28+
var customHeaders: [String: String]
29+
var credential: URLCredential?
30+
var requestBuilderFactory: RequestBuilderFactory
31+
var apiResponseQueue: DispatchQueue
32+
var codableHelper: CodableHelper
33+
var successfulStatusCodeRange: Range<Int>{{#useURLSession}}
34+
var interceptor: OpenAPIInterceptor{{/useURLSession}}{{#useAlamofire}}
35+
var interceptor: RequestInterceptor?
36+
var dataResponseSerializer: AnyResponseSerializer<Data>
37+
var stringResponseSerializer: AnyResponseSerializer<String>{{/useAlamofire}}{{/useVapor}}
38+
}
39+
40+
private let _state: OpenAPIMutex<State>
41+
42+
// MARK: - Public interface
43+
44+
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var basePath: String {
45+
get { _state.value.basePath }
46+
set { _state.withValue { $0.basePath = newValue } }
47+
}{{#useVapor}}
48+
49+
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var customHeaders: HTTPHeaders {
50+
get { _state.value.customHeaders }
51+
set { _state.withValue { $0.customHeaders = newValue } }
52+
}
53+
54+
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var apiClient: Vapor.Client? {
55+
get { _state.value.apiClient }
56+
set { _state.withValue { $0.apiClient = newValue } }
57+
}
58+
59+
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var apiWrapper: @Sendable (inout Vapor.ClientRequest) throws -> () {
60+
get { _state.value.apiWrapper }
61+
set { _state.withValue { $0.apiWrapper = newValue } }
62+
}
63+
64+
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var contentConfiguration: ContentConfiguration {
65+
get { _state.value.contentConfiguration }
66+
set { _state.withValue { $0.contentConfiguration = newValue } }
67+
}{{/useVapor}}{{^useVapor}}
68+
69+
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var customHeaders: [String: String] {
70+
get { _state.value.customHeaders }
71+
set { _state.withValue { $0.customHeaders = newValue } }
72+
}
73+
74+
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var credential: URLCredential? {
75+
get { _state.value.credential }
76+
set { _state.withValue { $0.credential = newValue } }
77+
}
78+
79+
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var requestBuilderFactory: RequestBuilderFactory {
80+
get { _state.value.requestBuilderFactory }
81+
set { _state.withValue { $0.requestBuilderFactory = newValue } }
82+
}
83+
84+
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var apiResponseQueue: DispatchQueue {
85+
get { _state.value.apiResponseQueue }
86+
set { _state.withValue { $0.apiResponseQueue = newValue } }
87+
}
88+
89+
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var codableHelper: CodableHelper {
90+
get { _state.value.codableHelper }
91+
set { _state.withValue { $0.codableHelper = newValue } }
92+
}
93+
94+
/// Configures the range of HTTP status codes that will result in a successful response.
3195
///
3296
/// If a HTTP status code is outside of this range the response will be interpreted as failed.
33-
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var successfulStatusCodeRange: Range<Int>{{#useURLSession}}
97+
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var successfulStatusCodeRange: Range<Int> {
98+
get { _state.value.successfulStatusCodeRange }
99+
set { _state.withValue { $0.successfulStatusCodeRange = newValue } }
100+
}{{#useURLSession}}
34101

35-
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var interceptor: OpenAPIInterceptor{{/useURLSession}}{{#useAlamofire}}
102+
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var interceptor: OpenAPIInterceptor {
103+
get { _state.value.interceptor }
104+
set { _state.withValue { $0.interceptor = newValue } }
105+
}{{/useURLSession}}{{#useAlamofire}}
36106

37-
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var interceptor: RequestInterceptor?
107+
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var interceptor: RequestInterceptor? {
108+
get { _state.value.interceptor }
109+
set { _state.withValue { $0.interceptor = newValue } }
110+
}
38111

39112
/// ResponseSerializer that will be used by the generator for `Data` responses
40113
///
41-
/// If unchanged, Alamofires default `DataResponseSerializer` will be used.
42-
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var dataResponseSerializer: AnyResponseSerializer<Data>
114+
/// If unchanged, Alamofires default `DataResponseSerializer` will be used.
115+
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var dataResponseSerializer: AnyResponseSerializer<Data> {
116+
get { _state.value.dataResponseSerializer }
117+
set { _state.withValue { $0.dataResponseSerializer = newValue } }
118+
}
119+
43120
/// ResponseSerializer that will be used by the generator for `String` responses
44121
///
45-
/// If unchanged, Alamofires default `StringResponseSerializer` will be used.
46-
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var stringResponseSerializer: AnyResponseSerializer<String>{{/useAlamofire}}{{/useVapor}}
122+
/// If unchanged, Alamofires default `StringResponseSerializer` will be used.
123+
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var stringResponseSerializer: AnyResponseSerializer<String> {
124+
get { _state.value.stringResponseSerializer }
125+
set { _state.withValue { $0.stringResponseSerializer = newValue } }
126+
}{{/useAlamofire}}{{/useVapor}}
127+
128+
// MARK: - Init
47129

48130
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} init(
49131
basePath: String = "{{{basePath}}}",{{#useVapor}}
@@ -62,54 +144,95 @@ import Alamofire{{/useAlamofire}}
62144
dataResponseSerializer: AnyResponseSerializer<Data> = AnyResponseSerializer(DataResponseSerializer()),
63145
stringResponseSerializer: AnyResponseSerializer<String> = AnyResponseSerializer(StringResponseSerializer()){{/useAlamofire}}{{/useVapor}}
64146
) {
65-
self.basePath = basePath{{#useVapor}}
66-
self.customHeaders = customHeaders
67-
self.apiClient = apiClient
68-
self.apiWrapper = apiWrapper
69-
self.contentConfiguration = contentConfiguration{{/useVapor}}{{^useVapor}}
70-
self.customHeaders = customHeaders
71-
self.credential = credential
72-
self.requestBuilderFactory = requestBuilderFactory
73-
self.apiResponseQueue = apiResponseQueue
74-
self.codableHelper = codableHelper
75-
self.successfulStatusCodeRange = successfulStatusCodeRange{{#useURLSession}}
76-
self.interceptor = interceptor{{/useURLSession}}{{#useAlamofire}}
77-
self.interceptor = interceptor
78-
self.dataResponseSerializer = dataResponseSerializer
79-
self.stringResponseSerializer = stringResponseSerializer{{/useAlamofire}}{{/useVapor}}
147+
_state = OpenAPIMutex(State(
148+
basePath: basePath,{{#useVapor}}
149+
customHeaders: customHeaders,
150+
apiClient: apiClient,
151+
apiWrapper: apiWrapper,
152+
contentConfiguration: contentConfiguration{{/useVapor}}{{^useVapor}}
153+
customHeaders: customHeaders,
154+
credential: credential,
155+
requestBuilderFactory: requestBuilderFactory,
156+
apiResponseQueue: apiResponseQueue,
157+
codableHelper: codableHelper,
158+
successfulStatusCodeRange: successfulStatusCodeRange{{#useURLSession}},
159+
interceptor: interceptor{{/useURLSession}}{{#useAlamofire}},
160+
interceptor: interceptor,
161+
dataResponseSerializer: dataResponseSerializer,
162+
stringResponseSerializer: stringResponseSerializer{{/useAlamofire}}{{/useVapor}}
163+
))
80164
}
81165

82166
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} static let shared = {{projectName}}APIConfiguration()
83167
}{{^useVapor}}
84168

85169
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}open{{/nonPublicApi}} class RequestBuilder<T: Sendable>: @unchecked Sendable, Identifiable {
86-
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var credential: URLCredential?
87-
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var headers: [String: String]
170+
171+
// MARK: - Immutable properties
172+
88173
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} let parameters: [String: any Sendable]?
89174
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} let method: String
90175
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} let URLString: String
91176
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} let requestTask: RequestTask = RequestTask()
92177
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} let requiresAuthentication: Bool
93178
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} let apiConfiguration: {{projectName}}APIConfiguration
94179

180+
// MARK: - Private mutable state
181+
182+
private struct MutableState {
183+
var credential: URLCredential? = nil
184+
var headers: [String: String]
185+
var onProgressReady: ((Progress) -> Void)? = nil
186+
}
187+
188+
private let _state: OpenAPIMutex<MutableState>
189+
190+
// MARK: - Public mutable interface
191+
192+
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var credential: URLCredential? {
193+
get { _state.value.credential }
194+
set { _state.withValue { $0.credential = newValue } }
195+
}
196+
197+
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var headers: [String: String] {
198+
get { _state.value.headers }
199+
set { _state.withValue { $0.headers = newValue } }
200+
}
201+
95202
/// Optional block to obtain a reference to the request's progress instance when available.
96-
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var onProgressReady: ((Progress) -> Void)?
203+
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var onProgressReady: ((Progress) -> Void)? {
204+
get { _state.value.onProgressReady }
205+
set { _state.withValue { $0.onProgressReady = newValue } }
206+
}
97207

98-
required {{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} init(method: String, URLString: String, parameters: [String: any Sendable]?, headers: [String: String] = [:], requiresAuthentication: Bool, apiConfiguration: {{projectName}}APIConfiguration = {{projectName}}APIConfiguration.shared) {
208+
// MARK: - Init
209+
210+
required {{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} init(
211+
method: String,
212+
URLString: String,
213+
parameters: [String: any Sendable]?,
214+
headers: [String: String] = [:],
215+
requiresAuthentication: Bool,
216+
apiConfiguration: {{projectName}}APIConfiguration = {{projectName}}APIConfiguration.shared
217+
) {
99218
self.method = method
100219
self.URLString = URLString
101220
self.parameters = parameters
102-
self.headers = headers
103221
self.requiresAuthentication = requiresAuthentication
104222
self.apiConfiguration = apiConfiguration
223+
self._state = OpenAPIMutex(MutableState(headers: headers))
105224
106225
addHeaders(apiConfiguration.customHeaders)
107226
addCredential()
108227
}
109228

229+
// MARK: - Public methods
230+
110231
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}open{{/nonPublicApi}} func addHeaders(_ aHeaders: [String: String]) {
111-
for (header, value) in aHeaders {
112-
headers[header] = value
232+
_state.withValue { state in
233+
for (header, value) in aHeaders {
234+
state.headers[header] = value
235+
}
113236
}
114237
}
115238

@@ -168,13 +291,15 @@ import Alamofire{{/useAlamofire}}
168291
{{/useAsyncAwait}}
169292
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} func addHeader(name: String, value: String) -> Self {
170293
if !value.isEmpty {
171-
headers[name] = value
294+
_state.withValue { $0.headers[name] = value }
172295
}
173296
return self
174297
}
175298

176299
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}open{{/nonPublicApi}} func addCredential() {
177-
credential = apiConfiguration.credential
300+
_state.withValue { [apiConfiguration] state in
301+
state.credential = apiConfiguration.credential
302+
}
178303
}
179304
}
180305

modules/openapi-generator/src/main/resources/swift6/CodableHelper.mustache

Lines changed: 41 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,37 +8,56 @@
88
import Foundation
99

1010
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}open{{/nonPublicApi}} class CodableHelper: @unchecked Sendable {
11-
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} init() {}
1211
13-
private var customDateFormatter: DateFormatter?
14-
private var defaultDateFormatter: DateFormatter = OpenISO8601DateFormatter()
12+
// MARK: - Private state
13+
14+
private struct State {
15+
var customDateFormatter: DateFormatter?
16+
var defaultDateFormatter: DateFormatter = OpenISO8601DateFormatter()
17+
18+
var customJSONDecoder: JSONDecoder?
19+
var defaultJSONDecoder: JSONDecoder = JSONDecoder()
20+
21+
var customJSONEncoder: JSONEncoder?
22+
var defaultJSONEncoder: JSONEncoder = JSONEncoder()
23+
24+
init() {
25+
defaultJSONEncoder.outputFormatting = .prettyPrinted
26+
rebuildDefaultCoders()
27+
}
28+
29+
mutating func rebuildDefaultCoders() {
30+
defaultJSONDecoder.dateDecodingStrategy = .formatted(customDateFormatter ?? defaultDateFormatter)
31+
defaultJSONEncoder.dateEncodingStrategy = .formatted(customDateFormatter ?? defaultDateFormatter)
32+
}
33+
}
34+
35+
private let _state = OpenAPIMutex(State())
36+
37+
// MARK: - Init
1538

16-
private var customJSONDecoder: JSONDecoder?
17-
private lazy var defaultJSONDecoder: JSONDecoder = {
18-
let decoder = JSONDecoder()
19-
decoder.dateDecodingStrategy = .formatted(dateFormatter)
20-
return decoder
21-
}()
39+
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} init() {}
2240

23-
private var customJSONEncoder: JSONEncoder?
24-
private lazy var defaultJSONEncoder: JSONEncoder = {
25-
let encoder = JSONEncoder()
26-
encoder.dateEncodingStrategy = .formatted(dateFormatter)
27-
encoder.outputFormatting = .prettyPrinted
28-
return encoder
29-
}()
41+
// MARK: - Public interface
3042

3143
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var dateFormatter: DateFormatter {
32-
get { return customDateFormatter ?? defaultDateFormatter }
33-
set { customDateFormatter = newValue }
44+
get { _state.withValue { $0.customDateFormatter ?? $0.defaultDateFormatter } }
45+
set {
46+
_state.withValue { state in
47+
state.customDateFormatter = newValue
48+
state.rebuildDefaultCoders()
49+
}
50+
}
3451
}
52+
3553
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var jsonDecoder: JSONDecoder {
36-
get { return customJSONDecoder ?? defaultJSONDecoder }
37-
set { customJSONDecoder = newValue }
54+
get { _state.withValue { $0.customJSONDecoder ?? $0.defaultJSONDecoder } }
55+
set { _state.withValue { $0.customJSONDecoder = newValue } }
3856
}
57+
3958
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var jsonEncoder: JSONEncoder {
40-
get { return customJSONEncoder ?? defaultJSONEncoder }
41-
set { customJSONEncoder = newValue }
59+
get { _state.withValue { $0.customJSONEncoder ?? $0.defaultJSONEncoder } }
60+
set { _state.withValue { $0.customJSONEncoder = newValue } }
4261
}
4362

4463
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}open{{/nonPublicApi}} func decode<T>(_ type: T.Type, from data: Data) -> Swift.Result<T, Error> where T: Decodable {

0 commit comments

Comments
 (0)