Views: 0
理想の娘を育てる育成シム『まじかる☆プリンセス』発表。ボードゲームで人気を博す「まじかる☆シリーズ」初のデジタル作品
Source link
Views: 0
webサーバー楽しそう
Rustを学びたいが,サーバーの経験がないからまずは慣れたSwiftで書きたい
LLMは次の順番で各領域の技術水準が低・中程度の人間プログラマーを駆逐していきそう(根拠なき直感)だから、より代替困難な側に避難したい
住所検索サービス
郵便番号を入力すると,それに紐づいた住所が返ってくる
動くものの動画
正常系 | 異常系 |
---|---|
![]() |
![]() |
まずは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,
...
DBは何もわからんが,PostgreSQLとGUIのpgAdmin 4で構築する.
インストールの手順は次の資料などに丸投げします.
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;
次のようにデータをインポートする
一般/ファイル名 で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に対応するエンティティオブジェクトを作成する.
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=#
)上で実行する
次は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 // コンパイルできることを確認したら消す.
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で定義した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プロジェクトを作成する.
上記openapi.yaml
をプロジェクトに追加する
次のopenapi-generator-config.yaml
も同じ階層に配置する
openapi-generator-config.yaml
generate:
- types
- client
公式資料を参考に以下3packageをXcode Projectに追加する.
追加するときのtargetの設定
None
{あなたのProject}
{あなたのProject}
コンパイル時に,openapi.yaml
に基づいてコードを自動生成するために,Project/Targetsで{あなたのプロジェクト}
を選択し,/Build Phase/Run Build Tool Plug-insで追加ボタンを押下し,OpenAPIGenerator(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よりもおすすめです)
Views: 0
残酷さへの徹底的なこだわりから注目を集め、クラファンでは5000人以上に支援された注目作。初週は20%オフで“お得に少女を処刑”できるという
Source link
Views: 0
Alliance Artsは、WHO YOU開発のリズムアドベンチャー『ゆんゆん電波シンドローム』を10月30日にSteamで発売すると「INDIE Live Expo 2025.4.13」にて発表。収録楽曲として新たに「True My Heart」「ねぇ、…しようよ!」「Mighty Heart~ある日のケンカ、いつもの恋心~」の3曲が発表されています。
本作はヒキコモリの少女「Qちゃん」を主人公としたリズムアドベンチャーゲームです。。「Qちゃん」は「ゆんゆん」という二次元キャラクターが大好きであり、電波ソングでハイになって「ゆんゆん」への愛に溢れた怪文書を生み出します。それをポストすることでSNS上の人間も狂っていきますが、物語の行きつく先はマルチエンディングとなっています。
怪文書を生み出すリズムゲームでは遊べる楽曲は全て電波ソングですが、「さくらんぼキッス ~爆発だも~ん~」「巫女みこナース・愛のテーマ」「患部で止まってすぐ溶ける ~ 狂気の優曇華院」などの人気曲を多数収録していると明らかにされていました。
この度「INDIE Live Expo 2025.4.13」にて本作の発売日が10月30日に決定したことを発表しました。ゲームシステムの一部が垣間見えるトレイラーも公開されています。
さらにリズムゲームで遊べる電波ソングについても、「True My Heart」「ねぇ、…しようよ!」「Mighty Heart~ある日のケンカ、いつもの恋心~」の3曲が新たに収録することが決定した3曲を新たに収録することが発表されました。
『ゆんゆん電波シンドローム』は、PC(Steam)向けに10月30日に発売予定。価格は未定となっています。
Views: 0
AllianceArtsは,ゲームブックライクな新作RPG「ほらふき山の魔理沙」を2025年9月19日に発売すると発表した。プラットフォームはPC(Steam)とNintendoSwitchで,価格は両プラットフォームともダウンロード版が2180円(税込)となっている。
Source link
Views: 0
本作は、ボードゲームで人気を博した『
まじかる☆シリーズ』の世界観をもとに制作された、“娘育成”シミュレーションゲーム。プレイヤーは娘の父親となり、幼少期から魔導学園卒業まで、愛する我が子を育てていく。以下、プレスリリースを引用
本作はPC(Steam)向けに鋭意開発中です。併せて、Steamストアページも公開していますので、ぜひウィッシュリスト登録をお願いいたします。
■まじかる☆プリンセスとは
本作は、ボードゲームで人気を博した「まじかる☆シリーズ」(※詳細は後述)の世界観をもとに制作された、“娘育成”シミュレーションゲームです。
真面目に勉強させて優等生にするか、悪行を重ねて不良少女にするか……プレイヤーの選択一つで娘の未来は大きく変化。用意されたエンディングは50種類以上にのぼります。
さらに物語は、娘との日々を通して、因果の鎖に繋がれた謎多き世界「パネテリア王国」の真実にも迫っていくことになります。
■ストーリー「なるほど、あなたが鍵……。」
果たして、娘に秘められた運命とは?
父として、あなたはどんな未来を選び取るのか――。
■ゲームの特徴
プレイヤーは娘(アリス)の父親となり、昼パートと夜パートを行き来しながら、アリスを理想の娘へと育てていきます。各パートでどんな行動をとったかによってアリスのステータスは上下し、最終的に誰と結ばれ、どんな職業につくかが決まります。
・自由度MAXの育成システム
夜の王国には誘惑がいっぱい。謎の白い粉をこねるアルバイトや、怪しげな「危ない服屋」など、ちょっとワケありな体験の数々がアリスを待ち受けています。しっかり導いてあげないと、いつのまにか夜遊びが大好きな不良少女になってしまうかもしれません。
・圧倒的ボリューム
総テキスト量は35万文字。さらにマンガ700枚、スチル100枚、デートイベント170種類以上の大ボリュームで、生き生きとしたキャラクターたちを描き出します。
・着せ替えで「娘」をもっとかわいく
アリスにどんな服を着せるかもプレイヤーが自由に選ぶことが可能。清楚なお嬢様風か、やんちゃな小悪魔風か……あなたならどんな服を着せますか?
・娘(アリス)(CV:鈴代紗弓)
・クロ
・コロネリア
今回「まじかる☆シリーズ」の作品世界をさらに広く・深く展開させたいという思いから、シリーズ初のデジタルゲーム作品として『まじかる☆プリンセス』の開発を決定いたしました。
・株式会社MAGIについて
※公式サイト・株式会社ネオトロについて
※公式サイト両社はこれまでもMAGIのボードゲーム制作において協力関係にあり、そうした経緯から本作の共同開発が実現しました。『まじかる☆プリンセス』では、「まじかる☆シリーズ」の世界観を原作とし、MAGIが主にシナリオとキャラクターデザイン、ネオトロがディレクションとプログラム開発を担当しています。
娘育成シミュレーション『まじかる☆プリンセス』発表。娘の父親となり愛する我が子を育てていく。プレイヤーの選択ひとつで娘の未来は大きく変化 | ゲーム・エンタメ最新情報のファミ通.com
{content}
Views: 0
2025年4月13日から10月13日までの半年間の日程で、「EXPO 2025」大阪・関西万博が始まった。その開幕初日に会場に足を運んだ私(佐藤)は、その場で「コレは!?」と思う注意点に気づいたので、2日目以降に行く予定 […]
Source link
Views: 0