@zipbul/baker 0.1.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.ko.md +368 -280
  2. package/README.md +369 -281
  3. package/dist/index-57gr0v18.js +6 -0
  4. package/dist/index-aegrb1kn.js +6 -0
  5. package/dist/index-xdn55cz3.js +4 -0
  6. package/dist/index.d.ts +6 -5
  7. package/dist/index.js +168 -2
  8. package/dist/src/configure.d.ts +33 -0
  9. package/dist/src/create-rule.d.ts +6 -15
  10. package/dist/src/decorators/field.d.ts +74 -0
  11. package/dist/src/decorators/index.d.ts +2 -14
  12. package/dist/src/decorators/index.js +2 -2
  13. package/dist/src/functions/deserialize.d.ts +2 -1
  14. package/dist/src/functions/serialize.d.ts +2 -2
  15. package/dist/src/functions/to-json-schema.d.ts +6 -0
  16. package/dist/src/interfaces.d.ts +5 -26
  17. package/dist/src/registry.d.ts +0 -8
  18. package/dist/src/rules/index.d.ts +2 -0
  19. package/dist/src/rules/index.js +11 -2
  20. package/dist/src/seal/circular-analyzer.d.ts +2 -6
  21. package/dist/src/seal/index.d.ts +1 -1
  22. package/dist/src/seal/seal.d.ts +21 -6
  23. package/dist/src/symbols.js +2 -2
  24. package/dist/src/types.d.ts +7 -7
  25. package/dist/src/utils.d.ts +2 -0
  26. package/package.json +10 -4
  27. package/dist/index-jzjz61tg.js +0 -14
  28. package/dist/index-jzjz61tg.js.map +0 -31
  29. package/dist/index-txxjqhgc.js +0 -6
  30. package/dist/index-txxjqhgc.js.map +0 -10
  31. package/dist/index.js.map +0 -9
  32. package/dist/src/decorators/array.d.ts +0 -13
  33. package/dist/src/decorators/common.d.ts +0 -39
  34. package/dist/src/decorators/date.d.ts +0 -5
  35. package/dist/src/decorators/index.js.map +0 -9
  36. package/dist/src/decorators/locales.d.ts +0 -9
  37. package/dist/src/decorators/nested.d.ts +0 -17
  38. package/dist/src/decorators/number.d.ts +0 -15
  39. package/dist/src/decorators/object.d.ts +0 -9
  40. package/dist/src/decorators/schema.d.ts +0 -13
  41. package/dist/src/decorators/string.d.ts +0 -72
  42. package/dist/src/decorators/transform.d.ts +0 -68
  43. package/dist/src/decorators/typechecker.d.ts +0 -18
  44. package/dist/src/rules/index.js.map +0 -9
  45. package/dist/src/symbols.js.map +0 -9
package/README.ko.md CHANGED
@@ -4,7 +4,7 @@
4
4
  <strong>데코레이터 기반 validate + transform — 인라인 코드 생성</strong>
5
5
  </p>
6
6
  <p align="center">
7
- class-validator DX · AOT급 성능 · reflect-metadata 불필요
7
+ 단일 <code>@Field()</code> 데코레이터 &middot; AOT급 성능 &middot; reflect-metadata 불필요
8
8
  </p>
9
9
  <p align="center">
10
10
  <a href="https://github.com/zipbul/baker/actions"><img src="https://github.com/zipbul/baker/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
@@ -20,42 +20,45 @@
20
20
 
21
21
  ---
22
22
 
23
- ## 🤔 왜 Baker인가?
23
+ ## 왜 Baker인가?
24
24
 
25
25
  | | class-validator | Zod | TypeBox | **Baker** |
26
26
  |---|---|---|---|---|
27
- | 스키마 방식 | 데코레이터 | 함수 체이닝 | JSON Schema 빌더 | **데코레이터** |
27
+ | 스키마 방식 | 데코레이터 | 함수 체이닝 | JSON Schema 빌더 | **단일 `@Field()` 데코레이터** |
28
28
  | 성능 | 런타임 인터프리터 | 런타임 인터프리터 | JIT 컴파일 | **`new Function()` 인라인 코드생성** |
29
- | Transform 내장 | 별도 패키지 | `.transform()` | | **통합** |
29
+ | Transform 내장 | 별도 패키지 | `.transform()` | N/A | **통합** |
30
30
  | reflect-metadata | 필수 | N/A | N/A | **불필요** |
31
31
  | class-validator 마이그레이션 | — | 전면 재작성 | 전면 재작성 | **거의 그대로** |
32
32
 
33
- Baker는 class-validator의 **익숙한 데코레이터 DX**를 유지하면서, `seal()` 시점에 `new Function()`으로 최적화된 검증+변환 함수를 생성합니다. **컴파일러 플러그인 없이 AOT 수준의 성능**을 제공합니다.
33
+ Baker는 검증, 변환, 노출 제어, 타입 힌트를 결합하는 **단일 `@Field()` 데코레이터**를 제공합니다. 첫 사용 시 `new Function()`으로 최적화된 함수를 생성하여 모든 DTO를 자동으로 seal합니다 — **컴파일러 플러그인 없이 AOT 수준의 성능**을 제공합니다.
34
34
 
35
35
  ---
36
36
 
