├── .github
├── CODEOWNERS
├── dependabot.yml
└── workflows
│ ├── build-and-deploy-docs-workflow.yml
│ └── deploy-api-docs.yml
├── .gitignore
├── LICENSE
├── README.md
├── api-docs.png
├── error.html
├── generate-api-docs.swift
├── generate-package-api-docs.swift
├── images
├── article.svg
├── collection.svg
├── curly-brackets.svg
└── technology.svg
├── index.html
├── stack.yaml
└── theme-settings.json
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @0xTim @gwynne
2 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "daily"
7 | groups:
8 | dependencies:
9 | patterns:
10 | - "*"
11 |
--------------------------------------------------------------------------------
/.github/workflows/build-and-deploy-docs-workflow.yml:
--------------------------------------------------------------------------------
1 | name: Shared Build DocC docs and Deploy
2 | concurrency:
3 | group: ${{ github.workflow }}-${{ github.ref }}
4 | cancel-in-progress: true
5 | on:
6 | workflow_call:
7 | inputs:
8 | package_name:
9 | type: string
10 | required: false
11 | description: "The name of the package to build docs for."
12 | modules:
13 | type: string
14 | required: true
15 | description: "The modules in the package to build docs for."
16 | pathsToInvalidate:
17 | type: string
18 | required: false
19 | description: "The paths to invalidate in CloudFront, e.g. '/vapor/* /xctvapor/*'."
20 | workflow_dispatch:
21 | inputs:
22 | repository:
23 | type: string
24 | required: true
25 | description: "The repository of the package to build docs for."
26 | package_name:
27 | type: string
28 | required: true
29 | description: "The name of the package to build docs for."
30 | modules:
31 | type: string
32 | required: true
33 | description: "The modules in the package to build docs for."
34 | pathsToInvalidate:
35 | type: string
36 | required: true
37 | description: "The paths to invalidate in CloudFront, e.g. '/vapor/* /xctvapor/*'."
38 | env:
39 | INPUT_REPOSITORY: ${{ inputs.repository || github.repository }}
40 | INPUT_PACKAGE_NAME: ${{ inputs.package_name }}
41 | INPUT_MODULES: ${{ inputs.modules }}
42 | INPUT_INVALIDATE_PATHS: ${{ inputs.pathsToInvalidate }}
43 |
44 | jobs:
45 | build-docs:
46 | runs-on: ubuntu-22.04
47 | permissions: { id-token: write, contents: read }
48 | env: { AWS_PAGER: '' }
49 | steps:
50 | - name: Check out code
51 | uses: actions/checkout@v4
52 | with:
53 | repository: ${{ inputs.repository || github.repository }}
54 | fetch-depth: 0
55 | - name: Install latest Swift
56 | uses: vapor/swiftly-action@v0.2
57 | with: { toolchain: latest }
58 | - name: Download files
59 | run: |
60 | curl -sL \
61 | "https://raw.githubusercontent.com/vapor/api-docs/main/generate-package-api-docs.swift" \
62 | -o generate-package-api-docs.swift \
63 | "https://raw.githubusercontent.com/vapor/api-docs/main/theme-settings.json" \
64 | -o theme-settings.json
65 | - name: Build docs
66 | run: 'swift generate-package-api-docs.swift "${INPUT_PACKAGE_NAME}" ${INPUT_MODULES}'
67 | - name: Configure AWS credentials
68 | uses: aws-actions/configure-aws-credentials@v4
69 | with:
70 | role-to-assume: ${{ vars.OIDC_ROLE_ARN }}
71 | aws-region: ${{ vars.OIDC_ROLE_REGION }}
72 | - name: Deploy to S3
73 | env:
74 | S3_BUCKET_URL: ${{ secrets.VAPOR_API_DOCS_S3_BUCKET_URL }}
75 | run: |
76 | aws s3 sync ./public "${S3_BUCKET_URL}" --no-progress --acl public-read
77 | - name: Invalidate CloudFront
78 | env:
79 | DISTRIBUTION_ID: ${{ secrets.VAPOR_API_DOCS_DISTRIBUTION_ID }}
80 | run: |
81 | aws cloudfront create-invalidation --distribution-id "${DISTRIBUTION_ID}" --paths ${INPUT_INVALIDATE_PATHS}
82 |
--------------------------------------------------------------------------------
/.github/workflows/deploy-api-docs.yml:
--------------------------------------------------------------------------------
1 | name: API Docs website deploy
2 | concurrency:
3 | group: ${{ github.workflow }}-${{ github.ref }}
4 | cancel-in-progress: true
5 | on:
6 | push: { branches: [ main ] }
7 |
8 | jobs:
9 | deploy-site:
10 | runs-on: ubuntu-22.04
11 | permissions: { id-token: write, contents: read }
12 | env: { AWS_PAGER: '' }
13 | steps:
14 | - name: Check out code
15 | uses: actions/checkout@v4
16 | - name: Install latest Swift
17 | uses: vapor/swiftly-action@v0.2
18 | with: { toolchain: latest }
19 | - name: Build site
20 | run: swift generate-api-docs.swift
21 | - name: Configure AWS credentials
22 | uses: aws-actions/configure-aws-credentials@v4
23 | with:
24 | role-to-assume: ${{ vars.OIDC_ROLE_ARN }}
25 | aws-region: ${{ vars.OIDC_ROLE_REGION }}
26 | - name: Deploy to CloudFormation
27 | uses: aws-actions/aws-cloudformation-github-deploy@v1
28 | with:
29 | name: vapor-api-docs
30 | template: stack.yaml
31 | no-fail-on-empty-changeset: '1'
32 | parameter-overrides: >-
33 | BucketName=vapor-api-docs-site,
34 | SubDomainName=api,
35 | HostedZoneName=vapor.codes,
36 | AcmCertificateArn=${{ secrets.API_DOCS_CERTIFICATE_ARN }}
37 | - name: Deploy to S3
38 | env:
39 | S3_BUCKET_URL: ${{ secrets.VAPOR_API_DOCS_S3_BUCKET_URL }}
40 | run: 'aws s3 sync ./public "${S3_BUCKET_URL}" --no-progress --acl public-read'
41 | - name: Invalidate CloudFront
42 | env:
43 | DISTRIBUTION_ID: ${{ secrets.VAPOR_API_DOCS_DISTRIBUTION_ID }}
44 | run: 'aws cloudfront create-invalidation --distribution-id "${DISTRIBUTION_ID}" --paths "/*"'
45 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | code
3 | /packages
4 | /swift-doc
5 | /public
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Qutheory, LLC
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # API Documentation
2 |
3 | View the documentation at [api.vapor.codes](http://api.vapor.codes).
4 |
5 | This repo contains the script that generates all vapor api-docs when a new release is cut.
6 | For every repo, on release a github action calls into a script that pulls in the latest version of this repo to the server, then running the provided script.
7 |
--------------------------------------------------------------------------------
/api-docs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vapor/api-docs/735b41fc6f13da3b9de5a62e3e373096b267da14/api-docs.png
--------------------------------------------------------------------------------
/error.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Error Page | Vapor API Docs
5 |
6 |
36 |
37 |
38 |
39 |
40 |
Oops! Something went wrong
41 |
42 |
43 | We couldn't find what you were looking for. Try heading back to the home page.
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/generate-api-docs.swift:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env swift
2 |
3 | import Foundation
4 |
5 | let packages: [String: [String]] = [
6 | "vapor": ["Vapor", "XCTVapor", "VaporTesting"],
7 |
8 | "async-kit": ["AsyncKit"],
9 | "routing-kit": ["RoutingKit"],
10 | "console-kit": ["ConsoleKit", "ConsoleKitCommands", "ConsoleKitTerminal"],
11 | "websocket-kit": ["WebSocketKit"],
12 | "multipart-kit": ["MultipartKit"],
13 |
14 | "postgres-nio": ["PostgresNIO"],
15 | "mysql-nio": ["MySQLNIO"],
16 | "sqlite-nio": ["SQLiteNIO"],
17 |
18 | "sql-kit": ["SQLKit"],
19 | "postgres-kit": ["PostgresKit"],
20 | "mysql-kit": ["MySQLKit"],
21 | "sqlite-kit": ["SQLiteKit"],
22 |
23 | "fluent-kit": ["FluentKit", "FluentSQL", "XCTFluent"],
24 | "fluent": ["Fluent"],
25 | "fluent-postgres-driver": ["FluentPostgresDriver"],
26 | "fluent-mongo-driver": ["FluentMongoDriver"],
27 | "fluent-mysql-driver": ["FluentMySQLDriver"],
28 | "fluent-sqlite-driver": ["FluentSQLiteDriver"],
29 |
30 | "redis": ["Redis"],
31 | "queues-redis-driver": ["QueuesRedisDriver"],
32 | "queues": ["Queues", "XCTQueues"],
33 |
34 | "leaf-kit": ["LeafKit"],
35 | "leaf": ["Leaf"],
36 |
37 | "jwt-kit": ["JWTKit"],
38 | "jwt": ["JWT"],
39 | "apns": ["VaporAPNS"],
40 | ]
41 |
42 | let htmlMenu = packages.values.flatMap { $0 }
43 | .sorted()
44 | .map { "" }
45 | .joined(separator: "\n")
46 |
47 | do {
48 | let publicDirUrl = URL(fileURLWithPath: "./public", isDirectory: true)
49 | try FileManager.default.removeItemIfExists(at: publicDirUrl)
50 | try FileManager.default.createDirectory(at: publicDirUrl, withIntermediateDirectories: true)
51 |
52 | var htmlIndex = try String(contentsOf: URL(fileURLWithPath: "./index.html", isDirectory: false), encoding: .utf8)
53 | htmlIndex.replace("{{Options}}", with: "\(htmlMenu)\n", maxReplacements: 1)
54 |
55 | try htmlIndex.write(to: publicDirUrl.appendingPathComponent("index.html", isDirectory: false), atomically: true, encoding: .utf8)
56 | try FileManager.default.copyItem(at: URL(fileURLWithPath: "./api-docs.png", isDirectory: false), into: publicDirUrl)
57 | try FileManager.default.copyItem(at: URL(fileURLWithPath: "./error.html", isDirectory: false), into: publicDirUrl)
58 | } catch let error as NSError {
59 | print("❌ ERROR: \(String(reflecting: error)): \(error.userInfo)")
60 | exit(1)
61 | }
62 |
63 | extension NSError {
64 | func isCocoaError(_ code: CocoaError.Code) -> Bool {
65 | self.domain == CocoaError.errorDomain && self.code == code.rawValue
66 | }
67 | }
68 |
69 | extension FileManager {
70 | func removeItemIfExists(at url: URL) throws {
71 | do {
72 | try self.removeItem(at: url)
73 | } catch let error as NSError where error.isCocoaError(.fileNoSuchFile) {
74 | // ignore
75 | }
76 | }
77 |
78 | func copyItem(at src: URL, into dst: URL) throws {
79 | assert(dst.hasDirectoryPath)
80 |
81 | let dstItem = dst.appendingPathComponent(src.lastPathComponent, isDirectory: dst.hasDirectoryPath)
82 | try self.copyItem(at: src, to: dstItem)
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/generate-package-api-docs.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | guard CommandLine.argc == 3 else {
4 | print("❌ ERROR: You must provide the package name and modules as a comma separated string.")
5 | exit(1)
6 | }
7 |
8 | let packageName = CommandLine.arguments[1]
9 | let moduleList = CommandLine.arguments[2]
10 |
11 | let modules = moduleList.components(separatedBy: ",")
12 |
13 | let publicDirectoryUrl = URL.currentDirectory().appending(component: "public")
14 |
15 | do {
16 | try run()
17 | } catch {
18 | print("❌ ERROR: \(error).")
19 | exit(1)
20 | }
21 |
22 | // MARK: Functions
23 |
24 | func run() throws {
25 | // Set up
26 | print("⚙️ Ensuring public directory...")
27 | try FileManager.default.removeItemIfExists(at: publicDirectoryUrl)
28 | try FileManager.default.createDirectory(at: publicDirectoryUrl, withIntermediateDirectories: true)
29 |
30 | print("⚙️ Ensuring plugin...")
31 | try ensurePluginAvailable()
32 |
33 | // Run
34 | for module in modules {
35 | print("⚙️ Generating api-docs for package: \(packageName), module: \(module)")
36 | try generateDocs(module: module)
37 | }
38 |
39 | print("✅ Finished generating api-docs for package: \(packageName)")
40 | }
41 |
42 | func ensurePluginAvailable() throws {
43 | var foundAtLeastOne = false
44 | for manifestName in ["6.2", "6.1", "6.0", "5.10", "5.9", "5.8", "5.7"].map({ "Package@swift-\($0).swift" }) + ["Package.swift"] {
45 | print("⚙️ Checking for manifest \(manifestName)")
46 | let manifestUrl = URL.currentDirectory().appending(component: manifestName)
47 | var manifestContents: String
48 | do { manifestContents = try String(contentsOf: manifestUrl, encoding: .utf8) }
49 | catch let error as NSError where error.isCocoaError(.fileReadNoSuchFile) { continue }
50 | catch let error as NSError where error.isPOSIXError(.ENOENT) { continue }
51 |
52 | if !manifestContents.contains(".package(url: \"https://github.com/apple/swift-docc-plugin") {
53 | // This is freely admitted to be quick and dirty. Unfortunately, swift package add-dependency doesn't understand version-tagged manifests.
54 | print("🧬 Injecting DocC plugin dependency into \(manifestName)")
55 | guard let depsArrayRange = manifestContents.firstRange(of: "dependencies: [") else {
56 | print("❌ ERROR: Can't inject swift-docc-plugin dependency (can't find deps array).")
57 | exit(1)
58 | }
59 | manifestContents.insert(
60 | contentsOf: "\n.package(url: \"https://github.com/apple/swift-docc-plugin.git\", from: \"1.4.0\"),\n",
61 | at: depsArrayRange.upperBound
62 | )
63 | try manifestContents.write(to: manifestUrl, atomically: true, encoding: .utf8)
64 | }
65 | foundAtLeastOne = true
66 | }
67 | guard foundAtLeastOne else {
68 | print("❌ ERROR: Can't inject swift-docc-plugin dependency (no usable manifest found).")
69 | exit(1)
70 | }
71 | }
72 |
73 | func generateDocs(module: String) throws {
74 | print("🔎 Finding DocC catalog")
75 | let doccCatalogs = try FileManager.default.contentsOfDirectory(
76 | at: URL.currentDirectory().appending(components: "Sources", "\(module)"),
77 | includingPropertiesForKeys: nil,
78 | options: [.skipsSubdirectoryDescendants]
79 | ).filter { $0.hasDirectoryPath && $0.pathExtension == "docc" }
80 | guard !doccCatalogs.isEmpty else {
81 | print("❌ ERROR: No DocC catalog found for \(module)")
82 | exit(1)
83 | }
84 | guard doccCatalogs.count == 1 else {
85 | print("❌ ERROR: More than one DocC catalog found for \(module):\n\(doccCatalogs.map(\.lastPathComponent))")
86 | exit(1)
87 | }
88 | let doccCatalogUrl = doccCatalogs[0]
89 | print("🗂️ Using DocC catalog \(doccCatalogUrl.lastPathComponent)")
90 |
91 | print("📐 Copying theme")
92 | try FileManager.default.copyItemIfExistsWithoutOverwrite(
93 | at: URL.currentDirectory().appending(component: "theme-settings.json"),
94 | to: doccCatalogUrl.appending(component: "theme-settings.json")
95 | )
96 |
97 | print("📝 Generating docs")
98 | try shell([
99 | "swift", "package",
100 | "--allow-writing-to-directory", publicDirectoryUrl.path,
101 | "generate-documentation",
102 | "--target", module,
103 | "--disable-indexing",
104 | "--experimental-skip-synthesized-symbols",
105 | "--enable-inherited-docs",
106 | "--enable-experimental-overloaded-symbol-presentation",
107 | "--enable-experimental-mentioned-in",
108 | "--hosting-base-path", "/\(module.lowercased())",
109 | "--output-path", publicDirectoryUrl.appending(component: "\(module.lowercased())").path,
110 | ])
111 | }
112 |
113 | func shell(_ args: String...) throws { try shell(args) }
114 | func shell(_ args: [String]) throws {
115 | // For fun, echo the command:
116 | var sawXOpt = false, seenOpt = false, lastWasOpt = false
117 | print("+ /usr/bin/env \\\n ", terminator: "")
118 | for arg in (args.dropLast() + [args.last! + "\n"]) {
119 | if (seenOpt && !lastWasOpt) || ((!seenOpt || (lastWasOpt && !sawXOpt)) && arg.starts(with: "-")) {
120 | print(" \\\n ", terminator: "")
121 | }
122 | print(" \(arg)", terminator: "")
123 | lastWasOpt = arg.starts(with: "-")
124 | (seenOpt, sawXOpt) = (seenOpt || lastWasOpt, arg.starts(with: "-X"))
125 | }
126 |
127 | // Run the command:
128 | let task = try Process.run(URL(filePath: "/usr/bin/env"), arguments: args)
129 | task.waitUntilExit()
130 | guard task.terminationStatus == 0 else {
131 | throw ShellError(terminationStatus: task.terminationStatus)
132 | }
133 | }
134 |
135 | struct ShellError: Error {
136 | var terminationStatus: Int32
137 | }
138 |
139 | extension FileManager {
140 | func removeItemIfExists(at url: URL) throws {
141 | do {
142 | try self.removeItem(at: url)
143 | } catch let error as NSError where error.isCocoaError(.fileNoSuchFile) {
144 | // ignore
145 | }
146 | }
147 |
148 | func copyItemIfExistsWithoutOverwrite(at src: URL, to dst: URL) throws {
149 | do {
150 | // https://github.com/apple/swift-corelibs-foundation/pull/4808
151 | #if !canImport(Darwin)
152 | do {
153 | _ = try dst.checkResourceIsReachable()
154 | throw NSError(domain: CocoaError.errorDomain, code: CocoaError.fileWriteFileExists.rawValue)
155 | } catch let error as NSError where error.isCocoaError(.fileReadNoSuchFile) {}
156 | #endif
157 | try self.copyItem(at: src, to: dst)
158 | } catch let error as NSError where error.isCocoaError(.fileReadNoSuchFile) {
159 | // ignore
160 | } catch let error as NSError where error.isCocoaError(.fileWriteFileExists) {
161 | // ignore
162 | }
163 | }
164 | }
165 |
166 | extension NSError {
167 | func isCocoaError(_ code: CocoaError.Code) -> Bool {
168 | self.domain == CocoaError.errorDomain && self.code == code.rawValue
169 | }
170 | func isPOSIXError(_ code: POSIXError.Code) -> Bool {
171 | self.domain == POSIXError.errorDomain && self.code == code.rawValue
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/images/article.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/images/collection.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/images/curly-brackets.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/images/technology.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Vapor API Docs
5 |
6 |
36 |
37 |
38 |
39 |
40 |
Vapor API Docs
41 |
42 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/stack.yaml:
--------------------------------------------------------------------------------
1 | AWSTemplateFormatVersion: "2010-09-09"
2 | Description: 'Create Create a static website on S3 served by CloudFront'
3 | Parameters:
4 | BucketName:
5 | Type: String
6 | Description: The S3 bucket name
7 | SubDomainName:
8 | Type: String
9 | Description: The sub domain name for the site
10 | AllowedPattern: (?!-)[a-zA-Z0-9-.]{1,63}(?