Skip to content

Commit d8debfe

Browse files
committed
papaparse に zod を組み合わせてより厳密な型チェックを行うように修正
1 parent 254aca9 commit d8debfe

File tree

2 files changed

+93
-15
lines changed

2 files changed

+93
-15
lines changed
Lines changed: 70 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,86 @@
11
import { parse } from '@/libs/papaparse'
22
import { describe, expect, test } from 'vitest'
3+
import { z } from 'zod'
34

45
describe('libs/papaparse', () => {
56
describe('parse', () => {
67
test('csv を渡すと指定したオブジェクトの型にパース変換する', async () => {
7-
type CSVRecord = {
8-
hoge: string
9-
fuga: number
10-
}
8+
const schema = z.array(
9+
z.object({
10+
hoge: z.coerce.string(),
11+
fuga: z.number().min(0).max(120),
12+
piyo: z.coerce.string(),
13+
}),
14+
)
1115

12-
const data = ['hoge,fuga', 'a,1', 'a,4'].join('\r\n')
16+
const data = ['hoge,fuga,piyo', 'a,1,3', 'a,4,piyo'].join('\r\n')
1317
const file = new File([data], 'foo.csv', {
1418
type: 'text/csv',
1519
})
1620

17-
const result = await parse<CSVRecord>(file)
21+
const result = await parse(file, schema)
1822
expect(result).toEqual([
19-
{ hoge: 'a', fuga: 1 },
20-
{ hoge: 'a', fuga: 4 },
23+
{ hoge: 'a', fuga: 1, piyo: '3' },
24+
{ hoge: 'a', fuga: 4, piyo: 'piyo' },
2125
])
2226
})
27+
28+
test('拡張子が csv 以外のファイルを渡すとエラーになる', async () => {
29+
const schema = z.array(
30+
z.object({
31+
hoge: z.coerce.string(),
32+
fuga: z.number().min(0).max(120),
33+
piyo: z.coerce.string(),
34+
}),
35+
)
36+
37+
const data = ['non defined header'].join('\r\n')
38+
const file = new File([data], 'foo.txt', {
39+
type: 'text/csv',
40+
})
41+
42+
await expect(parse(file, schema)).rejects.toThrowError(
43+
'CSV ファイルを選択してください',
44+
)
45+
})
46+
47+
test('ファイルタイプが text/csv 以外のファイルを渡すとエラーになる', async () => {
48+
const schema = z.array(
49+
z.object({
50+
hoge: z.coerce.string(),
51+
fuga: z.number().min(0).max(120),
52+
piyo: z.coerce.string(),
53+
}),
54+
)
55+
56+
const data = ['non defined header'].join('\r\n')
57+
const file = new File([data], 'foo.csv', {
58+
type: 'text/plain',
59+
})
60+
61+
await expect(parse(file, schema)).rejects.toThrowError(
62+
'CSV ファイルを選択してください',
63+
)
64+
})
65+
66+
test('カンマ区切りではないファイルを渡すとエラーになる', async () => {
67+
const schema = z.array(
68+
z.object({
69+
hoge: z.coerce.string(),
70+
fuga: z.number().min(0).max(120),
71+
piyo: z.coerce.string(),
72+
}),
73+
)
74+
75+
// 一行目はヘッダーとして認識されるため、パースエラーは二行以上必要
76+
const data = ['/', '/'].join('\r\n')
77+
const file = new File([data], 'foo.csv', {
78+
type: 'text/csv',
79+
})
80+
81+
await expect(parse(file, schema)).rejects.toThrowError(
82+
'ァイルのパースに失敗しました',
83+
)
84+
})
2385
})
2486
})

sample/src/libs/papaparse/index.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,32 @@
11
import { parse as papaparse, ParseResult } from 'papaparse'
2+
import { z } from 'zod'
23

3-
export function parse<CSVRecord>(file: File): Promise<readonly CSVRecord[]> {
4+
export function parse<Schema extends z.ZodArray<z.ZodObject<z.ZodRawShape>>>(
5+
file: File,
6+
schema: Schema,
7+
): Promise<z.infer<Schema>> {
48
return new Promise((resolve, reject) => {
9+
if ([...file.name.split('.')].pop() !== 'csv' || file.type !== 'text/csv') {
10+
reject(new Error('CSV ファイルを選択してください'))
11+
return
12+
}
13+
514
papaparse(file, {
6-
complete: (results: ParseResult<CSVRecord>) => {
7-
if (!results) reject(new Error('ファイルの読み込みに失敗しました'))
15+
complete: (results: ParseResult<z.infer<Schema>>) => {
16+
if (!results) {
17+
reject(new Error('ファイルの読み込みに失敗しました'))
18+
return
19+
}
820

9-
resolve(results.data)
10-
},
11-
error: () => {
12-
reject(new Error('ファイルの読み込みに失敗しました'))
21+
const parsed = schema.safeParse(results.data)
22+
if (!parsed.success || !parsed.data) {
23+
reject(new Error('ファイルのパースに失敗しました'))
24+
return
25+
}
26+
27+
resolve(parsed.data)
1328
},
29+
error: () => reject(new Error('ファイルの読み込みに失敗しました')),
1430
header: true,
1531
skipEmptyLines: true,
1632
dynamicTyping: true,

0 commit comments

Comments
 (0)