37
- ## 주요 기능
38
-
39
- - 🎯 **데코레이터 우선** — `@IsString()`, `@Min()`, `@IsEmail()` 등 80개 이상의 내장 검증기
40
- - **인라인 코드 생성** — `seal()`이 검증기를 최적화된 함수로 컴파일, 런타임 해석 없음
41
- - 🔄 **검증 + 변환 통합** `deserialize()`와 `serialize()`를 하나의 async 호출로
42
- - 🪶 **reflect-metadata 불필요** — `reflect-metadata` import 없이 동작
43
- - 🔁 **순환 참조 감지** seal 시점에 자동 정적 분석
44
- - 🏷️ **그룹 기반 검증** `groups` 옵션으로 요청별 다른 규칙 적용
45
- - 🧩 **커스텀 규칙** — `createRule()`로 코드생성을 지원하는 사용자 정의 검증기 작성
46
- - 📐 **JSON Schema 출력** — `toJsonSchema()`로 DTO에서 JSON Schema Draft 2020-12 생성
47
- - 🪆 **`@Nested`**단일 데코레이터 중첩 DTO 검증 + discriminator 지원
48
- - 🛡️ **Whitelist 모드** `seal({ whitelist: true })`로 미선언 필드 거부
37
+ ## 주요 기능
38
+
39
+ - **단일 데코레이터** — `@Field()`가 30개 이상의 개별 데코레이터를 대체
40
+ - **80개 이상의 내장 규칙** — `isString`, `min()`, `isEmail()` 등을 인자로 조합
41
+ - **인라인 코드 생성** `deserialize()`/`serialize()` 호출 auto-seal로 검증기를 컴파일
42
+ - **검증 + 변환 통합** — `deserialize()`와 `serialize()`를 하나의 async 호출로
43
+ - **reflect-metadata 불필요**`reflect-metadata` import 없이 동작
44
+ - **순환 참조 감지**seal 시점에 자동 정적 분석
45
+ - **그룹 기반 검증** — `groups` 옵션으로 요청별 다른 규칙 적용
46
+ - **커스텀 규칙** — `createRule()`로 코드생성을 지원하는 사용자 정의 검증기 작성
47
+ - **JSON Schema 출력** `toJsonSchema()`로 DTO에서 JSON Schema Draft 2020-12 생성
48
+ - **다형성 discriminator** — `@Field({ discriminator })`로 유니온 타입 지원
49
+ - **Whitelist 모드** — `configure({ forbidUnknown: true })`로 미선언 필드 거부
50
+ - **클래스 상속** — 자식 DTO가 부모 `@Field()` 데코레이터를 자동으로 상속
51
+ - **비동기 transform** — transform 함수에 async 사용 가능
49
52
 
50
53
  ---
51
54
 
52
- ## 📦 설치
55
+ ## 설치
53
56
 
54
57
  ```bash
55
58
  bun add @zipbul/baker
56
59
  ```
57
60
 
58
- > **요구사항:** Bun 1.0, tsconfig.json에 `experimentalDecorators: true` 설정
61
+ > **요구사항:** Bun >= 1.0, tsconfig.json에 `experimentalDecorators: true` 설정
59
62
 
60
63
  ```jsonc
61
64
  // tsconfig.json
@@ -68,37 +71,27 @@ bun add @zipbul/baker
68
71
 
69
72
  ---
70
73
 
71
- ## 🚀 빠른 시작
74
+ ## 빠른 시작
72
75
 
73
76
  ### 1. DTO 정의
74
77
 
75
78
  ```typescript
76
- import { IsString, IsInt, IsEmail, Min, Max } from '@zipbul/baker/decorators';
79
+ import { Field } from '@zipbul/baker';
80
+ import { isString, isInt, isEmail, min, max } from '@zipbul/baker/rules';
77
81
 
78
82
  class CreateUserDto {
79
- @IsString()
83
+ @Field(isString)
80
84
  name!: string;
81
85
 
82
- @IsInt()
83
- @Min(0)
84
- @Max(120)
86
+ @Field(isInt, min(0), max(120))
85
87
  age!: number;
86
88
 
87
- @IsEmail()
89
+ @Field(isEmail())
88
90
  email!: string;
89
91
  }
90
92
  ```
91
93
 
92
- ### 2. 시작 시 seal()
93
-
94
- ```typescript
95
- import { seal } from '@zipbul/baker';
96
-
97
- // 등록된 모든 DTO를 최적화된 검증 함수로 컴파일
98
- seal();
99
- ```
100
-
101
- ### 3. 요청마다 deserialize()
94
+ ### 2. Deserialize (첫 호출 auto-seal)
102
95
 
103
96
  ```typescript
104
97
  import { deserialize, BakerValidationError } from '@zipbul/baker';
@@ -113,7 +106,7 @@ try {
113
106
  }
114
107
  ```
115
108
 
116
- ### 4. serialize()
109
+ ### 3. Serialize
117
110
 
118
111
  ```typescript
119
112
  import { serialize } from '@zipbul/baker';
@@ -122,180 +115,271 @@ const plain = await serialize(userInstance);
122
115
  // plain: Record<string, unknown>
