土曜日, 6月 21, 2025
No menu items!
ホーム ブログ ページ 5022

営業から工場に転職したんやが


image (20)

続きを読む



Source link

Views: 0

「クレヨンしんちゃん」のしんのすけを現実世界に描いたら…… 予想外な仕上がりに「ワオ!」「よすぎる……」280万再生の反響【海外】



「クレヨンしんちゃん」のしんのすけを現実世界に描いたら…… 予想外な仕上がりに「ワオ!」「よすぎる……」280万再生の反響【海外】

こんな子、いそう!



Source link

Views: 0

理想の娘を育てる育成シム『まじかる☆プリンセス』発表。ボードゲームで人気を博す「まじかる☆シリーズ」初のデジタル作品



理想の娘を育てる育成シム『まじかる☆プリンセス』発表。ボードゲームで人気を博す「まじかる☆シリーズ」初のデジタル作品

理想の娘を育てる育成シム『まじかる☆プリンセス』発表。ボードゲームで人気を博す「まじかる☆シリーズ」初のデジタル作品



Source link

Views: 0

14日 雷雨・突風・ひょうに注意



14日 雷雨・突風・ひょうに注意

14日 雷雨・突風・ひょうに注意



Source link

Views: 0

iOSエンジニアがバックエンド入門する #Swift – Qiita



iOSエンジニアがバックエンド入門する #Swift - Qiita

  • webサーバー楽しそう

  • Rustを学びたいが,サーバーの経験がないからまずは慣れたSwiftで書きたい

  • LLMは次の順番で各領域の技術水準が低・中程度の人間プログラマーを駆逐していきそう(根拠なき直感)だから、より代替困難な側に避難したい

    1. WEBフロントエンド
    2. モバイル(クロスプラットフォーム,ネイティブ)
    3. バックエンド
    4. 組み込み系?(あまりよく知らない)

住所検索サービス
郵便番号を入力すると,それに紐づいた住所が返ってくる

動くものの動画

正常系 異常系

まずは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だと見やすい 参考
image.png

本章ではデータベースを構築する.

データの準備

住所データは住所の郵便番号(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
...

よくわからない数字が並ぶので,解説を見る

  1. 全国地方公共団体コード(JIS X0401、X0402)……… 半角数字
  2. (旧)郵便番号(5桁)……………………………………… 半角数字
  3. 郵便番号(7桁)……………………………………… 半角数字
  4. 都道府県名 ………… 全角カタカナ(コード順に掲載) (※1)
  5. 市区町村名 ………… 全角カタカナ(コード順に掲載) (※1)
  6. 町域名 ……………… 全角カタカナ(五十音順に掲載) (※1)
  7. 都道府県名 ………… 漢字(コード順に掲載) (※1,2)
  8. 市区町村名 ………… 漢字(コード順に掲載) (※1,2)
  9. 町域名 ……………… 漢字(五十音順に掲載) (※1,2)
  10. 一町域が二以上の郵便番号で表される場合の表示 (※3) (「1」は該当、「0」は該当せず)
  11. 小字毎に番地が起番されている町域の表示 (※4) (「1」は該当、「0」は該当せず)
  12. 丁目を有する町域の場合の表示 (「1」は該当、「0」は該当せず)
  13. 一つの郵便番号で二以上の町域を表す場合の表示 (※5) (「1」は該当、「0」は該当せず)
  14. 更新の表示(※6)(「0」は変更なし、「1」は変更あり、「2」廃止(廃止データのみ使用))
  15. 変更理由 (「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テーブルの作成

SQL何もわからんのでGUIからテーブルを作る
image.png

次のように名前をaddressInfoTableと指定する
image.png

次のように列を追加
image.png

DBの各行にはIDとなる主キーが欲しい.IDはnone-nullableで一意である必要がある.しかし,元データ(utf_ken_all.csv)にidなる列は含まれないので,今回作る.
uuid-ossp拡張を有効に設定(参考)してuuid_generate_v4()を初期値とする.

image.png

あとは次のように各列の名前と型,nullabilityを設定する.
image.png

ちなみに,上記操作に対応する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テーブルにデータをインポートする

次のようにデータをインポートする

image.png

一般/ファイル名 でutf_ken_all.csvを指定する
image.png

オプション/ヘッダ を有効にする
image.png

列/Columns to export からidを削除

image.png

OKボタンを押下すると数秒でプロセスが完了するはず.
image.png

これで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: APIProtocolsearchAddresses内部で,DBに保存された対応するDataを返したい.しかし,OpenAPIController.searchAddressesAddressInfoListController.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) {
    ... // 後述
    }
}

