npm package: @ripetchor/vivi
Description: WARNING: This package is primarily intended for personal use. It may contain bugs or unexpected behavior. Use at your own risk.
Author: ripetchor
Latest version: 0.0.13

# Vivi

WARNING: This package is primarily intended for personal use. It may contain bugs or unexpected behavior. Use at your own risk.

## Primitive Validators

- `v.string(options?)`
- `v.number(options?)`
- `v.boolean(options?)`
- `v.bigint(options?)`
- `v.null(options?)`
- `v.undefined(options?)`

## Other Validators

- `v.unknown()`
- `v.fallback(validator, defaultValue)`
- `v.email(options?)`
- `v.nullable(validator)`
- `v.optional(validator)`
- `v.literal(value, options?)`
- `v.date(options?)`
- `v.union([validators], options?)`
- `v.instance(Class, options?)`
- `v.object(shape, options?)`
- `v.array(validator, options?)`
- `v.map(keyValidator, valueValidator, options?)`
- `v.set(validator, options?)`
- `v.refine(validator, refinementFns[], options?)`
- `v.pipe(validator, ...transformFns)`

## Usage Examples

```ts
import { v } from '@ripetchor/vivi'
// or 
import v from '@ripetchor/vivi'
 // or 
import { stringValidator, numberValidator, refine /* etc. */ } from '@ripetchor/vivi'
```

### Primitive validation

```ts
const name = v.string().parse('Alice');
console.log(name); // Alice

const age = v.number().parse(30);
console.log(age); // 30

const active = v.boolean().parse(true);
console.log(active); // true
```

### Nullable and optional

```ts
const nullableName = v.nullable(v.string()).parse(null);
console.log(nullableName); // null

const optionalAge = v.optional(v.number()).parse(undefined);
console.log(optionalAge); // undefined
```

### Array validation

```ts
const numbers = v.array(v.number()).parse([1, 2, 3]);
console.log(numbers); // [1, 2, 3]

const users = v.array(v.object({ name: v.string(), age: v.number() })).parse([
  { name: "Lana", age: 28 },
  { name: "Mikaela", age: 25, friends: ["Nicole"] },
  { name: "Elsa", age: 28, job: "dev" },
]);
console.log(users); // [{ name: 'Lana', age: 28 }, { name: 'Mikaela', age: 25 }, { name: 'Elsa', age: 28 }]
```

### Object validation

```ts
const exactUser = v.object({ name: v.string(), age: v.number() }, { mode: 'exact' }).parse({ name: 'Alice', age: 25 });
console.log(exactUser); // { name: 'Alice', age: 25 }

const stripUser = v.object({ name: v.string() }, { mode: 'strip' }).parse({ name: 'Bob', age: 30 });
console.log(stripUser); // { name: 'Bob' }

const passthroughUser = v.object({ name: v.string() }, { mode: 'passthrough' }).parse({ name: 'Charlie', age: 40 });
console.log(passthroughUser); // { name: 'Charlie', age: 40 }
```

### Union validation

```ts
const value = v.union([v.string(), v.number()]).parse('Hello');
console.log(value); // 'Hello'
```

### Literal validation

```ts
const l1 = v.literal("yes").parse("yes");
console.log(l1); // 'yes'

v.literal("yes").parse("yeS");
// error
```

### Refinements

```ts
const schema = refine(
  v.string(),
  [
    (value) => value.length === 5 || "length must be 5",
    (value) => /^\d+$/.test(value) || "contains only numbers",
  ],
  { abort: true },
);

const result = schema.safeParse("abc");

if (!result.success) {
  console.log(result.error.issues); // [ { path: [], message: 'length must be 5' } ]
} else {
  console.log(result.data);
}

// =========================================

const userSchema = v.object({
  password: v.string(),
  confirmPassword: v.string(),
});

const refinedUserSchema = v.refine(userSchema, [
  (user) =>
    user.password === user.confirmPassword || {
      path: ["confirmPassword"],
      message: "Passwords do not match",
    },
]);

const result = refinedUserSchema.safeParse({
  password: "secret123",
  confirmPassword: "secret321",
});
console.log(result); // [ { path: ["confirmPassword"], message: "Passwordsdo not match"} ]
```

- `true` → validation passes
- `false` → generic "Refinement failed"
- `string` → error message with empy path
- `{ message, path }` → **precise error targeting**

### Fallback

```ts
const fallbackValue = v.fallback(v.number(), 42).parse('invalid');
console.log(fallbackValue); // 42
```
### Email validation

```ts
const email = v.email().parse('test@example.com');
console.log(email); // 'test@example.com'
```

### Date validation

```ts
const birthday = v.date().parse(new Date('1990-01-01'));
console.log(birthday); // 1990-01-01T00:00:00.000Z
```