123
116
  ```
124
117
 
118
+ > `seal()` 호출이 필요 없습니다 — baker는 첫 `deserialize()` 또는 `serialize()` 호출 시 등록된 모든 DTO를 자동으로 seal합니다.
119
+
120
+ ---
121
+
122
+ ## `@Field()` 데코레이터
123
+
124
+ `@Field()`는 모든 개별 데코레이터를 대체하는 단일 데코레이터입니다. 검증 규칙을 위치 인자로, 고급 기능은 옵션 객체로 전달합니다.
125
+
126
+ ### 시그니처
127
+
128
+ ```typescript
129
+ // 규칙만
130
+ @Field(isString, minLength(3), maxLength(100))
131
+
132
+ // 옵션만
133
+ @Field({ optional: true, nullable: true })
134
+
135
+ // 규칙 + 옵션
136
+ @Field(isString, { name: 'user_name', groups: ['create'] })
137
+
138
+ // 규칙 없이 (단순 필드)
139
+ @Field()
140
+ ```
141
+
142
+ ### FieldOptions
143
+
144
+ ```typescript
145
+ interface FieldOptions {
146
+ type?: () => Constructor | [Constructor]; // 중첩 DTO 타입 (순환 참조를 위한 thunk)
147
+ discriminator?: { // 다형성 유니온
148
+ property: string;
149
+ subTypes: { value: Function; name: string }[];
150
+ };
151
+ keepDiscriminatorProperty?: boolean; // 출력에 discriminator 키 유지
152
+ rules?: (EmittableRule | ArrayOfMarker)[]; // 검증 규칙 (위치 인자 대안)
153
+ optional?: boolean; // undefined 허용
154
+ nullable?: boolean; // null 허용
155
+ name?: string; // JSON 키 매핑 (양방향)
156
+ deserializeName?: string; // 역직렬화 전용 키 매핑
157
+ serializeName?: string; // 직렬화 전용 키 매핑
158
+ exclude?: boolean | 'deserializeOnly' | 'serializeOnly';
159
+ groups?: string[]; // 가시성 + 조건부 검증
160
+ when?: (obj: any) => boolean; // 조건부 검증
161
+ schema?: JsonSchemaOverride; // JSON Schema 메타데이터
162
+ transform?: (params: FieldTransformParams) => unknown;
163
+ transformDirection?: 'deserializeOnly' | 'serializeOnly';
164
+ }
165
+ ```
166
+
167
+ ### 규칙별 옵션 (message, groups)
168
+
169
+ 개별 규칙 함수에 `message`, `groups`, `context`를 직접 전달하는 것은 **불가능**합니다. 대신 `@Field()` 레벨에서 제어합니다:
170
+
171
+ - **`groups`** — `FieldOptions.groups`로 설정 (해당 필드의 모든 규칙에 적용)
172
+ - **`message`** / **`context`** — `createRule()`로 커스텀 에러 메시지 설정하거나 `BakerError.code`로 처리
173
+ - **`each` (배열 요소 검증)** — `arrayOf()` 사용 (아래 참조)
174
+
175
+ ### `arrayOf()` — 배열 요소 검증
176
+
177
+ `arrayOf()`는 배열의 각 요소에 규칙을 적용합니다. `@zipbul/baker/rules` 또는 `@zipbul/baker`에서 import합니다.
178
+
179
+ ```typescript
180
+ import { Field, arrayOf } from '@zipbul/baker';
181
+ import { isString, minLength } from '@zipbul/baker/rules';
182
+
183
+ class TagsDto {
184
+ @Field(arrayOf(isString, minLength(1)))
185
+ tags!: string[];
186
+ }
187
+ ```
188
+
189
+ `arrayOf()`를 최상위 배열 규칙과 함께 사용할 수 있습니다:
190
+
191
+ ```typescript
192
+ import { arrayMinSize, arrayMaxSize } from '@zipbul/baker/rules';
193
+
194
+ class ScoresDto {
195
+ @Field(arrayMinSize(1), arrayMaxSize(10), arrayOf(isInt, min(0), max(100)))
196
+ scores!: number[];
197
+ }
198
+ ```
199
+
125
200
  ---
126
201
 
127
- ## 🏗️ 데코레이터
202
+ ## 내장 규칙
203
+
204
+ 모든 규칙은 `@zipbul/baker/rules`에서 import하며 `@Field()`의 인자로 전달합니다.
205
+
206
+ > **상수 vs 팩토리 함수:** 일부 규칙은 미리 만들어진 상수로 `()` 없이 사용하고, 나머지는 매개변수를 받는 팩토리 함수로 `()`와 함께 사용합니다. 아래 표에서 상수는 별도로 표기합니다.
128
207
 
129
208
  ### 타입 검사
130
209
 
131
- | 데코레이터 | 설명 |
210
+ | 규칙 | 설명 |
132
211
  |---|---|
133
- | `@IsString()` | `typeof === 'string'` |
134
- | `@IsNumber(opts?)` | `typeof === 'number'` + NaN/Infinity 검사 |
135
- | `@IsInt()` | 정수 검사 |
136
- | `@IsBoolean()` | `typeof === 'boolean'` |
137
- | `@IsDate()` | `instanceof Date && !isNaN` |
138
- | `@IsEnum(enumObj)` | 열거형 값 검사 |
139
- | `@IsArray()` | `Array.isArray()` |
140
- | `@IsObject()` | `typeof === 'object'`, null/Array 제외 |
212
+ | `isString` | `typeof === 'string'` |
213
+ | `isNumber(opts?)` | `typeof === 'number'` + NaN/Infinity/maxDecimalPlaces 검사 |
214
+ | `isInt` | 정수 검사 |
215
+ | `isBoolean` | `typeof === 'boolean'` |
216
+ | `isDate` | `instanceof Date && !isNaN` |
217
+ | `isEnum(enumObj)` | 열거형 값 검사 |
218
+ | `isArray` | `Array.isArray()` |
219
+ | `isObject` | `typeof === 'object'`, null/Array 제외 |
220
+
221
+ > `isString`, `isInt`, `isBoolean`, `isDate`, `isArray`, `isObject`는 상수(괄호 불필요). `isNumber(opts?)`와 `isEnum(enumObj)`는 팩토리 함수.
141
222
 
142
223
  ### 공통
143
224
 
144
- | 데코레이터 | 설명 |
225
+ | 규칙 | 설명 |
145
226
  |---|---|
146
- | `@IsDefined()` | `!== undefined && !== null` |
147
- | `@IsOptional()` | 값이 없으면 이후 규칙 건너뜀 |
148
- | `@IsNullable()` | `null` 허용 (검증 건너뜀), `undefined` 거부 |
149
- | `@IsNotEmpty()` | `!== undefined && !== null && !== ''` |
150
- | `@IsEmpty()` | `=== undefined \|\| === null \|\| === ''` |
151
- | `@Equals(val)` | `=== val` |
152
- | `@NotEquals(val)` | `!== val` |
153
- | `@IsIn(values)` | 주어진 배열에 포함 |
154
- | `@IsNotIn(values)` | 주어진 배열에 미포함 |
155
- | `@ValidateNested()` | 중첩 DTO 검증 |
156
- | `@ValidateIf(fn)` | 조건부 검증 |
227
+ | `equals(val)` | 엄격 동등 비교 (`===`) |
228
+ | `notEquals(val)` | 엄격 비동등 비교 (`!==`) |
229
+ | `isEmpty` | `undefined`, `null`, 또는 `''` |
230
+ | `isNotEmpty` | `undefined`, `null`, `''`이 아님 |
231
+ | `isIn(arr)` | 주어진 배열에 포함 |
232
+ | `isNotIn(arr)` | 주어진 배열에 미포함 |
233
+
234
+ > `isEmpty`와 `isNotEmpty`는 상수. 나머지는 팩토리 함수.
157
235
 
158
236
  ### 숫자
159
237
 
160
- | 데코레이터 | 설명 |
238
+ | 규칙 | 설명 |
161
239
  |---|---|
162
- | `@Min(n)` | `value >= n` |
163
- | `@Max(n)` | `value <= n` |
164
- | `@IsPositive()` | `value > 0` |
165
- | `@IsNegative()` | `value < 0` |
166
- | `@IsInRange(min, max)` | `min <= value <= max` |
167
- | `@IsDivisibleBy(n)` | `value % n === 0` |
240
+ | `min(n, opts?)` | `value >= n` (`{ exclusive: true }` 지원) |
241
+ | `max(n, opts?)` | `value <= n` (`{ exclusive: true }` 지원) |
242
+ | `isPositive` | `value > 0` |
243
+ | `isNegative` | `value < 0` |
244
+ | `isDivisibleBy(n)` | `value % n === 0` |
245
+
246
+ > `isPositive`와 `isNegative`는 상수(괄호 없음). `min()`, `max()`, `isDivisibleBy()`는 팩토리 함수.
168
247
 
169
248
  ### 문자열
170
249
 
171
- <details>
172
- <summary>50개 이상의 문자열 검증기 — 클릭하여 펼치기</summary>
250
+ 모든 문자열 규칙은 값이 `string` 타입이어야 합니다.
251
+
252
+ | 규칙 | 종류 | 설명 |
253
+ |---|---|---|
254
+ | `minLength(n)` | 팩토리 | 최소 길이 |
255
+ | `maxLength(n)` | 팩토리 | 최대 길이 |
256
+ | `length(min, max)` | 팩토리 | 길이 범위 |
257
+ | `contains(seed)` | 팩토리 | 부분 문자열 포함 |
258
+ | `notContains(seed)` | 팩토리 | 부분 문자열 미포함 |
259
+ | `matches(pattern, modifiers?)` | 팩토리 | 정규식 매치 |
260
+ | `isLowercase` | 상수 | 전체 소문자 |
261
+ | `isUppercase` | 상수 | 전체 대문자 |
262
+ | `isAscii` | 상수 | ASCII만 |
263
+ | `isAlpha` | 상수 | 알파벳만 (en-US) |
264
+ | `isAlphanumeric` | 상수 | 알파벳/숫자만 (en-US) |
265
+ | `isBooleanString` | 상수 | `'true'`, `'false'`, `'1'`, 또는 `'0'` |
266
+ | `isNumberString(opts?)` | 팩토리 | 숫자 문자열 |
267
+ | `isDecimal(opts?)` | 팩토리 | 소수 문자열 |
268
+ | `isFullWidth` | 상수 | 전각 문자 |
269
+ | `isHalfWidth` | 상수 | 반각 문자 |
270
+ | `isVariableWidth` | 상수 | 전각/반각 혼합 |
271
+ | `isMultibyte` | 상수 | 멀티바이트 문자 |
272
+ | `isSurrogatePair` | 상수 | 서로게이트 페어 문자 |
273
+ | `isHexadecimal` | 상수 | 16진수 문자열 |
274
+ | `isOctal` | 상수 | 8진수 문자열 |
275
+ | `isEmail(opts?)` | 팩토리 | 이메일 형식 |
276
+ | `isURL(opts?)` | 팩토리 | URL 형식 (포트 범위 검증) |
277
+ | `isUUID(version?)` | 팩토리 | UUID v1-v5 |
278
+ | `isIP(version?)` | 팩토리 | IPv4 / IPv6 |
279
+ | `isHexColor` | 상수 | Hex 색상 (`#fff`, `#ffffff`) |
280
+ | `isRgbColor(includePercent?)` | 팩토리 | RGB 색상 문자열 |
281
+ | `isHSL` | 상수 | HSL 색상 문자열 |
282
+ | `isMACAddress(opts?)` | 팩토리 | MAC 주소 |
283
+ | `isISBN(version?)` | 팩토리 | ISBN-10 / ISBN-13 |
284
+ | `isISIN` | 상수 | ISIN (국제증권식별번호) |
285
+ | `isISO8601(opts?)` | 팩토리 | ISO 8601 날짜 문자열 |
286
+ | `isISRC` | 상수 | ISRC (국제표준녹음코드) |
287
+ | `isISSN(opts?)` | 팩토리 | ISSN (국제표준일련번호) |
288
+ | `isJWT` | 상수 | JSON Web Token |
289
+ | `isLatLong(opts?)` | 팩토리 | 위도/경도 문자열 |
290
+ | `isLocale` | 상수 | 로케일 문자열 (예: `en_US`) |
291
+ | `isDataURI` | 상수 | Data URI |
292
+ | `isFQDN(opts?)` | 팩토리 | 정규화된 도메인 이름 |
293
+ | `isPort` | 상수 | 포트 번호 문자열 (0-65535) |
294
+ | `isEAN` | 상수 | EAN (유럽상품번호) |
295
+ | `isISO31661Alpha2` | 상수 | ISO 3166-1 alpha-2 국가 코드 |
296
+ | `isISO31661Alpha3` | 상수 | ISO 3166-1 alpha-3 국가 코드 |
297
+ | `isBIC` | 상수 | BIC (은행식별코드) / SWIFT 코드 |
298
+ | `isFirebasePushId` | 상수 | Firebase Push ID |
299
+ | `isSemVer` | 상수 | 시맨틱 버전 문자열 |
300
+ | `isMongoId` | 상수 | MongoDB ObjectId (24자 hex) |
301
+ | `isJSON` | 상수 | JSON 파싱 가능 문자열 |
302
+ | `isBase32(opts?)` | 팩토리 | Base32 인코딩 |
303
+ | `isBase58` | 상수 | Base58 인코딩 |
304
+ | `isBase64(opts?)` | 팩토리 | Base64 인코딩 |
305
+ | `isDateString(opts?)` | 팩토리 | 날짜 문자열 (strict 모드 설정 가능) |
306
+ | `isMimeType` | 상수 | MIME 타입 문자열 |
307
+ | `isCurrency(opts?)` | 팩토리 | 통화 문자열 |
308
+ | `isMagnetURI` | 상수 | Magnet URI |
309
+ | `isCreditCard` | 상수 | 신용카드 번호 (Luhn) |
310
+ | `isIBAN(opts?)` | 팩토리 | IBAN |
311
+ | `isByteLength(min, max?)` | 팩토리 | 바이트 길이 범위 |
312
+ | `isHash(algorithm)` | 팩토리 | 해시 문자열 (md4, md5, sha1, sha256, sha384, sha512 등) |
313
+ | `isRFC3339` | 상수 | RFC 3339 날짜시간 문자열 |
314
+ | `isMilitaryTime` | 상수 | 군사 시간 (HH:MM) |
315
+ | `isLatitude` | 상수 | 위도 문자열 |
316
+ | `isLongitude` | 상수 | 경도 문자열 |
317
+ | `isEthereumAddress` | 상수 | 이더리움 주소 |
318
+ | `isBtcAddress` | 상수 | 비트코인 주소 |
319
+ | `isISO4217CurrencyCode` | 상수 | ISO 4217 통화 코드 |
320
+ | `isPhoneNumber` | 상수 | E.164 국제 전화번호 |
321
+ | `isStrongPassword(opts?)` | 팩토리 | 강력한 비밀번호 (최소 길이, 대/소문자, 숫자, 특수문자 설정) |
322
+ | `isTaxId(locale)` | 팩토리 | 주어진 로케일의 세금 ID |
173
323
 