そこでOpenAPIControllerRequestを差し込む.
差し込むために,依存注入ライブラリである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を追加する

  1. 上記openapi.yamlをプロジェクトに追加する

  2. 次のopenapi-generator-config.yamlも同じ階層に配置する

    openapi-generator-config.yaml

    generate:
      - types
      - client
    
  3. 公式資料を参考に以下3packageをXcode Projectに追加する.

    追加するときのtargetの設定

    • swift-openapi-generator : None
    • swift-openapi-runtime : {あなたのProject}
    • swift-openapi-urlsession : {あなたのProject}
  4. コンパイル時に,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よりもおすすめです)





Source link

Views: 0

仲間に紛れ込んだ“魔女”をあぶり出して「残酷に処刑」する不穏な推理ミステリーゲーム『魔法少女ノ魔女裁判』Steam版の予定価格は「税込3500円」に。5月10日開催の朗読劇イベントにおけるあらすじも公開




残酷さへの徹底的なこだわりから注目を集め、クラファンでは5000人以上に支援された注目作。初週は20%オフで“お得に少女を処刑”できるという



Source link

Views: 0

「True My Heart」も収録!電波ソングでハイになって怪文書をポストする『ゆんゆん電波シンドローム』10月30日にSteamで発売 | Game*Spark


Alliance Artsは、WHO YOU開発のリズムアドベンチャー『ゆんゆん電波シンドローム』を10月30日にSteamで発売すると「INDIE Live Expo 2025.4.13」にて発表。収録楽曲として新たに「True My Heart」「ねぇ、…しようよ!」「Mighty Heart~ある日のケンカ、いつもの恋心~」の3曲が発表されています。

ヒキコモリの少女が電波ソングにノッて怪文書で世界を壊す

本作はヒキコモリの少女「Qちゃん」を主人公としたリズムアドベンチャーゲームです。。「Qちゃん」は「ゆんゆん」という二次元キャラクターが大好きであり、電波ソングでハイになって「ゆんゆん」への愛に溢れた怪文書を生み出します。それをポストすることでSNS上の人間も狂っていきますが、物語の行きつく先はマルチエンディングとなっています。

怪文書を生み出すリズムゲームでは遊べる楽曲は全て電波ソングですが、「さくらんぼキッス ~爆発だも~ん~」「巫女みこナース・愛のテーマ」「患部で止まってすぐ溶ける ~ 狂気の優曇華院」などの人気曲を多数収録していると明らかにされていました。

10月30日発売と発表!新たに3曲の収録も決定

この度「INDIE Live Expo 2025.4.13」にて本作の発売日が10月30日に決定したことを発表しました。ゲームシステムの一部が垣間見えるトレイラーも公開されています。

さらにリズムゲームで遊べる電波ソングについても、「True My Heart」「ねぇ、…しようよ!」「Mighty Heart~ある日のケンカ、いつもの恋心~」の3曲が新たに収録することが決定した3曲を新たに収録することが発表されました。


『ゆんゆん電波シンドローム』は、PC(Steam)向けに10月30日に発売予定。価格は未定となっています。





Source link

Views: 0

東方二次創作RPG「ほらふき山の魔理沙」,発売日が9月19日に決定。紅魔館から参戦する4名のキャラクターも明らかに



 AllianceArtsは,ゲームブックライクな新作RPG「ほらふき山の魔理沙」を2025年9月19日に発売すると発表した。プラットフォームはPC(Steam)とNintendoSwitchで,価格は両プラットフォームともダウンロード版が2180円(税込)となっている。



Source link

