Getting Started with Swift OpenAPI Generator

With the launch of Bushel, I created the concept of a Hub. A Hub is an online repository or source that provides you with a collection of macOS restore images (.ipsw files). Hubs serve as convenient sources for obtaining various versions of macOS to import into your Library for setting up virtual machines.

At launch, there was only the default Apple Hub which returns the latest production release of macOS. Now I been wanting to support the use of IPSW IPSW Downloads which provides a plethora of images. Thankfully IPSW Downloads provides an API and documentation.

In this article I'm going to cover:

Getting Ready

In some cases the service, might not provide the exact spec needed. In the case of IPSW Downloads, it does not provide an OpenAPI document but rather an API Blueprint document. API Blueprint is a markdown alternative to OpenAPI which means I need a way to convert the API Blueprint markdown to OpenAPI yaml. For this I found apib2swagger, which takes a API Blueprint file and outputs a OpenAPI YAML file. Many developers do provide great OpenAPI documentation, however there will be cases where you'll need to convert their specifications with a tool of some sort or perhaps your favorite AI tool.

In any case it’s worth learning the details of OpenAPI docs at some point since all conversions are approximate and imperfect. For this I found Swagger docs pretty accessible for defining data types Stoplight for creating new APIs.

Now I can get started building integrating IPSW Downloads with Bushel.

From YAML to Swift

Let’s start by just creating a Swift Package for only the IPSW Downloads API. So let’s start by creating a Swift Package. Inside the Package.swift we need to add dependencies:

// swift-tools-version: 5.9
import PackageDescription

let package = Package(
	name: "IPSWDownloads",
	platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .visionOS(.v1)],
	products: [
		.library(name: "IPSWDownloads", targets: ["IPSWDownloads"])
	],
	dependencies: [
		.package(url: "https://github.com/apple/swift-openapi-generator", from: "1.0.0"),
		.package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.0.0"),
		.package(url: "https://github.com/apple/swift-openapi-urlsession", from: "1.0.0")
	],
	targets: [
		.target(
			name: "IPSWDownloads",
			dependencies: [
				.product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
				.product(name: "OpenAPIURLSession", package: "swift-openapi-urlsession")
			],
			plugins: [.plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator")]
		)
	]
)

In the target directory /Sources/IPSWDownloads, we need to add our openapi.yaml file to and a openapi-generator-config.yaml file:

generate:
	- types
	- client
accessModifier: internal

There are plenty of options available for the openapi-generator-config.yaml which configures the generator. Most importantly, we are:

By adding these files, in Xcode or via the command line, the OpenAPI generator plugin creates two Swift files which are compiled in the background: Types.swift and Client.swift.

Going Public

From here I can make a few modifications before publishing my Swift Package:

Curating vs Transparent

We could transparently provide the types generated to the public by modifying the accessModifier in openapi-generator-config.yaml :

generate:
	- types
	- client
accessModifier: public

However this isn’t really recommended practice. The curated example provided suggests creating an intermediary abstraction for your public consumers and the actual OpenAPI types. This is the practice I followed with the IPSW Downloads API.

Here's an example of a Firmware type which is it's own type but is converted from the OpenAPI Components.Schema.Firmware type:

import Foundation

public struct Firmware {
	public let identifier: String
	public let version: OperatingSystemVersion
	public let buildid: String
	public let sha1sum: String
	public let md5sum: String
	public let filesize: Int
	public let url: URL
	public let releasedate: Date
	public let uploaddate: Date
	public let signed: Bool
}

extension Firmware {
	// Convert the generated `Components.Schemas.Firmware` into our public `Firmware`
	internal init(component: Components.Schemas.Firmware) throws {
		// parse the OpenAPI version string into an actual `OperatingSystemVersion`
		let version = try OperatingSystemVersion(string: component.version)
		
		// parse the OpenAPI url string into an actual `URL`
		let url = try URL(validatingURL: component.url)
		
		try self.init(
			identifier: component.identifier,
			version: version,
			buildid: component.buildid,
			sha1sum: component.sha1sum,
			md5sum: component.md5sum,
			filesize: component.filesize,
			url: url,
			releasedate: component.releasedate,
			uploaddate: component.uploaddate,
			signed: component.signed
		)
	}
}

You can see we are parsing the OperatingSystemVersion and the URL into the appropriate types. If there's some conversion missing in the OpenAPI conversion (i.e. URL) or the spec may change and you wish to allow a something transition for consumers of your API, it's worth curating the OpenAPI in your public library.

Plugin vs CLI

Do we want to use the Swift Package plugin or generate manually and include the generated files in our repo?

If you go with the Swift Package plugin and make your package available, developers who consume it will be prompted in Xcode to allow the plugin to run. In my case, this causes friction for outside devs importing your library. So rather than using the plugin I decided to generate the code myself and include it in the repo. To generate the code in IPSWDownloads I created a script which calls generate manually:

swift run swift-openapi-generator generate \ 
	--output-directory Sources/IPSWDownloads/Generated \
	--config openapi-generator-config.yaml \
	openapi.yaml

Here you specify the output-directory and the last argument is the path to the openapi.yaml file. The configuration in this case uses our previously created openapi-generator-config.yaml. However we do have the option to specify configuration manually using options such as access-modifier and types.

Plugging into Bushel

Luckily once the library is completed, it's fairly simple plugging this into Bushel. You can use the library yourself or you can try this yourself by signing up the latest beta of Bushel.

If you plan to create your own API or adopt one for Swift using the OpenAPI generator, here are some tips:

Let me know if you've created any thing via the OpenAPI generator. What's been your biggest challenge? What did your learn? Follow me and let me know what you think of the Swift OpenAPI Generator.