174
- | 데코레이터 | 설명 |
324
+ ### 배열
325
+
326
+ | 규칙 | 설명 |
175
327
  |---|---|
176
- | `@MinLength(n)` | 최소 길이 |
177
- | `@MaxLength(n)` | 최대 길이 |
178
- | `@Length(min, max)` | 길이 범위 |
179
- | `@Contains(seed)` | 부분 문자열 포함 |
180
- | `@NotContains(seed)` | 부분 문자열 미포함 |
181
- | `@Matches(pattern)` | 정규식 매치 |
182
- | `@IsAlpha()` | 알파벳만 |
183
- | `@IsAlphanumeric()` | 알파벳/숫자만 |
184
- | `@IsNumeric()` | 숫자 문자열 |
185
- | `@IsEmail(opts?)` | 이메일 형식 |
186
- | `@IsURL(opts?)` | URL 형식 |
187
- | `@IsUUID(version?)` | UUID v1–v5 |
188
- | `@IsIP(version?)` | IPv4 / IPv6 |
189
- | `@IsMACAddress()` | MAC 주소 |
190
- | `@IsISBN(version?)` | ISBN-10 / ISBN-13 |
191
- | `@IsISIN()` | ISIN |
192
- | `@IsIBAN()` | IBAN |
193
- | `@IsJSON()` | JSON 파싱 가능 문자열 |
194
- | `@IsBase64()` | Base64 인코딩 |
195
- | `@IsBase32()` | Base32 인코딩 |
196
- | `@IsBase58()` | Base58 인코딩 |
197
- | `@IsHexColor()` | 16진수 색상 코드 |
198
- | `@IsHSL()` | HSL 색상 |
199
- | `@IsRgbColor()` | RGB 색상 |
200
- | `@IsHexadecimal()` | 16진수 문자열 |
201
- | `@IsBIC()` | BIC/SWIFT 코드 |
202
- | `@IsISRC()` | ISRC 코드 |
203
- | `@IsEAN()` | EAN 바코드 |
204
- | `@IsMimeType()` | MIME 타입 |
205
- | `@IsMagnetURI()` | Magnet URI |
206
- | `@IsCreditCard()` | 신용카드 번호 |
207
- | `@IsHash(algorithm)` | 해시 (`md5 \| sha1 \| sha256 \| sha512` 등) |
208
- | `@IsRFC3339()` | RFC 3339 날짜 |
209
- | `@IsMilitaryTime()` | 24시간 형식 (`HH:MM`) |
210
- | `@IsLatitude()` | 위도 (-90 ~ 90) |
211
- | `@IsLongitude()` | 경도 (-180 ~ 180) |
212
- | `@IsEthereumAddress()` | 이더리움 주소 |
213
- | `@IsBtcAddress()` | 비트코인 주소 (P2PKH/P2SH/bech32) |
214
- | `@IsISO4217CurrencyCode()` | ISO 4217 통화 코드 |
215
- | `@IsPhoneNumber()` | E.164 국제 전화번호 |
216
- | `@IsStrongPassword(opts?)` | 강력한 비밀번호 |
217
- | `@IsSemVer()` | 시맨틱 버전 |
218
- | `@IsISO8601()` | ISO 8601 날짜 문자열 |
219
- | `@IsMongoId()` | MongoDB ObjectId |
220
- | `@IsTaxId(locale)` | 국가별 납세자 번호 |
221
-
222
- </details>
328
+ | `arrayContains(values)` | 주어진 요소를 모두 포함 |
329
+ | `arrayNotContains(values)` | 주어진 요소를 포함하지 않음 |
330
+ | `arrayMinSize(n)` | 배열 최소 길이 |
331
+ | `arrayMaxSize(n)` | 배열 최대 길이 |
332
+ | `arrayUnique()` | 중복 없음 |
333
+ | `arrayNotEmpty()` | 배열이 아님 |
223
334
 
