💫 OpenAPI?
OpenAPI는 RESTful API Spec을 yaml이나 json으로 표현하는 표준화된 문서이다. 표현된 yaml이나 json을 이용하면 개발자가 생성한 API들을 문서화하거나 코드를 자동으로 생성하여 사용할 수 있다. 대표적으로 Swagger UI가 있다.
💫 Swift OpenAPI Generator
이미 기존에도 OpenAPI를 활용하여 코드를 생성해주는 라이브러리나 플러그인들이 있었지만 지금 소개하는 Swift OpenAPI Generator는 Apple에서 개발하고 Xcode와 최적화가 잘 되어있다고 한다.
깃허브 https://github.com/apple/swift-openapi-generator
GitHub - apple/swift-openapi-generator: Generate Swift client and server code from an OpenAPI document.
Generate Swift client and server code from an OpenAPI document. - apple/swift-openapi-generator
github.com
공식문서 https://www.swift.org/blog/introducing-swift-openapi-generator/
Introducing Swift OpenAPI Generator
We’re excited to announce a set of open source libraries designed to help both client and server developers streamline their workflow around HTTP communication using the industry‑standard OpenAPI specification.
www.swift.org
💫 실습해보기
프로젝트 생성 및 라이브러리 설치
OpenAPIGenerator라는 프로젝트를 생성하고 Swift OpenAPI Generator사용에 필요한 3개의 라이브러리를 설치한다.
플러그인 추가
OpenAPIGenerator 플러그인을 Build Phases에 추가한다.
문서 작성
플러그인을 통해 OpenAPI를 생성하기 위한 2개의 필수 파일이 필요하다. 하나는 설정을 관리하는 openapi-generator-config.yaml 다른 하나는 문서화된 openapi.yaml 가 필요하다.
openapi-generator-config.yaml
generate:
- types
- client
설정 파일은 간단하게 작성
openapi.yaml
openapi에는 내가 다룰 API문서를 작성하면 된다. 이번 예제로는 https://api.sampleapis.com/coffee/hot 커피리스트 샘플 api를 활용할 예정이다. 아래는 학습용으로 예제에 필요없는 코드들이 작성되어 있으니 예제를 실습하고자 한다면 단락 마지막에 작성된 최종코드를 참고하면 된다.
1. openapi 버전 작성
openapi: '3.0.3'
github를 보면 현재 지원하는 OpenAPI버전은 3.0과 3.1이 있다. 이번 예제에서는 3.0.3 버전을 사용할 예정이다.
2. info 작성
info:
title: HotCoffees
version: 1.0.0
info는 title과 version으로 자유롭게 작성
3. servers 작성
servers:
- url: https://example.com/api
description: Example service deployment.
- url: http://127.0.0.1:8080/api
description: Localhost deployment.
- url: https://api.sampleapis.com
description: 샘플 API의 Base URL
위 예에서는 3개의 server를 추가하였고 url은 서버주소, description은 설명을 자유롭게 작성하면된다.
4. components 작성
components는 API의 리퀘스트/리스폰스 객체를 작성하는 부분이다. 그러므로 우선 우리가 진행할 커피 리스트의 예제를 확인해보자.
[
{
"title": "Black Coffee",
"description": "Svart kaffe är så enkelt som det kan bli med malda kaffebönor dränkta i hett vatten, serverat varmt. Och om du vill låta fancy kan du kalla svart kaffe med sitt rätta namn: café noir.",
"ingredients": [
"Coffee"
],
"image": "https://images.unsplash.com/photo-1494314671902-399b18174975?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D",
"id": 1
},
...[중략]
]
API를 요청해보면 위와 같은 객체 리스트로 반환한다. 이제 이를 참고하여 components를 작성해보자.
components:
schemas:
Coffee:
type: object
properties:
title:
type: string
example: 커피 타이틀
description:
type: string
ingredients:
type: array
items:
type: string
image:
type: string
example: "https://images.unsplash.com/photo-1494314671902-399b18174975?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
id:
type: integer
required:
- title
- description
- ingredients
- id
Coffee라는 object 타입을 생성하고 properties는 리스폰스에 맞게 title, description, ingredients, image, id를 작성해주고 각각의 타입에 맞게 작성한다. example은 옵션으로 작성을 해도되고 안해도 된다.
그 다음 required는 위 properties의 필수값으로 image만 제외하고 모두 필수값으로 작성해보았다.
5. paths 작성
paths:
/greet:
get:
operationId: getGreeting
parameters:
- name: name
required: false
in: query
description: The name used in the returned greeting.
schema:
type: string
responses:
'200':
description: A success response with a greeting.
content:
application/json:
schema:
$ref: '#/components/schemas/Greeting'
/coffee/hot:
get:
operationId: getHotCoffees
responses:
'200':
description: 커피 리스트를 불러왔습니다.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Coffee'
파라미터가 필요하고 object를 리턴하는 /greet 예제와 파라미터가 없고 array object를 리턴하는 /coffee/hot 예제이다.
responses의 schema처럼 $ref: 를 사용하여 이전에 작성한 components에서 객체 참조를 불러와서 사용할 수도 있고 parameters의 schema처럼 간단하게 직접 입력할 수도있다.
전체 코드
openapi: '3.0.3'
info:
title: HotCoffees
version: 1.0.0
servers:
- url: https://api.sampleapis.com
description: 샘플 API의 Base URL
components:
schemas:
Coffee:
type: object
properties:
title:
type: string
example: 커피 타이틀
description:
type: string
ingredients:
type: array
items:
type: string
image:
type: string
example: "https://images.unsplash.com/photo-1494314671902-399b18174975?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
id:
type: integer
required:
- title
- description
- ingredients
- id
paths:
/coffee/hot:
get:
operationId: getHotCoffees
responses:
'200':
description: 커피 리스트를 불러왔습니다.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Coffee'
프로젝트에 적용
WebService 생성
import Foundation
import OpenAPIURLSession
final class WebService<C: APIProtocol> {
private let client: C
init(client: C) {
self.client = client
}
init() where C == Client {
self.client = Client(
serverURL: try! Servers.server1(),
transport: URLSessionTransport()
)
}
func getHotCoffees() async -> [Components.Schemas.Coffee] {
do {
let response = try await client.getHotCoffees()
switch response {
case .ok(let okResponse):
switch okResponse.body {
case .json(let coffees):
return coffees
}
case .undocumented(statusCode: let statusCode, _):
print("Undocumented Error: \(statusCode)")
return []
}
} catch {
print("error: \(error.localizedDescription)")
return []
}
}
}
OpenAPI Generator에 의해 생성된 코드를 사용하여 WebService를 구현한다. 생성된 코드의 설명은 아래와 같다.
- types
APIProtocol: openapi.yaml에서 작성한 paths의 각각의 path로 UseCases로 생각하면 된다.
Servers: openapi.yaml에서 작성한 servers부분으로 첫번째 URL은 server1(), 두번째는 server2() 이런식으로 불러오면 된다.
Components, Schemas: openapi.yaml에서 작성한 components, schemas부분으로 각각의 Model로 생각하면 된다.
- client
Client: OpenAPI에서 생성해준 HTTP통신 클라이언트
MockClient 생성
struct MockClient: APIProtocol {
func getHotCoffees(_ input: Operations.getHotCoffees.Input) async throws -> Operations.getHotCoffees.Output {
let postsBody = [
Components.Schemas.Coffee(title: "아메리카노", description: "아메 아메 아메리카노", ingredients: ["커피"], image: "https://images.unsplash.com/photo-1494314671902-399b18174975?auto=format&fit=crop&q=80&w=1887&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", id: 1),
Components.Schemas.Coffee(title: "핫초코", description: "맛있는 핫-초코", ingredients: [], id: 2)
]
return .ok(Operations.getHotCoffees.Output.Ok(body: .json(postsBody)))
}
}
자동 생성된 client는 실제 HTTP통신을 하므로 테스트를 위해 APIProtocol을 상속받은 MockClient를 구현하여 사용한다.
UI
import SwiftUI
import OpenAPIURLSession
// Coffee extensions
extension Components.Schemas.Coffee: Identifiable { }
extension Components.Schemas.Coffee {
var imageURL: URL? {
URL(string: image ?? "")
}
}
struct ContentView: View {
@State private var coffees = [Components.Schemas.Coffee]()
var body: some View {
List(coffees) { coffee in
HStack {
Group {
if let imageURL = coffee.imageURL {
AsyncImage(url: imageURL) { image in
image
.resizable()
} placeholder: {
ProgressView()
}
} else {
Image(systemName: "cup.and.saucer.fill")
.scaleEffect(2)
}
}
.frame(width: 100, height: 100)
VStack(alignment: .leading) {
Text(coffee.title)
.font(.title2)
.bold()
Text(coffee.description)
.lineLimit(2)
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(coffee.ingredients, id: \.self) { ingredient in
Text(ingredient)
.padding(.vertical, 4)
.padding(.horizontal, 8)
.overlay(
RoundedRectangle(cornerRadius: 5)
.stroke(.gray)
)
}
}
.padding(1)
}
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.background(.thinMaterial)
}
.task {
// 테스트 클라이언트
coffees = await WebService(client: MockClient()).getHotCoffees()
// 실제 클라이언트
// coffees = await WebService().getHotCoffees()
}
}
}
테스트에서는 MockClient()를 WebService에 주입하여 사용하고 프로덕트에서는 기본 생성자를 사용한다.
이로써 쉽게 테스트 가능하고 문서화까지 되는 Swift OpenAPI Generator에 대해 알아봤다. 끗!
💫 참고
https://www.swift.org/blog/swift-openapi-generator-1.0/
Swift OpenAPI Generator 1.0 Released
We’re happy to announce the stable 1.0 release of Swift OpenAPI Generator! OpenAPI is an open standard for describing the behavior of HTTP services with a rich ecosystem of tooling. One thing OpenAPI is particularly known for is tooling to generate inter
www.swift.org
https://swiftpackageindex.com/apple/swift-openapi-generator/1.2.1/tutorials/swift-openapi-generator
swift-openapi-generator Documentation – Swift Package Index
swiftpackageindex.com
'iOS > Swift' 카테고리의 다른 글
Keychain을 사용해보자 (0) | 2023.01.30 |
---|---|
UserDefaults / Keychain / Core Data (0) | 2022.08.17 |
Swift에서 DI/DIP 이해하기 (0) | 2022.06.21 |
Swift CommonCrypto로 AES 암/복호화 하기! (without CryptoSwift) (0) | 2022.05.26 |
UserDefaults에서 .value(forKey:)와 .object(forKey:)는 뭐가 다른 건가요? (0) | 2022.05.13 |