# bookish-potato-dto

A TypeScript DTO (Data Transfer Object) parsing and validation library.
Define a schema once — get runtime validation and a fully inferred TypeScript type for free.

## Table of Contents
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Field builders](#field-builders)
- [All field options](#all-field-options)
- [Composing and extending DTOs](#composing-and-extending-dtos)
- [Nested DTOs](#nested-dtos)
- [Enum fields](#enum-fields)
- [Custom parsers](#custom-parsers)
- [OpenAPI Schema Generation](#openapi-schema-generation)
- [Type inference with InferDto](#type-inference-with-inferdto)
- [Use cases](#use-cases)
  - [REST API body parsing](#rest-api-body-parsing)
  - [Environment / config parsing](#environment--config-parsing)
  - [Data transformation](#data-transformation)
- [Feature Requests & Bug Reports](#feature-requests-bugs-reports-and-contributions)

---

## Installation

```bash
npm install bookish-potato-dto
```

No `tsconfig.json` flags required. Works in Node.js, Bun, Deno, and any ESM environment.

---

## Quick Start

```typescript
import { defineDto, field, InferDto, parseObject } from 'bookish-potato-dto';

const PersonDto = defineDto({
  name:     field.string(),
  age:      field.integer({ strictDataTypes: true }),
  height:   field.number(),
  weight:   field.number({ defaultValue: 70 }),
  eyeColor: field.string({ isOptional: true }),
  active:   field.boolean(),
});

// Derive the TypeScript type — no duplication
type PersonDto = InferDto<typeof PersonDto>;

const person = parseObject(PersonDto, {
  name: 'John Doe',
  age: 30,
  height: 180.5,
  active: true,
});

// person.name     === 'John Doe'
// person.weight   === 70         (defaultValue applied)
// person.eyeColor === undefined  (optional, not provided)
```

---

## Field builders

| Builder | Description |
|---|---|
| `field.string(opts?)` | String field |
| `field.number(opts?)` | Floating-point number |
| `field.integer(opts?)` | Integer |
| `field.boolean(opts?)` | Boolean |
| `field.enum(E, opts?)` | Enum value |
| `field.date(opts?)` | Date instance or ISO string |
| `field.regex(re, opts?)` | String validated against a regex |
| `field.array(type, opts?)` | Array of `'string'`, `'number'`, or `'boolean'` |
| `field.arrayDto(Dto, opts?)` | Array of nested DTOs |
| `field.dto(Dto, opts?)` | Single nested DTO |
| `field.custom(opts)` | Custom parser instance |

---

## All field options

### Common options (all fields)

| Option | Type | Description |
|---|---|---|
| `isOptional` | `boolean` | Field may be absent. TypeScript type becomes `T \| undefined`. |
| `isNullable` | `boolean` | Field may be `null`. TypeScript type becomes `T \| null`. |
| `defaultValue` | `T \| null` | Fallback value when field is absent. |
| `useDefaultValueOnParseError` | `boolean` | Use `defaultValue` instead of throwing on bad input. |
| `mapFrom` | `string` | Read from a differently-named key in the raw input. |
| `parsingErrorMessage` | `(key, value, error) => string` | Custom error message function. |
| `openApi` | `OpenApiSchema` | Manual overrides or additional metadata for OpenAPI generation. |

### String options (`field.string`)

| Option | Type | Description |
|---|---|---|
| `minLength` | `number` | Minimum string length. |
| `maxLength` | `number` | Maximum string length. |

### Number and integer options (`field.number`, `field.integer`)

| Option | Type | Description |
|---|---|---|
| `strictDataTypes` | `boolean` | Disable string→number coercion. |
| `minValue` | `number` | Minimum value. |
| `maxValue` | `number` | Maximum value. |

### Boolean options (`field.boolean`)

| Option | Type | Description |
|---|---|---|
| `strictDataTypes` | `boolean` | Disable `"true"`/`"false"` string coercion. |

### Array options (`field.array`)

| Option | Type | Description |
|---|---|---|
| `minLength` | `number` | Minimum array length. |
| `maxLength` | `number` | Maximum array length. |
| `strictDataTypes` | `boolean` | Disable coercion for items. |
| `stringsLength` | `{ minLength?, maxLength? }` | Per-item length constraint (string arrays). |
| `numbersRange` | `{ minValue?, maxValue? }` | Per-item range constraint (number arrays). |

### Array of DTO options (`field.arrayDto`)

| Option | Type | Description |
|---|---|---|
| `minLength` | `number` | Minimum array length. |
| `maxLength` | `number` | Maximum array length. |

---

## Composing and extending DTOs

Schemas are plain objects. Use the spread operator to compose them:

```typescript
const PersonDto = defineDto({
  name: field.string(),
  age:  field.integer(),
});

// Extend with new fields
const EmployeeDto = defineDto({
  ...PersonDto.fields,
  position: field.string(),
});

// Three-way composition
const TimestampedDto = defineDto({ createdAt: field.date() });
const AuditedEmployeeDto = defineDto({
  ...EmployeeDto.fields,
  ...TimestampedDto.fields,
});
```

---

## Nested DTOs

```typescript
const AddressDto = defineDto({
  street: field.string(),
  city:   field.string(),
});

const PersonDto = defineDto({
  name:      field.string(),
  address:   field.dto(AddressDto),          // single nested DTO
  addresses: field.arrayDto(AddressDto),     // array of nested DTOs
});
```

---

## Enum fields

```typescript
enum Role { Admin = 'admin', User = 'user', Guest = 'guest' }

const UserDto = defineDto({
  name: field.string(),
  role: field.enum(Role),
});
```

---

## Custom parsers

```typescript
class CsvParser {
  parse(value: unknown): string[] {
    if (typeof value !== 'string') throw new Error('not a string');
    return value.split(',').filter(Boolean);
  }
}

const ConfigDto = defineDto({
  port:     field.integer({ mapFrom: 'PORT', defaultValue: 3000 }),
  origins:  field.custom({ mapFrom: 'ALLOWED_ORIGINS', parser: new CsvParser() }),
});
```

---

## OpenAPI Schema Generation

`bookish-potato-dto` can automatically generate OpenAPI 3.0/3.1 compatible schemas from your DTO definitions. It infers types, constraints (like `minLength`, `minimum`), nullability, and default values.

```typescript
import { defineDto, field, generateOpenApi } from 'bookish-potato-dto';

const UserDto = defineDto({
  id:    field.string({ openApi: { format: 'uuid' } }),
  email: field.string({ openApi: { format: 'email', description: 'User contact email' } }),
  age:   field.integer({ minValue: 18 }),
});

const { schema, refs } = generateOpenApi(UserDto, {
  // Optional: provide meaningful names for $ref resolution
  nameResolver: (dto) => dto === UserDto ? 'User' : dto._uuid
});

// schema: { $ref: '#/components/schemas/User' }
// refs.User: {
//   type: 'object',
//   properties: {
//     id: { type: 'string', format: 'uuid' },
//     email: { type: 'string', format: 'email', description: 'User contact email' },
//     age: { type: 'integer', minimum: 18 }
//   },
//   required: ['id', 'email', 'age']
// }
```

### Manual Overrides

Use the `openApi` option on any field to add descriptions, examples, or override the automatically inferred schema.

```typescript
field.string({
  minLength: 5,
  openApi: {
    description: 'A unique username',
    example: 'john_doe',
    maxLength: 20
  }
})
```

---

## Type inference with InferDto

`InferDto<T>` produces the full TypeScript type from a `DtoDefinition`. Optional fields
(`isOptional: true`) become `T | undefined`. Nullable fields (`isNullable: true`) become `T | null`.

```typescript
const PersonDto = defineDto({
  name:     field.string(),
  age:      field.integer(),
  nickname: field.string({ isOptional: true }),
  score:    field.number({ isNullable: true }),
});

type PersonDto = InferDto<typeof PersonDto>;
// Equivalent to:
// {
//   name:      string;
//   age:       number;
//   nickname?: string;
//   score:     number | null;
// }
```

---

## Use cases

### REST API body parsing

```typescript
enum Roles { Admin = 'admin', User = 'user' }

const RoleDto = defineDto({
  name: field.string(),
  role: field.enum(Roles),
});

const UserDto = defineDto({
  name:  field.string(),
  email: field.string(),
  age:   field.integer(),
  role:  field.dto(RoleDto),
});

// In your request handler:
const user = parseObject(UserDto, req.body);
```

### Environment / config parsing

```typescript
enum LogLevel { Info = 'info', Debug = 'debug', Error = 'error' }

const ConfigDto = defineDto({
  port:           field.integer({ mapFrom: 'PORT', defaultValue: 3000 }),
  logLevel:       field.enum(LogLevel, { mapFrom: 'LOG_LEVEL', defaultValue: LogLevel.Info }),
  dbSecret:       field.string({ mapFrom: 'DB_SECRET' }),
  allowedOrigins: field.custom({ mapFrom: 'ALLOWED_ORIGINS', parser: new CsvParser() }),
});

export const config = parseObject(ConfigDto, process.env);
```

### Data transformation

```typescript
const PersonDto = defineDto({
  id:     field.string({ mapFrom: 'uuid' }),
  name:   field.string(),
  status: field.string({ defaultValue: 'active' }),
});

const person = parseObject(PersonDto, { uuid: '123', name: 'Alice' });
// person.id === '123'
// person.status === 'active'
```

---

## Feature Requests, Bugs Reports, and Contributions

Please use the [GitHub Issues](https://github.com/andrei-trukhin/bookish-potato-dto-issues)
repository to report bugs, request features, or ask questions.