224
335
  ### 날짜
225
336
 
226
- | 데코레이터 | 설명 |
337
+ | 규칙 | 설명 |
227
338
  |---|---|
228
- | `@MinDate(date)` | 최소 날짜 |
229
- | `@MaxDate(date)` | 최대 날짜 |
339
+ | `minDate(date)` | 최소 날짜 |
340
+ | `maxDate(date)` | 최대 날짜 |
230
341
 
231
- ### 배열
342
+ ### 객체
232
343
 
233
- | 데코레이터 | 설명 |
344
+ | 규칙 | 설명 |
234
345
  |---|---|
235
- | `@ArrayContains(values)` | 주어진 요소를 모두 포함 |
236
- | `@ArrayNotContains(values)` | 주어진 요소를 포함하지 않음 |
237
- | `@ArrayMinSize(n)` | 배열 최소 길이 |
238
- | `@ArrayMaxSize(n)` | 배열 최대 길이 |
239
- | `@ArrayUnique()` | 중복 없음 |
240
- | `@ArrayNotEmpty()` | 빈 배열이 아님 |
346
+ | `isNotEmptyObject(opts?)` | 최소 1개의 보유 (`{ nullable: true }` 옵션으로 null 값 키 무시) |
347
+ | `isInstance(Class)` | 주어진 클래스에 대한 `instanceof` 검사 |
241
348
 
