Skip to content

Commit ccbbd88

Browse files
franklinschQuietMisdreavus
authored andcommitted
Add models for HTTP endpoints
rdar://101408429 rdar://101409112 rdar://101408868 rdar://101408667
1 parent 94f4488 commit ccbbd88

File tree

4 files changed

+283
-0
lines changed

4 files changed

+283
-0
lines changed
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2023 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See https://swift.org/LICENSE.txt for license information
8+
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import Foundation
12+
13+
extension SymbolGraph.Symbol {
14+
/// The HTTP endpoint for a request.
15+
public var httpEndpoint: HTTP.Endpoint? {
16+
(mixins[HTTP.Endpoint.mixinKey] as? HTTP.Endpoint)
17+
}
18+
19+
/// The source location of an HTTP parameter.
20+
public var httpParameterSource: String? {
21+
(mixins[HTTP.ParameterSource.mixinKey] as? HTTP.ParameterSource)?.value
22+
}
23+
24+
/// The encoding media type for an HTTP payload.
25+
public var httpMediaType: String? {
26+
(mixins[HTTP.MediaType.mixinKey] as? HTTP.MediaType)?.value
27+
}
28+
29+
/// Namespace to hold mixins specific to HTTP requests
30+
public enum HTTP {
31+
32+
/// The HTTP endpoint for a request.
33+
///
34+
/// Defines the HTTP method, base URL, and path relative to the base URL.
35+
public struct Endpoint: Mixin {
36+
public static let mixinKey = "httpEndpoint"
37+
38+
private var _method: String
39+
40+
/// The HTTP method of the request.
41+
///
42+
/// Expected values include GET, PUT, POST, DELETE.
43+
/// The value is always uppercased.
44+
public var method: String {
45+
get {
46+
return self._method
47+
}
48+
set {
49+
self._method = newValue.uppercased()
50+
}
51+
}
52+
53+
/// The base URL of the request.
54+
///
55+
/// This portion of the URL is usually shared across all endpoints provided by a server.
56+
/// It can be optionally swapped out of the request by the ``sandboxURL`` when accessing
57+
/// the endpoint within a test environment.
58+
public var baseURL: URL
59+
60+
/// The alternate base URL of the request when used within a test environment.
61+
public var sandboxURL: URL?
62+
63+
/// The relative path specific to the endpoint.
64+
///
65+
/// The string can encode the location of parameters in the path using `{parameterName}` syntax.
66+
/// The embedded parameter name must match an `httpParameter` symbol related to this endpoint's `httpRequest` symbol.
67+
public var path: String
68+
69+
public init(method: String, baseURL: URL, sandboxURL: URL? = nil, path: String) {
70+
self._method = method.uppercased()
71+
self.baseURL = baseURL
72+
self.sandboxURL = sandboxURL
73+
self.path = path
74+
}
75+
76+
enum CodingKeys: CodingKey {
77+
case method
78+
case baseURL
79+
case sandboxURL
80+
case path
81+
}
82+
83+
public init(from decoder: Decoder) throws {
84+
let container = try decoder.container(keyedBy: CodingKeys.self)
85+
_method = try container.decode(String.self, forKey: .method).uppercased()
86+
baseURL = try container.decode(URL.self, forKey: .baseURL)
87+
sandboxURL = try container.decodeIfPresent(URL.self, forKey: .sandboxURL)
88+
path = try container.decode(String.self, forKey: .path)
89+
}
90+
91+
public func encode(to encoder: Encoder) throws {
92+
var container = encoder.container(keyedBy: CodingKeys.self)
93+
try container.encode(method, forKey: .method)
94+
try container.encode(baseURL, forKey: .baseURL)
95+
try container.encodeIfPresent(sandboxURL, forKey: .sandboxURL)
96+
try container.encode(path, forKey: .path)
97+
}
98+
}
99+
100+
/// The source location of an HTTP parameter.
101+
///
102+
/// Expected values are path, query, header, or cookie.
103+
public struct ParameterSource: SingleValueMixin {
104+
public static let mixinKey = "httpParameterSource"
105+
public typealias ValueType = String
106+
public var value: ValueType
107+
public init(_ value: ValueType) {
108+
self.value = value
109+
}
110+
}
111+
112+
/// The encoding media type for an HTTP payload.
113+
///
114+
/// Common values are "application/json" and "application/x-www-form-urlencoded".
115+
public struct MediaType: SingleValueMixin {
116+
public static let mixinKey = "httpMediaType"
117+
public typealias ValueType = String
118+
public var value: ValueType
119+
public init(_ value: ValueType) {
120+
self.value = value
121+
}
122+
}
123+
}
124+
}

