Skip to content

Commit c49376e

Browse files
committed
Рефактор сетевого слоя
Теперь тело создается внутри пакета `SWNetwork` с уникальным `boundary`
1 parent 501cec1 commit c49376e

File tree

6 files changed

+415
-264
lines changed

6 files changed

+415
-264
lines changed

SwiftUI-WorkoutApp.xcodeproj/project.pbxproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -844,7 +844,7 @@
844844
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
845845
CLANG_WARN_SEMICOLON_BEFORE_METHOD_BODY = YES;
846846
CODE_SIGN_STYLE = Automatic;
847-
CURRENT_PROJECT_VERSION = 6;
847+
CURRENT_PROJECT_VERSION = 7;
848848
DEVELOPMENT_ASSET_PATHS = "SwiftUI-WorkoutApp/Preview\\ Content/PreviewContent.swift SwiftUI-WorkoutApp/Preview\\ Content";
849849
DEVELOPMENT_TEAM = CR68PP2Z3F;
850850
ENABLE_PREVIEWS = YES;
@@ -894,7 +894,7 @@
894894
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
895895
CLANG_WARN_SEMICOLON_BEFORE_METHOD_BODY = YES;
896896
CODE_SIGN_STYLE = Automatic;
897-
CURRENT_PROJECT_VERSION = 6;
897+
CURRENT_PROJECT_VERSION = 7;
898898
DEVELOPMENT_ASSET_PATHS = "SwiftUI-WorkoutApp/Preview\\ Content/PreviewContent.swift SwiftUI-WorkoutApp/Preview\\ Content";
899899
DEVELOPMENT_TEAM = CR68PP2Z3F;
900900
ENABLE_PREVIEWS = YES;

SwiftUI-WorkoutApp/Libraries/SWNetwork/Sources/SWNetwork/BodyMaker.swift

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import Foundation
22