Views: 0

娘育成シミュレーション『まじかる☆プリンセス』発表。娘の父親となり愛する我が子を育てていく。プレイヤーの選択ひとつで娘の未来は大きく変化 | ゲーム・エンタメ最新情報のファミ通.com


娘育成シミュレーション『まじかる☆プリンセス』発表。娘の父親となり愛する我が子を育てていく。プレイヤーの選択ひとつで娘の未来は大きく変化
 MAGIは、2025年4月13日に開催されたインディーゲームイベント“INDIE Live Expo 2025”にて、娘育成ゲーム『まじかる☆プリンセス』を発表した。Steam向けで発売日は未定。

 本作は、ボードゲームで人気を博した『

まじかる☆シリーズ』の世界観をもとに制作された、“娘育成”シミュレーションゲーム。プレイヤーは娘の父親となり、幼少期から魔導学園卒業まで、愛する我が子を育てていく。

以下、プレスリリースを引用

あなたの選択が、娘の未来を創る。“世界一かわいい”娘を育てるゲーム『まじかる☆プリンセス』Steamでリリース決定

株式会社MAGIは、2025年4月13日に開催されたインディーゲームイベント「INDIE Live Expo 2025」にて、娘育成ゲーム『まじかる☆プリンセス』を発表しました。

本作はPC(Steam)向けに鋭意開発中です。併せて、Steamストアページも公開していますので、ぜひウィッシュリスト登録をお願いいたします。

[IMAGE]

▶ Steamストアページ


■まじかる☆プリンセスとは
本作は、ボードゲームで人気を博した「まじかる☆シリーズ」(※詳細は後述)の世界観をもとに制作された、“娘育成”シミュレーションゲームです。

[IMAGE]


プレイヤーは娘の父親となり、幼少期から魔導学園卒業まで、愛する我が子を育てていきます。

真面目に勉強させて優等生にするか、悪行を重ねて不良少女にするか……プレイヤーの選択一つで娘の未来は大きく変化。用意されたエンディングは50種類以上にのぼります。

さらに物語は、娘との日々を通して、因果の鎖に繋がれた謎多き世界「パネテリア王国」の真実にも迫っていくことになります。

■ストーリー
物語の舞台は、パンと魔法を主要産業とする「パネテリア王国」。この国に、父と娘の二人家族が新たな生活を始めるためにやってきました。
娘はやがて「王立魔導学園」へと入学し、仲間と出会い、学園生活を楽しんでいきます。

[IMAGE][IMAGE]


しかし、満月が紅く染まる夜――「紅い月の夜」の異変とともに、魔物たちが現れ、世界は静かに崩れ始めます。
日に日に激しくなる魔物の襲撃。調査を進める女王コロネリアは、やがて娘にこう告げます。

「なるほど、あなたが鍵……。」

果たして、娘に秘められた運命とは?
父として、あなたはどんな未来を選び取るのか――。

[IMAGE]

■ゲームの特徴
プレイヤーは娘(アリス)の父親となり、昼パートと夜パートを行き来しながら、アリスを理想の娘へと育てていきます。各パートでどんな行動をとったかによってアリスのステータスは上下し、最終的に誰と結ばれ、どんな職業につくかが決まります。

[IMAGE][IMAGE][IMAGE][IMAGE]

・自由度MAXの育成システム
夜の王国には誘惑がいっぱい。謎の白い粉をこねるアルバイトや、怪しげな「危ない服屋」など、ちょっとワケありな体験の数々がアリスを待ち受けています。しっかり導いてあげないと、いつのまにか夜遊びが大好きな不良少女になってしまうかもしれません。

[IMAGE][IMAGE][IMAGE][IMAGE]

・圧倒的ボリューム
総テキスト量は35万文字。さらにマンガ700枚、スチル100枚、デートイベント170種類以上の大ボリュームで、生き生きとしたキャラクターたちを描き出します。

[IMAGE][IMAGE]

・着せ替えで「娘」をもっとかわいく
アリスにどんな服を着せるかもプレイヤーが自由に選ぶことが可能。清楚なお嬢様風か、やんちゃな小悪魔風か……あなたならどんな服を着せますか?