Sources/SymbolKit/SymbolGraph/Symbol/KindIdentifier.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,14 @@ extension SymbolGraph.Symbol {
7777

7878
public static let dictionaryKey = KindIdentifier(rawValue: "dictionaryKey")
7979

80+
public static let httpRequest = KindIdentifier(rawValue: "httpRequest")
81+
82+
public static let httpParameter = KindIdentifier(rawValue: "httpParameter")
83+
84+
public static let httpResponse = KindIdentifier(rawValue: "httpResponse")
85+
86+
public static let httpBody = KindIdentifier(rawValue: "httpBody")
87+
8088
/// A string that uniquely identifies the symbol kind.
8189
///
8290
/// If the original kind string was not recognized, this will return `"unknown"`.
@@ -115,6 +123,10 @@ extension SymbolGraph.Symbol {
115123
Self.extension.rawValue: .extension,
116124
Self.dictionary.rawValue: .dictionary,
117125
Self.dictionaryKey.rawValue: .dictionaryKey,
126+
Self.httpRequest.rawValue: .httpRequest,
127+
Self.httpParameter.rawValue: .httpParameter,
128+
Self.httpResponse.rawValue: .httpResponse,
129+
Self.httpBody.rawValue: .httpBody,
118130
]
119131

120132
/// Register custom ``SymbolGraph/Symbol/KindIdentifier``s so they can be parsed correctly and

Sources/SymbolKit/SymbolGraph/Symbol/Symbol.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,9 @@ extension SymbolGraph.Symbol {
232232
static let maximumLength = MaximumLength.symbolCodingInfo
233233
static let allowedValues = AllowedValues.symbolCodingInfo
234234
static let defaultValue = DefaultValue.symbolCodingInfo
235+
static let httpEndpoint = HTTP.Endpoint.symbolCodingInfo
236+
static let httpParameterSource = HTTP.ParameterSource.symbolCodingInfo
237+
static let httpMediaType = HTTP.MediaType.symbolCodingInfo
235238

236239
static let mixinCodingInfo: [String: SymbolMixinCodingInfo] = [
237240
CodingKeys.availability.codingKey.stringValue: Self.availability,
@@ -252,6 +255,9 @@ extension SymbolGraph.Symbol {
252255
CodingKeys.maximumLength.codingKey.stringValue: Self.maximumLength,
253256
CodingKeys.allowedValues.codingKey.stringValue: Self.allowedValues,
254257
CodingKeys.defaultValue.codingKey.stringValue: Self.defaultValue,
258+
CodingKeys.httpEndpoint.codingKey.stringValue: Self.httpEndpoint,
259+
CodingKeys.httpParameterSource.codingKey.stringValue: Self.httpParameterSource,
260+
CodingKeys.httpMediaType.codingKey.stringValue: Self.httpMediaType,
255261
]
256262

257263
static func == (lhs: SymbolGraph.Symbol.CodingKeys, rhs: SymbolGraph.Symbol.CodingKeys) -> Bool {
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2023 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See https://swift.org/LICENSE.txt for license information
8+
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import XCTest
12+
import SymbolKit
13+
14+
final class HTTPTests: XCTestCase {
15+
func testRequestCanBeDecoded() throws {
16+
let jsonData = """
17+
{
18+
"accessLevel" : "public",
19+
"identifier" : {
20+
"interfaceLanguage" : "data",
21+
"precise" : "data:example:get:path1"
22+
},
23+
"kind" : {
24+
"displayName" : "HTTP Request",
25+
"identifier" : "httpRequest"
26+
},
27+
"names" : {
28+
"title" : "Get Something"
29+
},
30+
"pathComponents": [],
31+
"httpEndpoint": {
32+
"method": "get",
33+
"baseURL": "http://example.com",
34+
"path": "path1"
35+
}
36+
}
37+
""".data(using: .utf8)
38+
39+
let decoder = JSONDecoder()
40+
let symbol = try decoder.decode(SymbolGraph.Symbol.self, from: jsonData!)
41+
42+
XCTAssertEqual(symbol.kind.identifier, .httpRequest)
43+
XCTAssertNotNil(symbol.httpEndpoint)
44+
if let endpoint = symbol.httpEndpoint {
45+
XCTAssertEqual(endpoint.method, "GET")
46+
XCTAssertEqual(endpoint.baseURL, URL(string: "http://example.com"))
47+
XCTAssertEqual(endpoint.path, "path1")
48+
XCTAssertNil(endpoint.sandboxURL)
49+
}
50+
51+
// Verify that the endpoint's method is always forced to uppercase
52+
var endpoint = SymbolGraph.Symbol.HTTP.Endpoint(method: "get", baseURL: URL(string: "http://example.com")!, path: "path")
53+
XCTAssertEqual(endpoint.method, "GET")
54+
endpoint.method = "put"
55+
XCTAssertEqual(endpoint.method, "PUT")
56+
}
57+
58+
func testParameterCanBeDecoded() throws {
59+
let jsonData = """
60+
{
61+
"accessLevel" : "public",
62+
"identifier" : {
63+
"interfaceLanguage" : "data",
64+
"precise" : "data:example:get:path1@q=param1"
65+
},
66+
"kind" : {
67+
"displayName" : "HTTP Parameter",
68+
"identifier" : "httpParameter"
69+
},
70+
"names" : {
71+
"title" : "param1"
72+
},
73+
"pathComponents": [],
74+
"httpParameterSource": "query",
75+
}
76+
""".data(using: .utf8)
77+
78+
let decoder = JSONDecoder()
79+
let symbol = try decoder.decode(SymbolGraph.Symbol.self, from: jsonData!)
80+
81+
XCTAssertEqual(symbol.kind.identifier, .httpParameter)
82+
XCTAssertEqual(symbol.httpParameterSource, "query")
83+
XCTAssertNil(symbol.httpEndpoint)
84+
}
85+
86+
func testResponseCanBeDecoded() throws {
87+
let jsonData = """
88+
{
89+
"accessLevel" : "public",
90+
"identifier" : {
91+
"interfaceLanguage" : "data",
92+
"precise" : "data:example:get:path1=200-application/json"
93+
},
94+
"kind" : {
95+
"displayName" : "HTTP Response",
96+
"identifier" : "httpResponse"
97+
},
98+
"names" : {
99+
"title" : "200"
100+
},
101+
"pathComponents": [],
102+
"httpMediaType": "application/json",
103+
}
104+
""".data(using: .utf8)
105+
106+
let decoder = JSONDecoder()
107+
let symbol = try decoder.decode(SymbolGraph.Symbol.self, from: jsonData!)
108+
109+
XCTAssertEqual(symbol.kind.identifier, .httpResponse)
110+
XCTAssertEqual(symbol.httpMediaType, "application/json")
111+
XCTAssertNil(symbol.httpEndpoint)
112+
}
113+
114+
func testBodyCanBeDecoded() throws {
115+
let jsonData = """
116+
{
117+
"accessLevel" : "public",
118+
"identifier" : {
119+
"interfaceLanguage" : "data",
120+
"precise" : "data:example:get:path1@body=application/json"
121+
},
122+
"kind" : {
123+
"displayName" : "HTTP Body",
124+
"identifier" : "httpBody"
125+
},
126+
"names" : {
127+
"title" : "body"
128+
},
129+
"pathComponents": [],
130+
"httpMediaType": "application/json",
131+
}
132+
""".data(using: .utf8)
133+
134+
let decoder = JSONDecoder()
135+
let symbol = try decoder.decode(SymbolGraph.Symbol.self, from: jsonData!)
136+
137+
XCTAssertEqual(symbol.kind.identifier, .httpBody)
138+
XCTAssertEqual(symbol.httpMediaType, "application/json")
139+
XCTAssertNil(symbol.httpEndpoint)
140+
}
141+
}

0 commit comments

Comments
 (0)