From 72bb5d43b49595abbf5149fa23e04aabbcd5c15a Mon Sep 17 00:00:00 2001 From: ikura-hamu <104292023+ikura-hamu@users.noreply.github.com> Date: Fri, 10 Jan 2025 15:36:04 +0900 Subject: [PATCH 1/7] =?UTF-8?q?:sparkles:=20FinishedBenchmark=E3=81=ABresu?= =?UTF-8?q?lt=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openapi/openapi.yml | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/openapi/openapi.yml b/openapi/openapi.yml index 44ce5f0..6310c2a 100644 --- a/openapi/openapi.yml +++ b/openapi/openapi.yml @@ -979,9 +979,9 @@ components: - discriminator: propertyName: status oneOf: - - $ref: "#/components/schemas/WaitingBenchmark" - - $ref: "#/components/schemas/RunningBenchmark" - - $ref: "#/components/schemas/FinishedBenchmark" + - $ref: "#/components/schemas/WaitingBenchmark" + - $ref: "#/components/schemas/RunningBenchmark" + - $ref: "#/components/schemas/FinishedBenchmark" - properties: log: type: "string" @@ -997,9 +997,9 @@ components: - discriminator: propertyName: status oneOf: - - $ref: "#/components/schemas/WaitingBenchmark" - - $ref: "#/components/schemas/RunningBenchmark" - - $ref: "#/components/schemas/FinishedBenchmark" + - $ref: "#/components/schemas/WaitingBenchmark" + - $ref: "#/components/schemas/RunningBenchmark" + - $ref: "#/components/schemas/FinishedBenchmark" - properties: log: type: "string" @@ -1012,7 +1012,7 @@ components: required: - log - adminLog - + WaitingBenchmark: type: "object" description: "status=waiting のベンチマーク結果" @@ -1089,6 +1089,12 @@ components: - "finished" score: $ref: "#/components/schemas/Score" + result: + type: "string" + enum: + - "passed" + - "failed" + - "error" createdAt: $ref: "#/components/schemas/CreatedAt" startedAt: @@ -1102,6 +1108,7 @@ components: - userId - status - score + - result - createdAt - startedAt - finishedAt From 25935c44826b4373ca239bc23fff94af38de271a Mon Sep 17 00:00:00 2001 From: ikura-hamu <104292023+ikura-hamu@users.noreply.github.com> Date: Fri, 10 Jan 2025 15:36:39 +0900 Subject: [PATCH 2/7] =?UTF-8?q?:adhesive=5Fbandage:=20Go=E3=81=AE=E3=82=B3?= =?UTF-8?q?=E3=83=BC=E3=83=89=E7=94=9F=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/handler/openapi/oas_json_gen.go | 85 ++++++++++++++++++-- server/handler/openapi/oas_schemas_gen.go | 59 ++++++++++++++ server/handler/openapi/oas_validators_gen.go | 24 ++++++ 3 files changed, 160 insertions(+), 8 deletions(-) diff --git a/server/handler/openapi/oas_json_gen.go b/server/handler/openapi/oas_json_gen.go index 15abc04..2055e6a 100644 --- a/server/handler/openapi/oas_json_gen.go +++ b/server/handler/openapi/oas_json_gen.go @@ -190,6 +190,10 @@ func (s Benchmark) encodeFields(e *jx.Encoder) { e.FieldStart("score") s.Score.Encode(e) } + { + e.FieldStart("result") + s.Result.Encode(e) + } { e.FieldStart("createdAt") s.CreatedAt.Encode(e) @@ -377,6 +381,10 @@ func (s BenchmarkAdminResult) encodeFields(e *jx.Encoder) { e.FieldStart("score") s.Score.Encode(e) } + { + e.FieldStart("result") + s.Result.Encode(e) + } { e.FieldStart("createdAt") s.CreatedAt.Encode(e) @@ -654,6 +662,10 @@ func (s BenchmarkListItemSum) encodeFields(e *jx.Encoder) { e.FieldStart("score") s.Score.Encode(e) } + { + e.FieldStart("result") + s.Result.Encode(e) + } { e.FieldStart("createdAt") s.CreatedAt.Encode(e) @@ -983,6 +995,10 @@ func (s *FinishedBenchmark) encodeFields(e *jx.Encoder) { e.FieldStart("score") s.Score.Encode(e) } + { + e.FieldStart("result") + s.Result.Encode(e) + } { e.FieldStart("createdAt") s.CreatedAt.Encode(e) @@ -997,16 +1013,17 @@ func (s *FinishedBenchmark) encodeFields(e *jx.Encoder) { } } -var jsonFieldsNameOfFinishedBenchmark = [9]string{ +var jsonFieldsNameOfFinishedBenchmark = [10]string{ 0: "id", 1: "instanceId", 2: "teamId", 3: "userId", 4: "status", 5: "score", - 6: "createdAt", - 7: "startedAt", - 8: "finishedAt", + 6: "result", + 7: "createdAt", + 8: "startedAt", + 9: "finishedAt", } // Decode decodes FinishedBenchmark from json. @@ -1078,8 +1095,18 @@ func (s *FinishedBenchmark) Decode(d *jx.Decoder) error { }(); err != nil { return errors.Wrap(err, "decode field \"score\"") } - case "createdAt": + case "result": requiredBitSet[0] |= 1 << 6 + if err := func() error { + if err := s.Result.Decode(d); err != nil { + return err + } + return nil + }(); err != nil { + return errors.Wrap(err, "decode field \"result\"") + } + case "createdAt": + requiredBitSet[0] |= 1 << 7 if err := func() error { if err := s.CreatedAt.Decode(d); err != nil { return err @@ -1089,7 +1116,7 @@ func (s *FinishedBenchmark) Decode(d *jx.Decoder) error { return errors.Wrap(err, "decode field \"createdAt\"") } case "startedAt": - requiredBitSet[0] |= 1 << 7 + requiredBitSet[1] |= 1 << 0 if err := func() error { if err := s.StartedAt.Decode(d); err != nil { return err @@ -1099,7 +1126,7 @@ func (s *FinishedBenchmark) Decode(d *jx.Decoder) error { return errors.Wrap(err, "decode field \"startedAt\"") } case "finishedAt": - requiredBitSet[1] |= 1 << 0 + requiredBitSet[1] |= 1 << 1 if err := func() error { if err := s.FinishedAt.Decode(d); err != nil { return err @@ -1119,7 +1146,7 @@ func (s *FinishedBenchmark) Decode(d *jx.Decoder) error { var failures []validate.FieldError for i, mask := range [2]uint8{ 0b11111111, - 0b00000001, + 0b00000011, } { if result := (requiredBitSet[i] & mask) ^ mask; result != 0 { // Mask only required fields and check equality to mask using XOR. @@ -1165,6 +1192,48 @@ func (s *FinishedBenchmark) UnmarshalJSON(data []byte) error { return s.Decode(d) } +// Encode encodes FinishedBenchmarkResult as json. +func (s FinishedBenchmarkResult) Encode(e *jx.Encoder) { + e.Str(string(s)) +} + +// Decode decodes FinishedBenchmarkResult from json. +func (s *FinishedBenchmarkResult) Decode(d *jx.Decoder) error { + if s == nil { + return errors.New("invalid: unable to decode FinishedBenchmarkResult to nil") + } + v, err := d.StrBytes() + if err != nil { + return err + } + // Try to use constant string. + switch FinishedBenchmarkResult(v) { + case FinishedBenchmarkResultPassed: + *s = FinishedBenchmarkResultPassed + case FinishedBenchmarkResultFailed: + *s = FinishedBenchmarkResultFailed + case FinishedBenchmarkResultError: + *s = FinishedBenchmarkResultError + default: + *s = FinishedBenchmarkResult(v) + } + + return nil +} + +// MarshalJSON implements stdjson.Marshaler. +func (s FinishedBenchmarkResult) MarshalJSON() ([]byte, error) { + e := jx.Encoder{} + s.Encode(&e) + return e.Bytes(), nil +} + +// UnmarshalJSON implements stdjson.Unmarshaler. +func (s *FinishedBenchmarkResult) UnmarshalJSON(data []byte) error { + d := jx.DecodeBytes(data) + return s.Decode(d) +} + // Encode encodes FinishedBenchmarkStatus as json. func (s FinishedBenchmarkStatus) Encode(e *jx.Encoder) { e.Str(string(s)) diff --git a/server/handler/openapi/oas_schemas_gen.go b/server/handler/openapi/oas_schemas_gen.go index 9a74d2c..fa4abc9 100644 --- a/server/handler/openapi/oas_schemas_gen.go +++ b/server/handler/openapi/oas_schemas_gen.go @@ -473,6 +473,7 @@ type FinishedBenchmark struct { UserId UserId `json:"userId"` Status FinishedBenchmarkStatus `json:"status"` Score Score `json:"score"` + Result FinishedBenchmarkResult `json:"result"` CreatedAt CreatedAt `json:"createdAt"` StartedAt StartedAt `json:"startedAt"` FinishedAt FinishedAt `json:"finishedAt"` @@ -508,6 +509,11 @@ func (s *FinishedBenchmark) GetScore() Score { return s.Score } +// GetResult returns the value of Result. +func (s *FinishedBenchmark) GetResult() FinishedBenchmarkResult { + return s.Result +} + // GetCreatedAt returns the value of CreatedAt. func (s *FinishedBenchmark) GetCreatedAt() CreatedAt { return s.CreatedAt @@ -553,6 +559,11 @@ func (s *FinishedBenchmark) SetScore(val Score) { s.Score = val } +// SetResult sets the value of Result. +func (s *FinishedBenchmark) SetResult(val FinishedBenchmarkResult) { + s.Result = val +} + // SetCreatedAt sets the value of CreatedAt. func (s *FinishedBenchmark) SetCreatedAt(val CreatedAt) { s.CreatedAt = val @@ -568,6 +579,54 @@ func (s *FinishedBenchmark) SetFinishedAt(val FinishedAt) { s.FinishedAt = val } +type FinishedBenchmarkResult string + +const ( + FinishedBenchmarkResultPassed FinishedBenchmarkResult = "passed" + FinishedBenchmarkResultFailed FinishedBenchmarkResult = "failed" + FinishedBenchmarkResultError FinishedBenchmarkResult = "error" +) + +// AllValues returns all FinishedBenchmarkResult values. +func (FinishedBenchmarkResult) AllValues() []FinishedBenchmarkResult { + return []FinishedBenchmarkResult{ + FinishedBenchmarkResultPassed, + FinishedBenchmarkResultFailed, + FinishedBenchmarkResultError, + } +} + +// MarshalText implements encoding.TextMarshaler. +func (s FinishedBenchmarkResult) MarshalText() ([]byte, error) { + switch s { + case FinishedBenchmarkResultPassed: + return []byte(s), nil + case FinishedBenchmarkResultFailed: + return []byte(s), nil + case FinishedBenchmarkResultError: + return []byte(s), nil + default: + return nil, errors.Errorf("invalid value: %q", s) + } +} + +// UnmarshalText implements encoding.TextUnmarshaler. +func (s *FinishedBenchmarkResult) UnmarshalText(data []byte) error { + switch FinishedBenchmarkResult(data) { + case FinishedBenchmarkResultPassed: + *s = FinishedBenchmarkResultPassed + return nil + case FinishedBenchmarkResultFailed: + *s = FinishedBenchmarkResultFailed + return nil + case FinishedBenchmarkResultError: + *s = FinishedBenchmarkResultError + return nil + default: + return errors.Errorf("invalid value: %q", data) + } +} + type FinishedBenchmarkStatus string const ( diff --git a/server/handler/openapi/oas_validators_gen.go b/server/handler/openapi/oas_validators_gen.go index 44e4de8..a8c677e 100644 --- a/server/handler/openapi/oas_validators_gen.go +++ b/server/handler/openapi/oas_validators_gen.go @@ -163,12 +163,36 @@ func (s *FinishedBenchmark) Validate() error { Error: err, }) } + if err := func() error { + if err := s.Result.Validate(); err != nil { + return err + } + return nil + }(); err != nil { + failures = append(failures, validate.FieldError{ + Name: "result", + Error: err, + }) + } if len(failures) > 0 { return &validate.Error{Fields: failures} } return nil } +func (s FinishedBenchmarkResult) Validate() error { + switch s { + case "passed": + return nil + case "failed": + return nil + case "error": + return nil + default: + return errors.Errorf("invalid value: %v", s) + } +} + func (s FinishedBenchmarkStatus) Validate() error { switch s { case "finished": From acfba7cac9e29c627c9f0a9bab4d5f48fd70de18 Mon Sep 17 00:00:00 2001 From: ikura-hamu <104292023+ikura-hamu@users.noreply.github.com> Date: Fri, 10 Jan 2025 15:42:44 +0900 Subject: [PATCH 3/7] =?UTF-8?q?:construction:=20=E3=83=95=E3=83=AD?= =?UTF-8?q?=E3=83=B3=E3=83=88=E3=82=A8=E3=83=B3=E3=83=89=E3=81=AE=E3=82=B3?= =?UTF-8?q?=E3=83=BC=E3=83=89=E7=94=9F=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/api/openapi.ts | 45 +++++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/client/src/api/openapi.ts b/client/src/api/openapi.ts index e6b34f5..832f80e 100644 --- a/client/src/api/openapi.ts +++ b/client/src/api/openapi.ts @@ -549,23 +549,29 @@ export interface components { | components['schemas']['RunningBenchmark'] | components['schemas']['FinishedBenchmark'] /** @description ベンチマーク結果 */ - Benchmark: ( + Benchmark: { + status: 'Benchmark' + } & (Omit< | components['schemas']['WaitingBenchmark'] | components['schemas']['RunningBenchmark'] - | components['schemas']['FinishedBenchmark'] - ) & { + | components['schemas']['FinishedBenchmark'], + 'status' + > & { /** * @description ベンチマークの競技者用ログ(標準出力) * @example log */ log: string - } + }) /** @description Adminが見ることができるベンチマーク結果 */ - BenchmarkAdminResult: ( + BenchmarkAdminResult: { + status: 'BenchmarkAdminResult' + } & (Omit< | components['schemas']['WaitingBenchmark'] | components['schemas']['RunningBenchmark'] - | components['schemas']['FinishedBenchmark'] - ) & { + | components['schemas']['FinishedBenchmark'], + 'status' + > & { /** * @description ベンチマークの競技者用ログ(標準出力) * @example log @@ -576,15 +582,18 @@ export interface components { * @example admin log */ adminLog: string - } + }) /** @description status=waiting のベンチマーク結果 */ WaitingBenchmark: { id: components['schemas']['BenchmarkId'] instanceId: components['schemas']['InstanceId'] teamId: components['schemas']['TeamId'] userId: components['schemas']['UserId'] - /** @enum {string} */ - status: 'waiting' + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + status: 'WaitingBenchmark' createdAt: components['schemas']['CreatedAt'] } /** @description status=running のベンチマーク結果 */ @@ -593,8 +602,11 @@ export interface components { instanceId: components['schemas']['InstanceId'] teamId: components['schemas']['TeamId'] userId: components['schemas']['UserId'] - /** @enum {string} */ - status: 'running' + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + status: 'RunningBenchmark' score: components['schemas']['Score'] createdAt: components['schemas']['CreatedAt'] startedAt: components['schemas']['StartedAt'] @@ -605,9 +617,14 @@ export interface components { instanceId: components['schemas']['InstanceId'] teamId: components['schemas']['TeamId'] userId: components['schemas']['UserId'] - /** @enum {string} */ - status: 'finished' + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + status: 'FinishedBenchmark' score: components['schemas']['Score'] + /** @enum {string} */ + result: 'passed' | 'failed' | 'error' createdAt: components['schemas']['CreatedAt'] startedAt: components['schemas']['StartedAt'] finishedAt: components['schemas']['FinishedAt'] From b7dc579a13babfd0178e789d2a8ffe76f9da91f8 Mon Sep 17 00:00:00 2001 From: cp-20 Date: Fri, 10 Jan 2025 16:45:01 +0900 Subject: [PATCH 4/7] =?UTF-8?q?client=20=E3=81=AE=20openapi=20=E7=94=9F?= =?UTF-8?q?=E6=88=90=E3=81=AB=20preprocess=20=E5=87=A6=E7=90=86=E3=82=92?= =?UTF-8?q?=E5=9A=99=E3=81=BE=E3=81=9B=E3=81=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/bun.lockb | Bin 167807 -> 169955 bytes client/package.json | 7 +++--- client/scripts/openapi-gen.ts | 20 ++++++++++++++++ client/src/api/openapi.ts | 43 +++++++++++----------------------- client/tsconfig.app.json | 2 +- client/tsconfig.json | 9 +++---- client/tsconfig.script.json | 28 ++++++++++++++++++++++ 7 files changed, 70 insertions(+), 39 deletions(-) create mode 100644 client/scripts/openapi-gen.ts create mode 100644 client/tsconfig.script.json diff --git a/client/bun.lockb b/client/bun.lockb index 0c1634f61db356253eb3cd9417bdea68985086f0..7606f580e6662974aee163c36a2bc769363bf043 100755 GIT binary patch delta 30603 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 diff --git a/client/package.json b/client/package.json index 4ed8859..88baa6a 100644 --- a/client/package.json +++ b/client/package.json @@ -13,7 +13,7 @@ "lint:ci": "eslint .", "format": "prettier --write src/", "format:ci": "prettier --check src/", - "openapi": "openapi-typescript ../openapi/openapi.yml -o src/api/openapi.ts" + "openapi": "bun run scripts/openapi-gen.ts && openapi-typescript /tmp/openapi.yml --output src/api/openapi.ts" }, "dependencies": { "@tanstack/vue-query": "^5.62.16", @@ -39,11 +39,12 @@ "typescript": "~5.6.3", "vite": "^6.0.1", "vite-plugin-vue-devtools": "^7.6.5", - "vue-tsc": "^2.1.10" + "vue-tsc": "^2.1.10", + "yaml": "^2.7.0" }, "msw": { "workerDirectory": [ "public" ] } -} \ No newline at end of file +} diff --git a/client/scripts/openapi-gen.ts b/client/scripts/openapi-gen.ts new file mode 100644 index 0000000..9c93326 --- /dev/null +++ b/client/scripts/openapi-gen.ts @@ -0,0 +1,20 @@ +import * as Bun from 'bun' +import * as yaml from 'yaml' + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const stripDiscriminator = (schema: any) => { + Object.entries(schema).forEach(([key, value]) => { + if (!(typeof value === 'object')) return + if (key === 'discriminator') { + delete schema[key] + } else { + stripDiscriminator(value) + } + }) +} + +const file = await Bun.file('../openapi/openapi.yml').text() +const parsed = yaml.parse(file) +stripDiscriminator(parsed) +const temp = Bun.env['FILE'] ?? '/tmp/openapi.yml' +await Bun.write(temp, yaml.stringify(parsed)) diff --git a/client/src/api/openapi.ts b/client/src/api/openapi.ts index 832f80e..799a07a 100644 --- a/client/src/api/openapi.ts +++ b/client/src/api/openapi.ts @@ -549,29 +549,23 @@ export interface components { | components['schemas']['RunningBenchmark'] | components['schemas']['FinishedBenchmark'] /** @description ベンチマーク結果 */ - Benchmark: { - status: 'Benchmark' - } & (Omit< + Benchmark: ( | components['schemas']['WaitingBenchmark'] | components['schemas']['RunningBenchmark'] - | components['schemas']['FinishedBenchmark'], - 'status' - > & { + | components['schemas']['FinishedBenchmark'] + ) & { /** * @description ベンチマークの競技者用ログ(標準出力) * @example log */ log: string - }) + } /** @description Adminが見ることができるベンチマーク結果 */ - BenchmarkAdminResult: { - status: 'BenchmarkAdminResult' - } & (Omit< + BenchmarkAdminResult: ( | components['schemas']['WaitingBenchmark'] | components['schemas']['RunningBenchmark'] - | components['schemas']['FinishedBenchmark'], - 'status' - > & { + | components['schemas']['FinishedBenchmark'] + ) & { /** * @description ベンチマークの競技者用ログ(標準出力) * @example log @@ -582,18 +576,15 @@ export interface components { * @example admin log */ adminLog: string - }) + } /** @description status=waiting のベンチマーク結果 */ WaitingBenchmark: { id: components['schemas']['BenchmarkId'] instanceId: components['schemas']['InstanceId'] teamId: components['schemas']['TeamId'] userId: components['schemas']['UserId'] - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - status: 'WaitingBenchmark' + /** @enum {string} */ + status: 'waiting' createdAt: components['schemas']['CreatedAt'] } /** @description status=running のベンチマーク結果 */ @@ -602,11 +593,8 @@ export interface components { instanceId: components['schemas']['InstanceId'] teamId: components['schemas']['TeamId'] userId: components['schemas']['UserId'] - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - status: 'RunningBenchmark' + /** @enum {string} */ + status: 'running' score: components['schemas']['Score'] createdAt: components['schemas']['CreatedAt'] startedAt: components['schemas']['StartedAt'] @@ -617,11 +605,8 @@ export interface components { instanceId: components['schemas']['InstanceId'] teamId: components['schemas']['TeamId'] userId: components['schemas']['UserId'] - /** - * @description discriminator enum property added by openapi-typescript - * @enum {string} - */ - status: 'FinishedBenchmark' + /** @enum {string} */ + status: 'finished' score: components['schemas']['Score'] /** @enum {string} */ result: 'passed' | 'failed' | 'error' diff --git a/client/tsconfig.app.json b/client/tsconfig.app.json index 93f952f..0c81cf1 100644 --- a/client/tsconfig.app.json +++ b/client/tsconfig.app.json @@ -1,6 +1,6 @@ { "extends": "@vue/tsconfig/tsconfig.dom.json", - "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], + "include": ["env.d.ts", "src/**/*", "src/**/*.vue", "tsconfig.script.json"], "exclude": ["src/**/__tests__/*"], "compilerOptions": { "composite": true, diff --git a/client/tsconfig.json b/client/tsconfig.json index 66b5e57..c2ab1ee 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -1,11 +1,8 @@ { "files": [], "references": [ - { - "path": "./tsconfig.node.json" - }, - { - "path": "./tsconfig.app.json" - } + { "path": "./tsconfig.node.json" }, + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.script.json" } ] } diff --git a/client/tsconfig.script.json b/client/tsconfig.script.json new file mode 100644 index 0000000..4373bba --- /dev/null +++ b/client/tsconfig.script.json @@ -0,0 +1,28 @@ +{ + "include": ["scripts/**/*"], + "compilerOptions": { + // Enable latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags + "noUnusedLocals": true, + "noUnusedParameters": true, + "noPropertyAccessFromIndexSignature": true + } +} From f6daaf880cab91a7c30428e76a8d7eee7136b3e7 Mon Sep 17 00:00:00 2001 From: cp-20 Date: Fri, 10 Jan 2025 16:48:43 +0900 Subject: [PATCH 5/7] fix(client): type-check --- client/src/mock/handlers.ts | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/client/src/mock/handlers.ts b/client/src/mock/handlers.ts index bc32711..dfce84b 100644 --- a/client/src/mock/handlers.ts +++ b/client/src/mock/handlers.ts @@ -113,6 +113,7 @@ const benchmarks: paths['/benchmarks/{benchmarkId}']['get']['responses']['200'][ score: 2000, log: '', adminLog: '', + result: 'passed', }, { id: '01943f68-7d22-7abb-8b13-0b727cd4597e', @@ -148,6 +149,7 @@ const benchmarks: paths['/benchmarks/{benchmarkId}']['get']['responses']['200'][ score: 100, log: '', adminLog: '', + result: 'passed', }, { id: '01943f6e-69dd-7167-84b3-478cf9c3253d', @@ -161,6 +163,7 @@ const benchmarks: paths['/benchmarks/{benchmarkId}']['get']['responses']['200'][ score: 1000, log: '', adminLog: '', + result: 'passed', }, { id: '01943f6e-8b29-79af-8430-7b06ae9307e5', @@ -174,6 +177,7 @@ const benchmarks: paths['/benchmarks/{benchmarkId}']['get']['responses']['200'][ score: 1000, log: '', adminLog: '', + result: 'passed', }, ] @@ -328,10 +332,17 @@ setInterval(() => { // running のまま 60 秒経過したら finished にする for (const b of runningBenchmarks) { if (new Date(b.startedAt).getTime() + 60 * 1000 < Date.now()) { - // @ts-expect-error running -> finished で型の変換を行う必要があるが無視 - b.status = 'finished' - // @ts-expect-error running -> finished で型の変換を行う必要があるが無視 - b.finishedAt = new Date().toISOString() + const index = benchmarks.findIndex((bb) => bb.id === b.id) + const result = ['passed', 'failed', 'error'][Math.floor(Math.random() * 3)] as + | 'passed' + | 'failed' + | 'error' + benchmarks[index] = { + ...b, + status: 'finished', + finishedAt: new Date().toISOString(), + result, + } } } @@ -339,12 +350,13 @@ setInterval(() => { if (runningBenchmarks.length === 0) { const waitingBenchmark = benchmarks.find((b) => b.status === 'waiting') if (waitingBenchmark !== undefined) { - // @ts-expect-error waiting -> running で型の変換を行う必要があるが無視 - waitingBenchmark.status = 'running' - // @ts-expect-error waiting -> running で型の変換を行う必要があるが無視 - waitingBenchmark.startedAt = new Date().toISOString() - // @ts-expect-error waiting -> running で型の変換を行う必要があるが無視 - waitingBenchmark.score = 0 + const index = benchmarks.findIndex((b) => b.id === waitingBenchmark.id) + benchmarks[index] = { + ...waitingBenchmark, + status: 'running', + startedAt: new Date().toISOString(), + score: 0, + } } } From 5ac90a47ca9dde8baae53389b6fab900b40497dc Mon Sep 17 00:00:00 2001 From: cp-20 Date: Fri, 10 Jan 2025 16:50:45 +0900 Subject: [PATCH 6/7] add @types/bun --- client/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/client/package.json b/client/package.json index 88baa6a..13c9344 100644 --- a/client/package.json +++ b/client/package.json @@ -25,6 +25,7 @@ "devDependencies": { "@iconify/vue": "^4.3.0", "@tsconfig/node22": "^22.0.0", + "@types/bun": "^1.1.16", "@types/node": "^22.9.3", "@vitejs/plugin-vue": "^5.2.1", "@vue/eslint-config-prettier": "^10.1.0", From 7d6be430736cddfce26caaf981986808a5228731 Mon Sep 17 00:00:00 2001 From: ikura-hamu <104292023+ikura-hamu@users.noreply.github.com> Date: Sun, 12 Jan 2025 14:54:17 +0900 Subject: [PATCH 7/7] =?UTF-8?q?:wrench:=20OpenAPI=E3=81=AE=E7=94=9F?= =?UTF-8?q?=E6=88=90=E3=82=92=E5=85=A8=E9=83=A8openapi-gen.ts=E3=81=AB?= =?UTF-8?q?=E7=A7=BB=E5=8B=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/package.json | 2 +- client/scripts/openapi-gen.ts | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/client/package.json b/client/package.json index 13c9344..24f63cd 100644 --- a/client/package.json +++ b/client/package.json @@ -13,7 +13,7 @@ "lint:ci": "eslint .", "format": "prettier --write src/", "format:ci": "prettier --check src/", - "openapi": "bun run scripts/openapi-gen.ts && openapi-typescript /tmp/openapi.yml --output src/api/openapi.ts" + "openapi": "bun run scripts/openapi-gen.ts" }, "dependencies": { "@tanstack/vue-query": "^5.62.16", diff --git a/client/scripts/openapi-gen.ts b/client/scripts/openapi-gen.ts index 9c93326..1207108 100644 --- a/client/scripts/openapi-gen.ts +++ b/client/scripts/openapi-gen.ts @@ -1,6 +1,12 @@ import * as Bun from 'bun' import * as yaml from 'yaml' +import openapiTS, { astToString } from 'openapi-typescript' + +// openapi-typescript does not support the combination of oneOf and allOf. +// This is a workaround to strip the discriminator property from the schema. +// ref: https://openapi-ts.dev/advanced#use-oneof-by-itself + // eslint-disable-next-line @typescript-eslint/no-explicit-any const stripDiscriminator = (schema: any) => { Object.entries(schema).forEach(([key, value]) => { @@ -18,3 +24,16 @@ const parsed = yaml.parse(file) stripDiscriminator(parsed) const temp = Bun.env['FILE'] ?? '/tmp/openapi.yml' await Bun.write(temp, yaml.stringify(parsed)) + +const ast = await openapiTS(new URL(temp, import.meta.url).toString()) +const contents = astToString(ast) + +const contentsWithHeader = + `/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +` + contents + +await Bun.write('./src/api/openapi.ts', contentsWithHeader)