[IMAGE][IMAGE]

■キャラクター紹介
娘の友人や先生など、総勢30人以上の個性的なキャラクターが登場。選択次第では最終的に「運命の相手」として結ばれる展開も……?

・娘(アリス)(CV:鈴代紗弓)

[IMAGE]

本作の主人公で、プレイヤーの娘。
王立魔導学園へ通うため、父と飼い猫とともに王都へ引っ越してきた。
プレイ次第で善か悪か、また将来どうなるかも変化する。

・クロ

[IMAGE]

田舎から王都へ引っ越す際についてきた猫。
アリスが極度に「良い子」になるように過保護である。

・コロネリア

[IMAGE]

パネテリア王国の女王。
なぜかアリスのことを知っており、アリスに接触しようとする。
大人びた言動から冷たい印象を受けるが、本来は明るくて活発な雰囲気の持ち主。
■まじかる☆シリーズとは
『まじかる☆プリンセス』は、ボードゲームとして高い評価を受けてきた「まじかる☆シリーズ」の世界観をもとに開発された初のデジタルゲーム作品です。
「まじかる☆シリーズ」は、2018年の第1作『まじかる☆ベーカリー』から始まり、『まじかる☆パティスリー』や『まじかる☆キングダム』など、これまでに全11作品を展開してきました(拡張パッケージを含む)。パン屋の見習いだった魔法少女が、やがて王国の頂点へと上り詰めていく姿を、ボードゲームという形で描いています。

今回「まじかる☆シリーズ」の作品世界をさらに広く・深く展開させたいという思いから、シリーズ初のデジタルゲーム作品として『まじかる☆プリンセス』の開発を決定いたしました。

[IMAGE]

■開発元について
『まじかる☆プリンセス』は、株式会社MAGIと株式会社ネオトロによる共同開発タイトルです。MAGIの2人、ネオトロの1人、合計3人をコアメンバーとする少数精鋭の体制で開発が進められています。

・株式会社MAGIについて

※公式サイト
2015年に設立されたボードゲーム制作会社で、これまでに
『まじかる☆ベーカリー』『まじかる☆パティスリー』『まじかる☆キングダム』など、「まじかる☆シリーズ」を中心に10作以上のボードゲームを手がけてきました。

・株式会社ネオトロについて

※公式サイト
2016年設立のゲーム開発会社で、“悪夢系”アクションシューター『
NeverAwake』やアーケードSTG『VRITRA HEXA』など、シューティング、アクション、パズルといった多彩なジャンルでの開発実績を持ちます。

両社はこれまでもMAGIのボードゲーム制作において協力関係にあり、そうした経緯から本作の共同開発が実現しました。『まじかる☆プリンセス』では、「まじかる☆シリーズ」の世界観を原作とし、MAGIが主にシナリオとキャラクターデザイン、ネオトロがディレクションとプログラム開発を担当しています。

■『まじかる☆プリンセス』商品情報

  • タイトル:まじかる☆プリンセス
  • 開発元:株式会社MAGI・株式会社ネオトロ
  • 販売元:株式会社MAGI
  • ジャンル:娘育成ゲーム
  • プラットフォーム:PC(Steam)
  • リリース日:未定
  • プレイ人数:1人
  • 対応言語:日本語、英語、簡体字中国語
  • 販売価格:未定



娘育成シミュレーション『まじかる☆プリンセス』発表。娘の父親となり愛する我が子を育てていく。プレイヤーの選択ひとつで娘の未来は大きく変化 | ゲーム・エンタメ最新情報のファミ通.com

{content}

Source link

Views: 0

【EXPO 2025】大阪・関西万博に行く予定の人に絶対に準備しておいてほしいこと / 運が悪いと入場前に詰むかも



2025年4月13日から10月13日までの半年間の日程で、「EXPO 2025」大阪・関西万博が始まった。その開幕初日に会場に足を運んだ私(佐藤)は、その場で「コレは!?」と思う注意点に気づいたので、2日目以降に行く予定 […]



Source link

Views: 0