242
349
  ### 로케일
243
350
 
244
- | 데코레이터 | 설명 |
245
- |---|---|
246
- | `@IsMobilePhone(locale)` | 국가별 이동전화 번호 |
247
- | `@IsPostalCode(locale)` | 국가별 우편번호 |
248
- | `@IsIdentityCard(locale)` | 국가별 신분증 번호 |
249
- | `@IsPassportNumber(locale)` | 국가별 여권 번호 |
250
-
251
- ### Transform & Type
351
+ 로케일 문자열 매개변수를 받는 지역별 검증기입니다.
252
352
 
253
- | 데코레이터 | 설명 |
353
+ | 규칙 | 설명 |
254
354
  |---|---|
255
- | `@Transform(fn, opts?)` | 커스텀 변환 함수 |
256
- | `@Type(fn)` | 중첩 DTO 타입 지정 + 암묵적 변환 |
257
- | `@Nested(fn, opts?)` | `@ValidateNested()` + `@Type(fn)` 축약 + discriminator 지원 |
258
- | `@Expose(opts?)` | 프로퍼티 노출 제어 |
259
- | `@Exclude(opts?)` | 직렬화에서 프로퍼티 제외 |
260
- | `@Schema(schema)` | JSON Schema 메타데이터 부착 (클래스 또는 프로퍼티 레벨) |
355
+ | `isMobilePhone(locale)` | 주어진 로케일의 휴대전화 번호 (예: `'ko-KR'`, `'en-US'`, `'ja-JP'`) |
356
+ | `isPostalCode(locale)` | 주어진 로케일/국가 코드의 우편번호 (예: `'US'`, `'KR'`, `'GB'`) |
357
+ | `isIdentityCard(locale)` | 주어진 로케일의 주민등록번호/신분증 번호 (예: `'KR'`, `'US'`, `'CN'`) |
358
+ | `isPassportNumber(locale)` | 주어진 로케일의 여권 번호 (예: `'US'`, `'KR'`, `'GB'`) |
261
359
 
262
360
  ---
263
361
 
264
- ## ⚙️ Validation Options
362
+ ## 설정
265
363
 
266
- 모든 검증 데코레이터는 마지막 인자로 `ValidationOptions`를 받습니다:
364
+ `deserialize()`/`serialize()` 호출 **이전에** `configure()`를 호출하세요:
267
365
 
268
366
  ```typescript
269
- interface ValidationOptions {
270
- each?: boolean; // 배열의 각 원소에 규칙 적용
271
- groups?: string[]; // 이 규칙이 속하는 그룹
272
- message?: string | ((args: {
273
- property: string;
274
- value: unknown;
275
- constraints: Record<string, unknown>;
276
- }) => string); // 커스텀 에러 메시지
277
- context?: unknown; // 에러에 첨부할 임의 컨텍스트
278
- }
367
+ import { configure } from '@zipbul/baker';
368
+
369
+ configure({
370
+ autoConvert: false, // 암묵적 타입 변환 ("123" -> 123)
371
+ allowClassDefaults: false, // 누락된 키에 클래스 기본값 사용
372
+ stopAtFirstError: false, // 첫 에러에서 중단 또는 전체 수집
373
+ forbidUnknown: false, // 미선언 필드 거부
374
+ debug: false, // 생성된 코드에 필드 제외 주석 포함
375
+ });
279
376
  ```
280
377
 
281
- **예시:**
282
-
283
- ```typescript
284
- class UserDto {
285
- @IsString({ message: '이름은 문자열이어야 합니다' })
286
- name!: string;
287
-
288
- @IsInt({
289
- message: ({ property }) => `${property}는 정수여야 합니다`,
290
- context: { httpStatus: 400 },
291
- })
292
- age!: number;
293
- }
294
- ```
378
+ `configure()`는 `{ warnings: string[] }`을 반환합니다 — auto-seal 이후에 호출된 경우, 영향을 받지 않는 클래스를 알려주는 경고가 포함됩니다.
295
379
 
296
380
  ---
297
381
 
298
- ## 🚨 에러 처리
382
+ ## 에러 처리
299
383
 
300
384
  검증 실패 시 `deserialize()`는 `BakerValidationError`를 throw합니다:
301
385
 
@@ -304,86 +388,50 @@ class BakerValidationError extends Error {
304
388
  readonly errors: BakerError[];
305
389
  readonly className: string;
306
390
  }
307
- ```
308
391
 
309
- 각 에러는 `BakerError` 인터페이스를 따릅니다:
310
-
311
- ```typescript
312
392
  interface BakerError {
313
- readonly path: string; // 필드 경로 ('user.address.city')
314
- readonly code: string; // 에러 코드 ('isString', 'min', 'isEmail')
315
- readonly message?: string; // 커스텀 메시지 (message 옵션 설정 시)
316
- readonly context?: unknown; // 커스텀 컨텍스트 (context 옵션 설정 시)
317
- }
318
- ```
319
-
320
- ---
321
-
322
- ## 📋 배열 검증
323
-
324
- `each: true` 옵션으로 Array, Set, Map의 각 원소에 규칙을 적용합니다:
325
-
326
- ```typescript
327
- class TagsDto {
328
- @IsString({ each: true })
329
- tags!: string[];
393
+ readonly path: string; // 'user.address.city'
394
+ readonly code: string; // 'isString', 'min', 'isEmail'
395
+ readonly message?: string; // 커스텀 메시지
396
+ readonly context?: unknown; // 커스텀 컨텍스트
330
397
  }
