Skip to content

Commit

Permalink
using-openapi-generator-swift5 (#249)
Browse files Browse the repository at this point in the history
* using-openapi-generator-swift5

* Using generated Profile everywhere

* Adding format step to generated code

* Custom generator templates

* Use concrete type for decoding profile response
  • Loading branch information
etoledom committed May 24, 2024
1 parent b5ea040 commit 2696fd3
Show file tree
Hide file tree
Showing 36 changed files with 1,023 additions and 1,013 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,4 @@ Carthage/Build
fastlane/README.md
fastlane/report.xml
fastlane/test_output
openapi-generator/
14 changes: 7 additions & 7 deletions Demo/Demo/Gravatar-Demo/DemoFetchProfileViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -107,15 +107,15 @@ class DemoFetchProfileViewController: UIViewController {
}
}

func setProfile(with profile: UserProfile) {
func setProfile(with profile: Profile) {
activityIndicator.stopAnimating()
profileTextView.text = """
Profile URL: \(profile.profileURLString)
Display name: \(profile.displayName ?? "")
Name: \(profile.displayName ?? "")
Preferred User Name: \(profile.preferredUsername)
Thumbnail URL: \(profile.thumbnailURLString)
Last edit: \(String(describing: profile.lastProfileEditDate))
Profile URL: \(profile.profileUrl)
Display name: \(profile.displayName)
Name: \(profile.displayName)
Preferred User Name: \(profile.displayName)
Thumbnail URL: \(profile.avatarUrl)
Last edit: \(String(describing: profile.lastProfileEdit))
"""
}

Expand Down
81 changes: 77 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,11 +1,36 @@
.PHONY: all clean run

# To see how to drive this makefile use:
#
# % make help

# Cache
SWIFTFORMAT_CACHE = ~/Library/Caches/com.charcoaldesign.swiftformat

dev:
# The following values can be changed here, or passed on the command line.
OPENAPI_GENERATOR_GIT_URL ?= https://github.com/openapitools/openapi-generator
OPENAPI_GENERATOR_GIT_TAG ?= v7.5.0
OPENAPI_GENERATOR_CLONE_DIR ?= $(CURRENT_MAKEFILE_DIR)/openapi-generator
OPENAPI_YAML_PATH ?= $(CURRENT_MAKEFILE_DIR)/openapi/spec.yaml
MODEL_TEMPLATE_PATH ?= $(CURRENT_MAKEFILE_DIR)/openapi
OUTPUT_DIRECTORY ?= $(CURRENT_MAKEFILE_DIR)/Sources/Gravatar/OpenApi/Generated

# Derived values (don't change these).
CURRENT_MAKEFILE_PATH := $(abspath $(lastword $(MAKEFILE_LIST)))
CURRENT_MAKEFILE_DIR := $(patsubst %/,%,$(dir $(CURRENT_MAKEFILE_PATH)))

# If no target is specified, display help
.DEFAULT_GOAL := help

help: # Display this help.
@-+echo "Run make with one of the following targets:"
@-+echo
@-+grep -Eh "^[a-z-]+:.*#" $(CURRENT_MAKEFILE_PATH) | sed -E 's/^(.*:)(.*#+)(.*)/ \1 @@@ \3 /' | column -t -s "@@@"

dev: # Open the package in xcode
xed .

dev-demo:
dev-demo: # Open an xcode project with the package and a demo project
xed Demo/

test: bundle-install
Expand All @@ -22,13 +47,13 @@ build-demo-swiftui: bundle-install
bundle-install:
bundle install

swiftformat:
swiftformat: # Automatically find and fixes lint issues
swift package plugin \
--allow-writing-to-package-directory \
--allow-writing-to-directory "$(SWIFTFORMAT_CACHE)" \
swiftformat

lint:
lint: # Use swiftformat to warn about format issues
swift package plugin \
--allow-writing-to-package-directory \
--allow-writing-to-directory "$(SWIFTFORMAT_CACHE)" \
Expand All @@ -51,3 +76,51 @@ update-example-snapshots:
cd ./Sources/GravatarUI/GravatarUI.docc/Resources/ProfileExamples && \
for filePath in *; do name=$${filePath%.*}; mv $$filePath $${name//-dark/~dark}@2x$${filePath#$$name}; done

install-and-generate: $(OPENAPI_GENERATOR_CLONE_DIR) # Clones and setup the openapi-generator.
"$(OPENAPI_GENERATOR_CLONE_DIR)"/run-in-docker.sh mvn package
make generate

generate: $(OUTPUT_DIRECTORY) # Generates the open-api model
cp "$(OPENAPI_YAML_PATH)" "$(OPENAPI_GENERATOR_CLONE_DIR)"/openapi.yaml
mkdir -p "$(OPENAPI_GENERATOR_CLONE_DIR)"/templates
cp "$(MODEL_TEMPLATE_PATH)"/*.mustache "$(OPENAPI_GENERATOR_CLONE_DIR)"/templates/
"$(OPENAPI_GENERATOR_CLONE_DIR)"/run-in-docker.sh generate -i openapi.yaml \
--global-property models \
-t templates \
-g swift5 \
-o ./generated \
-p packageName=Gravatar \
--additional-properties=useJsonEncodable=false,readonlyProperties=true && \
cp "$(OPENAPI_GENERATOR_CLONE_DIR)"/generated/OpenAPIClient/Classes/OpenAPIs/Models/* "$(OUTPUT_DIRECTORY)" && \
make swiftformat && \
echo "DONE! 🎉"

clean-generated: # Delete the output directory used for generated sources.
@echo 'Delete entire directory: $(OUTPUT_DIRECTORY)? [y/N] ' && read ans && [ $${ans:-N} = y ] || (echo "Aborted"; exit 1)
rm -rf "$(OUTPUT_DIRECTORY)"

clean: # Clean everything, including the checkout of swift-openapi-generator.
@echo 'Delete checkout of openapi-generator $(OPENAPI_GENERATOR_CLONE_DIR)? [y/N] ' && read ans && [ $${ans:-N} = y ] || (echo "Aborted"; exit 1)
rm -rf "$(OPENAPI_GENERATOR_CLONE_DIR)"


dump: # Dump all derived values used by the Makefile.
@echo "CURRENT_MAKEFILE_PATH = $(CURRENT_MAKEFILE_PATH)"
@echo "CURRENT_MAKEFILE_DIR = $(CURRENT_MAKEFILE_DIR)"
@echo "OPENAPI_GENERATOR_GIT_URL = $(OPENAPI_GENERATOR_GIT_URL)"
@echo "OPENAPI_GENERATOR_GIT_TAG = $(OPENAPI_GENERATOR_GIT_TAG)"
@echo "OPENAPI_GENERATOR_CLONE_DIR = $(OPENAPI_GENERATOR_CLONE_DIR)"
@echo "OPENAPI_YAML_PATH = $(OPENAPI_YAML_PATH)"
@echo "OUTPUT_DIRECTORY = $(OUTPUT_DIRECTORY)"

$(OPENAPI_GENERATOR_CLONE_DIR):
git \
-c advice.detachedHead=false \
clone \
--branch "$(OPENAPI_GENERATOR_GIT_TAG)" \
--depth 1 \
"$(OPENAPI_GENERATOR_GIT_URL)" \
$@

$(OUTPUT_DIRECTORY):
mkdir -p "$@"
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/nicklockwood/SwiftFormat", from: "0.53.0"),
.package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.8.1")
.package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.8.1"),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
Expand Down
2 changes: 1 addition & 1 deletion Sources/Gravatar/Network/Services/ProfileFetching.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ protocol ProfileFetching {
/// Fetches a Gravatar user's profile information, and delivers the user profile asynchronously.
/// - Parameter profileID: a ProfileIdentifier
/// - Returns: An asynchronously-delivered user profile.
func fetch(with profileID: ProfileIdentifier) async throws -> UserProfile
func fetch(with profileID: ProfileIdentifier) async throws -> Profile
}
60 changes: 16 additions & 44 deletions Sources/Gravatar/Network/Services/ProfileService.swift
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import Foundation

private let baseURL = "https://gravatar.com/"
private let baseURL = URL(string: "https://api.gravatar.com/v3/profiles/")!

public enum GravatarProfileFetchResult: Sendable {
case success(UserProfile)
case success(Profile)
case failure(ProfileServiceError)
}

Expand Down Expand Up @@ -36,56 +36,36 @@ public struct ProfileService: ProfileFetching, Sendable {
}
}

public func fetch(with profileID: ProfileIdentifier) async throws -> UserProfile {
try await fetch(withPath: profileID.id)
public func fetch(with profileID: ProfileIdentifier) async throws -> Profile {
let url = baseURL.appending(pathComponent: profileID.id)
let request = URLRequest(url: url)
// TODO: Add API key to headers
return try await fetch(with: request)
}
}

extension ProfileService {
/// Error thrown when URL can not be created with the given baseURL and path.
struct CannotCreateURLFromGivenPath: Error {
let baseURL: String
let path: String
}

private func url(from path: String) throws -> URL {
guard let url = URL(string: baseURL + path) else {
throw CannotCreateURLFromGivenPath(baseURL: baseURL, path: path)
}
return url
}

private func fetch(withPath path: String) async throws -> UserProfile {
let url = try url(from: path + ".json")
return try await fetch(with: URLRequest(url: url))
}

private func fetch(with request: URLRequest) async throws -> UserProfile {
private func fetch(with request: URLRequest) async throws -> Profile {
do {
let (data, response) = try await client.fetchData(with: request)
let fetchProfileResult = map(data, response)
switch fetchProfileResult {
case .success(let profile):
return profile
case .failure(let error):
throw error
let profileResult: Result<Profile, ProfileServiceError> = map(data, response)
switch profileResult {
case .success(let success):
return success
case .failure(let failure):
throw failure
}
} catch let error as HTTPClientError {
throw ProfileServiceError.responseError(reason: error.map())
}
}

private func map(_ data: Data, _: HTTPURLResponse) -> Result<UserProfile, ProfileServiceError> {
private func map(_ data: Data, _: HTTPURLResponse) -> Result<Profile, ProfileServiceError> {
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let root = try decoder.decode(Root.self, from: data)
let profile = try profile(from: root.entry)
let profile = try JSONDecoder().decode(Profile.self, from: data)
return .success(profile)
} catch let error as HTTPClientError {
return .failure(.responseError(reason: error.map()))
} catch _ as ProfileService.CannotCreateURLFromGivenPath {
return .failure(.requestError(reason: .urlInitializationFailed))
} catch let error as ProfileServiceError {
return .failure(error)
} catch _ as DecodingError {
Expand All @@ -94,12 +74,4 @@ extension ProfileService {
return .failure(.responseError(reason: .unexpected(error)))
}
}

private func profile(from profiles: [UserProfile]) throws -> UserProfile {
guard let profile = profiles.first else {
throw ProfileServiceError.noProfileInResponse
}

return profile
}
}

0 comments on commit 2696fd3

Please sign in to comment.