From ff54cb2d1a72e0faf39631e7c150f9f215ba13dc Mon Sep 17 00:00:00 2001 From: "novr (Nobuhisa Komiya)" Date: Fri, 17 May 2024 13:12:51 +0900 Subject: [PATCH 01/13] feat: vapor --- .github/workflows/openapi.yaml | 28 -- .github/workflows/redoc.yaml | 8 +- Package.resolved | 177 ++++++- Package.swift | 47 +- Public/index.html | 404 +++++++++++++++ Sources/App/Controllers/.gitkeep | 0 .../Controllers/APIController.swift} | 45 +- Sources/App/configure.swift | 14 + Sources/App/entrypoint.swift | 29 ++ Sources/App/openapi-generator-config.yaml | 3 + openapi.yaml => Sources/App/openapi.yaml | 0 Sources/App/routes.swift | 11 + Sources/EndPoint/EndPoint.swift | 25 - Sources/EndPoint/Index.swift | 427 ---------------- Sources/EndPoint/OpenAPI/Server.swift | 127 ----- Sources/EndPoint/OpenAPI/Types.swift | 459 ------------------ Sources/EndPoint/Prefecture+LogoURL.swift | 32 -- Tests/AppTests/AppTests.swift | 23 + Tests/EndPointTests/EndPointTests.swift | 47 -- 19 files changed, 701 insertions(+), 1205 deletions(-) delete mode 100644 .github/workflows/openapi.yaml create mode 100644 Public/index.html create mode 100644 Sources/App/Controllers/.gitkeep rename Sources/{EndPoint/APIService.swift => App/Controllers/APIController.swift} (69%) create mode 100644 Sources/App/configure.swift create mode 100644 Sources/App/entrypoint.swift create mode 100644 Sources/App/openapi-generator-config.yaml rename openapi.yaml => Sources/App/openapi.yaml (100%) create mode 100644 Sources/App/routes.swift delete mode 100644 Sources/EndPoint/EndPoint.swift delete mode 100644 Sources/EndPoint/Index.swift delete mode 100644 Sources/EndPoint/OpenAPI/Server.swift delete mode 100644 Sources/EndPoint/OpenAPI/Types.swift delete mode 100644 Sources/EndPoint/Prefecture+LogoURL.swift create mode 100644 Tests/AppTests/AppTests.swift delete mode 100644 Tests/EndPointTests/EndPointTests.swift diff --git a/.github/workflows/openapi.yaml b/.github/workflows/openapi.yaml deleted file mode 100644 index 0d97211..0000000 --- a/.github/workflows/openapi.yaml +++ /dev/null @@ -1,28 +0,0 @@ -name: openapi - -on: - push: - branches: [ "main" ] - paths: - - 'openapi.yaml' - workflow_dispatch: - -jobs: - build: - runs-on: macos-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Build - run: | - swift run swift-openapi-generator generate \ - --mode types --mode server \ - --output-directory Sources/EndPoint/OpenAPI/ \ - openapi.yaml - - - uses: peter-evans/create-pull-request@v6 - with: - branch: _api - commit-message: Update OpenAPI generated - team-reviewers: ios_wg_recruitment diff --git a/.github/workflows/redoc.yaml b/.github/workflows/redoc.yaml index 33e5a3b..ad54996 100644 --- a/.github/workflows/redoc.yaml +++ b/.github/workflows/redoc.yaml @@ -2,9 +2,9 @@ name: redoc on: push: - branches: [ "main" ] + branches: ["main"] paths: - - 'openapi.yaml' + - "Sources/App/openapi.yaml" workflow_dispatch: jobs: @@ -24,7 +24,7 @@ jobs: - name: Build ReDoc HTML run: | npm install -g redoc-cli - redoc-cli bundle openapi.yaml + redoc-cli bundle Sources/App/openapi.yaml mkdir pages mv redoc-static.html pages/openapi.html @@ -37,7 +37,7 @@ jobs: deploy: needs: build - runs-on: ubuntu-latest + runs-on: ubuntu-latest timeout-minutes: 5 environment: name: github-pages diff --git a/Package.resolved b/Package.resolved index 106c300..27f8ae3 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,12 +1,31 @@ { + "originHash" : "b2abcfde15a6668548e5ed090464c6c8f2c9874f53045fadcb1a58229e71a34f", "pins" : [ { - "identity" : "compute", + "identity" : "async-http-client", "kind" : "remoteSourceControl", - "location" : "https://github.com/swift-cloud/Compute", + "location" : "https://github.com/swift-server/async-http-client.git", "state" : { - "revision" : "5d48d24c9515f3a0e760fe2837d174fa2a8ffcda", - "version" : "3.1.0" + "revision" : "a22083713ee90808d527d0baa290c2fb13ca3096", + "version" : "1.21.1" + } + }, + { + "identity" : "async-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/async-kit.git", + "state" : { + "revision" : "7ece208cd401687641c88367a00e3ea2b04311f1", + "version" : "1.19.0" + } + }, + { + "identity" : "console-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/console-kit.git", + "state" : { + "revision" : "9c24ac496c97cfb49c1bd5e7162008bb50abafc8", + "version" : "4.14.2" } }, { @@ -18,6 +37,15 @@ "version" : "0.2.5" } }, + { + "identity" : "multipart-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/multipart-kit.git", + "state" : { + "revision" : "12ee56f25bd3fc4c2d09c2aa16e69de61dc786e8", + "version" : "4.6.0" + } + }, { "identity" : "openapikit", "kind" : "remoteSourceControl", @@ -27,6 +55,15 @@ "version" : "3.1.2" } }, + { + "identity" : "routing-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/routing-kit.git", + "state" : { + "revision" : "8c9a227476555c55837e569be71944e02a056b72", + "version" : "4.9.1" + } + }, { "identity" : "swift-algorithms", "kind" : "remoteSourceControl", @@ -45,13 +82,31 @@ "version" : "1.3.0" } }, + { + "identity" : "swift-atomics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-atomics.git", + "state" : { + "revision" : "cd142fd2f64be2100422d658e7411e39489da985", + "version" : "1.2.0" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "94cf62b3ba8d4bed62680a282d4c25f9c63c2efb", + "version" : "1.1.0" + } + }, { "identity" : "swift-crypto", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-crypto", + "location" : "https://github.com/apple/swift-crypto.git", "state" : { - "revision" : "f0525da24dc3c6cbb2b6b338b65042bc91cbc4bb", - "version" : "3.3.0" + "revision" : "bc1c29221f6dfeb0ebbfbc98eb95cd3d4967868e", + "version" : "3.4.0" } }, { @@ -64,21 +119,75 @@ } }, { - "identity" : "swift-numerics", + "identity" : "swift-log", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-numerics.git", + "location" : "https://github.com/apple/swift-log.git", "state" : { - "revision" : "0a5bc04095a675662cf24757cc0640aa2204253b", - "version" : "1.0.2" + "revision" : "e97a6fcb1ab07462881ac165fdbb37f067e205d5", + "version" : "1.5.4" + } + }, + { + "identity" : "swift-metrics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-metrics.git", + "state" : { + "revision" : "ce594e71e92a1610015017f83f402894df540e51", + "version" : "2.4.4" + } + }, + { + "identity" : "swift-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio.git", + "state" : { + "revision" : "359c461e5561d22c6334828806cc25d759ca7aa6", + "version" : "2.65.0" } }, { - "identity" : "swift-openapi-compute", + "identity" : "swift-nio-extras", "kind" : "remoteSourceControl", - "location" : "https://github.com/novr/swift-openapi-compute", + "location" : "https://github.com/apple/swift-nio-extras.git", "state" : { - "revision" : "feaf3fe85db1afa1627a97044bee3d144cf3bf38", - "version" : "0.2.0" + "revision" : "a3b640d7dc567225db7c94386a6e71aded1bfa63", + "version" : "1.22.0" + } + }, + { + "identity" : "swift-nio-http2", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-http2.git", + "state" : { + "revision" : "c6afe04165c865faaa687b42c32ed76dfcc91076", + "version" : "1.31.0" + } + }, + { + "identity" : "swift-nio-ssl", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-ssl.git", + "state" : { + "revision" : "7c381eb6083542b124a6c18fae742f55001dc2b5", + "version" : "2.26.0" + } + }, + { + "identity" : "swift-nio-transport-services", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio-transport-services.git", + "state" : { + "revision" : "38ac8221dd20674682148d6451367f89c2652980", + "version" : "1.21.0" + } + }, + { + "identity" : "swift-numerics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-numerics.git", + "state" : { + "revision" : "0a5bc04095a675662cf24757cc0640aa2204253b", + "version" : "1.0.2" } }, { @@ -99,6 +208,42 @@ "version" : "1.3.2" } }, + { + "identity" : "swift-openapi-vapor", + "kind" : "remoteSourceControl", + "location" : "https://github.com/swift-server/swift-openapi-vapor", + "state" : { + "revision" : "b53c1517e889d5479567729fbe143a59da302ffc", + "version" : "1.0.1" + } + }, + { + "identity" : "swift-system", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-system.git", + "state" : { + "revision" : "025bcb1165deab2e20d4eaba79967ce73013f496", + "version" : "1.2.1" + } + }, + { + "identity" : "vapor", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/vapor.git", + "state" : { + "revision" : "90da64ad52ee595d199212338b89995bcab99d0e", + "version" : "4.100.0" + } + }, + { + "identity" : "websocket-kit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vapor/websocket-kit.git", + "state" : { + "revision" : "4232d34efa49f633ba61afde365d3896fc7f8740", + "version" : "2.15.0" + } + }, { "identity" : "yams", "kind" : "remoteSourceControl", @@ -109,5 +254,5 @@ } } ], - "version" : 2 + "version" : 3 } diff --git a/Package.swift b/Package.swift index a8b9c24..99503cb 100644 --- a/Package.swift +++ b/Package.swift @@ -1,45 +1,52 @@ -// swift-tools-version: 5.9 -// The swift-tools-version declares the minimum version of Swift required to build this package. - +// swift-tools-version:5.10 import PackageDescription let package = Package( name: "ios-junior-engineer-codecheck-backend", platforms: [.macOS(.v13)], dependencies: [ - // Dependencies declare other packages that this package depends on. - // .package(url: /* package url */, from: "1.0.0"), - .package( - url: "https://github.com/swift-cloud/Compute", - from: "3.1.0" - ), -// .package( -// url: "https://github.com/johnsundell/ink.git", -// from: "0.5.1" -// ), + // 💧 A server-side Swift web framework. + .package(url: "https://github.com/vapor/vapor.git", from: "4.99.3"), + // 🔵 Non-blocking, event-driven networking for Swift. Used for custom executors + .package(url: "https://github.com/apple/swift-nio.git", from: "2.65.0"), .package( url: "https://github.com/yumemi-inc/fake-fortune-telling", from: "0.2.5" ), .package(url: "https://github.com/apple/swift-openapi-generator", from: "1.2.1"), .package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.3.2"), - .package(url: "https://github.com/novr/swift-openapi-compute", from: "0.2.0"), + .package(url: "https://github.com/swift-server/swift-openapi-vapor", from: "1.0.1"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets can depend on other targets in this package, and on products in packages this package depends on. .executableTarget( - name: "EndPoint", + name: "App", dependencies: [ - .product(name: "Compute", package: "Compute"), -// .product(name: "Ink", package: "ink"), + .product(name: "Vapor", package: "vapor"), + .product(name: "NIOCore", package: "swift-nio"), + .product(name: "NIOPosix", package: "swift-nio"), .product(name: "FakeFortuneTelling", package: "fake-fortune-telling"), .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"), - .product(name: "OpenAPICompute", package: "swift-openapi-compute"), + .product(name: "OpenAPIVapor", package: "swift-openapi-vapor"), + ], + swiftSettings: swiftSettings, + plugins: [ + .plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator"), ] ), .testTarget( - name: "EndPointTests", - dependencies: ["EndPoint"]), + name: "AppTests", + dependencies: [ + .target(name: "App"), + .product(name: "XCTVapor", package: "vapor"), + ], + swiftSettings: swiftSettings + ), ] ) + +var swiftSettings: [SwiftSetting] { [ + .enableUpcomingFeature("DisableOutwardActorInference"), + .enableExperimentalFeature("StrictConcurrency"), +] } diff --git a/Public/index.html b/Public/index.html new file mode 100644 index 0000000..6173c42 --- /dev/null +++ b/Public/index.html @@ -0,0 +1,404 @@ + + + + +株式会社ゆめみ iOS 未経験者エンジニア向けコードチェック課題 + + + +
+
+

株式会社ゆめみ iOS 未経験者エンジニア向けコードチェック課題

+ +

概要

+

本エンドポイントは株式会社ゆめみ(以下弊社)が、弊社に iOS エンジニアを希望する未経験の方に出す課題のバックエンドです。未経験者の方はリファクタリングの課題もしくは本課題のいずれかを提出してください。本課題をお選びの方は、下記を詳しく読んだ上で課題に取り組んでください。 +

+ +

アプリ仕様

+

「あなたと相性のいい都道府県を占ってあげる!」をテーマに iPhone アプリを作ってください。API 仕様下記の通りです。

+ +

API 仕様

+

(詳細は OpenAPI のページもご参考にしてください)

+
    +
  • Base URL:
  • +

    "https://yumemi-ios-junior-engineer-codecheck.app.swift.cloud"

    + +
  • End Point:
  • +

    "/my_fortune"

    + +
  • HTTP Method:
  • +

    "POST"

    + +
  • HTTP Request Headers:
  • + + + + + + + + + +
    KeyValue
    "API-Version""v1"
    +

    ※"v1"を他の文字列で置き換えると EndPoint.APIVersion.InitializationError + エラーになります;またこのヘッダーを省略すること自体は可能ですが、将来API仕様が変わる可能性があるため、入れることをお勧めします。

    + +
  • HTTP Body:
  • + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    KeyTypeDescriptionSample Value
    "name"String占う人の名前"ゆめみん"
    "birthday"YearMonthDay(後述)占う人の生年月日-
    "blood_type"String占う人の血液型"ab"
    "today"YearMonthDay(後述)今日の日付-
    + +

    YearMonthDay 型の仕様:

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    KeyTypeDescriptionSample Value
    "year"Int2000
    "month"Int1
    "day"Int27
    + +

    JSON サンプル

    +
    
    +        {
    +            "name": "ゆめみん",
    +            "birthday": {
    +                "year": 2000,
    +                "month": 1,
    +                "day": 27
    +            },
    +            "blood_type": "ab",
    +            "today": {
    +                "year": 2023,
    +                "month": 5,
    +                "day": 5
    +            }
    +        }
    + +
  • Response Body
  • + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    KeyTypeDescriptionSample Value
    "name"String都道府県の名前"富山県"
    "capital"String県庁所在地"富山市"
    "citizen_day"MonthDay?(後述)県民の日(もしあれば)-
    "has_coast_line"Bool海岸線があるかどうかtrue
    "logo_url"StringロゴのURL"https://japan-map.com/wp-content/uploads/toyama.png"
    "brief"String都道府県の概要"富山県(とやまけん)は、日本の中部地方に位置する県。県庁所在地は富山市。\n中部地方の日本海側、新潟県を含めた場合の北陸地方のほぼ中央にある。\n※出典: フリー百科事典『ウィキペディア(Wikipedia)』"
    + +

    MonthDay 型の仕様:

    + + + + + + + + + + + + + + + + + + + +
    KeyTypeDescriptionSample Value
    "month"Int5
    "day"Int9
    + +

    JSON サンプル

    +
    
    +        {
    +            "name": "富山県",
    +            "has_coast_line": true,
    +            "citizen_day": {
    +                "month": 5,
    +                "day": 9
    +            },
    +            "capital": "富山市",
    +            "logo_url": "https://japan-map.com/wp-content/uploads/toyama.png"
    +            "brief": "富山県(とやまけん)は、日本の中部地方に位置する県。県庁所在地は富山市。\n中部地方の日本海側、新潟県を含めた場合の北陸地方のほぼ中央にある。\n※出典: フリー百科事典『ウィキペディア(Wikipedia)』",
    +        }
    +
+ +

環境

+
    +
  • IDE:基本最新の安定版(本概要最終更新時点では Xcode 14.3)
  • +
  • Swift:基本最新の安定版(本概要最終更新時点では Swift 5.8)
  • +
  • 開発ターゲット:基本最新の安定版(本概要最終更新時点では iOS 16.4)
  • +
+ +

動作

+
    +
  1. ユーザから名前、生年月日及び血液型を入力してもらいます。
  2. +
  3. 上記のデータと併せて、送信時の日付も一緒に入れて、上記の API に問い合わせてください。
  4. +
  5. 結果をもらったらその結果を表示してください。
  6. +
      +
    1. そのうち logo_url の部分は、その URL をパースし、該当 URL から画像データを取得し、その画像を表示してください。
    2. +
    +
+ +

注意点

+
    +
  1. 必ず git でプロジェクトを管理し、git ホスティングサービスで提出してください。
  2. +
      +
    1. ホスティングサービスは GitHub 推奨です。ただし git リポジトリーをそのまま zip 化して提出することは NG です。
    2. +
    3. git で提出してもらう理由は実装の途中経過を評価するためなので、いわゆるワンコミットの提出や、パッと全部やっつけた雑なコミットが含まれる提出物は評価できないため NG + です。必ずコミット粒度を意識して作ってください。もちろんブランチ運用や PR 運用も高評価ポイントになります。
    4. +
    5. GitHub で提出時に公開リポジトリーで提出したくない場合は、提出時にその旨をキャスターさんに伝えてください、コードチェック担当者の GitHub アカウントを返送しますので該当者に Read + 権限を付与してください。
    6. +
    +
  3. UI の指定はありませんが、UI/UX への考慮も評価ポイントになります。
  4. +
      +
    1. 特に Dark Mode や横画面など、iOS がデフォルトで利用可能な機能について気をつけてください。例えば Dark Mode で文字が読めないような実装は減点ポイントになります。
    2. +
    +
  5. 上記の動作さえ満たす限り、機能の追加等はウェルカムです。
  6. +
      +
    1. 例:占い結果をローカルに保存する、iPad/Mac 対応、などなど。
    2. +
    +
  7. サードパーティーライブラリーの利用については、オープンソースのものに限り制限しません(むしろ推奨です)。
  8. +
      +
    1. CocoaPods と Carthage についても利用 OK ですが、SwiftPM の方を推奨します。
    2. +
    +
  9. SDK は UIKit および SwiftUI どちらでも OK です。
  10. +
      +
    1. 両方の混在も OK です。
    2. +
    +
  11. ChatGPTなどAIサービスの利用は禁止しておりません。
  12. +
      +
    1. 利用にあたって工夫したプロンプトやソースコメント等をご提出頂くと加点評価する場合がございます。 (減点評価はありません)
    2. +
    +
+ +

参考記事

+
    +
  • 本特別課題の評価方法等に関しては、こちらの記事に詳しく書かれてありますので、ぜひご覧ください。
  • +
  • CocoaPods の利用に関しては こちらの記事も参照ください。
  • +
+ +

その他注意

+
    +
  • 本課題はあくまで未経験者向けの特別課題です。中途採用のご応募では本課題を選択できません。
  • +
      +
    • 第二新卒等の場合は、エンジニア職経験がない方に限定せていただきます。
    • +
    +
  • 本課題を提出する場合、通常課題をこなす必要がありません。
  • +
      +
    • ただし課題の性質上、レビュー期間は通常課題より長めに設けさせていただきます(目安としておおよそ 1 週間です)。
    • +
    • また通常課題と違い、明確なチケットに基づいた選考基準は現在ありません。あなたが思う「いいアプリ」を作ってください。
    • +
    +
  • 本課題は、iOS アプリ開発のスキルを選考するためのものです。そのため、本課題の内容をもとにしたアプリを App Store に公開することは禁止しております。
  • +
+ +

謝辞

+

各都道府県のロゴ画像は、日本地図無料イラスト素材集様の都道府県シルエット画像を利用しています。

+

本ページは、ChatGPTの協力で作成しました。

+
+
+ + diff --git a/Sources/App/Controllers/.gitkeep b/Sources/App/Controllers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Sources/EndPoint/APIService.swift b/Sources/App/Controllers/APIController.swift similarity index 69% rename from Sources/EndPoint/APIService.swift rename to Sources/App/Controllers/APIController.swift index e0717f8..c7e8108 100644 --- a/Sources/EndPoint/APIService.swift +++ b/Sources/App/Controllers/APIController.swift @@ -1,32 +1,20 @@ // -// File.swift -// +// APIController.swift // -// Created by 古宮 伸久 on 2023/06/19. +// +// Created by 古宮 伸久 on 2024/05/16. // -import Compute +import Foundation import FakeFortuneTelling enum APIServiceError: Error { case InvalidateInput } -struct APIService : APIProtocol { - - init(router: Router) { - router.get("", handleIndexRoute(request:response:)) - } - - func handleIndexRoute(request: IncomingRequest, response: OutgoingResponse) async throws { - try await response - .status(.ok) - .send(html: Index().html) - } - - func post_hyphen_my_hyphen_fortune(_ input: Operations.post_hyphen_my_hyphen_fortune.Input) async throws - -> Operations.post_hyphen_my_hyphen_fortune.Output { - let version = input.headers.API_hyphen_Version ?? .v1 +struct APIController: APIProtocol { + func post_hyphen_my_hyphen_fortune(_ input: Operations.post_hyphen_my_hyphen_fortune.Input) async throws -> Operations.post_hyphen_my_hyphen_fortune.Output { + let version = input.headers.API_hyphen_Version ?? .v1 switch version { case .v1: return try await askFortune(input) @@ -47,7 +35,7 @@ struct APIService : APIProtocol { brief: result.brief, capital: result.capital, has_coast_line: result.hasCoastLine, - logo_url: result.logoURL.absoluteString)))) + logo_url: result.logoURL?.absoluteString ?? "")))) } } @@ -56,6 +44,7 @@ extension Name { try self.init(text: input) } } + extension BloodType { init(_ input: Components.Schemas.MyFortuneRequest.blood_typePayload) throws { switch input { @@ -72,3 +61,19 @@ extension YearMonthDay { try self.init(year: input.year, month: input.month, day: input.day) } } + +extension Prefecture { + private var spell: String { + switch self { + case .gunma: + return "gumma" + + default: + return "\(self)" + } + } + + var logoURL: URL? { + .init(string: "https://japan-map.com/wp-content/uploads/\(spell).png") + } +} diff --git a/Sources/App/configure.swift b/Sources/App/configure.swift new file mode 100644 index 0000000..ac9e8a9 --- /dev/null +++ b/Sources/App/configure.swift @@ -0,0 +1,14 @@ +import Vapor +import OpenAPIVapor + +// configures your application +public func configure(_ app: Application) async throws { + // uncomment to serve files from /Public folder + app.middleware.use( + FileMiddleware(publicDirectory: app.directory.publicDirectory, defaultFile: "index.html") + ) + // register routes + let transport = VaporTransport(routesBuilder: app) + let controller = APIController() + try controller.registerHandlers(on: transport) +} diff --git a/Sources/App/entrypoint.swift b/Sources/App/entrypoint.swift new file mode 100644 index 0000000..2e4c7cb --- /dev/null +++ b/Sources/App/entrypoint.swift @@ -0,0 +1,29 @@ +import Vapor +import Logging +import NIOCore +import NIOPosix + +@main +enum Entrypoint { + static func main() async throws { + var env = try Environment.detect() + try LoggingSystem.bootstrap(from: &env) + + let app = try await Application.make(env) + + // This attempts to install NIO as the Swift Concurrency global executor. + // You should not call any async functions before this point. + let executorTakeoverSuccess = NIOSingletons.unsafeTryInstallSingletonPosixEventLoopGroupAsConcurrencyGlobalExecutor() + app.logger.debug("Running with \(executorTakeoverSuccess ? "SwiftNIO" : "standard") Swift Concurrency default executor") + + do { + try await configure(app) + } catch { + app.logger.report(error: error) + try? await app.asyncShutdown() + throw error + } + try await app.execute() + try await app.asyncShutdown() + } +} diff --git a/Sources/App/openapi-generator-config.yaml b/Sources/App/openapi-generator-config.yaml new file mode 100644 index 0000000..99604c2 --- /dev/null +++ b/Sources/App/openapi-generator-config.yaml @@ -0,0 +1,3 @@ +generate: + - types + - server \ No newline at end of file diff --git a/openapi.yaml b/Sources/App/openapi.yaml similarity index 100% rename from openapi.yaml rename to Sources/App/openapi.yaml diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift new file mode 100644 index 0000000..2edcc8f --- /dev/null +++ b/Sources/App/routes.swift @@ -0,0 +1,11 @@ +import Vapor + +func routes(_ app: Application) throws { + app.get { req async in + "It works!" + } + + app.get("hello") { req async -> String in + "Hello, world!" + } +} diff --git a/Sources/EndPoint/EndPoint.swift b/Sources/EndPoint/EndPoint.swift deleted file mode 100644 index 33e32cb..0000000 --- a/Sources/EndPoint/EndPoint.swift +++ /dev/null @@ -1,25 +0,0 @@ -import Foundation -import Compute -import FakeFortuneTelling -import OpenAPICompute - -@main -struct EndPoint { - public static func main() async throws { - let service: APIProtocol = APIService(router: router) - try service.registerHandlers(on: ComputeTransport(router: router)) - - try await onIncomingRequest(router.run) - } - - static let router = Router() -} - -extension EndPoint { - - static func handleIndexRoute(request: IncomingRequest, response: OutgoingResponse) async throws { - try await response - .status(.ok) - .send(html: Index().html) - } -} diff --git a/Sources/EndPoint/Index.swift b/Sources/EndPoint/Index.swift deleted file mode 100644 index 65b9b7d..0000000 --- a/Sources/EndPoint/Index.swift +++ /dev/null @@ -1,427 +0,0 @@ -// -// Index.swift -// -// -// Created by 史 翔新 on 2022/12/16. -// - -import Foundation -//import Ink - -struct Index { - - let html: String - - init() { - html = content - } - -} - -// TODO: Find a new MarkDown parser -private let content = #""" - - - - - 株式会社ゆめみ iOS 未経験者エンジニア向けコードチェック課題 - - - -
-
-

株式会社ゆめみ iOS 未経験者エンジニア向けコードチェック課題

- -

概要

-

本エンドポイントは株式会社ゆめみ(以下弊社)が、弊社に iOS エンジニアを希望する未経験の方に出す課題のバックエンドです。未経験者の方はリファクタリングの課題もしくは本課題のいずれかを提出してください。本課題をお選びの方は、下記を詳しく読んだ上で課題に取り組んでください。 -

- -

アプリ仕様

-

「あなたと相性のいい都道府県を占ってあげる!」をテーマに iPhone アプリを作ってください。API 仕様下記の通りです。

- -

API 仕様

-

(詳細は OpenAPI のページもご参考にしてください)

-
    -
  • Base URL:
  • -

    "https://yumemi-ios-junior-engineer-codecheck.app.swift.cloud"

    - -
  • End Point:
  • -

    "/my_fortune"

    - -
  • HTTP Method:
  • -

    "POST"

    - -
  • HTTP Request Headers:
  • - - - - - - - - - -
    KeyValue
    "API-Version""v1"
    -

    ※"v1"を他の文字列で置き換えると EndPoint.APIVersion.InitializationError - エラーになります;またこのヘッダーを省略すること自体は可能ですが、将来API仕様が変わる可能性があるため、入れることをお勧めします。

    - -
  • HTTP Body:
  • - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    KeyTypeDescriptionSample Value
    "name"String占う人の名前"ゆめみん"
    "birthday"YearMonthDay(後述)占う人の生年月日-
    "blood_type"String占う人の血液型"ab"
    "today"YearMonthDay(後述)今日の日付-
    - -

    YearMonthDay 型の仕様:

    - - - - - - - - - - - - - - - - - - - - - - - - - -
    KeyTypeDescriptionSample Value
    "year"Int2000
    "month"Int1
    "day"Int27
    - -

    JSON サンプル

    -
    
    -            {
    -                "name": "ゆめみん",
    -                "birthday": {
    -                    "year": 2000,
    -                    "month": 1,
    -                    "day": 27
    -                },
    -                "blood_type": "ab",
    -                "today": {
    -                    "year": 2023,
    -                    "month": 5,
    -                    "day": 5
    -                }
    -            }
    - -
  • Response Body
  • - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    KeyTypeDescriptionSample Value
    "name"String都道府県の名前"富山県"
    "capital"String県庁所在地"富山市"
    "citizen_day"MonthDay?(後述)県民の日(もしあれば)-
    "has_coast_line"Bool海岸線があるかどうかtrue
    "logo_url"StringロゴのURL"https://japan-map.com/wp-content/uploads/toyama.png"
    "brief"String都道府県の概要"富山県(とやまけん)は、日本の中部地方に位置する県。県庁所在地は富山市。\n中部地方の日本海側、新潟県を含めた場合の北陸地方のほぼ中央にある。\n※出典: フリー百科事典『ウィキペディア(Wikipedia)』"
    - -

    MonthDay 型の仕様:

    - - - - - - - - - - - - - - - - - - - -
    KeyTypeDescriptionSample Value
    "month"Int5
    "day"Int9
    - -

    JSON サンプル

    -
    
    -            {
    -                "name": "富山県",
    -                "has_coast_line": true,
    -                "citizen_day": {
    -                    "month": 5,
    -                    "day": 9
    -                },
    -                "capital": "富山市",
    -                "logo_url": "https://japan-map.com/wp-content/uploads/toyama.png"
    -                "brief": "富山県(とやまけん)は、日本の中部地方に位置する県。県庁所在地は富山市。\n中部地方の日本海側、新潟県を含めた場合の北陸地方のほぼ中央にある。\n※出典: フリー百科事典『ウィキペディア(Wikipedia)』",
    -            }
    -
- -

環境

-
    -
  • IDE:基本最新の安定版(本概要最終更新時点では Xcode 14.3)
  • -
  • Swift:基本最新の安定版(本概要最終更新時点では Swift 5.8)
  • -
  • 開発ターゲット:基本最新の安定版(本概要最終更新時点では iOS 16.4)
  • -
- -

動作

-
    -
  1. ユーザから名前、生年月日及び血液型を入力してもらいます。
  2. -
  3. 上記のデータと併せて、送信時の日付も一緒に入れて、上記の API に問い合わせてください。
  4. -
  5. 結果をもらったらその結果を表示してください。
  6. -
      -
    1. そのうち logo_url の部分は、その URL をパースし、該当 URL から画像データを取得し、その画像を表示してください。
    2. -
    -
- -

注意点

-
    -
  1. 必ず git でプロジェクトを管理し、git ホスティングサービスで提出してください。
  2. -
      -
    1. ホスティングサービスは GitHub 推奨です。ただし git リポジトリーをそのまま zip 化して提出することは NG です。
    2. -
    3. git で提出してもらう理由は実装の途中経過を評価するためなので、いわゆるワンコミットの提出や、パッと全部やっつけた雑なコミットが含まれる提出物は評価できないため NG - です。必ずコミット粒度を意識して作ってください。もちろんブランチ運用や PR 運用も高評価ポイントになります。
    4. -
    5. GitHub で提出時に公開リポジトリーで提出したくない場合は、提出時にその旨をキャスターさんに伝えてください、コードチェック担当者の GitHub アカウントを返送しますので該当者に Read - 権限を付与してください。
    6. -
    -
  3. UI の指定はありませんが、UI/UX への考慮も評価ポイントになります。
  4. -
      -
    1. 特に Dark Mode や横画面など、iOS がデフォルトで利用可能な機能について気をつけてください。例えば Dark Mode で文字が読めないような実装は減点ポイントになります。
    2. -
    -
  5. 上記の動作さえ満たす限り、機能の追加等はウェルカムです。
  6. -
      -
    1. 例:占い結果をローカルに保存する、iPad/Mac 対応、などなど。
    2. -
    -
  7. サードパーティーライブラリーの利用については、オープンソースのものに限り制限しません(むしろ推奨です)。
  8. -
      -
    1. CocoaPods と Carthage についても利用 OK ですが、SwiftPM の方を推奨します。
    2. -
    -
  9. SDK は UIKit および SwiftUI どちらでも OK です。
  10. -
      -
    1. 両方の混在も OK です。
    2. -
    -
  11. ChatGPTなどAIサービスの利用は禁止しておりません。
  12. -
      -
    1. 利用にあたって工夫したプロンプトやソースコメント等をご提出頂くと加点評価する場合がございます。 (減点評価はありません)
    2. -
    -
- -

参考記事

-
    -
  • 本特別課題の評価方法等に関しては、こちらの記事に詳しく書かれてありますので、ぜひご覧ください。
  • -
  • CocoaPods の利用に関しては こちらの記事も参照ください。
  • -
- -

その他注意

-
    -
  • 本課題はあくまで未経験者向けの特別課題です。中途採用のご応募では本課題を選択できません。
  • -
      -
    • 第二新卒等の場合は、エンジニア職経験がない方に限定せていただきます。
    • -
    -
  • 本課題を提出する場合、通常課題をこなす必要がありません。
  • -
      -
    • ただし課題の性質上、レビュー期間は通常課題より長めに設けさせていただきます(目安としておおよそ 1 週間です)。
    • -
    • また通常課題と違い、明確なチケットに基づいた選考基準は現在ありません。あなたが思う「いいアプリ」を作ってください。
    • -
    -
  • 本課題は、iOS アプリ開発のスキルを選考するためのものです。そのため、本課題の内容をもとにしたアプリを App Store に公開することは禁止しております。
  • -
- -

謝辞

-

各都道府県のロゴ画像は、日本地図無料イラスト素材集様の都道府県シルエット画像を利用しています。

-

本ページは、ChatGPTの協力で作成しました。

-
-
- - - """# diff --git a/Sources/EndPoint/OpenAPI/Server.swift b/Sources/EndPoint/OpenAPI/Server.swift deleted file mode 100644 index d36792f..0000000 --- a/Sources/EndPoint/OpenAPI/Server.swift +++ /dev/null @@ -1,127 +0,0 @@ -// Generated by swift-openapi-generator, do not modify. -@_spi(Generated) import OpenAPIRuntime -#if os(Linux) -@preconcurrency import struct Foundation.URL -@preconcurrency import struct Foundation.Data -@preconcurrency import struct Foundation.Date -#else -import struct Foundation.URL -import struct Foundation.Data -import struct Foundation.Date -#endif -import HTTPTypes -extension APIProtocol { - /// Registers each operation handler with the provided transport. - /// - Parameters: - /// - transport: A transport to which to register the operation handlers. - /// - serverURL: A URL used to determine the path prefix for registered - /// request handlers. - /// - configuration: A set of configuration values for the server. - /// - middlewares: A list of middlewares to call before the handler. - internal func registerHandlers( - on transport: any ServerTransport, - serverURL: Foundation.URL = .defaultOpenAPIServerURL, - configuration: Configuration = .init(), - middlewares: [any ServerMiddleware] = [] - ) throws { - let server = UniversalServer( - serverURL: serverURL, - handler: self, - configuration: configuration, - middlewares: middlewares - ) - try transport.register( - { - try await server.post_hyphen_my_hyphen_fortune( - request: $0, - body: $1, - metadata: $2 - ) - }, - method: .post, - path: server.apiPathComponentsWithServerPrefix("/my_fortune") - ) - } -} - -fileprivate extension UniversalServer where APIHandler: APIProtocol { - /// Get fortune for given person - /// - /// This endpoint returns the fortune of a person based on their name, birthday, and blood type, as well as the current date. - /// - /// - Remark: HTTP `POST /my_fortune`. - /// - Remark: Generated from `#/paths//my_fortune/post(post-my-fortune)`. - func post_hyphen_my_hyphen_fortune( - request: HTTPTypes.HTTPRequest, - body: OpenAPIRuntime.HTTPBody?, - metadata: OpenAPIRuntime.ServerRequestMetadata - ) async throws -> (HTTPTypes.HTTPResponse, OpenAPIRuntime.HTTPBody?) { - try await handle( - request: request, - requestBody: body, - metadata: metadata, - forOperation: Operations.post_hyphen_my_hyphen_fortune.id, - using: { - APIHandler.post_hyphen_my_hyphen_fortune($0) - }, - deserializer: { request, requestBody, metadata in - let headers: Operations.post_hyphen_my_hyphen_fortune.Input.Headers = .init( - API_hyphen_Version: try converter.getOptionalHeaderFieldAsURI( - in: request.headerFields, - name: "API-Version", - as: Operations.post_hyphen_my_hyphen_fortune.Input.Headers.API_hyphen_VersionPayload.self - ), - accept: try converter.extractAcceptHeaderIfPresent(in: request.headerFields) - ) - let contentType = converter.extractContentTypeIfPresent(in: request.headerFields) - let body: Operations.post_hyphen_my_hyphen_fortune.Input.Body - let chosenContentType = try converter.bestContentType( - received: contentType, - options: [ - "application/json" - ] - ) - switch chosenContentType { - case "application/json": - body = try await converter.getRequiredRequestBodyAsJSON( - Components.Schemas.MyFortuneRequest.self, - from: requestBody, - transforming: { value in - .json(value) - } - ) - default: - preconditionFailure("bestContentType chose an invalid content type.") - } - return Operations.post_hyphen_my_hyphen_fortune.Input( - headers: headers, - body: body - ) - }, - serializer: { output, request in - switch output { - case let .ok(value): - suppressUnusedWarning(value) - var response = HTTPTypes.HTTPResponse(soar_statusCode: 200) - suppressMutabilityWarning(&response) - let body: OpenAPIRuntime.HTTPBody - switch value.body { - case let .json(value): - try converter.validateAcceptIfPresent( - "application/json", - in: request.headerFields - ) - body = try converter.setResponseBodyAsJSON( - value, - headerFields: &response.headerFields, - contentType: "application/json; charset=utf-8" - ) - } - return (response, body) - case let .undocumented(statusCode, _): - return (.init(soar_statusCode: statusCode), nil) - } - } - ) - } -} diff --git a/Sources/EndPoint/OpenAPI/Types.swift b/Sources/EndPoint/OpenAPI/Types.swift deleted file mode 100644 index 4535b28..0000000 --- a/Sources/EndPoint/OpenAPI/Types.swift +++ /dev/null @@ -1,459 +0,0 @@ -// Generated by swift-openapi-generator, do not modify. -@_spi(Generated) import OpenAPIRuntime -#if os(Linux) -@preconcurrency import struct Foundation.URL -@preconcurrency import struct Foundation.Data -@preconcurrency import struct Foundation.Date -#else -import struct Foundation.URL -import struct Foundation.Data -import struct Foundation.Date -#endif -/// A type that performs HTTP operations defined by the OpenAPI document. -internal protocol APIProtocol: Sendable { - /// Get fortune for given person - /// - /// This endpoint returns the fortune of a person based on their name, birthday, and blood type, as well as the current date. - /// - /// - Remark: HTTP `POST /my_fortune`. - /// - Remark: Generated from `#/paths//my_fortune/post(post-my-fortune)`. - func post_hyphen_my_hyphen_fortune(_ input: Operations.post_hyphen_my_hyphen_fortune.Input) async throws -> Operations.post_hyphen_my_hyphen_fortune.Output -} - -/// Convenience overloads for operation inputs. -extension APIProtocol { - /// Get fortune for given person - /// - /// This endpoint returns the fortune of a person based on their name, birthday, and blood type, as well as the current date. - /// - /// - Remark: HTTP `POST /my_fortune`. - /// - Remark: Generated from `#/paths//my_fortune/post(post-my-fortune)`. - internal func post_hyphen_my_hyphen_fortune( - headers: Operations.post_hyphen_my_hyphen_fortune.Input.Headers = .init(), - body: Operations.post_hyphen_my_hyphen_fortune.Input.Body - ) async throws -> Operations.post_hyphen_my_hyphen_fortune.Output { - try await post_hyphen_my_hyphen_fortune(Operations.post_hyphen_my_hyphen_fortune.Input( - headers: headers, - body: body - )) - } -} - -/// Server URLs defined in the OpenAPI document. -internal enum Servers { - internal static func server1() throws -> Foundation.URL { - try Foundation.URL( - validatingOpenAPIServerURL: "https://yumemi-ios-junior-engineer-codecheck.app.swift.cloud", - variables: [] - ) - } -} - -/// Types generated from the components section of the OpenAPI document. -internal enum Components { - /// Types generated from the `#/components/schemas` section of the OpenAPI document. - internal enum Schemas { - /// - Remark: Generated from `#/components/schemas/MyFortuneRequest`. - internal struct MyFortuneRequest: Codable, Hashable, Sendable { - /// Target's name. - /// - /// - Remark: Generated from `#/components/schemas/MyFortuneRequest/name`. - internal var name: Swift.String - /// - Remark: Generated from `#/components/schemas/MyFortuneRequest/birthday`. - internal struct birthdayPayload: Codable, Hashable, Sendable { - /// - Remark: Generated from `#/components/schemas/MyFortuneRequest/birthday/value1`. - internal var value1: Components.Schemas.YearMonthDay - /// Target's birthday. - /// - /// - Remark: Generated from `#/components/schemas/MyFortuneRequest/birthday/value2`. - internal var value2: OpenAPIRuntime.OpenAPIValueContainer - /// Creates a new `birthdayPayload`. - /// - /// - Parameters: - /// - value1: - /// - value2: Target's birthday. - internal init( - value1: Components.Schemas.YearMonthDay, - value2: OpenAPIRuntime.OpenAPIValueContainer - ) { - self.value1 = value1 - self.value2 = value2 - } - internal init(from decoder: any Decoder) throws { - value1 = try .init(from: decoder) - value2 = try .init(from: decoder) - } - internal func encode(to encoder: any Encoder) throws { - try value1.encode(to: encoder) - try value2.encode(to: encoder) - } - } - /// - Remark: Generated from `#/components/schemas/MyFortuneRequest/birthday`. - internal var birthday: Components.Schemas.MyFortuneRequest.birthdayPayload - /// Target's blood type. - /// - /// - Remark: Generated from `#/components/schemas/MyFortuneRequest/blood_type`. - @frozen internal enum blood_typePayload: String, Codable, Hashable, Sendable { - case a = "a" - case b = "b" - case ab = "ab" - case o = "o" - } - /// Target's blood type. - /// - /// - Remark: Generated from `#/components/schemas/MyFortuneRequest/blood_type`. - internal var blood_type: Components.Schemas.MyFortuneRequest.blood_typePayload - /// - Remark: Generated from `#/components/schemas/MyFortuneRequest/today`. - internal struct todayPayload: Codable, Hashable, Sendable { - /// - Remark: Generated from `#/components/schemas/MyFortuneRequest/today/value1`. - internal var value1: Components.Schemas.YearMonthDay - /// Today's date. - /// - /// - Remark: Generated from `#/components/schemas/MyFortuneRequest/today/value2`. - internal var value2: OpenAPIRuntime.OpenAPIValueContainer - /// Creates a new `todayPayload`. - /// - /// - Parameters: - /// - value1: - /// - value2: Today's date. - internal init( - value1: Components.Schemas.YearMonthDay, - value2: OpenAPIRuntime.OpenAPIValueContainer - ) { - self.value1 = value1 - self.value2 = value2 - } - internal init(from decoder: any Decoder) throws { - value1 = try .init(from: decoder) - value2 = try .init(from: decoder) - } - internal func encode(to encoder: any Encoder) throws { - try value1.encode(to: encoder) - try value2.encode(to: encoder) - } - } - /// - Remark: Generated from `#/components/schemas/MyFortuneRequest/today`. - internal var today: Components.Schemas.MyFortuneRequest.todayPayload - /// Creates a new `MyFortuneRequest`. - /// - /// - Parameters: - /// - name: Target's name. - /// - birthday: - /// - blood_type: Target's blood type. - /// - today: - internal init( - name: Swift.String, - birthday: Components.Schemas.MyFortuneRequest.birthdayPayload, - blood_type: Components.Schemas.MyFortuneRequest.blood_typePayload, - today: Components.Schemas.MyFortuneRequest.todayPayload - ) { - self.name = name - self.birthday = birthday - self.blood_type = blood_type - self.today = today - } - internal enum CodingKeys: String, CodingKey { - case name - case birthday - case blood_type - case today - } - } - /// - Remark: Generated from `#/components/schemas/MyFortuneResponse`. - internal struct MyFortuneResponse: Codable, Hashable, Sendable { - /// Result prefecture's name. - /// - /// - Remark: Generated from `#/components/schemas/MyFortuneResponse/name`. - internal var name: Swift.String - /// Result prefecture's brief instruction. - /// - /// - Remark: Generated from `#/components/schemas/MyFortuneResponse/brief`. - internal var brief: Swift.String - /// Result prefecture's capital city. - /// - /// - Remark: Generated from `#/components/schemas/MyFortuneResponse/capital`. - internal var capital: Swift.String - /// - Remark: Generated from `#/components/schemas/MyFortuneResponse/citizen_day`. - internal struct citizen_dayPayload: Codable, Hashable, Sendable { - /// - Remark: Generated from `#/components/schemas/MyFortuneResponse/citizen_day/value1`. - internal var value1: Components.Schemas.MonthDay - /// Result prefecture's citizen date (if it has one). - /// - /// - Remark: Generated from `#/components/schemas/MyFortuneResponse/citizen_day/value2`. - internal var value2: OpenAPIRuntime.OpenAPIValueContainer - /// Creates a new `citizen_dayPayload`. - /// - /// - Parameters: - /// - value1: - /// - value2: Result prefecture's citizen date (if it has one). - internal init( - value1: Components.Schemas.MonthDay, - value2: OpenAPIRuntime.OpenAPIValueContainer - ) { - self.value1 = value1 - self.value2 = value2 - } - internal init(from decoder: any Decoder) throws { - value1 = try .init(from: decoder) - value2 = try .init(from: decoder) - } - internal func encode(to encoder: any Encoder) throws { - try value1.encode(to: encoder) - try value2.encode(to: encoder) - } - } - /// - Remark: Generated from `#/components/schemas/MyFortuneResponse/citizen_day`. - internal var citizen_day: Components.Schemas.MyFortuneResponse.citizen_dayPayload? - /// Whether result prefecture has a coast line or not. - /// - /// - Remark: Generated from `#/components/schemas/MyFortuneResponse/has_coast_line`. - internal var has_coast_line: Swift.Bool - /// Result prefecture's logo's URL. - /// - /// - Remark: Generated from `#/components/schemas/MyFortuneResponse/logo_url`. - internal var logo_url: Swift.String - /// Creates a new `MyFortuneResponse`. - /// - /// - Parameters: - /// - name: Result prefecture's name. - /// - brief: Result prefecture's brief instruction. - /// - capital: Result prefecture's capital city. - /// - citizen_day: - /// - has_coast_line: Whether result prefecture has a coast line or not. - /// - logo_url: Result prefecture's logo's URL. - internal init( - name: Swift.String, - brief: Swift.String, - capital: Swift.String, - citizen_day: Components.Schemas.MyFortuneResponse.citizen_dayPayload? = nil, - has_coast_line: Swift.Bool, - logo_url: Swift.String - ) { - self.name = name - self.brief = brief - self.capital = capital - self.citizen_day = citizen_day - self.has_coast_line = has_coast_line - self.logo_url = logo_url - } - internal enum CodingKeys: String, CodingKey { - case name - case brief - case capital - case citizen_day - case has_coast_line - case logo_url - } - } - /// - Remark: Generated from `#/components/schemas/YearMonthDay`. - internal struct YearMonthDay: Codable, Hashable, Sendable { - /// Year number - /// - /// - Remark: Generated from `#/components/schemas/YearMonthDay/year`. - internal var year: Swift.Int - /// Month number - /// - /// - Remark: Generated from `#/components/schemas/YearMonthDay/month`. - internal var month: Swift.Int - /// Day number - /// - /// - Remark: Generated from `#/components/schemas/YearMonthDay/day`. - internal var day: Swift.Int - /// Creates a new `YearMonthDay`. - /// - /// - Parameters: - /// - year: Year number - /// - month: Month number - /// - day: Day number - internal init( - year: Swift.Int, - month: Swift.Int, - day: Swift.Int - ) { - self.year = year - self.month = month - self.day = day - } - internal enum CodingKeys: String, CodingKey { - case year - case month - case day - } - } - /// - Remark: Generated from `#/components/schemas/MonthDay`. - internal struct MonthDay: Codable, Hashable, Sendable { - /// Month number - /// - /// - Remark: Generated from `#/components/schemas/MonthDay/month`. - internal var month: Swift.Int - /// Day number - /// - /// - Remark: Generated from `#/components/schemas/MonthDay/day`. - internal var day: Swift.Int - /// Creates a new `MonthDay`. - /// - /// - Parameters: - /// - month: Month number - /// - day: Day number - internal init( - month: Swift.Int, - day: Swift.Int - ) { - self.month = month - self.day = day - } - internal enum CodingKeys: String, CodingKey { - case month - case day - } - } - } - /// Types generated from the `#/components/parameters` section of the OpenAPI document. - internal enum Parameters {} - /// Types generated from the `#/components/requestBodies` section of the OpenAPI document. - internal enum RequestBodies {} - /// Types generated from the `#/components/responses` section of the OpenAPI document. - internal enum Responses {} - /// Types generated from the `#/components/headers` section of the OpenAPI document. - internal enum Headers {} -} - -/// API operations, with input and output types, generated from `#/paths` in the OpenAPI document. -internal enum Operations { - /// Get fortune for given person - /// - /// This endpoint returns the fortune of a person based on their name, birthday, and blood type, as well as the current date. - /// - /// - Remark: HTTP `POST /my_fortune`. - /// - Remark: Generated from `#/paths//my_fortune/post(post-my-fortune)`. - internal enum post_hyphen_my_hyphen_fortune { - internal static let id: Swift.String = "post-my-fortune" - internal struct Input: Sendable, Hashable { - /// - Remark: Generated from `#/paths/my_fortune/POST/header`. - internal struct Headers: Sendable, Hashable { - /// - Remark: Generated from `#/paths/my_fortune/POST/header/API-Version`. - @frozen internal enum API_hyphen_VersionPayload: String, Codable, Hashable, Sendable { - case v1 = "v1" - } - /// API version you'd like to use in your sending request. Currently only `v1` available. If you don't attach this parameter in your header, system will use the latest API version. - /// - /// - Remark: Generated from `#/paths/my_fortune/POST/header/API-Version`. - internal var API_hyphen_Version: Operations.post_hyphen_my_hyphen_fortune.Input.Headers.API_hyphen_VersionPayload? - internal var accept: [OpenAPIRuntime.AcceptHeaderContentType] - /// Creates a new `Headers`. - /// - /// - Parameters: - /// - API_hyphen_Version: API version you'd like to use in your sending request. Currently only `v1` available. If you don't attach this parameter in your header, system will use the latest API version. - /// - accept: - internal init( - API_hyphen_Version: Operations.post_hyphen_my_hyphen_fortune.Input.Headers.API_hyphen_VersionPayload? = nil, - accept: [OpenAPIRuntime.AcceptHeaderContentType] = .defaultValues() - ) { - self.API_hyphen_Version = API_hyphen_Version - self.accept = accept - } - } - internal var headers: Operations.post_hyphen_my_hyphen_fortune.Input.Headers - /// - Remark: Generated from `#/paths/my_fortune/POST/requestBody`. - @frozen internal enum Body: Sendable, Hashable { - /// - Remark: Generated from `#/paths/my_fortune/POST/requestBody/content/application\/json`. - case json(Components.Schemas.MyFortuneRequest) - } - internal var body: Operations.post_hyphen_my_hyphen_fortune.Input.Body - /// Creates a new `Input`. - /// - /// - Parameters: - /// - headers: - /// - body: - internal init( - headers: Operations.post_hyphen_my_hyphen_fortune.Input.Headers = .init(), - body: Operations.post_hyphen_my_hyphen_fortune.Input.Body - ) { - self.headers = headers - self.body = body - } - } - @frozen internal enum Output: Sendable, Hashable { - internal struct Ok: Sendable, Hashable { - /// - Remark: Generated from `#/paths/my_fortune/POST/responses/200/content`. - @frozen internal enum Body: Sendable, Hashable { - /// - Remark: Generated from `#/paths/my_fortune/POST/responses/200/content/application\/json`. - case json(Components.Schemas.MyFortuneResponse) - /// The associated value of the enum case if `self` is `.json`. - /// - /// - Throws: An error if `self` is not `.json`. - /// - SeeAlso: `.json`. - internal var json: Components.Schemas.MyFortuneResponse { - get throws { - switch self { - case let .json(body): - return body - } - } - } - } - /// Received HTTP response body - internal var body: Operations.post_hyphen_my_hyphen_fortune.Output.Ok.Body - /// Creates a new `Ok`. - /// - /// - Parameters: - /// - body: Received HTTP response body - internal init(body: Operations.post_hyphen_my_hyphen_fortune.Output.Ok.Body) { - self.body = body - } - } - /// Success - /// - /// - Remark: Generated from `#/paths//my_fortune/post(post-my-fortune)/responses/200`. - /// - /// HTTP response code: `200 ok`. - case ok(Operations.post_hyphen_my_hyphen_fortune.Output.Ok) - /// The associated value of the enum case if `self` is `.ok`. - /// - /// - Throws: An error if `self` is not `.ok`. - /// - SeeAlso: `.ok`. - internal var ok: Operations.post_hyphen_my_hyphen_fortune.Output.Ok { - get throws { - switch self { - case let .ok(response): - return response - default: - try throwUnexpectedResponseStatus( - expectedStatus: "ok", - response: self - ) - } - } - } - /// Undocumented response. - /// - /// A response with a code that is not documented in the OpenAPI document. - case undocumented(statusCode: Swift.Int, OpenAPIRuntime.UndocumentedPayload) - } - @frozen internal enum AcceptableContentType: AcceptableProtocol { - case json - case other(Swift.String) - internal init?(rawValue: Swift.String) { - switch rawValue.lowercased() { - case "application/json": - self = .json - default: - self = .other(rawValue) - } - } - internal var rawValue: Swift.String { - switch self { - case let .other(string): - return string - case .json: - return "application/json" - } - } - internal static var allCases: [Self] { - [ - .json - ] - } - } - } -} diff --git a/Sources/EndPoint/Prefecture+LogoURL.swift b/Sources/EndPoint/Prefecture+LogoURL.swift deleted file mode 100644 index c1dd78d..0000000 --- a/Sources/EndPoint/Prefecture+LogoURL.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// Prefecture+LogoURL.swift -// -// -// Created by 史 翔新 on 2023/03/28. -// - -import Foundation -import FakeFortuneTelling - -extension Prefecture { - - private var spell: String { - switch self { - case .gunma: - return "gumma" - - default: - return "\(self)" - } - } - - var logoURL: URL { - - return .init(string: "https://japan-map.com/wp-content/uploads/\(spell).png") ?? { - assertionFailure("Invalid prefecture spell: \(spell)") - return NSURL(fileURLWithPath: "") as URL - }() - - } - -} diff --git a/Tests/AppTests/AppTests.swift b/Tests/AppTests/AppTests.swift new file mode 100644 index 0000000..c0e5e2c --- /dev/null +++ b/Tests/AppTests/AppTests.swift @@ -0,0 +1,23 @@ +@testable import App +import XCTVapor + +final class AppTests: XCTestCase { + var app: Application! + + override func setUp() async throws { + self.app = try await Application.make(.testing) + try await configure(app) + } + + override func tearDown() async throws { + try await self.app.asyncShutdown() + self.app = nil + } + + func testHelloWorld() async throws { + try await self.app.test(.GET, "hello", afterResponse: { res async in + XCTAssertEqual(res.status, .ok) + XCTAssertEqual(res.body.string, "Hello, world!") + }) + } +} diff --git a/Tests/EndPointTests/EndPointTests.swift b/Tests/EndPointTests/EndPointTests.swift deleted file mode 100644 index bf48a40..0000000 --- a/Tests/EndPointTests/EndPointTests.swift +++ /dev/null @@ -1,47 +0,0 @@ -import XCTest -import class Foundation.Bundle - -final class EndPointTests: XCTestCase { - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. - - // Some of the APIs that we use below are available in macOS 10.13 and above. - guard #available(macOS 10.13, *) else { - return - } - - // Mac Catalyst won't have `Process`, but it is supported for executables. - #if !targetEnvironment(macCatalyst) - - let fooBinary = productsDirectory.appendingPathComponent("ios-junior-engineer-codecheck-backend") - - let process = Process() - process.executableURL = fooBinary - - let pipe = Pipe() - process.standardOutput = pipe - - try process.run() - process.waitUntilExit() - - let data = pipe.fileHandleForReading.readDataToEndOfFile() - let output = String(data: data, encoding: .utf8) - - XCTAssertEqual(output, "Hello, world!\n") - #endif - } - - /// Returns path to the built products directory. - var productsDirectory: URL { - #if os(macOS) - for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") { - return bundle.bundleURL.deletingLastPathComponent() - } - fatalError("couldn't find the products directory") - #else - return Bundle.main.bundleURL - #endif - } -} From b89366ee9dcca438a92840394bdb5b89cdc1a034 Mon Sep 17 00:00:00 2001 From: "novr (Nobuhisa Komiya)" Date: Fri, 17 May 2024 13:13:09 +0900 Subject: [PATCH 02/13] add: docker files --- .dockerignore | 2 ++ Dockerfile | 88 ++++++++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 30 ++++++++++++++++ 3 files changed, 120 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2d9f16e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +.build/ +.swiftpm/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2f3a343 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,88 @@ +# ================================ +# Build image +# ================================ +FROM swift:5.10-jammy as build + +# Install OS updates +RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ + && apt-get -q update \ + && apt-get -q dist-upgrade -y \ + && apt-get install -y libjemalloc-dev + +# Set up a build area +WORKDIR /build + +# First just resolve dependencies. +# This creates a cached layer that can be reused +# as long as your Package.swift/Package.resolved +# files do not change. +COPY ./Package.* ./ +RUN swift package resolve --skip-update \ + $([ -f ./Package.resolved ] && echo "--force-resolved-versions" || true) + +# Copy entire repo into container +COPY . . + +# Build everything, with optimizations, with static linking, and using jemalloc +# N.B.: The static version of jemalloc is incompatible with the static Swift runtime. +RUN swift build -c release \ + --static-swift-stdlib \ + -Xlinker -ljemalloc + +# Switch to the staging area +WORKDIR /staging + +# Copy main executable to staging area +RUN cp "$(swift build --package-path /build -c release --show-bin-path)/App" ./ + +# Copy static swift backtracer binary to staging area +RUN cp "/usr/libexec/swift/linux/swift-backtrace-static" ./ + +# Copy resources bundled by SPM to staging area +RUN find -L "$(swift build --package-path /build -c release --show-bin-path)/" -regex '.*\.resources$' -exec cp -Ra {} ./ \; + +# Copy any resources from the public directory and views directory if the directories exist +# Ensure that by default, neither the directory nor any of its contents are writable. +RUN [ -d /build/Public ] && { mv /build/Public ./Public && chmod -R a-w ./Public; } || true +RUN [ -d /build/Resources ] && { mv /build/Resources ./Resources && chmod -R a-w ./Resources; } || true + +# ================================ +# Run image +# ================================ +FROM ubuntu:jammy + +# Make sure all system packages are up to date, and install only essential packages. +RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ + && apt-get -q update \ + && apt-get -q dist-upgrade -y \ + && apt-get -q install -y \ + libjemalloc2 \ + ca-certificates \ + tzdata \ +# If your app or its dependencies import FoundationNetworking, also install `libcurl4`. + # libcurl4 \ +# If your app or its dependencies import FoundationXML, also install `libxml2`. + # libxml2 \ + && rm -r /var/lib/apt/lists/* + +# Create a vapor user and group with /app as its home directory +RUN useradd --user-group --create-home --system --skel /dev/null --home-dir /app vapor + +# Switch to the new home directory +WORKDIR /app + +# Copy built executable and any staged resources from builder +COPY --from=build --chown=vapor:vapor /staging /app + +# Provide configuration needed by the built-in crash reporter and some sensible default behaviors. +ENV SWIFT_BACKTRACE=enable=yes,sanitize=yes,threads=all,images=all,interactive=no,swift-backtrace=./swift-backtrace-static + +# Ensure all further commands run as the vapor user +USER vapor:vapor + +# Let Docker bind to port 8080 +EXPOSE 8080 + +# Start the Vapor service when the image is run, default to listening on 8080 in production environment +ENTRYPOINT ["./App"] +CMD ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..fd325f1 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,30 @@ +# Docker Compose file for Vapor +# +# Install Docker on your system to run and test +# your Vapor app in a production-like environment. +# +# Note: This file is intended for testing and does not +# implement best practices for a production deployment. +# +# Learn more: https://docs.docker.com/compose/reference/ +# +# Build images: docker-compose build +# Start app: docker-compose up app +# Stop all: docker-compose down +# +version: '3.7' + +x-shared_environment: &shared_environment + LOG_LEVEL: ${LOG_LEVEL:-debug} + +services: + app: + image: ios-junior-engineer-codecheck-backend:latest + build: + context: . + environment: + <<: *shared_environment + ports: + - '8080:8080' + # user: '0' # uncomment to run as root for testing purposes even though Dockerfile defines 'vapor' user. + command: ["serve", "--env", "production", "--hostname", "0.0.0.0", "--port", "8080"] From 71712750b7a9a5579e48d447cdc35f1d3f54d03c Mon Sep 17 00:00:00 2001 From: "novr (Nobuhisa Komiya)" Date: Fri, 17 May 2024 17:32:01 +0900 Subject: [PATCH 03/13] add: cloudbuild.yaml --- .../branch-cloudbuild.yaml | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 labs/cloudrun-progression/branch-cloudbuild.yaml diff --git a/labs/cloudrun-progression/branch-cloudbuild.yaml b/labs/cloudrun-progression/branch-cloudbuild.yaml new file mode 100644 index 0000000..9d39a50 --- /dev/null +++ b/labs/cloudrun-progression/branch-cloudbuild.yaml @@ -0,0 +1,44 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Default Values +substitutions: + _SERVICE_NAME: app + _REGION: us-central1 + +steps: + +### Build + - id: "build image" + name: "gcr.io/cloud-builders/docker" + args: ["build", "-t", "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}", "."] + +### Push + - id: "push image" + name: "gcr.io/cloud-builders/docker" + args: ["push", "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}"] + +### Deploy + - id: "deploy prod service" + name: "gcr.io/google.com/cloudsdktool/cloud-sdk" + entrypoint: "bash" + args: + - '-c' + - | + gcloud run deploy ${_SERVICE_NAME} \ + --platform managed \ + --region ${_REGION} \ + --image gcr.io/${PROJECT_ID}/${_SERVICE_NAME} \ + --tag=${BRANCH_NAME} \ + --no-traffic From 3c801746704a8f6ac0fd2c3d7a4021105bf011fc Mon Sep 17 00:00:00 2001 From: "novr (Nobuhisa Komiya)" Date: Fri, 17 May 2024 18:02:27 +0900 Subject: [PATCH 04/13] fix: use SHORT_SHA --- labs/cloudrun-progression/branch-cloudbuild.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/labs/cloudrun-progression/branch-cloudbuild.yaml b/labs/cloudrun-progression/branch-cloudbuild.yaml index 9d39a50..9de947c 100644 --- a/labs/cloudrun-progression/branch-cloudbuild.yaml +++ b/labs/cloudrun-progression/branch-cloudbuild.yaml @@ -40,5 +40,5 @@ steps: --platform managed \ --region ${_REGION} \ --image gcr.io/${PROJECT_ID}/${_SERVICE_NAME} \ - --tag=${BRANCH_NAME} \ + --tag=${SHORT_SHA} \ --no-traffic From 97383e1be5084a5f9034775da6d003d5b5bb4fc9 Mon Sep 17 00:00:00 2001 From: "novr (Nobuhisa Komiya)" Date: Mon, 20 May 2024 10:59:14 +0900 Subject: [PATCH 05/13] fix: issue 49 --- Sources/App/Controllers/APIController.swift | 39 +++++++-------------- Sources/App/openapi.yaml | 6 ++-- 2 files changed, 15 insertions(+), 30 deletions(-) diff --git a/Sources/App/Controllers/APIController.swift b/Sources/App/Controllers/APIController.swift index c7e8108..c417e00 100644 --- a/Sources/App/Controllers/APIController.swift +++ b/Sources/App/Controllers/APIController.swift @@ -25,28 +25,25 @@ struct APIController: APIProtocol { guard case let .json(body) = input.body else { throw APIServiceError.InvalidateInput } + let birthday = body.birthday.value1 + let today = body.today.value1 let result = try FortuneTeller.prefectureForYou( - name: .init(body.name), - birthday: .init(body.birthday.value1), + name: .init(text: body.name), + birthday: .init(year: birthday.year, month: birthday.month, day: birthday.day), bloodType: .init(body.blood_type), - today: .init(body.today.value1)) + today: .init(year: today.year, month: today.month, day: today.day)) return .ok(.init(body: .json(.init( name: result.name, brief: result.brief, capital: result.capital, + citizen_day: result.citizenDay.map { .init(value1: .init(month: $0.month, day: $0.day)) }, has_coast_line: result.hasCoastLine, logo_url: result.logoURL?.absoluteString ?? "")))) } } -extension Name { - init(_ input: String) throws { - try self.init(text: input) - } -} - extension BloodType { - init(_ input: Components.Schemas.MyFortuneRequest.blood_typePayload) throws { + init(_ input: Components.Schemas.MyFortuneRequest.blood_typePayload) { switch input { case .a: self = .a case .b: self = .b @@ -56,24 +53,12 @@ extension BloodType { } } -extension YearMonthDay { - init(_ input: Components.Schemas.YearMonthDay) throws { - try self.init(year: input.year, month: input.month, day: input.day) - } -} - extension Prefecture { - private var spell: String { - switch self { - case .gunma: - return "gumma" - - default: - return "\(self)" - } - } - var logoURL: URL? { - .init(string: "https://japan-map.com/wp-content/uploads/\(spell).png") + let name = switch self { + case .gunma: "gumma" + default: "\(self)" + } + return .init(string: "https://japan-map.com/wp-content/uploads/\(name).png") } } diff --git a/Sources/App/openapi.yaml b/Sources/App/openapi.yaml index 0f2613a..29e93cd 100644 --- a/Sources/App/openapi.yaml +++ b/Sources/App/openapi.yaml @@ -53,18 +53,18 @@ components: description: "Target's name." example: "ゆめみん" birthday: + description: "Target's birthday." allOf: - $ref: "#/components/schemas/YearMonthDay" - - description: "Target's birthday." blood_type: type: string enum: ["a", "b", "ab", "o"] description: "Target's blood type." example: "ab" today: + description: "Today's date." allOf: - $ref: "#/components/schemas/YearMonthDay" - - description: "Today's date." required: - name - birthday @@ -86,9 +86,9 @@ components: description: "Result prefecture's capital city." example: "富山市" citizen_day: + description: "Result prefecture's citizen date (if it has one)." allOf: - $ref: "#/components/schemas/MonthDay" - - description: "Result prefecture's citizen date (if it has one)." has_coast_line: type: boolean description: "Whether result prefecture has a coast line or not." From d4c53caf9f7e405317b1effda15182ffa2f9fa64 Mon Sep 17 00:00:00 2001 From: "novr (Nobuhisa Komiya)" Date: Mon, 20 May 2024 13:11:09 +0900 Subject: [PATCH 06/13] fix: replace / to - --- labs/cloudrun-progression/branch-cloudbuild.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/labs/cloudrun-progression/branch-cloudbuild.yaml b/labs/cloudrun-progression/branch-cloudbuild.yaml index 9de947c..adda40e 100644 --- a/labs/cloudrun-progression/branch-cloudbuild.yaml +++ b/labs/cloudrun-progression/branch-cloudbuild.yaml @@ -40,5 +40,5 @@ steps: --platform managed \ --region ${_REGION} \ --image gcr.io/${PROJECT_ID}/${_SERVICE_NAME} \ - --tag=${SHORT_SHA} \ + --tag=${BRANCH_NAME//\//-} \ --no-traffic From 04001faa792a08cf9f7789e86b7758bd99a35090 Mon Sep 17 00:00:00 2001 From: "novr (Nobuhisa Komiya)" Date: Mon, 20 May 2024 14:13:41 +0900 Subject: [PATCH 07/13] feat: cache --- labs/cloudrun-progression/branch-cloudbuild.yaml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/labs/cloudrun-progression/branch-cloudbuild.yaml b/labs/cloudrun-progression/branch-cloudbuild.yaml index adda40e..e003b55 100644 --- a/labs/cloudrun-progression/branch-cloudbuild.yaml +++ b/labs/cloudrun-progression/branch-cloudbuild.yaml @@ -22,12 +22,20 @@ steps: ### Build - id: "build image" name: "gcr.io/cloud-builders/docker" - args: ["build", "-t", "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}", "."] + args: + - "build" + - "-t" + - "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}:${_BRANCH_TARGET}" + - "." + - "--cache-from=gcr.io/${PROJECT_ID}/${_SERVICE_NAME}:${_BRANCH_TARGET}" + - "--cache-from=gcr.io/${PROJECT_ID}/${_SERVICE_NAME}" ### Push - id: "push image" name: "gcr.io/cloud-builders/docker" - args: ["push", "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}"] + args: + - "push" + - "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}:${_BRANCH_TARGET}" ### Deploy - id: "deploy prod service" @@ -39,6 +47,5 @@ steps: gcloud run deploy ${_SERVICE_NAME} \ --platform managed \ --region ${_REGION} \ - --image gcr.io/${PROJECT_ID}/${_SERVICE_NAME} \ - --tag=${BRANCH_NAME//\//-} \ + --image gcr.io/${PROJECT_ID}/${_SERVICE_NAME}:${_BRANCH_TARGET} \ --no-traffic From 0afc7c4d283e9da3857fbd19c1c16e91a41d38cd Mon Sep 17 00:00:00 2001 From: "novr (Nobuhisa Komiya)" Date: Mon, 20 May 2024 17:47:21 +0900 Subject: [PATCH 08/13] update: service name --- README.md | 6 ++- .../branch-cloudbuild.yaml | 16 +++--- .../cloudrun-progression/main-cloudbuild.yaml | 53 +++++++++++++++++++ 3 files changed, 67 insertions(+), 8 deletions(-) create mode 100644 labs/cloudrun-progression/main-cloudbuild.yaml diff --git a/README.md b/README.md index 386a067..d42b296 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # ios-junior-engineer-codecheck-backend -iOS未経験者エンジニア向けの採用コードチェックに使うバックエンドです。詳細は[デプロイ先](https://yumemi-ios-junior-engineer-codecheck.app.swift.cloud)をご覧ください。 +iOS 未経験者エンジニア向けの採用コードチェックに使うバックエンドです。詳細は[デプロイ先][service]をご覧ください。 ## warning -PRのbranch名は6文字以内にしてください \ No newline at end of file +PR の branch 名は 6 文字以内にしてください + +[service]: https://ios-junior-engineer-codecheck-snefnyqv2q-an.a.run.app diff --git a/labs/cloudrun-progression/branch-cloudbuild.yaml b/labs/cloudrun-progression/branch-cloudbuild.yaml index e003b55..e614585 100644 --- a/labs/cloudrun-progression/branch-cloudbuild.yaml +++ b/labs/cloudrun-progression/branch-cloudbuild.yaml @@ -14,8 +14,8 @@ # Default Values substitutions: - _SERVICE_NAME: app - _REGION: us-central1 + _SERVICE_NAME: ios-junior-engineer-codecheck + _REGION: asia-northeast1 steps: @@ -25,6 +25,8 @@ steps: args: - "build" - "-t" + - "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}:$SHORT_SHA" + - "-t" - "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}:${_BRANCH_TARGET}" - "." - "--cache-from=gcr.io/${PROJECT_ID}/${_SERVICE_NAME}:${_BRANCH_TARGET}" @@ -35,10 +37,11 @@ steps: name: "gcr.io/cloud-builders/docker" args: - "push" - - "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}:${_BRANCH_TARGET}" + - "-a" + - "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}" ### Deploy - - id: "deploy prod service" + - id: "deploy feature service" name: "gcr.io/google.com/cloudsdktool/cloud-sdk" entrypoint: "bash" args: @@ -47,5 +50,6 @@ steps: gcloud run deploy ${_SERVICE_NAME} \ --platform managed \ --region ${_REGION} \ - --image gcr.io/${PROJECT_ID}/${_SERVICE_NAME}:${_BRANCH_TARGET} \ - --no-traffic + --image gcr.io/${PROJECT_ID}/${_SERVICE_NAME}:$SHORT_SHA \ + --no-traffic \ + --allow-unauthenticated diff --git a/labs/cloudrun-progression/main-cloudbuild.yaml b/labs/cloudrun-progression/main-cloudbuild.yaml new file mode 100644 index 0000000..0072c75 --- /dev/null +++ b/labs/cloudrun-progression/main-cloudbuild.yaml @@ -0,0 +1,53 @@ +# Copyright 2021 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Default Values +substitutions: + _SERVICE_NAME: ios-junior-engineer-codecheck + _REGION: asia-northeast1 + +steps: + +### Build + - id: "build image" + name: "gcr.io/cloud-builders/docker" + args: + - "build" + - "-t" + - "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}:$SHORT_SHA" + - "-t" + - "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}:latest" + - "." + - "--cache-from=gcr.io/${PROJECT_ID}/${_SERVICE_NAME}:latest" + +### Push + - id: "push image" + name: "gcr.io/cloud-builders/docker" + args: + - "push" + - "-a" + - "gcr.io/${PROJECT_ID}/${_SERVICE_NAME}" + +### Deploy + - id: "deploy prod service" + name: "gcr.io/google.com/cloudsdktool/cloud-sdk" + entrypoint: "bash" + args: + - '-c' + - | + gcloud run deploy ${_SERVICE_NAME} \ + --platform managed \ + --region ${_REGION} \ + --image gcr.io/${PROJECT_ID}/${_SERVICE_NAME}:$SHORT_SHA \ + --allow-unauthenticated From aeb41211424d7ebbe89c10ed5dc297bfcc865374 Mon Sep 17 00:00:00 2001 From: "novr (Nobuhisa Komiya)" Date: Tue, 21 May 2024 10:26:46 +0900 Subject: [PATCH 09/13] update: branch tag --- labs/cloudrun-progression/branch-cloudbuild.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/labs/cloudrun-progression/branch-cloudbuild.yaml b/labs/cloudrun-progression/branch-cloudbuild.yaml index e614585..ffddf3b 100644 --- a/labs/cloudrun-progression/branch-cloudbuild.yaml +++ b/labs/cloudrun-progression/branch-cloudbuild.yaml @@ -52,4 +52,5 @@ steps: --region ${_REGION} \ --image gcr.io/${PROJECT_ID}/${_SERVICE_NAME}:$SHORT_SHA \ --no-traffic \ - --allow-unauthenticated + --allow-unauthenticated \ + --tag ${_BRANCH_TARGET} From 12552be48f5c5062f5bc56be4efb907b50818512 Mon Sep 17 00:00:00 2001 From: "novr (Nobuhisa Komiya)" Date: Tue, 21 May 2024 13:10:16 +0900 Subject: [PATCH 10/13] feat: use build cache --- Dockerfile | 44 +++++++++---------- .../branch-cloudbuild.yaml | 2 + .../cloudrun-progression/main-cloudbuild.yaml | 2 + 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2f3a343..ad3b564 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,9 +5,9 @@ FROM swift:5.10-jammy as build # Install OS updates RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ - && apt-get -q update \ - && apt-get -q dist-upgrade -y \ - && apt-get install -y libjemalloc-dev + && apt-get -q update \ + && apt-get -q dist-upgrade -y \ + && apt-get install -y libjemalloc-dev # Set up a build area WORKDIR /build @@ -17,29 +17,29 @@ WORKDIR /build # as long as your Package.swift/Package.resolved # files do not change. COPY ./Package.* ./ -RUN swift package resolve --skip-update \ - $([ -f ./Package.resolved ] && echo "--force-resolved-versions" || true) +RUN --mount=type=cache,target=/build/.build swift package resolve --skip-update \ + $([ -f ./Package.resolved ] && echo "--force-resolved-versions" || true) # Copy entire repo into container COPY . . # Build everything, with optimizations, with static linking, and using jemalloc # N.B.: The static version of jemalloc is incompatible with the static Swift runtime. -RUN swift build -c release \ - --static-swift-stdlib \ - -Xlinker -ljemalloc +RUN --mount=type=cache,target=/build/.build swift build -c release \ + --static-swift-stdlib \ + -Xlinker -ljemalloc # Switch to the staging area WORKDIR /staging # Copy main executable to staging area -RUN cp "$(swift build --package-path /build -c release --show-bin-path)/App" ./ +RUN --mount=type=cache,target=/build/.build cp "$(swift build --package-path /build -c release --show-bin-path)/App" ./ # Copy static swift backtracer binary to staging area -RUN cp "/usr/libexec/swift/linux/swift-backtrace-static" ./ +RUN --mount=type=cache,target=/build/.build cp "/usr/libexec/swift/linux/swift-backtrace-static" ./ # Copy resources bundled by SPM to staging area -RUN find -L "$(swift build --package-path /build -c release --show-bin-path)/" -regex '.*\.resources$' -exec cp -Ra {} ./ \; +RUN --mount=type=cache,target=/build/.build find -L "$(swift build --package-path /build -c release --show-bin-path)/" -regex '.*\.resources$' -exec cp -Ra {} ./ \; # Copy any resources from the public directory and views directory if the directories exist # Ensure that by default, neither the directory nor any of its contents are writable. @@ -53,17 +53,17 @@ FROM ubuntu:jammy # Make sure all system packages are up to date, and install only essential packages. RUN export DEBIAN_FRONTEND=noninteractive DEBCONF_NONINTERACTIVE_SEEN=true \ - && apt-get -q update \ - && apt-get -q dist-upgrade -y \ - && apt-get -q install -y \ - libjemalloc2 \ - ca-certificates \ - tzdata \ -# If your app or its dependencies import FoundationNetworking, also install `libcurl4`. - # libcurl4 \ -# If your app or its dependencies import FoundationXML, also install `libxml2`. - # libxml2 \ - && rm -r /var/lib/apt/lists/* + && apt-get -q update \ + && apt-get -q dist-upgrade -y \ + && apt-get -q install -y \ + libjemalloc2 \ + ca-certificates \ + tzdata \ + # If your app or its dependencies import FoundationNetworking, also install `libcurl4`. + # libcurl4 \ + # If your app or its dependencies import FoundationXML, also install `libxml2`. + # libxml2 \ + && rm -r /var/lib/apt/lists/* # Create a vapor user and group with /app as its home directory RUN useradd --user-group --create-home --system --skel /dev/null --home-dir /app vapor diff --git a/labs/cloudrun-progression/branch-cloudbuild.yaml b/labs/cloudrun-progression/branch-cloudbuild.yaml index ffddf3b..c1e0126 100644 --- a/labs/cloudrun-progression/branch-cloudbuild.yaml +++ b/labs/cloudrun-progression/branch-cloudbuild.yaml @@ -22,6 +22,8 @@ steps: ### Build - id: "build image" name: "gcr.io/cloud-builders/docker" + env: + - DOCKER_BUILDKIT=1 args: - "build" - "-t" diff --git a/labs/cloudrun-progression/main-cloudbuild.yaml b/labs/cloudrun-progression/main-cloudbuild.yaml index 0072c75..8c2109a 100644 --- a/labs/cloudrun-progression/main-cloudbuild.yaml +++ b/labs/cloudrun-progression/main-cloudbuild.yaml @@ -22,6 +22,8 @@ steps: ### Build - id: "build image" name: "gcr.io/cloud-builders/docker" + env: + - DOCKER_BUILDKIT=1 args: - "build" - "-t" From 1ee65f6c468aed14dadd130965b87fc157e795e5 Mon Sep 17 00:00:00 2001 From: "novr (Nobuhisa Komiya)" Date: Tue, 21 May 2024 17:41:13 +0900 Subject: [PATCH 11/13] feat: testMyFortune --- Tests/AppTests/AppTests.swift | 42 +++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/Tests/AppTests/AppTests.swift b/Tests/AppTests/AppTests.swift index c0e5e2c..573577f 100644 --- a/Tests/AppTests/AppTests.swift +++ b/Tests/AppTests/AppTests.swift @@ -3,21 +3,45 @@ import XCTVapor final class AppTests: XCTestCase { var app: Application! - + override func setUp() async throws { self.app = try await Application.make(.testing) try await configure(app) } - - override func tearDown() async throws { + + override func tearDown() async throws { try await self.app.asyncShutdown() self.app = nil } - - func testHelloWorld() async throws { - try await self.app.test(.GET, "hello", afterResponse: { res async in - XCTAssertEqual(res.status, .ok) - XCTAssertEqual(res.body.string, "Hello, world!") - }) + + func testMyFortune() throws { + try self.app.test( + .POST, + "/my_fortune", + headers: ["API-Version": "v1"], + beforeRequest: { req in + let content = Components.Schemas.MyFortuneRequest( + name: "ゆめみん", + birthday: .init(value1: .init(year: 2000, month: 1, day: 27)), + blood_type: .ab, + today: .init(value1: .init(year: 2023, month: 5, day: 7))) + try req.content.encode(content) + }, + afterResponse: { res in + XCTAssertEqual(res.status, .ok) + let result = try res.content.decode(Components.Schemas.MyFortuneResponse.self) + XCTAssertEqual(result.name, "埼玉県") + XCTAssertEqual(result.brief, "埼玉県(さいたまけん)は、日本の関東地方に位置する県。県庁所在地はさいたま市。\n※出典: フリー百科事典『ウィキペディア(Wikipedia)』") + XCTAssertEqual(result.capital, "さいたま市") + let citizenDay = try XCTUnwrap(result.citizen_day?.value1) + XCTAssertEqual(citizenDay.month, 11) + XCTAssertEqual(citizenDay.day, 14) + XCTAssertEqual(result.has_coast_line, false) + XCTAssertEqual(result.logo_url, "https://japan-map.com/wp-content/uploads/saitama.png") + } + ) } } + +extension Components.Schemas.MyFortuneRequest: Content {} +extension Components.Schemas.MyFortuneResponse: Content {} From 4837d26b36c07f35aaf18280604f912248495a7c Mon Sep 17 00:00:00 2001 From: "novr (Nobuhisa Komiya)" Date: Tue, 21 May 2024 17:41:30 +0900 Subject: [PATCH 12/13] fix: doc [skip:ci] --- Public/index.html | 2 +- docker-compose.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Public/index.html b/Public/index.html index 6173c42..e9a9793 100644 --- a/Public/index.html +++ b/Public/index.html @@ -142,7 +142,7 @@

API 仕様

(詳細は OpenAPI のページもご参考にしてください)

  • Base URL:
  • -

    "https://yumemi-ios-junior-engineer-codecheck.app.swift.cloud"

    +

    "https://ios-junior-engineer-codecheck-snefnyqv2q-an.a.run.app"

  • End Point:
  • "/my_fortune"

    diff --git a/docker-compose.yml b/docker-compose.yml index fd325f1..7a8184f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,7 +19,7 @@ x-shared_environment: &shared_environment services: app: - image: ios-junior-engineer-codecheck-backend:latest + image: ios-junior-engineer-codecheck:latest build: context: . environment: From 48aa01f37573d08dc3093bbb529e87c483fb0e76 Mon Sep 17 00:00:00 2001 From: "novr (Nobuhisa Komiya)" Date: Tue, 21 May 2024 18:06:59 +0900 Subject: [PATCH 13/13] add: test on ci --- .github/workflows/test.yaml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/test.yaml diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..d42be46 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,24 @@ +name: test + +on: + pull_request: + workflow_dispatch: + +env: + SWIFT_VERSION: "5.10" + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Swift + uses: swift-actions/setup-swift@v2 + with: + swift-version: ${{ env.SWIFT_VERSION }} + + - name: Run tests + run: swift test --enable-test-discovery