### Pipe transformations

```ts
const transformed = v.pipe(
  v.string(),
  (s: string) => s.trim(),
  (s: string) => s.toUpperCase(),
).parse(' hello ');
console.log(transformed); // 'HELLO'
```

### Safe parsing

```ts
const result1 = v.number().safeParse(123);
console.log(result1); // { success: true, data: 123 }

const result2 = v.number().safeParse('not a number');
console.log(result2); // { success: false, error: ViviError }
```

### Abort on first issue

```ts
try {
  v.array(v.number(), { abort: true }).parse([1, "x", 3, "y"]);
} catch (err) {
  if (err instanceof ViviError) {
    console.log("Aborted on first error:", err.issues);
  }
}
```

### Pattern matching (`match`)

`match` is a alternative to `if / else` for handling the result of `safeParse`.

- enforces handling of all possible states

- provides proper type narrowing

- eliminates if (result.success) checks

```ts
const schema = v.string();

const result = schema.safeParse("hello");

v.match(
  result,
  (data) => {
    console.log("Success:", data);
  },
  (error) => {
    console.log("Validation failed:", error.issues);
  },
);

// Еhe return type is inferred from the callbacks.
const value = v.match(
  result,
  (data) => data.toUpperCase(),
  () => "DEFAULT",
);
// value: string
```

### Type inference

```ts
const userValidator = v.object({ name: v.string(), age: v.number() });
type User = v.infer<typeof userValidator>;

const user: User = { name: 'Alice', age: 25 };
console.log(user); // { name: 'Alice', age: 25 }
```
### Issues utils

```ts
import { v, flattenErrors, issuesToObject, issuesToString } from "@ripetchor/vivi";

const schema = v.object({
  user: v.object({
    name: v.string(),
    email: v.email(),
    address: v.object({
      street: v.string(),
      city: v.string(),
      zip: v.string(),
    }),
  }),
  credentials: v.object({
    password: v.string(),
    confirmPassword: v.string(),
  }),
  agreeToTerms: v.literal(true),
});

const invalidData = {
  user: {
    name: 1,
    email: 2,
    address: {
      street: 3,
      city: 4,
      zip: 5,
    },
  },
  credentials: {
    password: 6,
    confirmPassword: 7,
  },
  agreeToTerms: false,
};

const result = schema.safeParse(invalidData);

console.log(issuesToString(result.error.issues));
// user.name: Expected string
// user.email: Expected string
// user.address.street: Expected string
// user.address.city: Expected string
// user.address.zip: Expected string
// credentials.password: Expected string
// credentials.confirmPassword: Expected string
// agreeToTerms: Expected literal true

console.log(issuesToObject(result.error.issues));
// {
//   user: {
//     name: 'Expected string',
//     email: 'Expected string',
//     address: {
//       street: 'Expected string',
//       city: 'Expected string',
//       zip: 'Expected string'
//     }
//   },
//   credentials: { password: 'Expected string', confirmPassword: 'Expected string' },
//   agreeToTerms: 'Expected literal true'
// }

console.log(flattenErrors(result.error.issues));
// {
//   'user.name': 'Expected string',
//   'user.email': 'Expected string',
//   'user.address.street': 'Expected string',
//   'user.address.city': 'Expected string',
//   'user.address.zip': 'Expected string',
//   'credentials.password': 'Expected string',
//   'credentials.confirmPassword': 'Expected string',
//   agreeToTerms: 'Expected literal true'
// }
```

## Custom validator

```ts
import { customValidator, ViviError, type ParseFn, type Path } from '@ripetchor/vivi';


interface User {
  name: string;
  age: number;
  email: string;
}

const parseFn: ParseFn<User> = (input: unknown, path: Path): User => {
  if (typeof input !== "object" || input === null) {
    throw ViviError.single("Expected object", path);
  }

  const obj = input as Record<string, unknown>;

  const name = obj.name;
  if (typeof name !== "string" || name.trim() === "") {
    throw ViviError.single("Name must be a non-empty string", [...path, "name"]);
  }

  const age = obj.age;
  if (typeof age !== "number" || age < 0) {
    throw ViviError.single("Age must be a non-negative number", [...path, "age"]);
  }

  const email = obj.email;
  if (typeof email !== "string" || !/^\S+@\S+\.\S+$/.test(email)) {
    throw ViviError.single("Invalid email", [...path, "email"]);
  }

  return { name: name.trim(), age, email };
};

const userValidator = customValidator<User>(parseFn);

const result = userValidator.safeParse({ name: " Alice ", age: 25, email: "alice@example.com" });

if (result.success) {
  console.log(result.data); // { name: "Alice", age: 25, email: "alice@example.com" }
}
```