33
/// Делает `body` для запроса
44
public enum BodyMaker {
5-
public struct Parameter<T: Hashable & RawRepresentable> where T.RawValue == String {
5+
public struct Parameter {
66
let key: String
77
let value: String
88

9-
public init(from element: Dictionary<T, String>.Element) {
10-
self.key = element.key.rawValue
9+
public init(from element: Dictionary<String, String>.Element) {
10+
self.key = element.key
1111
self.value = element.value
1212
}
1313

@@ -19,7 +19,7 @@ public enum BodyMaker {
1919

2020
/// Делает `body` из словаря
2121
public static func makeBody(
22-
with parameters: [Parameter<some Hashable & RawRepresentable>]
22+
with parameters: [Parameter]
2323
) -> Data? {
2424
parameters.isEmpty
2525
? nil
@@ -31,10 +31,10 @@ public enum BodyMaker {
3131

3232
/// Делает `body` из словаря и медиа-файлов
3333
public static func makeBodyWithMultipartForm(
34-
with parameters: [Parameter<some Hashable & RawRepresentable>],
35-
and media: [MediaFile]?
34+
parameters: [Parameter],
35+
media: [MediaFile]?,
36+
boundary: String
3637
) -> Data? {
37-
let boundary = "FFF"
3838
let lineBreak = "\r\n"
3939
var body = Data()
4040
if !parameters.isEmpty {
@@ -56,9 +56,8 @@ public enum BodyMaker {
5656
if !body.isEmpty {
5757
body.append("--\(boundary)--\(lineBreak)")
5858
return body
59-
} else {
60-
return nil
6159
}
60+
return nil
6261
}
6362

6463
/// Медиа-файл для отправки на сервер

SwiftUI-WorkoutApp/Libraries/SWNetwork/Sources/SWNetwork/RequestComponents.swift

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ public struct RequestComponents {
55
let queryItems: [URLQueryItem]
66
let httpMethod: HTTPMethod
77
let hasMultipartFormData: Bool
8-
let body: Data?
8+
let body: (parameters: [String: String], mediaFiles: [BodyMaker.MediaFile]?)?
9+
let boundary: String
910
let token: String?
1011

1112
/// Инициализатор
@@ -14,21 +15,24 @@ public struct RequestComponents {
1415
/// - queryItems: Параметры `query`, по умолчанию отсутствуют
1516
/// - httpMethod: Метод запроса
1617
/// - hasMultipartFormData: Есть ли в запросе файлы для отправки (в нашем случае картинки), по умолчанию `false`
17-
/// - body: Тело запроса, по умолчанию `nil`
18+
/// - body: Данные для тела запроса, по умолчанию `nil`
19+
/// - boundary: `Boundary` для `body`, по умолчанию `UUID().uuidString`
1820
/// - token: Токен для авторизации, по умолчанию `nil`
1921
public init(
2022
path: String,
2123
queryItems: [URLQueryItem] = [],
2224
httpMethod: HTTPMethod,
2325
hasMultipartFormData: Bool = false,
24-
body: Data? = nil,
26+
body: (parameters: [String: String], mediaFiles: [BodyMaker.MediaFile]?)? = nil,
27+
boundary: String = UUID().uuidString,
2528
token: String? = nil
2629
) {
2730
self.path = path
2831
self.queryItems = queryItems
2932
self.httpMethod = httpMethod
3033
self.hasMultipartFormData = hasMultipartFormData
3134
self.body = body
35+
self.boundary = boundary
3236
self.token = token
3337
}
3438

@@ -50,19 +54,37 @@ extension RequestComponents {
5054
guard let url else { return nil }
5155
var request = URLRequest(url: url)
5256
request.httpMethod = httpMethod.rawValue
53-
request.httpBody = body
57+
5458
var allHeaders = [HTTPHeaderField]()
55-
// TODO: генерировать boundary в одном месте (вместо FFF)
59+
var httpBodyData: Data?
60+
5661
if let body {
57-
allHeaders.append(.init(key: "Content-Length", value: "\(body.count)"))
58-
}
59-
if hasMultipartFormData {
60-
allHeaders.append(.init(key: "Content-Type", value: "multipart/form-data; boundary=FFF"))
62+
let parameters = body.parameters.map(BodyMaker.Parameter.init)
63+
if hasMultipartFormData {
64+
httpBodyData = BodyMaker.makeBodyWithMultipartForm(
65+
parameters: parameters,
66+
media: body.mediaFiles,
67+
boundary: boundary
68+
)
69+
allHeaders.append(.init(
70+
key: "Content-Type",
71+
value: "multipart/form-data; boundary=\(boundary)"
72+
))
73+
} else {
74+
httpBodyData = BodyMaker.makeBody(with: parameters)
75+
}
76+
if let httpBodyData {
77+
allHeaders.append(.init(key: "Content-Length", value: "\(httpBodyData.count)"))
78+
}
6179
}
80+
6281
if let token, !token.isEmpty {
6382
allHeaders.append(.init(key: "Authorization", value: "Basic \(token)"))
6483
}
65-
request.allHTTPHeaderFields = Dictionary(uniqueKeysWithValues: allHeaders.map { ($0.key, $0.value) })
84+
request.allHTTPHeaderFields = Dictionary(
85+
uniqueKeysWithValues: allHeaders.map { ($0.key, $0.value) }
86+
)
87+
request.httpBody = httpBodyData
6688
return request
6789
}
6890
}

SwiftUI-WorkoutApp/Libraries/SWNetwork/Tests/SWNetworkTests/BodyMakerTests.swift

Lines changed: 131 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -3,85 +3,158 @@ import Foundation
33
import Testing
44

55
struct BodyMakerTests {
6-
private typealias Parameter = BodyMaker.Parameter
6+
@Test
7+
func parameterInitializationFromDictionaryElement() {
8+
let element = ("testKey", "testValue")
9+
let parameter = BodyMaker.Parameter(from: element)
10+
#expect(parameter.key == "testKey")
11+
#expect(parameter.value == "testValue")
12+
}
13+
14+
// MARK: - makeBody
715

816
@Test
9-
func makeBody_noParameters() {
10-
let parameters = [Parameter<TestKey>]()
11-
let result = BodyMaker.makeBody(with: parameters)
17+
func makeBodyWithNoParametersReturnsNil() {
18+
let result = BodyMaker.makeBody(with: [])
1219
#expect(result == nil)
1320
}
1421

1522
@Test
16-
func makeBody_validParameters() throws {
17-
let parameters: [Parameter<TestKey>] = [
18-
.init(key: TestKey.name.rawValue, value: "John"),
19-
.init(key: TestKey.age.rawValue, value: "30")
23+
func makeBodyWithSingleParameter() throws {
24+
let parameter = BodyMaker.Parameter(key: "name", value: "John")
25+
let result = try #require(BodyMaker.makeBody(with: [parameter]))
26+
let expectedString = "name=John"
27+
#expect(String(data: result, encoding: .utf8) == expectedString)
28+
}
29+
30+
@Test
31+
func makeBodyWithMultipleParameters() throws {
32+
let params = [
33+
BodyMaker.Parameter(key: "a", value: "1"),
34+
BodyMaker.Parameter(key: "b", value: "2")
2035
]
21-
let expectedData = try #require("name=John&age=30".data(using: .utf8))
22-
let result = try #require(BodyMaker.makeBody(with: parameters))
23-
#expect(result == expectedData)
36+
let result = try #require(BodyMaker.makeBody(with: params))
37+
let expectedString = "a=1&b=2"
38+
#expect(String(data: result, encoding: .utf8) == expectedString)
2439
}
2540

41+
// MARK: - makeBodyWithMultipartForm
42+
2643
@Test
27-
func makeBodyWithMultipartForm_noParameters_noMedia() {
28-
let parameters = [Parameter<TestKey>]()
29-
let result = BodyMaker.makeBodyWithMultipartForm(with: parameters, and: nil)
44+
func multipartFormWithNoContentReturnsNil() {
45+
let result = BodyMaker.makeBodyWithMultipartForm(
46+
parameters: [],
47+
media: nil,
48+
boundary: "BOUNDARY"
49+
)
3050
#expect(result == nil)
3151
}
3252

3353
@Test
34-
func makeBodyWithMultipartForm_onlyDictionary() throws {
35-
let parameters: [Parameter<TestKey>] = [
36-
.init(key: TestKey.name.rawValue, value: "John"),
37-
.init(key: TestKey.age.rawValue, value: "30")
54+
func multipartFormWithParametersOnly() throws {
55+
let params = [BodyMaker.Parameter(key: "text", value: "Hello")]
56+
let boundary = "TESTBOUNDARY"
57+
let result = try #require(BodyMaker.makeBodyWithMultipartForm(
58+
parameters: params,
59+
media: nil,
60+
boundary: boundary
61+
))
62+
let string = try #require(String(data: result, encoding: .utf8))
63+
let expectedPatterns = [
64+
"--TESTBOUNDARY\r\n",
65+
"Content-Disposition: form-data; name=\"text\"\r\n\r\n",
66+
"Hello\r\n",
67+
"--TESTBOUNDARY--\r\n"
3868
]
39-
let expectedData = try #require(
40-
"--FFF\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\nJohn\r\n--FFF\r\nContent-Disposition: form-data; name=\"age\"\r\n\r\n30\r\n--FFF--\r\n"
41-
.data(using: .utf8)
42-
)
43-
let result = try #require(BodyMaker.makeBodyWithMultipartForm(with: parameters, and: nil))
44-
#expect(result == expectedData)
69+
for pattern in expectedPatterns {
70+
#expect(string.contains(pattern))
71+
}
4572
}
4673

4774
@Test
48-
func makeBodyWithMultipartForm_onlyMedia() throws {
49-
let parameters = [Parameter<TestKey>]()
50-
let mediaFile = BodyMaker.MediaFile(
51-
key: "file",
52-
filename: "test.png",
53-
data: Data("Test image content".utf8),
54-
mimeType: "image/png"
55-
)
56-
let expectedData = try #require(
57-
"--FFF\r\nContent-Disposition: form-data; name=\"file\"; filename=\"test.png\"\r\nContent-Type: image/png\r\n\r\nTest image content\r\n--FFF--\r\n"
58-
.data(using: .utf8)
59-
)
60-
let result = try #require(BodyMaker.makeBodyWithMultipartForm(with: parameters, and: [mediaFile]))
61-
#expect(result == expectedData)
75+
func multipartFormWithMediaOnly() throws {
76+
let media = [
77+
BodyMaker.MediaFile(
78+
key: "file",
79+
filename: "test.txt",
80+
data: Data("file content".utf8),
81+
mimeType: "text/plain"
82+
)
83+
]
84+
let boundary = "MEDIA_BOUNDARY"
85+
let result = try #require(BodyMaker.makeBodyWithMultipartForm(
86+
parameters: [],
87+
media: media,
88+
boundary: boundary
89+
))
90+
let string = try #require(String(data: result, encoding: .utf8))
91+
let expectedPatterns = [
92+
"--MEDIA_BOUNDARY\r\n",
93+
"Content-Disposition: form-data; name=\"file\"; filename=\"test.txt\"\r\n",
94+
"Content-Type: text/plain\r\n\r\n",
95+
"file content\r\n",
96+
"--MEDIA_BOUNDARY--\r\n"
97+
]
98+
for pattern in expectedPatterns {
99+
#expect(string.contains(pattern))
100+
}
62101
}
63102

64103
@Test
65-
func makeBodyWithMultipartForm_dictionaryAndMedia() throws {
66-
let parameters: [Parameter<TestKey>] = [.init(key: TestKey.description.rawValue, value: "A test image")]
67-
let mediaFile = BodyMaker.MediaFile(
68-
key: "file",
69-
filename: "test.png",
70-
data: Data("Test image content".utf8),
71-
mimeType: "image/png"
72-
)
73-
let expectedData = try #require(
74-
"--FFF\r\nContent-Disposition: form-data; name=\"description\"\r\n\r\nA test image\r\n--FFF\r\nContent-Disposition: form-data; name=\"file\"; filename=\"test.png\"\r\nContent-Type: image/png\r\n\r\nTest image content\r\n--FFF--\r\n"
75-
.data(using: .utf8)
76-
)
77-
let result = try #require(BodyMaker.makeBodyWithMultipartForm(with: parameters, and: [mediaFile]))
78-
#expect(result == expectedData)
104+
func multipartFormWithMixedContent() throws {
105+
let params = [BodyMaker.Parameter(key: "title", value: "Document")]
106+
let media = [
107+
BodyMaker.MediaFile(
108+
key: "doc",
109+
filename: "doc.pdf",
110+
data: Data("pdf content".utf8),
111+
mimeType: "application/pdf"
112+
)
113+
]
114+
let boundary = "MIXEDBOUNDARY"
115+
let result = try #require(BodyMaker.makeBodyWithMultipartForm(
116+
parameters: params,
117+
media: media,
118+
boundary: boundary
119+
))
120+
121+
let string = try #require(String(data: result, encoding: .utf8))
122+
123+
// Проверяем порядок: сначала параметры, потом медиа
124+
let paramSection = """
125+
--MIXEDBOUNDARY\r\n\
126+
Content-Disposition: form-data; name="title"\r\n\r\n\
127+
Document\r\n
128+
"""
129+
130+
let mediaSection = """
131+
--MIXEDBOUNDARY\r\n\
132+
Content-Disposition: form-data; name="doc"; filename="doc.pdf"\r\n\
133+
Content-Type: application/pdf\r\n\r\n\
134+
pdf content\r\n
135+
"""
136+
137+
let closing = "--MIXEDBOUNDARY--\r\n"
138+
139+
#expect(string.contains(paramSection))
140+
#expect(string.contains(mediaSection))
141+
#expect(string.contains(closing))
79142
}
80-
}
81143

82-
/// Пример ключа для тестирования
83-
private enum TestKey: String {
84-
case name
85-
case age
86-
case description
144+
// MARK: - MediaFile Tests
145+
146+
@Test
147+
func mediaFileInitialization() {
148+
let data = Data("test".utf8)
149+
let media = BodyMaker.MediaFile(
150+
key: "avatar",
151+
filename: "image.jpg",
152+
data: data,
153+
mimeType: "image/jpeg"
154+
)
155+
#expect(media.key == "avatar")
156+
#expect(media.filename == "image.jpg")
157+
#expect(media.data == data)
158+
#expect(media.mimeType == "image/jpeg")
159+
}
87160
}

0 commit comments

Comments
 (0)