331
398
  ```
332
399
 
333
400
  ---
334
401
 
335
- ## 🏷️ 그룹 기반 검증
402
+ ## 중첩 객체
336
403
 
337
- 용도에 따라 다른 규칙을 적용할 수 있습니다:
404
+ `type` 옵션으로 중첩 DTO를 검증합니다:
338
405
 
339
406
  ```typescript
340
- class UserDto {
341
- @IsString({ groups: ['create'] })
342
- name!: string;
343
-
344
- @IsEmail({ groups: ['create', 'update'] })
345
- email!: string;
346
- }
347
-
348
- // 'create' 그룹의 규칙만 검증
349
- const user = await deserialize(UserDto, body, { groups: ['create'] });
350
- ```
351
-
352
- ---
353
-
354
- ## 🪆 중첩 객체
355
-
356
- `@Nested`로 중첩 DTO를 검증합니다:
357
-
358
- ```typescript
359
- import { Nested, IsString } from '@zipbul/baker';
360
-
361
407
  class AddressDto {
362
- @IsString()
408
+ @Field(isString)
363
409
  city!: string;
364
410
  }
365
411
 
366
412
  class UserDto {
367
- @Nested(() => AddressDto)
413
+ @Field({ type: () => AddressDto })
368
414
  address!: AddressDto;
415
+
416
+ // 중첩 DTO 배열
417
+ @Field({ type: () => [AddressDto] })
418
+ addresses!: AddressDto[];
369
419
  }
370
420
  ```
371
421
 
372
- 배열 중첩과 discriminator 다형성:
422
+ ### Discriminator (다형성)
373
423
 
374
424
  ```typescript
