Skip to content

Commit af0f21b

Browse files
Fix imports in test files and add documentation generation
1 parent a6eba5d commit af0f21b

File tree

143 files changed

+4392
-205
lines changed

Some content is hidden

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

143 files changed

+4392
-205
lines changed

API.docc/Getting-Started.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,26 +29,26 @@ func fetchUsers(completion: @escaping ([User]?, Error?) -> Void) {
2929
var request = URLRequest(url: url)
3030
request.httpMethod = "GET"
3131
request.setValue("Bearer YOUR_API_KEY", forHTTPHeaderField: "Authorization")
32-
32+
3333
let task = URLSession.shared.dataTask(with: request) { data, response, error in
3434
if let error = error {
3535
completion(nil, error)
3636
return
3737
}
38-
38+
3939
guard let data = data else {
4040
completion(nil, NSError(domain: "No data", code: 0, userInfo: nil))
4141
return
4242
}
43-
43+
4444
do {
4545
let users = try JSONDecoder().decode([User].self, from: data)
4646
completion(users, nil)
4747
} catch {
4848
completion(nil, error)
4949
}
5050
}
51-
51+
5252
task.resume()
5353
}
5454
```
@@ -65,7 +65,7 @@ func fetchUsers() async throws -> [User] {
6565
var request = URLRequest(url: url)
6666
request.httpMethod = "GET"
6767
request.setValue("Bearer YOUR_API_KEY", forHTTPHeaderField: "Authorization")
68-
68+
6969
let (data, _) = try await URLSession.shared.data(for: request)
7070
return try JSONDecoder().decode([User].self, from: data)
7171
}
@@ -83,4 +83,4 @@ Our API uses standard HTTP status codes:
8383

8484
## Rate Limits
8585

86-
API requests are limited to 100 requests per minute per API key.
86+
API requests are limited to 100 requests per minute per API key.

Package.swift

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import PackageDescription
88
let package = Package(
99
name: "OpenAPItoSymbolGraph",
1010
products: [
11-
.executable(name: "openapi-to-symbolgraph", targets: ["OpenAPItoSymbolGraph"])
11+
.executable(name: "openapi-to-symbolgraph", targets: ["CLI"])
1212
],
1313
dependencies: [
1414
.package(url: "https://github.com/mattpolzin/OpenAPIKit.git", from: "3.1.0"),
@@ -17,19 +17,28 @@ let package = Package(
1717
.package(url: "https://github.com/jpsim/Yams.git", from: "5.0.0"),
1818
],
1919
targets: [
20-
.executableTarget(
21-
name: "OpenAPItoSymbolGraph",
20+
.target(
21+
name: "Core",
2222
dependencies: [
2323
.product(name: "OpenAPIKit", package: "OpenAPIKit"),
24-
.product(name: "SymbolKit", package: "swift-docc-symbolkit"),
24+
.product(name: "SymbolKit", package: "swift-docc-symbolkit")
25+
],
26+
path: "Sources/Core"
27+
),
28+
.executableTarget(
29+
name: "CLI",
30+
dependencies: [
31+
"Core",
2532
.product(name: "ArgumentParser", package: "swift-argument-parser"),
2633
.product(name: "Yams", package: "Yams"),
34+
.product(name: "OpenAPIKit", package: "OpenAPIKit"),
35+
.product(name: "SymbolKit", package: "swift-docc-symbolkit")
2736
],
28-
path: "Sources"
37+
path: "Sources/CLI"
2938
),
3039
.testTarget(
3140
name: "OpenAPItoSymbolGraphTests",
32-
dependencies: ["OpenAPItoSymbolGraph"],
41+
dependencies: ["Core", "CLI"],
3342
path: "Tests"
3443
)
3544
]
Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
import Foundation
2+
import OpenAPIKit
3+
import SymbolKit
4+
import ArgumentParser
5+
import Yams
6+
import Core
7+
8+
@main
9+
struct OpenAPItoSymbolGraph: ParsableCommand {
10+
static let configuration = CommandConfiguration(
11+
commandName: "openapi-to-symbolgraph",
12+
abstract: "Convert OpenAPI documents to DocC symbol graphs",
13+
version: "1.0.0"
14+
)
15+
16+
@Argument(help: "Path to the OpenAPI document")
17+
var inputPath: String
18+
19+
@Option(name: .long, help: "Output path for the symbol graph")
20+
var outputPath: String = "openapi.symbolgraph.json"
21+
22+
func run() throws {
23+
let inputURL = URL(fileURLWithPath: inputPath)
24+
let data = try Data(contentsOf: inputURL)
25+
let fileExtension = inputURL.pathExtension.lowercased()
26+
27+
// Parse the OpenAPI document manually to avoid version parsing issues
28+
var rawDict: [String: Any]
29+
30+
do {
31+
if fileExtension == "json" {
32+
print("Parsing JSON...")
33+
guard let jsonDict = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
34+
throw RunError.parsingError("Failed to parse JSON as dictionary")
35+
}
36+
rawDict = jsonDict
37+
} else if fileExtension == "yaml" || fileExtension == "yml" {
38+
print("Parsing YAML...")
39+
let yamlString = String(data: data, encoding: .utf8)!
40+
guard let yamlDict = try Yams.load(yaml: yamlString) as? [String: Any] else {
41+
throw RunError.parsingError("Failed to parse YAML as dictionary")
42+
}
43+
rawDict = yamlDict
44+
} else {
45+
throw RunError.invalidFileType("Unsupported file type: \(fileExtension). Please use .json or .yaml/.yml")
46+
}
47+
} catch {
48+
print("Error during initial parsing: \(error)")
49+
throw error
50+
}
51+
52+
// Manually extract key information
53+
let infoDict = rawDict["info"] as? [String: Any] ?? [:]
54+
let title = infoDict["title"] as? String ?? "API"
55+
let description = infoDict["description"] as? String
56+
let apiVersion = infoDict["version"] as? String ?? "1.0.0"
57+
58+
// Sanitize the title for use as a module name (remove spaces, hyphens, and periods)
59+
let moduleName = title.replacingOccurrences(of: " ", with: "").replacingOccurrences(of: "-", with: "").replacingOccurrences(of: ".", with: "")
60+
print("Using module name: \(moduleName)") // Debug print
61+
62+
// Extract paths and components
63+
let pathsDict = rawDict["paths"] as? [String: Any] ?? [:]
64+
let componentsDict = rawDict["components"] as? [String: Any] ?? [:]
65+
let schemasDict = componentsDict["schemas"] as? [String: Any] ?? [:]
66+
67+
// --- Symbol graph generation logic ---
68+
var symbols: [SymbolKit.SymbolGraph.Symbol] = []
69+
var relationships: [SymbolKit.SymbolGraph.Relationship] = []
70+
71+
// Add API namespace using SymbolMapper
72+
let (apiSymbol, _) = SymbolMapper.createSymbol(
73+
kind: .namespace,
74+
identifierPrefix: "s",
75+
moduleName: moduleName,
76+
localIdentifier: "", // Root namespace has no local identifier part - will result in "s:moduleName."
77+
title: title,
78+
description: description,
79+
pathComponents: [moduleName], // Path is just the module name
80+
parentIdentifier: nil // parentIdentifier is nil for root
81+
)
82+
83+
// Fix the namespace identifier to remove the trailing dot
84+
let fixedNamespaceSymbol = SymbolKit.SymbolGraph.Symbol(
85+
identifier: SymbolKit.SymbolGraph.Symbol.Identifier(
86+
precise: "s:\(moduleName)", // No dot at the end
87+
interfaceLanguage: "swift"
88+
),
89+
names: apiSymbol.names,
90+
pathComponents: apiSymbol.pathComponents,
91+
docComment: apiSymbol.docComment,
92+
accessLevel: apiSymbol.accessLevel,
93+
kind: apiSymbol.kind,
94+
mixins: apiSymbol.mixins
95+
)
96+
97+
symbols.append(fixedNamespaceSymbol)
98+
99+
// Process paths using SymbolMapper
100+
print("Processing paths...")
101+
print("DEBUG: pathsDict keys: \(pathsDict.keys)")
102+
103+
for (path, pathItemObj) in pathsDict {
104+
print("DEBUG: Processing path: \(path)")
105+
guard let pathItemDict = pathItemObj as? [String: Any] else {
106+
print("DEBUG: pathItemObj is not a dictionary")
107+
continue
108+
}
109+
print("DEBUG: pathItemDict keys: \(pathItemDict.keys)")
110+
111+
for (method, operationObj) in pathItemDict {
112+
print("DEBUG: Processing method: \(method)")
113+
guard let validMethod = OpenAPI.HttpMethod(rawValue: method.lowercased()) else {
114+
print("DEBUG: Not a valid HTTP method: \(method)")
115+
continue
116+
}
117+
guard let operationDict = operationObj as? [String: Any] else {
118+
print("DEBUG: operationObj is not a dictionary")
119+
continue
120+
}
121+
print("DEBUG: operationDict keys: \(operationDict.keys)")
122+
123+
// ---- Reconstruct OpenAPI.Operation (partially) ----
124+
let operationId = operationDict["operationId"] as? String
125+
print("DEBUG: operationId: \(operationId ?? "nil")")
126+
let summary = operationDict["summary"] as? String
127+
let operationDescription = operationDict["description"] as? String
128+
let tags = operationDict["tags"] as? [String]
129+
let deprecated = operationDict["deprecated"] as? Bool ?? false
130+
131+
let reconstructedOp = OpenAPI.Operation(
132+
tags: tags, summary: summary, description: operationDescription,
133+
externalDocs: nil, operationId: operationId, parameters: [],
134+
requestBody: nil, responses: [:], callbacks: [:],
135+
deprecated: deprecated, security: [], servers: [], vendorExtensions: [:])
136+
137+
// Pass moduleName to SymbolMapper functions
138+
print("DEBUG: Creating operation symbol for \(operationId ?? "unknown"), method: \(method), path: \(path)")
139+
let (opSymbol, opRelationships) = SymbolMapper.createOperationSymbol(
140+
operation: reconstructedOp,
141+
path: path,
142+
method: validMethod.rawValue.uppercased(),
143+
moduleName: moduleName // Pass moduleName
144+
)
145+
print("DEBUG: Created operation symbol with ID: \(opSymbol.identifier.precise)")
146+
print("DEBUG: Adding operation relationships: \(opRelationships.map { "\($0.source)|\($0.target)" }.joined(separator: ", "))")
147+
148+
symbols.append(opSymbol)
149+
relationships.append(contentsOf: opRelationships)
150+
}
151+
}
152+
153+
// Process schemas using SymbolMapper
154+
print("Processing schemas...")
155+
for (schemaName, schemaObj) in schemasDict {
156+
guard let schemaDict = schemaObj as? [String: Any] else { continue }
157+
158+
do {
159+
let schemaData = try JSONSerialization.data(withJSONObject: schemaDict, options: [])
160+
let decoder = JSONDecoder()
161+
let jsonSchema = try decoder.decode(OpenAPIKit.JSONSchema.self, from: schemaData)
162+
163+
// Pass moduleName to SymbolMapper functions
164+
let (schemaSymbols, schemaRelationships) = SymbolMapper.createSchemaSymbol(
165+
name: schemaName,
166+
schema: jsonSchema,
167+
moduleName: moduleName // Pass moduleName
168+
)
169+
symbols.append(contentsOf: schemaSymbols)
170+
relationships.append(contentsOf: schemaRelationships)
171+
172+
} catch {
173+
print("Warning: Failed to decode schema '\(schemaName)' into OpenAPIKit.JSONSchema: \(error). Creating basic symbol.")
174+
// Also update call here for basic symbol creation
175+
let (basicSchemaSymbol, basicSchemaRelationship) = SymbolMapper.createSymbol(
176+
kind: .schema,
177+
identifierPrefix: "s",
178+
moduleName: moduleName,
179+
localIdentifier: schemaName, // Schema name is local ID
180+
title: schemaName,
181+
description: schemaDict["description"] as? String ?? "Schema for \(schemaName) (Decoding Failed)",
182+
pathComponents: [moduleName, schemaName],
183+
parentIdentifier: "s:\(moduleName)" // Use fixed namespace identifier (no dot)
184+
)
185+
symbols.append(basicSchemaSymbol)
186+
if let rel = basicSchemaRelationship { relationships.append(rel) }
187+
}
188+
}
189+
190+
// Create symbol graph (using sanitized moduleName)
191+
let symbolGraph = SymbolKit.SymbolGraph(
192+
metadata: SymbolKit.SymbolGraph.Metadata(
193+
formatVersion: SymbolKit.SymbolGraph.SemanticVersion(major: 1, minor: 0, patch: 0),
194+
generator: "OpenAPItoSymbolGraph"
195+
),
196+
module: SymbolKit.SymbolGraph.Module(
197+
name: moduleName, // Ensure this uses the fully sanitized name
198+
platform: SymbolKit.SymbolGraph.Platform(
199+
architecture: nil,
200+
vendor: nil,
201+
operatingSystem: SymbolKit.SymbolGraph.OperatingSystem(name: "macosx")
202+
),
203+
version: parseVersion(apiVersion)
204+
),
205+
symbols: symbols,
206+
relationships: relationships
207+
)
208+
209+
// Write symbol graph to file
210+
let outputURL = URL(fileURLWithPath: outputPath)
211+
let encoder = JSONEncoder()
212+
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
213+
let symbolGraphData = try encoder.encode(symbolGraph)
214+
try symbolGraphData.write(to: outputURL)
215+
216+
print("Symbol graph generated at \(outputURL.path)")
217+
}
218+
219+
/// Parses a version string (e.g., "1.2.3") into a `SymbolKit.SymbolGraph.SemanticVersion`.
220+
///
221+
/// This function handles basic `major.minor.patch` formats. Pre-release identifiers
222+
/// and build metadata in the version string are ignored.
223+
/// - Parameter versionString: The version string to parse.
224+
/// - Returns: A `SemanticVersion` instance.
225+
private func parseVersion(_ versionString: String) -> SymbolKit.SymbolGraph.SemanticVersion {
226+
let components = versionString.split(separator: ".").compactMap { Int($0) }
227+
let major = components.count > 0 ? components[0] : 0
228+
let minor = components.count > 1 ? components[1] : 0
229+
let patch = components.count > 2 ? components[2] : 0
230+
// Note: Pre-release identifiers and build metadata are ignored in this simple parse.
231+
return SymbolKit.SymbolGraph.SemanticVersion(major: major, minor: minor, patch: patch)
232+
}
233+
234+
// Helper function to map JSON types to Swift types
235+
func mapJsonTypeToSwift(type: String, format: String?) -> String {
236+
switch type.lowercased() {
237+
case "string":
238+
if let format = format {
239+
switch format.lowercased() {
240+
case "date": return "Date"
241+
case "date-time": return "Date"
242+
case "email": return "String"
243+
case "uri": return "URL"
244+
case "uuid": return "UUID"
245+
case "binary", "byte": return "Data"
246+
default: return "String"
247+
}
248+
}
249+
return "String"
250+
251+
case "integer":
252+
if let format = format {
253+
switch format.lowercased() {
254+
case "int32": return "Int32"
255+
case "int64": return "Int64"
256+
default: return "Int"
257+
}
258+
}
259+
return "Int"
260+
261+
case "number":
262+
if let format = format {
263+
switch format.lowercased() {
264+
case "float": return "Float"
265+
case "double": return "Double"
266+
default: return "Double"
267+
}
268+
}
269+
return "Double"
270+
271+
case "boolean":
272+
return "Bool"
273+
274+
case "array":
275+
return "[Any]" // A more robust solution would extract the items type
276+
277+
case "object":
278+
return "[String: Any]"
279+
280+
default:
281+
return "Any"
282+
}
283+
}
284+
285+
// Define custom error
286+
enum RunError: Error, CustomStringConvertible {
287+
case invalidFileType(String)
288+
case parsingError(String)
289+
case dataEncodingError(String)
290+
291+
var description: String {
292+
switch self {
293+
case .invalidFileType(let msg): return msg
294+
case .parsingError(let msg): return msg
295+
case .dataEncodingError(let msg): return msg
296+
}
297+
}
298+
}
299+
}

0 commit comments

Comments
 (0)