Skip to content

Commit

Permalink
papaparse に zod を組み合わせてより厳密な型チェックを行うように修正
Browse files Browse the repository at this point in the history
  • Loading branch information
kuramapommel committed Aug 30, 2024
1 parent 254aca9 commit d8debfe
Show file tree
Hide file tree
Showing 2 changed files with 93 additions and 15 deletions.
78 changes: 70 additions & 8 deletions sample/src/libs/papaparse/__tests__/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,86 @@
import { parse } from '@/libs/papaparse'
import { describe, expect, test } from 'vitest'
import { z } from 'zod'

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

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

const result = await parse<CSVRecord>(file)
const result = await parse(file, schema)
expect(result).toEqual([
{ hoge: 'a', fuga: 1 },
{ hoge: 'a', fuga: 4 },
{ hoge: 'a', fuga: 1, piyo: '3' },
{ hoge: 'a', fuga: 4, piyo: 'piyo' },
])
})

test('拡張子が csv 以外のファイルを渡すとエラーになる', async () => {
const schema = z.array(
z.object({
hoge: z.coerce.string(),
fuga: z.number().min(0).max(120),
piyo: z.coerce.string(),
}),
)

const data = ['non defined header'].join('\r\n')
const file = new File([data], 'foo.txt', {
type: 'text/csv',
})

await expect(parse(file, schema)).rejects.toThrowError(
'CSV ファイルを選択してください',
)
})

test('ファイルタイプが text/csv 以外のファイルを渡すとエラーになる', async () => {
const schema = z.array(
z.object({
hoge: z.coerce.string(),
fuga: z.number().min(0).max(120),
piyo: z.coerce.string(),
}),
)

const data = ['non defined header'].join('\r\n')
const file = new File([data], 'foo.csv', {
type: 'text/plain',
})

await expect(parse(file, schema)).rejects.toThrowError(
'CSV ファイルを選択してください',
)
})

test('カンマ区切りではないファイルを渡すとエラーになる', async () => {
const schema = z.array(
z.object({
hoge: z.coerce.string(),
fuga: z.number().min(0).max(120),
piyo: z.coerce.string(),
}),
)

// 一行目はヘッダーとして認識されるため、パースエラーは二行以上必要
const data = ['/', '/'].join('\r\n')
const file = new File([data], 'foo.csv', {
type: 'text/csv',
})

await expect(parse(file, schema)).rejects.toThrowError(
'ァイルのパースに失敗しました',
)
})
})
})
30 changes: 23 additions & 7 deletions sample/src/libs/papaparse/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,32 @@
import { parse as papaparse, ParseResult } from 'papaparse'
import { z } from 'zod'

export function parse<CSVRecord>(file: File): Promise<readonly CSVRecord[]> {
export function parse<Schema extends z.ZodArray<z.ZodObject<z.ZodRawShape>>>(
file: File,
schema: Schema,
): Promise<z.infer<Schema>> {
return new Promise((resolve, reject) => {
if ([...file.name.split('.')].pop() !== 'csv' || file.type !== 'text/csv') {
reject(new Error('CSV ファイルを選択してください'))
return
}

papaparse(file, {
complete: (results: ParseResult<CSVRecord>) => {
if (!results) reject(new Error('ファイルの読み込みに失敗しました'))
complete: (results: ParseResult<z.infer<Schema>>) => {
if (!results) {
reject(new Error('ファイルの読み込みに失敗しました'))
return
}

resolve(results.data)
},
error: () => {
reject(new Error('ファイルの読み込みに失敗しました'))
const parsed = schema.safeParse(results.data)
if (!parsed.success || !parsed.data) {
reject(new Error('ファイルのパースに失敗しました'))
return
}

resolve(parsed.data)
},
error: () => reject(new Error('ファイルの読み込みに失敗しました')),
header: true,
skipEmptyLines: true,
dynamicTyping: true,
Expand Down

0 comments on commit d8debfe

Please sign in to comment.