375
- class ItemDto {
376
- @IsString()
377
- label!: string;
425
+ class DogDto {
426
+ @Field(isString) breed!: string;
378
427
  }
379
-
380
- class ListDto {
381
- @Nested(() => ItemDto, { each: true })
382
- items!: ItemDto[];
428
+ class CatDto {
429
+ @Field(isBoolean) indoor!: boolean;
383
430
  }
384
431
 
385
432
  class PetOwnerDto {
386
- @Nested(() => DogDto, {
433
+ @Field({
434
+ type: () => DogDto,
387
435
  discriminator: {
388
436
  property: 'type',
389
437
  subTypes: [
@@ -396,105 +444,145 @@ class PetOwnerDto {
396
444
  }
397
445
  ```
398
446
 
447
+ Discriminator는 양방향으로 동작합니다 — `deserialize()`는 프로퍼티 값으로 분기하고, `serialize()`는 `instanceof`로 분기합니다.
448
+
399
449
  ---
400
450
 
401
- ## 🧩 커스텀 규칙
451
+ ## 상속
402
452
 
403
- 코드생성을 지원하는 사용자 정의 검증 규칙을 만들 수 있습니다:
453
+ Baker는 클래스 상속을 지원합니다. 자식 DTO는 부모 클래스의 모든 `@Field()` 데코레이터를 자동으로 상속합니다. 자식 클래스에서 필드를 오버라이드하거나 확장할 수 있습니다:
404
454
 
405
455
  ```typescript
406
- import { createRule } from '@zipbul/baker';
456
+ class BaseDto {
457
+ @Field(isString)
458
+ name!: string;
459
+ }
407
460
 
408
- const isPositiveInt = createRule({
409
- name: 'isPositiveInt',
410
- validate: (value) => Number.isInteger(value) && (value as number) > 0,
411
- emit: (varName, ctx) =>
412
- `if (!Number.isInteger(${varName}) || ${varName} <= 0) ${ctx.fail('isPositiveInt')};`,
413
- });
461
+ class ExtendedDto extends BaseDto {
462
+ @Field(isInt, min(0))
463
+ age!: number;
464
+ // `name`은 BaseDto에서 상속
465
+ }
414
466
  ```
415
467
 
416
468
  ---
417
469
 
418
- ## ⚙️ Seal 옵션
470
+ ## Transform
471
+
472
+ `FieldOptions`의 `transform` 옵션으로 역직렬화/직렬화 시 값을 변환할 수 있습니다. Transform 함수는 **비동기(async)**일 수 있습니다.
419
473
 
420
474
  ```typescript
421
- seal({
422
- enableImplicitConversion: false, // 데코레이터 기반 자동 타입 변환
423
- enableCircularCheck: 'auto', // 순환 참조 감지 ('auto' | true | false)
424
- exposeDefaultValues: false, // 누락된 키에 클래스 기본값 사용
425
- stopAtFirstError: false, // 에러에서 중단 또는 전체 수집
426
- whitelist: false, // 미선언 필드 거부
427
- debug: false, // 생성된 소스를 검사용으로 저장
428
- });
475
+ class UserDto {
476
+ @Field(isString, {
477
+ transform: ({ value, direction }) => {
478
+ return direction === 'deserialize'
479
+ ? (value as string).trim().toLowerCase()
480
+ : value;
481
+ },
482
+ })
483
+ email!: string;
484
+
485
+ @Field(isString, {
486
+ transform: async ({ value }) => {
487
+ return await someAsyncOperation(value);
488
+ },
489
+ transformDirection: 'deserializeOnly',
490
+ })
491
+ data!: string;
492
+ }
429
493
  ```
430
494
 
431
495
  ---
432
496
 
433
- ## 📐 JSON Schema
434
-
435
- DTO에서 JSON Schema Draft 2020-12를 생성합니다:
497
+ ## 커스텀 규칙
436
498
 
437
499
  ```typescript
438
- import { toJsonSchema } from '@zipbul/baker';
500
+ import { createRule } from '@zipbul/baker';
439
501
 
440
- const schema = toJsonSchema(CreateUserDto);
441
- // { $schema: "https://json-schema.org/draft/2020-12/schema", type: "object", properties: { ... } }
502
+ const isPositiveInt = createRule({
503
+ name: 'isPositiveInt',
504
+ validate: (value) => Number.isInteger(value) && (value as number) > 0,
505
+ });
506
+
507
+ class Dto {
508
+ @Field(isPositiveInt)
509
+ count!: number;
510
+ }
442
511
  ```
443
512
 
444
- 옵션:
513
+ ---
514
+
515
+ ## 클래스 레벨 JSON Schema 메타데이터
516
+
517
+ `collectClassSchema()`를 사용하여 DTO에 클래스 레벨 JSON Schema 메타데이터(title, description 등)를 부착합니다. 이 메타데이터는 `toJsonSchema()` 출력에 병합됩니다.
518
+
519
+ > `collectClassSchema`는 `src/collect.ts`에서 제공하는 저수준 API입니다. 서브패스 익스포트로는 사용할 수 없으며 직접 import해야 합니다.
445
520
 
446
521
  ```typescript
447
- toJsonSchema(CreateUserDto, {
448
- direction: 'deserialize', // 'deserialize' | 'serialize' — @Expose/@Exclude 방향 필터링
449
- groups: ['create'], // 그룹별 규칙/필드 필터링
522
+ import { collectClassSchema } from '@zipbul/baker/src/collect';
523
+
524
+ class CreateUserDto {
525
+ @Field(isString) name!: string;
526
+ @Field(isEmail()) email!: string;
527
+ }
528
+
529
+ collectClassSchema(CreateUserDto, {
530
+ title: 'CreateUserRequest',
531
+ description: '새 사용자 생성을 위한 페이로드',
450
532
  });
451
533
  ```
452
534
 
453
- `@Schema()`로 추가 JSON Schema 메타데이터를 부착합니다:
535
+ 프로퍼티 레벨 스키마 오버라이드는 `@Field()`의 `schema` 옵션을 사용합니다:
454
536
 
455
537
  ```typescript
456
- @Schema({ title: 'CreateUser', description: '새 사용자 생성' })
457
- class CreateUserDto {
458
- @IsString()
459
- @Schema({ description: '표시 이름', examples: ['Alice'] })
538
+ class Dto {
539
+ @Field(isString, minLength(1), {
540
+ schema: { description: '사용자 표시 이름', minLength: 5 },
541
+ })
460
542
  name!: string;
461
543
  }
462
544
  ```
463
545
 
464
546
  ---
465
547
 
466
- ## 📂 서브패스 익스포트
548
+ ## JSON Schema
467
549
 
468
- | 임포트 경로 | 용도 |
469
- |---|---|
470
- | `@zipbul/baker` | 메인 API: `seal`, `deserialize`, `serialize`, `toJsonSchema`, 모든 데코레이터 |
471
- | `@zipbul/baker/decorators` | 데코레이터만 |
472
- | `@zipbul/baker/rules` | 원시 규칙 객체 |
473
- | `@zipbul/baker/symbols` | 내부 심볼 |
550
+ DTO에서 JSON Schema Draft 2020-12를 생성합니다:
551
+
552
+ ```typescript
553
+ import { toJsonSchema } from '@zipbul/baker';
554
+
555
+ const schema = toJsonSchema(CreateUserDto, {
556
+ direction: 'deserialize', // 'deserialize' | 'serialize'
557
+ groups: ['create'], // 그룹별 필터링
558
+ onUnmappedRule: (name) => { /* 스키마 매핑이 없는 커스텀 규칙 */ },
559
+ });
560
+ ```
474
561
 
475
562
  ---
476
563
 
477
- ## 🔍 동작 원리
564
+ ## 동작 원리
478
565
 
479
566
  ```
480
- ┌─────────────┐ ┌──────────────┐ ┌─────────────────────┐
481
- │ 데코레이터 │ ──▶ │ seal() │ ──▶ │ new Function() 코드
482
- │ (메타데이터) │ │ 앱 시작 시 │ │ (인라인 코드생성) │
483
- └─────────────┘ └──────────────┘ └──────────┬──────────┘
484
-
485
- ┌──────────▼──────────┐
486
- │ deserialize() / │
487
- │ serialize() │
488
- │ (생성된 코드 실행) │
489
- └─────────────────────┘
567
+ Decorators (@Field) auto-seal (첫 호출) deserialize() / serialize()
568
+ 메타데이터 -> new Function() 코드생성 -> 생성된 코드 실행
490
569
  ```
491
570
 
492
- 1. **데코레이터**가 클래스 프로퍼티에 검증 메타데이터를 부착합니다
493
- 2. **`seal()`**이 모든 메타데이터를 읽고, 순환 참조를 분석하고, `new Function()`으로 인라인 JavaScript 함수를 생성합니다
494
- 3. **`deserialize()` / `serialize()`**가 생성된 함수를 실행합니다 — 해석 루프 없이, 직선적인 최적화 코드만 실행
571
+ 1. `@Field()`가 정의 시점에 클래스 프로퍼티에 검증 메타데이터를 부착합니다
572
+ 2. 첫 `deserialize()`/`serialize()` 호출이 **auto-seal**을 트리거합니다 — 모든 메타데이터를 읽고, 순환 참조를 분석하고, `new Function()`으로 최적화된 JavaScript 함수를 생성합니다
573
+ 3. 이후 호출은 생성된 함수를 직접 실행합니다 — 해석 루프 없음
574
+
575
+ ---
576
+
577
+ ## 서브패스 익스포트
578
+
579
+ | 임포트 경로 | 용도 |
580
+ |---|---|
581
+ | `@zipbul/baker` | 메인 API: `deserialize`, `serialize`, `configure`, `toJsonSchema`, `Field`, `arrayOf`, `createRule` |
582
+ | `@zipbul/baker/rules` | 규칙 함수 및 상수: `isString`, `min()`, `isEmail()`, `arrayOf()` 등 |
495
583
 
496
584
  ---
497
585
 
498
- ## 📄 라이선스
586
+ ## 라이선스
499
587
 
500
- [MIT](./LICENSE) © [Junhyung Park](https://github.com/parkrevil)
588
+ [MIT](./LICENSE)