@@ -17,103 +17,175 @@ import OpenAPIKit30
17
17
///
18
18
/// Represents the serialization method of the payload and affects
19
19
/// the generated serialization code.
20
- enum ContentType : Hashable {
20
+ struct ContentType : Hashable {
21
21
22
- /// A content type for JSON.
23
- case json( String )
22
+ /// The category of a content type.
23
+ ///
24
+ /// This categorization helps the generator decide how to handle
25
+ /// the content's raw body data.
26
+ enum Category : Hashable {
27
+
28
+ /// A content type for JSON.
29
+ ///
30
+ /// The bytes are provided to a JSON encoder or decoder.
31
+ case json
32
+
33
+ /// A content type for any plain text.
34
+ ///
35
+ /// The bytes are encoded or decoded as a UTF-8 string.
36
+ case text
37
+
38
+ /// A content type for raw binary data.
39
+ ///
40
+ /// This case covers both explicit binary data content types, such
41
+ /// as `application/octet-stream`, and content types that no further
42
+ /// introspection is performed on, such as `image/png`.
43
+ ///
44
+ /// The bytes are not further processed, they are instead passed along
45
+ /// either to the network (requests) or to the caller (responses).
46
+ case binary
47
+
48
+ /// Creates a category from the provided type and subtype.
49
+ ///
50
+ /// First checks if the provided content type is a JSON, then text,
51
+ /// and uses binary if none of the two match.
52
+ /// - Parameters:
53
+ /// - type: The first component of the MIME type.
54
+ /// - subtype: The second component of the MIME type.
55
+ init ( type: String , subtype: String ) {
56
+ let lowercasedType = type. lowercased ( )
57
+ let lowercasedSubtype = subtype. lowercased ( )
58
+
59
+ // https://json-schema.org/draft/2020-12/json-schema-core.html#section-4.2
60
+ if ( lowercasedType == " application " && lowercasedSubtype == " json " ) || lowercasedSubtype. hasSuffix ( " +json " )
61
+ {
62
+ self = . json
63
+ } else if lowercasedType == " text " {
64
+ self = . text
65
+ } else {
66
+ self = . binary
67
+ }
68
+ }
24
69
25
- /// A content type for any plain text.
26
- case text( String )
70
+ /// The coding strategy appropriate for this content type.
71
+ var codingStrategy : CodingStrategy {
72
+ switch self {
73
+ case . json:
74
+ return . json
75
+ case . text:
76
+ return . text
77
+ case . binary:
78
+ return . binary
79
+ }
80
+ }
81
+ }
27
82
28
- /// A content type for raw binary data.
29
- case binary( String )
83
+ /// The mapped content type category.
84
+ var category : Category {
85
+ Category ( type: type, subtype: subtype)
86
+ }
87
+
88
+ /// The first component of the MIME type.
89
+ private let type : String
90
+
91
+ /// The first component of the MIME type, as a lowercase string.
92
+ ///
93
+ /// The raw value in its original casing is only provided by `rawTypeAndSubtype`.
94
+ var lowercasedType : String {
95
+ type. lowercased ( )
96
+ }
97
+
98
+ /// The second component of the MIME type.
99
+ private let subtype : String
100
+
101
+ /// The second component of the MIME type, as a lowercase string.
102
+ ///
103
+ /// The raw value in its original casing is only provided by `originallyCasedTypeAndSubtype`.
104
+ var lowercasedSubtype : String {
105
+ subtype. lowercased ( )
106
+ }
30
107
31
108
/// Creates a new content type by parsing the specified MIME type.
32
- /// - Parameter rawValue: A MIME type, for example "application/json".
33
- init ? ( _ rawValue: String ) {
34
- if rawValue. hasPrefix ( " application/ " ) && rawValue. hasSuffix ( " json " ) {
35
- self = . json( rawValue)
36
- return
37
- }
38
- if rawValue. hasPrefix ( " text/ " ) {
39
- self = . text( rawValue)
40
- return
41
- }
42
- self = . binary( rawValue)
109
+ /// - Parameter rawValue: A MIME type, for example "application/json". Must
110
+ /// not be empty.
111
+ init ( _ rawValue: String ) {
112
+ precondition ( !rawValue. isEmpty, " rawValue of a ContentType cannot be empty. " )
113
+ let rawTypeAndSubtype =
114
+ rawValue
115
+ . split ( separator: " ; " ) [ 0 ]
116
+ . trimmingCharacters ( in: . whitespaces)
117
+ let typeAndSubtype =
118
+ rawTypeAndSubtype
119
+ . split ( separator: " / " )
120
+ . map ( String . init)
121
+ precondition (
122
+ typeAndSubtype. count == 2 ,
123
+ " Invalid ContentType string, must have 2 components separated by a slash. "
124
+ )
125
+ self . type = typeAndSubtype [ 0 ]
126
+ self . subtype = typeAndSubtype [ 1 ]
43
127
}
44
128
45
- /// Returns the original raw MIME type.
46
- var rawMIMEType : String {
47
- switch self {
48
- case . json( let string) , . text( let string) , . binary( let string) :
49
- return string
50
- }
129
+ /// Returns the type and subtype as a "<type>/<subtype>" string.
130
+ ///
131
+ /// Respects the original casing provided as input.
132
+ var originallyCasedTypeAndSubtype : String {
133
+ " \( type) / \( subtype) "
134
+ }
135
+
136
+ /// Returns the type and subtype as a "<type>/<subtype>" string.
137
+ ///
138
+ /// Lowercased to ease case-insensitive comparisons.
139
+ var lowercasedTypeAndSubtype : String {
140
+ " \( lowercasedType) / \( lowercasedSubtype) "
51
141
}
52
142
53
143
/// The header value used when sending a content-type header.
54
144
var headerValueForSending : String {
55
- switch self {
56
- case . json( let string) :
57
- // We always encode JSON using JSONEncoder which uses UTF-8.
58
- return string + " ; charset=utf-8 "
59
- case . text( let string) :
60
- return string
61
- case . binary( let string) :
62
- return string
145
+ guard case . json = category else {
146
+ return lowercasedTypeAndSubtype
63
147
}
148
+ // We always encode JSON using JSONEncoder which uses UTF-8.
149
+ return lowercasedTypeAndSubtype + " ; charset=utf-8 "
64
150
}
65
151
66
152
/// The header value used when validating a content-type header.
67
153
///
68
154
/// This should be less strict, e.g. not require `charset`.
69
155
var headerValueForValidation : String {
70
- switch self {
71
- case . json( let string) :
72
- return string
73
- case . text( let string) :
74
- return string
75
- case . binary( let string) :
76
- return string
77
- }
156
+ lowercasedTypeAndSubtype
78
157
}
79
158
80
159
/// The coding strategy appropriate for this content type.
81
160
var codingStrategy : CodingStrategy {
82
- switch self {
83
- case . json:
84
- return . json
85
- case . text:
86
- return . text
87
- case . binary:
88
- return . binary
89
- }
161
+ category. codingStrategy
90
162
}
91
163
92
164
/// A Boolean value that indicates whether the content type
93
165
/// is a type of JSON.
94
166
var isJSON : Bool {
95
- if case . json = self {
96
- return true
97
- }
98
- return false
167
+ category == . json
99
168
}
100
169
101
170
/// A Boolean value that indicates whether the content type
102
171
/// is a type of plain text.
103
172
var isText : Bool {
104
- if case . text = self {
105
- return true
106
- }
107
- return false
173
+ category == . text
108
174
}
109
175
110
176
/// A Boolean value that indicates whether the content type
111
177
/// is just binary data.
112
178
var isBinary : Bool {
113
- if case . binary = self {
114
- return true
115
- }
116
- return false
179
+ category == . binary
180
+ }
181
+
182
+ static func == ( lhs: Self , rhs: Self ) -> Bool {
183
+ // MIME type equality is case-insensitive.
184
+ lhs. lowercasedTypeAndSubtype == rhs. lowercasedTypeAndSubtype
185
+ }
186
+
187
+ func hash( into hasher: inout Hasher ) {
188
+ hasher. combine ( lowercasedTypeAndSubtype)
117
189
}
118
190
}
119
191
@@ -122,27 +194,24 @@ extension OpenAPI.ContentType {
122
194
/// A Boolean value that indicates whether the content type
123
195
/// is a type of JSON.
124
196
var isJSON : Bool {
125
- guard let contentType = ContentType ( typeAndSubtype) else {
126
- return false
127
- }
128
- return contentType. isJSON
197
+ asGeneratorContentType. isJSON
129
198
}
130
199
131
200
/// A Boolean value that indicates whether the content type
132
201
/// is a type of plain text.
133
202
var isText : Bool {
134
- guard let contentType = ContentType ( typeAndSubtype) else {
135
- return false
136
- }
137
- return contentType. isText
203
+ asGeneratorContentType. isText
138
204
}
139
205
140
206
/// A Boolean value that indicates whether the content type
141
207
/// is just binary data.
142
208
var isBinary : Bool {
143
- guard let contentType = ContentType ( typeAndSubtype) else {
144
- return false
145
- }
146
- return contentType. isBinary
209
+ asGeneratorContentType. isBinary
210
+ }
211
+
212
+ /// Returns the content type wrapped in the generator's representation
213
+ /// of a content type, as opposed to the one from OpenAPIKit.
214
+ var asGeneratorContentType : ContentType {
215
+ ContentType ( typeAndSubtype)
147
216
}
148
217
}
0 commit comments