USB C ケーブル タイプc 1M/2M 各2本 PD対応 60W超急速充電 断線防止 高速データ転送 Type-C to Type-Cケーブル for iPhone 16/15 Pro/Plus/Pro Max MacBook iPad Galaxy Sony Google Pixel 7a等USB-C各種対応
¥699 (2025年4月26日 13:05 GMT +09:00 時点 - 詳細はこちら価格および発送可能時期は表示された日付/時刻の時点のものであり、変更される場合があります。本商品の購入においては、購入の時点で当該の Amazon サイトに表示されている価格および発送可能時期の情報が適用されます。)ALLDOCUBE iPlay 70 mini Ultra タブレット 8.8インチ Snapdragon7+Gen3 2560×1600解像度 144Hz高リフレッシュレート 20GB+256GB+1TB拡張 7300mAh PD20W デュアルスピーカーDTSサウンド 6軸ジャイロ WiFi6 BT5.4 WidevineL1 重力センサー 光センサー Androidタブレットアンドロイド
(2025年4月26日 13:07 GMT +09:00 時点 - 詳細はこちら価格および発送可能時期は表示された日付/時刻の時点のものであり、変更される場合があります。本商品の購入においては、購入の時点で当該の Amazon サイトに表示されている価格および発送可能時期の情報が適用されます。)
-
webサーバー楽しそう
-
Rustを学びたいが,サーバーの経験がないからまずは慣れたSwiftで書きたい
-
LLMは次の順番で各領域の技術水準が低・中程度の人間プログラマーを駆逐していきそう(根拠なき直感)だから、より代替困難な側に避難したい
- WEBフロントエンド
- モバイル(クロスプラットフォーム,ネイティブ)
- バックエンド
- 組み込み系?(あまりよく知らない)
住所検索サービス
郵便番号を入力すると,それに紐づいた住所が返ってくる
動くものの動画
正常系 | 異常系 |
---|---|
![]() |
![]() |
まずはAPI仕様から決める.
openapi.yaml
openapi: "3.1.0"
info:
title: AddressSearchService
version: "1.0.0"
servers:
- url: "http://127.0.0.1:8080/openapi"
description: "住所検索!"
paths:
/address:
get:
operationId: searchAddresses
summary: 郵便番号に基づく住所候補の検索
description: 郵便番号(またはその一部)をクエリパラメータとして受け取り、候補となる住所情報の配列を返します。
parameters:
- in: query
name: postal_code
required: false
description: 郵便番号(例:"1000001" または途中入力 "123")
schema:
type: string
pattern: "^[0-9]{0,7}$"
example: "1000001"
responses:
"200":
description: 郵便番号に基づく住所候補一覧
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/AddressInfo"
"400":
description: 不正な郵便番号の形式です。
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
components:
schemas:
AddressInfo:
type: object
properties:
id:
type: string
format: uuid
description: 一意な識別子
postalCode:
type: string
description: 郵便番号
prefectureKana:
type: string
description: 都道府県のカナ表記
prefectureKanji:
type: string
description: 都道府県の漢字表記
cityKana:
type: string
description: 市区町村のカナ表記
cityKanji:
type: string
description: 市区町村の漢字表記
streetKana:
type: string
description: 町域等のカナ表記
streetKanji:
type: string
description: 町域等の漢字表記
required:
- postalCode
ErrorResponse:
type: object
properties:
error:
type: string
description: エラー詳細
required:
- error
ちなみにOpenAPIはVSCode + 拡張機能のOpenAPI (Swagger) Editorだと見やすい 参考
本章ではデータベースを構築する.
データの準備
住所データは住所の郵便番号(1レコード1行、UTF-8形式)(CSV形式)の最新全データ(utf_ken_all.zip
)を用いる.
ダウンロードして中身を見てみると次のとおり.
utf_ken_all.csv
01101,"060 ","0600000","ホッカイドウ","サッポロシチュウオウク","イカニケイサイガナイバアイ","北海道","札幌市中央区","以下に掲載がない場合",0,0,0,0,0,0
01101,"064 ","0640941","ホッカイドウ","サッポロシチュウオウク","アサヒガオカ","北海道","札幌市中央区","旭ケ丘",0,0,1,0,0,0
01101,"060 ","0600041","ホッカイドウ","サッポロシチュウオウク","オオドオリヒガシ","北海道","札幌市中央区","大通東",0,0,1,0,0,0
...
よくわからない数字が並ぶので,解説を見る
- 全国地方公共団体コード(JIS X0401、X0402)……… 半角数字
- (旧)郵便番号(5桁)……………………………………… 半角数字
- 郵便番号(7桁)……………………………………… 半角数字
- 都道府県名 ………… 全角カタカナ(コード順に掲載) (※1)
- 市区町村名 ………… 全角カタカナ(コード順に掲載) (※1)
- 町域名 ……………… 全角カタカナ(五十音順に掲載) (※1)
- 都道府県名 ………… 漢字(コード順に掲載) (※1,2)
- 市区町村名 ………… 漢字(コード順に掲載) (※1,2)
- 町域名 ……………… 漢字(五十音順に掲載) (※1,2)
- 一町域が二以上の郵便番号で表される場合の表示 (※3) (「1」は該当、「0」は該当せず)
- 小字毎に番地が起番されている町域の表示 (※4) (「1」は該当、「0」は該当せず)
- 丁目を有する町域の場合の表示 (「1」は該当、「0」は該当せず)
- 一つの郵便番号で二以上の町域を表す場合の表示 (※5) (「1」は該当、「0」は該当せず)
- 更新の表示(※6)(「0」は変更なし、「1」は変更あり、「2」廃止(廃止データのみ使用))
- 変更理由 (「0」は変更なし、「1」市政・区政・町政・分区・政令指定都市施行、「2」住居表示の実施、「3」区画整理、「4」郵便区調整等、「5」訂正、「6」廃止(廃止データのみ使用))
はええ.なんだか難しい.
今回はCSVを編集するのも面倒なので,次のようにヘッダーを追加するのみ.
あとはそのまま用いる.(機能拡張したいときに,テーブル作り直すのも面倒そうだし…)
csv.diff
+ zenkoku_chiho_kokyodantai_code,"old_postal_code","postal_code","prefecture_kana","city_kana","street_kana","prefecture_kanji","city_kanji","street_kanji","street_has_multiple_postalcode","street_has_koazas_with_conflictable_banch","street_has_chome","street_shares_postalcode_with_others","has_update","update_reason"
01101,"060 ","0600000","ホッカイドウ","サッポロシチュウオウク","イカニケイサイガナイバアイ","北海道","札幌市中央区","以下に掲載がない場合",0,0,0,0,0,0,
...
PostgreSQLとGUIのpgAdmin 4を導入する
DBは何もわからんが,PostgreSQLとGUIのpgAdmin 4で構築する.
インストールの手順は次の資料などに丸投げします.
DBテーブルの作成
DBの各行にはIDとなる主キーが欲しい.IDはnone-nullableで一意である必要がある.しかし,元データ(utf_ken_all.csv
)にid
なる列は含まれないので,今回作る.uuid-ossp拡張
を有効に設定(参考)してuuid_generate_v4()
を初期値とする.
あとは次のように各列の名前と型,nullabilityを設定する.
ちなみに,上記操作に対応するSQLコマンドは次のよう.
CREATE TABLE public."addressInfoTable"
(
id uuid NOT NULL DEFAULT uuid_generate_v4(),
zenkoku_chiho_kokyodantai_code text,
old_postal_code text,
postal_code text NOT NULL,
prefecture_kana text,
city_kana text,
street_kana text,
prefecture_kanji text,
city_kanji text,
street_kanji text,
street_has_multiple_postalcode smallint,
street_has_koazas_with_conflictable_banch smallint,
street_has_chome smallint,
street_shares_postalcode_with_others smallint,
has_update smallint,
update_reason smallint,
PRIMARY KEY (id)
);
ALTER TABLE IF EXISTS public."addressInfoTable"
OWNER to postgres;
DBテーブルにデータをインポートする
次のようにデータをインポートする
一般/ファイル名 でutf_ken_all.csv
を指定する
列/Columns to export からid
を削除
これでDBの用意は完了.
Vaporをhomebrewでインストールする.
VaporはSwiftでweb apps APIが作れるweb framework.
vaporを用いたXcode Projectを作成する
$ vapor new AddressSearchServer
Cloning template...
Would you like to use Fluent (ORM)? (--fluent/--no-fluent)
y/n> y
fluent: Yes
# FluentはORMライブラリ.
# ChatGPT: プログラムで使うオブジェクト(クラスやインスタンスなど)と、リレーショナルデータベースのテーブルやレコードとの対応関係を自動的に管理する仕組みです。
# これにより、SQL文を直接書かなくてもデータベース操作ができるようになり、コードとデータベース間の橋渡し的な役割を果たします。
Which database would you like to use? (--fluent.db)
1: Postgres (Recommended)
2: MySQL
3: SQLite
> 1
db: Postgres (Recommended)
# 推奨のPostgresを使用する
Would you like to use Leaf (templating)? (--leaf/--no-leaf)
y/n> n
leaf: No
# leafは動的なHTMLが作成できるライブラリ.今は不要.
Generating project files
Creating git repository
Adding first commit
Project AddressSearchServer has been created!
Use cd 'AddressSearchServer' to enter the project directory
Then open your project, for example if using Xcode type open Package.swift or code . if using VSCode
これでvaporを用いたXcode Projectが作成できた.
Databaseに保存されたDataを返してみる
Databaseに保存したDataに対応するエンティティオブジェクトを作成する.
AddressInfo.swift
import Fluent
import Vapor
final class AddressInfo: Model, Content, @unchecked Sendable {
static let schema = "addressInfoTable"
@ID(key: .id)
var id: UUID?
@Field(key: "postal_code")
var postalCode: String
@Field(key: "prefecture_kana")
var prefectureKana: String?
@Field(key: "prefecture_kanji")
var prefectureKanji: String?
@Field(key: "city_kana")
var cityKana: String?
@Field(key: "city_kanji")
var cityKanji: String?
@Field(key: "street_kana")
var streetKana: String?
@Field(key: "street_kanji")
var streetKanji: String?
init(){}
init(
id: UUID? = nil,
postalCode: String,
prefectureKana: String?,
prefectureKanji: String?,
cityKana: String?,
cityKanji: String?,
streetKana: String?,
streetKanji: String?
) {
self.id = id
self.postalCode = postalCode
self.prefectureKana = prefectureKana
self.prefectureKanji = prefectureKanji
self.cityKana = cityKana
self.cityKanji = cityKanji
self.streetKana = streetKana
self.streetKanji = streetKanji
}
}
使用するdatabaseを設定する
configure.swift
public func configure(_ app: Application) async throws {
app.databases.use(
.postgres(
configuration: .init(
hostname: "localhost",
username: "vapor",
password: "",
database: "addressdb",
tls: .disable
)
),
as: .psql
)
}
新しいパスと,それに対応する処理を追加する.
AddressInfoListController.swift
import Fluent
import Vapor
struct AddressInfoListController: RouteCollection {
func boot(routes: any RoutesBuilder) throws {
// `{baseURL}/getAddressInfoList` に対してGET methodを設定する
routes.get("getAddressInfoList", use: getAddressInfoList)
}
@Sendable
func getAddressInfoList(req: Request) async throws -> Response {
let addressInfoList: [AddressInfo] = try await AddressInfo.query(on: req.db)
.filter(\.$postalCode == "1000001") // 一旦DB内の郵便番号1000001に該当するデータを正しく返せるか確認
.all()
return try await addressInfoList.encodeResponse(for: req)
}
}
routes.swift.diff
import Fluent
import Vapor
func routes(_ app: Application) throws {
...
try app.register(collection: TodoController())
+ try app.register(collection: AddressInfoListController())
}
これでビルドして実行する.
問題なければhttp://127.0.0.1:8080/getAddressInfoList にアクセスすると次のようなデータが返ってくる(見やすく整形した.またidの値はDBごとに異なる)
[
{
"id": "FB5FE262-7A0D-461E-9268-E04B2BFD18CA",
"cityKanji": "千代田区",
"postalCode": "1000001",
"prefectureKanji": "東京都",
"streetKana": "チヨダ",
"streetKanji": "千代田",
"cityKana": "チヨダク",
"prefectureKana": "トウキョウト"
}
]
これで,サーバーはDBに保存されたデータを返せるようになった.
なお,次のようなSQLコマンドで権限を付与する必要があるかも.addressdb=# GRANT SELECT, INSERT ON public.addressInfoTable To vapor;
*psqlというPostgreSQLの対話的なコマンドラインツールのプロンプト(addressdb=#
)上で実行する
Swift-OpenAPI-Generatorを追加する
次はOpenAPIを設定する.
AddressSearchServer/Sources/AddressSearchServer直下に次の2つのファイルを配置する
- 上述の
openapi.yaml
- 次の
openapi-generator-config.yaml
openapi-generator-config.yaml
generate: - types - server
次にSwift OpenAPI Generatorなどへの依存を追加する
Package.swift.diff
diff --git a/Package.swift b/Package.swift
index 71bbe70..a23cfff 100644
--- a/Package.swift
+++ b/Package.swift
@@ -15,6 +15,11 @@ let package = Package(
.package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.8.0"),
// 🔵 Non-blocking, event-driven networking for Swift. Used for custom executors
.package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"),
+
+ // OpenAPI
+ .package(url: "https://github.com/apple/swift-openapi-generator", from: "1.3.0"),
+ .package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.5.0"),
+ .package(url: "https://github.com/swift-server/swift-openapi-vapor", from: "1.0.1"),
],
targets: [
.executableTarget(
@@ -25,8 +30,14 @@ let package = Package(
.product(name: "Vapor", package: "vapor"),
.product(name: "NIOCore", package: "swift-nio"),
.product(name: "NIOPosix", package: "swift-nio"),
+ // OpenAPI
+ .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
+ .product(name: "OpenAPIVapor", package: "swift-openapi-vapor"),
],
- swiftSettings: swiftSettings
+ swiftSettings: swiftSettings,
+ plugins: [
+ .plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator"),
+ ]
),
.testTarget(
name: "AddressSearchServerTests",
ビルドするとビルドフェーズでAPIエンドポイントのコードが自動生成される.自動生成に問題がなければ,例えばentrypoint.swift
で次のコードを追加しても問題なくコンパイルできる.
entrypoint.swift
...
typealias Generated_AddressInfo_JustForTest = Components.Schemas.AddressInfo // コンパイルできることを確認したら消す.
Swift-OpenAPI-Generatorが生成したAPIエンドポイントを実装する
OpenAPIController.swift
で自動生成されたAPI仕様(APIProtocol)を満たす
OpenAPIController.swift
import Vapor
struct OpenAPIController: APIProtocol { // この`APIProtocol`がOpenAPIによって生成された
// APIProtocolがopenapi.yamlに定義されたAPIエンドポイントの実装を義務化してくれるので実装漏れが防げる
func searchAddresses(_ input: Operations.searchAddresses.Input) async throws -> Operations.searchAddresses.Output {
// 一旦stubを返す
.ok(
.init(
body: .json(
[
.init(
id: "1",
postalCode: "1000001",
prefectureKana: "トウキョウト",
prefectureKanji: "東京都",
cityKana: "チヨダク",
cityKanji: "千代田区",
streetKana: "チヨダ",
streetKanji: "千代田"
)
]
)
)
)
}
}
Swift-OpenAPI-Generatorで生成されたAPIのエンドポイントを、Vaporのルーティングシステムに登録する.
entrypoint.swift.diff
import Vapor
import Logging
import NIOCore
import NIOPosix
+import OpenAPIVapor
@main
enum Entrypoint {
@@ -17,7 +18,9 @@ enum Entrypoint {
// If enabled, you should be careful about calling async functions before this point as it can cause assertion failures.
// let executorTakeoverSuccess = NIOSingletons.unsafeTryInstallSingletonPosixEventLoopGroupAsConcurrencyGlobalExecutor()
// app.logger.debug("Tried to install SwiftNIO's EventLoopGroup as Swift's global concurrency executor", metadata: ["success": .stringConvertible(executorTakeoverSuccess)])
+ // VaporTransport:OpenAPIで生成されたコードとVapor(HTTPサーバー)の間を橋渡しするためのアダプタ.
+ // app:Vaporのルート登録(Routing)のためのオブジェクト(RoutesBuilder)
+ let transport = VaporTransport(routesBuilder: app)
+
+ let handler = OpenAPIController()
+
+ // Servers.Server1.url():OpenAPIで生成されたURL, 今回は`http://127.0.0.1:8080/openapi`
+ try handler.registerHandlers(on: transport, serverURL: Servers.Server1.url())
do {
try await configure(app)
try await app.execute()
ここで一旦XcodeのtargetをMy Macにしてビルド+実行する.
好きなブラウザでhttp://127.0.0.1:8080/openapi/address にアクセスする.
問題がなければ次が返ってくる.
[
{
"cityKana" : "チヨダク",
"cityKanji" : "千代田区",
"id" : "1",
"postalCode" : "1000001",
"prefectureKana" : "トウキョウト",
"prefectureKanji" : "東京都",
"streetKana" : "チヨダ",
"streetKanji" : "千代田"
}
]
OpenAPIエンドポイントとDBを接続する
現在OpenAPIで定義したURL(http://127.0.0.1:8080/openapi/address )にアクセスするとStubが返ってくる.
これから,Databaseに保存されたDataが返ってくるようにする.
Databaseに保存されたDataを返してみるの章では,次のようにDBからデータを取り出して返していた.
AddressInfoListController.swift
struct AddressInfoListController: RouteCollection {
...
@Sendable
func getAddressInfoList(req: Request) async throws -> Response {
let addressInfoList: [AddressInfo] = try await AddressInfo.query(on: req.db)
.filter(\.$postalCode == "1000001")
.all()
return try await addressInfoList.encodeResponse(for: req)
}
}
今回はstruct OpenAPIController: APIProtocol
のsearchAddresses
内部で,DBに保存された対応するDataを返したい.しかし,OpenAPIController.searchAddresses
はAddressInfoListController.getAddressInfoList
と異なりRequest
を持たないので,req.db
にアクセスできない.
OpenAPIController.swift
// 本コードは正常に動作しない.あくまでも説明のためのもの
import Fluent
import Vapor
struct OpenAPIController: APIProtocol {
var request: Request! // こんなふうにRequestにアクセスしたい!
func searchAddresses(_ input: Operations.searchAddresses.Input) async throws -> Operations.searchAddresses.Output {
let addressInfoList: [AddressInfo] = try await AddressInfo.query(on: request.db) // ここでDatabaseにアクセスするから
.filter(\.$postalCode == request.query.get(at: "postal_code")) // ここで検索すべき郵便番号にもアクセスしたい
.all()
return .ok(
.init(
body: .json(
addressInfoList.map(Components.Schemas.AddressInfo.init)
)
)
)
}
}
// DatabaseのエンティティオブジェクトであるAddressInfoから,APIエンドポイントの型であるComponents.Schemas.AddressInfoを生成する
extension Components.Schemas.AddressInfo {
init(from entity: AddressInfo) {
... // 後述
}
}
そこでOpenAPIController
にRequest
を差し込む.
差し込むために,依存注入ライブラリであるPointFreeのswift-dependenciesを用いる.
swift-dependenciesを追加
Package.swift.diff
@@ -20,6 +20,9 @@ let package = Package(
.package(url: "https://github.com/apple/swift-openapi-generator", from: "1.3.0"),
.package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.5.0"),
.package(url: "https://github.com/swift-server/swift-openapi-vapor", from: "1.0.1"),
+ // DI
+ .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.3.9"),
],
targets: [
.executableTarget(
@@ -33,6 +36,8 @@ let package = Package(
// OpenAPI
.product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
.product(name: "OpenAPIVapor", package: "swift-openapi-vapor"),
+ // DI
+ .product(name: "Dependencies", package: "swift-dependencies"),
],
swiftSettings: swiftSettings,
plugins: [
差し込み可能なプロパティrequest: Request
を定義
DependencyValues+App.swift
import Dependencies
import Vapor
extension DependencyValues {
var request: Request {
get { self[RequestKey.self] }
set { self[RequestKey.self] = newValue }
}
private enum RequestKey: DependencyKey {
static var liveValue: Request {
fatalError("Value of type \(Value.self) is not registered in this context")
}
}
}
OpenAPIController.swift.diff
+import Fluent
import Vapor
+import Dependencies
struct OpenAPIController: APIProtocol {
+ @Dependency(\.request) var request // これで差し込まれたrequestを使用できる
+
func searchAddresses(_ input: Operations.searchAddresses.Input) async throws -> Operations.searchAddresses.Output {
- // 一旦stubを返す
- .ok(
+ let addressInfoList: [AddressInfo] = try await AddressInfo.query(on: request.db)
+ .filter(\.$postalCode == request.query.get(at: "postal_code"))
+ .all()
+ return .ok(
.init(
body: .json(
- [
- .init(
- id: "1",
- postalCode: "1000001",
- prefectureKana: "トウキョウト",
- prefectureKanji: "東京都",
- cityKana: "チヨダク",
- cityKanji: "千代田区",
- streetKana: "チヨダ",
- streetKanji: "千代田"
- )
- ]
+ addressInfoList.map(Components.Schemas.AddressInfo.init)
)
)
)
}
}
+
+extension Components.Schemas.AddressInfo {
+ init(from entity: AddressInfo) {
+ id = entity.id?.uuidString
+ postalCode = entity.postalCode
+ prefectureKana = entity.prefectureKana
+ prefectureKanji = entity.prefectureKanji
+ cityKana = entity.cityKana
+ cityKanji = entity.cityKanji
+ streetKana = entity.streetKana
+ streetKanji = entity.streetKanji
+ }
+}
requestを差し込むためのAsyncMiddlewareを作成する
AsyncMiddleware は Vapor における非同期処理が可能なミドルウェアの仕組み
リクエストの受け渡し時に前処理や後処理、共通処理を挟める
OpenAPIRequestInjectionMiddleware.swift
import Dependencies
import Vapor
struct OpenAPIRequestInjectionMiddleware: AsyncMiddleware {
func respond(
to request: Request,
chainingTo responder: any AsyncResponder
) async throws -> Response {
try await withDependencies {
$0.request = request // 新しいrequestが来るたびに,Dependenciesの環境値のrequestを上書きする
} operation: {
try await responder.respond(to: request) // 上書きしたrequestで続きの処理をする
}
}
}
AsyncMiddlewareを使用してrequestを差し込む
entrypoint.swift.diff
enum Entrypoint {
// let executorTakeoverSuccess = NIOSingletons.unsafeTryInstallSingletonPosixEventLoopGroupAsConcurrencyGlobalExecutor()
// app.logger.debug("Tried to install SwiftNIO's EventLoopGroup as Swift's global concurrency executor", metadata: ["success": .stringConvertible(executorTakeoverSuccess)])
- let transport = VaporTransport(routesBuilder: app)
+ let requestInjectionMiddleware = OpenAPIRequestInjectionMiddleware()
+ let transport = VaporTransport(routesBuilder: app.grouped(requestInjectionMiddleware))
let handler = OpenAPIController()
try handler.registerHandlers(on: transport, serverURL: Servers.Server1.url())
do {
これでOpenAPI経由でもDatabaseの値を得られるようになった!
http://127.0.0.1:8080/openapi/address?postal_code=1000001 なら
[
{
"cityKana" : "チヨダク",
"cityKanji" : "千代田区",
"id" : "FB5FE262-7A0D-461E-9268-E04B2BFD18CA",
"postalCode" : "1000001",
"prefectureKana" : "トウキョウト",
"prefectureKanji" : "東京都",
"streetKana" : "チヨダ",
"streetKanji" : "千代田"
}
]
http://127.0.0.1:8080/openapi/address?postal_code=1000002 なら
[
{
"cityKana" : "チヨダク",
"cityKanji" : "千代田区",
"id" : "5C0A6E12-D474-4FD9-B86E-9081E31777D5",
"postalCode" : "1000002",
"prefectureKana" : "トウキョウト",
"prefectureKanji" : "東京都",
"streetKana" : "コウキョガイエン",
"streetKanji" : "皇居外苑"
}
]
複数の住所を返す
今はレスポンス配列だが,郵便番号が完全一致した住所しか返さない.
そこで,郵便番号の前方一致でも返るようにする.
OpenAPIController.swift.diff
let addressInfoList: [AddressInfo] = try await AddressInfo.query(on: request.db)
- .filter(\.$postalCode == request.query.get(at: "postal_code"))
+ .filter(\.$postalCode =~ request.query.get(at: "postal_code"))
.all()
この辺の演算子面白い.
資料にはいろいろ載っている.
演算子 | 条件の内容 | 日本語訳 |
---|---|---|
~~ | Value in set. | 含まれる |
!~ | Value not in set. | 含まれない |
~~ | Contains substring. | 文字列を含む |
!~ | Does not contain substring. | 文字列を含まない |
=~ | Matches prefix. | 前方一致 |
!=~ | Does not match prefix. | 前方一致しない |
~= | Matches suffix. | 後方一致 |
!~= | Does not match suffix. | 後方一致しない |
これdhttp://127.0.0.1:8080/openapi/address?postal_code=100000 に対して複数の値が変えるようになった.
[
{
"cityKana" : "チヨダク",
"cityKanji" : "千代田区",
"id" : "9D9DBEB6-892B-4A67-8B9D-92CC6416A39F",
"postalCode" : "1000000",
"prefectureKana" : "トウキョウト",
"prefectureKanji" : "東京都",
"streetKana" : "イカニケイサイガナイバアイ",
"streetKanji" : "以下に掲載がない場合"
},
{
"cityKana" : "チヨダク",
"cityKanji" : "千代田区",
"id" : "BEC21571-B730-4D91-83B3-C63B174C8A66",
"postalCode" : "1000004",
"prefectureKana" : "トウキョウト",
"prefectureKanji" : "東京都",
"streetKana" : "オオテマチ(ツギノビルヲノゾク)",
"streetKanji" : "大手町(次のビルを除く)"
},
{
"cityKana" : "チヨダク",
"cityKanji" : "千代田区",
"id" : "5C0A6E12-D474-4FD9-B86E-9081E31777D5",
"postalCode" : "1000002",
"prefectureKana" : "トウキョウト",
"prefectureKanji" : "東京都",
"streetKana" : "コウキョガイエン",
"streetKanji" : "皇居外苑"
},
...
]
エラーを返す
openapi.yaml
にはエラーが定義されている.queryとして渡された郵便番号が不正な場合は,空配列ではなくエラーを返すことで利用者に教えてあげたい.
OpenAPIController.swift.diff
struct OpenAPIController: APIProtocol {
@Dependency(\.request) var request
func searchAddresses(_ input: Operations.searchAddresses.Input) async throws -> Operations.searchAddresses.Output {
+ func validate(postalCode: String) -> Bool {
+ let postalCodePattern: Regex = /[0-9]{0, 7}/
+ if let match = try! postalCodePattern.wholeMatch(in: postalCode) {
+ return match.0 == postalCode
+ } else {
+ return false
+ }
+ }
+
+ var postalCode: String = try request.query.get(at: "postal_code")
+ postalCode.replace("-", with: "") // ハイフンを取り除いて正規化する.例:100-0001→1000001
+
+ guard validate(postalCode: postalCode) else {
+ return .badRequest(.init(body: .json(.init(error: "不正な郵便番号です"))))
+ }
let addressInfoList: [AddressInfo] = try await AddressInfo.query(on: request.db)
- .filter(\.$postalCode =~ request.query.get(at: "postal_code"))
+ .filter(\.$postalCode =~ postalCode)
.all()
return .ok(
.init(
これで次のような不正な郵便番号でアクセスするとエラーが返るようになった.
{
"error" : "不正な郵便番号です"
}
新規SwiftUIプロジェクトを作成する.
Swift-OpenAPI-Generatorを追加する
-
上記
openapi.yaml
をプロジェクトに追加する -
次の
openapi-generator-config.yaml
も同じ階層に配置するopenapi-generator-config.yaml
generate: - types - client
-
公式資料を参考に以下3packageをXcode Projectに追加する.
追加するときのtargetの設定
- swift-openapi-generator :
None
- swift-openapi-runtime :
{あなたのProject}
- swift-openapi-urlsession :
{あなたのProject}
- swift-openapi-generator :
-
コンパイル時に,
openapi.yaml
に基づいてコードを自動生成するために,Project/Targetsで{あなたのプロジェクト}
を選択し,/Build Phase/Run Build Tool Plug-insで追加ボタンを押下し,OpenAPIGenerator(swift-openapi-generator)
を追加する.
ビルドするとAPIエンドポイントが自動生成される
Swift-OpenAPI-Generatorが生成したAPIエンドポイントを使用する
ContentView.swift
import SwiftUI
import OpenAPIRuntime
import OpenAPIURLSession
typealias AddressInfo = Components.Schemas.AddressInfo
struct ContentView: View {
@State private var postalCode = ""
@State private var addressInfoList: [AddressInfo] = []
@State private var errorMessage: String?
private let addressInfoService = AddressInfoService()
var body: some View {
VStack {
HStack {
TextField(
"郵便番号",
text: $postalCode,
prompt: Text("1000001")
)
.keyboardType(.numberPad)
Button("Search", systemImage: "magnifyingglass") {
// guard validate(postalCode: postalCode) else { return } // 普段はフロント側でもvalidationすべきだが,今回はAPIがエラーを返した時の挙動を確認するためコメントアウト
Task {
do {
addressInfoList = try await addressInfoService.getAddress(postalCode: postalCode)
} catch {
errorMessage = error.localizedDescription
}
}
}
}
List(addressInfoList, id: \.id, rowContent: makeAddressInfoView)
}
.padding()
.alert(
"エラー",
isPresented: Binding(
get: { errorMessage != nil },
set: { _,_ in }
),
actions: {
Button("OK") {
errorMessage = nil
}
},
message: {
Text(errorMessage ?? "")
}
)
}
private func makeAddressInfoView(_ addressInfo: AddressInfo) -> some View {
VStack(alignment: .leading) {
Text(addressInfo.postalCode)
HStack {
Text(addressInfo.prefectureKana ?? "")
Text(addressInfo.cityKana ?? "")
Text(addressInfo.streetKana ?? "")
}
.font(.caption)
HStack {
Text(addressInfo.prefectureKanji ?? "")
Text(addressInfo.cityKanji ?? "")
Text(addressInfo.streetKanji ?? "")
}
}
}
private func validate(postalCode: String) -> Bool {
let match = try! /[0-9]{0,7}/.wholeMatch(in: postalCode)
if let matchedString = match?.0 {
return matchedString == postalCode
} else {
return false
}
}
}
struct AddressInfoService {
init() {}
func getAddress(postalCode: String) async throws -> [AddressInfo] {
let transport: ClientTransport = URLSessionTransport()
let client = Client(serverURL: try Servers.Server1.url(), transport: transport)
let response = try await client.searchAddresses(query: .init(postalCode: postalCode))
return try response.ok.body.json
}
}
#Preview {
ContentView()
}
Webを支える技術 -HTTP、URI、HTML、そしてREST (WEB+DB PRESSプラスシリーズ)
(技術評論社公式ならepub版を購入できるのでKindleよりもおすすめです)