diff --git a/client/bun.lockb b/client/bun.lockb index 0c1634f..7606f58 100755 Binary files a/client/bun.lockb and b/client/bun.lockb differ diff --git a/client/package.json b/client/package.json index 4ed8859..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": "openapi-typescript ../openapi/openapi.yml -o src/api/openapi.ts" + "openapi": "bun run scripts/openapi-gen.ts" }, "dependencies": { "@tanstack/vue-query": "^5.62.16", @@ -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", @@ -39,11 +40,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..1207108 --- /dev/null +++ b/client/scripts/openapi-gen.ts @@ -0,0 +1,39 @@ +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]) => { + 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)) + +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) diff --git a/client/src/api/openapi.ts b/client/src/api/openapi.ts index e6b34f5..799a07a 100644 --- a/client/src/api/openapi.ts +++ b/client/src/api/openapi.ts @@ -608,6 +608,8 @@ export interface components { /** @enum {string} */ status: 'finished' score: components['schemas']['Score'] + /** @enum {string} */ + result: 'passed' | 'failed' | 'error' createdAt: components['schemas']['CreatedAt'] startedAt: components['schemas']['StartedAt'] finishedAt: components['schemas']['FinishedAt'] 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, + } } } 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 + } +} 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 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":