GenerateDoccReference.swift 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. //===----------------------------------------------------------------------===//
  2. //
  3. // This source file is part of the Swift Argument Parser open source project
  4. //
  5. // Copyright (c) 2025 Apple Inc. and the Swift project authors
  6. // Licensed under Apache License v2.0 with Runtime Library Exception
  7. //
  8. // See https://swift.org/LICENSE.txt for license information
  9. //
  10. //===----------------------------------------------------------------------===//
  11. import ArgumentParser
  12. import ArgumentParserToolInfo
  13. import Foundation
  14. enum GenerateDoccReferenceError: Error {
  15. case failedToRunSubprocess(error: Error)
  16. case unableToParseToolOutput(error: Error)
  17. case unsupportedDumpHelpVersion(expected: Int, found: Int)
  18. case failedToGenerateDoccReference(error: Error)
  19. }
  20. extension GenerateDoccReferenceError: CustomStringConvertible {
  21. var description: String {
  22. switch self {
  23. case .failedToRunSubprocess(let error):
  24. return "Failed to run subprocess: \(error)"
  25. case .unableToParseToolOutput(let error):
  26. return "Failed to parse tool output: \(error)"
  27. case .unsupportedDumpHelpVersion(let expected, let found):
  28. return
  29. "Unsupported dump help version, expected '\(expected)' but found: '\(found)'"
  30. case .failedToGenerateDoccReference(let error):
  31. return "Failed to generated docc reference: \(error)"
  32. }
  33. }
  34. }
  35. /// The flavor of generated markdown to emit.
  36. enum OutputStyle: String, EnumerableFlag, ExpressibleByArgument {
  37. /// DocC-supported markdown
  38. case docc
  39. /// GitHub-flavored markdown
  40. case github
  41. }
  42. @main
  43. struct GenerateDoccReference: ParsableCommand {
  44. static let configuration = CommandConfiguration(
  45. commandName: "generate-docc-reference",
  46. abstract: "Generate a docc reference for the provided tool.")
  47. @Argument(help: "Tool to generate docc reference for.")
  48. var tool: String
  49. @Option(
  50. name: .shortAndLong,
  51. help: "Directory to save generated docc reference. Use '-' for stdout.")
  52. var outputDirectory: String
  53. @Option(
  54. name: .shortAndLong,
  55. help: "Use docc flavored markdown for the generated output.")
  56. var style: OutputStyle = .github
  57. func validate() throws {
  58. if outputDirectory != "-" {
  59. // outputDirectory must already exist, `GenerateDoccReference` will not create it.
  60. var objcBool: ObjCBool = true
  61. guard
  62. FileManager.default.fileExists(
  63. atPath: outputDirectory, isDirectory: &objcBool)
  64. else {
  65. throw ValidationError(
  66. "Output directory \(outputDirectory) does not exist")
  67. }
  68. guard objcBool.boolValue else {
  69. throw ValidationError(
  70. "Output directory \(outputDirectory) is not a directory")
  71. }
  72. }
  73. }
  74. func run() throws {
  75. let data: Data
  76. // runs the tool with the --experimental-dump-help argument to capture
  77. // the output.
  78. do {
  79. let tool = URL(fileURLWithPath: tool)
  80. let output = try executeCommand(
  81. executable: tool, arguments: ["--experimental-dump-help"])
  82. data = output.data(using: .utf8) ?? Data()
  83. } catch {
  84. throw GenerateDoccReferenceError.failedToRunSubprocess(error: error)
  85. }
  86. // ToolInfoHeader is intentionally kept internal to argument parser to
  87. // allow the library some flexibility to update/change its content/format.
  88. do {
  89. let toolInfoThin = try JSONDecoder().decode(
  90. ToolInfoHeader.self, from: data)
  91. // verify the serialization version is known/expected
  92. guard toolInfoThin.serializationVersion == 0 else {
  93. throw GenerateDoccReferenceError.unsupportedDumpHelpVersion(
  94. expected: 0,
  95. found: toolInfoThin.serializationVersion)
  96. }
  97. } catch {
  98. throw GenerateDoccReferenceError.unableToParseToolOutput(error: error)
  99. }
  100. let toolInfo: ToolInfoV0
  101. do {
  102. toolInfo = try JSONDecoder().decode(ToolInfoV0.self, from: data)
  103. } catch {
  104. throw GenerateDoccReferenceError.unableToParseToolOutput(error: error)
  105. }
  106. do {
  107. if self.outputDirectory == "-" {
  108. try self.generatePages(
  109. from: toolInfo.command, savingTo: nil, flavor: style)
  110. } else {
  111. try self.generatePages(
  112. from: toolInfo.command,
  113. savingTo: URL(fileURLWithPath: outputDirectory),
  114. flavor: style)
  115. }
  116. } catch {
  117. throw GenerateDoccReferenceError.failedToGenerateDoccReference(
  118. error: error)
  119. }
  120. }
  121. /// Generates a markdown file from the CommandInfoV0 object you provide.
  122. /// - Parameters:
  123. /// - command: The command to parse into a markdown output.
  124. /// - directory: The directory to save the generated markdown file, printing it if `nil`.
  125. /// - flavor: The flavor of markdown to use when generating the content.
  126. /// - Throws: An error if the markdown file cannot be generated or saved.
  127. func generatePages(
  128. from command: CommandInfoV0, savingTo directory: URL?, flavor: OutputStyle
  129. )
  130. throws
  131. {
  132. let page = command.toMarkdown([], markdownStyle: style)
  133. if let directory = directory {
  134. let fileName = command.doccReferenceFileName
  135. let outputPath = directory.appendingPathComponent(fileName)
  136. try page.write(to: outputPath, atomically: false, encoding: .utf8)
  137. } else {
  138. print(page)
  139. }
  140. }
  141. }