✨ feat: implement core Cereale library for JSON mapping and validation
- 🎨 add Spring-like decorators (@JsonSerialize, @JsonDeserialize, etc.) - ⚙️ implement JsonMapper and metadata storage for transformations - 🧪 add comprehensive test suite using Vitest - 📝 add README, CONTRIBUTING, and API documentation - 👷 setup GitHub Actions CI workflow - 🔧 configure TypeScript and project settings
This commit is contained in:
@@ -22,3 +22,6 @@ yarn-error.log*
|
|||||||
|
|
||||||
# Coverage
|
# Coverage
|
||||||
/coverage/
|
/coverage/
|
||||||
|
|
||||||
|
# ESLint
|
||||||
|
.eslintcache
|
||||||
|
|||||||
+5
-5
@@ -1,6 +1,6 @@
|
|||||||
# Contributing to Optimus
|
# Contributing to Cereale
|
||||||
|
|
||||||
Thank you for your interest in contributing to Optimus! We welcome all contributions, from bug reports to new features and documentation improvements.
|
Thank you for your interest in contributing to Cereale! We welcome all contributions, from bug reports to new features and documentation improvements.
|
||||||
|
|
||||||
## Development Setup
|
## Development Setup
|
||||||
|
|
||||||
@@ -8,8 +8,8 @@ To get started with development, follow these steps:
|
|||||||
|
|
||||||
1. **Clone the repository:**
|
1. **Clone the repository:**
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/Avalon-Vanguard/optimus.git
|
git clone https://github.com/Avalon-Vanguard/cereale.git
|
||||||
cd optimus
|
cd cereale
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Install dependencies:**
|
2. **Install dependencies:**
|
||||||
@@ -56,4 +56,4 @@ Please use GitHub Issues to report bugs or suggest new features. Provide as much
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
By contributing to this project, you agree to abide by the terms of the ISC License.
|
By contributing to this project, you agree to abide by the terms of the MIT License.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Optimus
|
# Cereale
|
||||||
|
|
||||||
Optimus is a lightweight TypeScript library that provides Spring-like decorators for JSON mapping and validation. Built with ZERO external dependencies, it simplifies the process of converting between plain JSON and class instances with full validation support.
|
Cereale is a lightweight TypeScript library that provides Spring-like decorators for JSON mapping and validation. Built with ZERO external dependencies, it simplifies the process of converting between plain JSON and class instances with full validation support.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ Optimus is a lightweight TypeScript library that provides Spring-like decorators
|
|||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install optimus
|
npm install cereale
|
||||||
```
|
```
|
||||||
|
|
||||||
Make sure to enable `experimentalDecorators` and `emitDecoratorMetadata` in your `tsconfig.json`:
|
Make sure to enable `experimentalDecorators` and `emitDecoratorMetadata` in your `tsconfig.json`:
|
||||||
@@ -24,7 +24,7 @@ Make sure to enable `experimentalDecorators` and `emitDecoratorMetadata` in your
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
"target": "ES6"
|
"target": "ES2025"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -47,7 +47,7 @@ import {
|
|||||||
JsonPolymorphic,
|
JsonPolymorphic,
|
||||||
JsonSerializer,
|
JsonSerializer,
|
||||||
JsonDeserializer
|
JsonDeserializer
|
||||||
} from 'optimus';
|
} from 'cereale';
|
||||||
|
|
||||||
// Custom Date Serializer
|
// Custom Date Serializer
|
||||||
class DateSerializer implements JsonSerializer<Date, string> {
|
class DateSerializer implements JsonSerializer<Date, string> {
|
||||||
@@ -96,22 +96,22 @@ class Library {
|
|||||||
|
|
||||||
### 2. Map JSON with Validation
|
### 2. Map JSON with Validation
|
||||||
|
|
||||||
Use `JsonMapper` to handle the conversion process.
|
Use standalone utility functions to handle the conversion process directly.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { JsonMapper, JsonValidationError } from 'optimus';
|
import { fromJson, toJson, JsonValidationError } from 'cereale';
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const json = '{"name": "Central Library", "items": [{"type": "book", "title": "The Great Gatsby", "author": "F. Scott Fitzgerald", "publishedAt": "1925-04-10"}]}';
|
const json = '{"name": "Central Library", "items": [{"type": "book", "title": "The Great Gatsby", "author": "F. Scott Fitzgerald", "publishedAt": "1925-04-10"}]}';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Deserialize JSON to Class Instance
|
// Deserialize JSON to Class Instance
|
||||||
const library = await JsonMapper.fromJson(Library, json);
|
const library = await fromJson(Library, json);
|
||||||
console.log(library.name); // "Central Library"
|
console.log(library.name); // "Central Library"
|
||||||
console.log(library.items[0] instanceof Book); // true
|
console.log(library.items[0] instanceof Book); // true
|
||||||
|
|
||||||
// Serialize Class Instance back to JSON
|
// Serialize Class Instance back to JSON
|
||||||
const outputJson = await JsonMapper.toJson(library);
|
const outputJson = await toJson(library);
|
||||||
console.log(outputJson);
|
console.log(outputJson);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof JsonValidationError) {
|
if (error instanceof JsonValidationError) {
|
||||||
@@ -121,6 +121,20 @@ async function main() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 3. Modern Web Frameworks (Request Integration)
|
||||||
|
|
||||||
|
Cereale is compatible with Fetch-based frameworks like Hono, Next.js, and Remix. Use the `fromRequest` async helper.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { fromRequest, toPlain } from 'cereale';
|
||||||
|
|
||||||
|
// Hono Example
|
||||||
|
app.post('/books', async (c) => {
|
||||||
|
const book = await fromRequest(Book, c.req.raw);
|
||||||
|
return c.json(await toPlain(book));
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
## API Reference
|
## API Reference
|
||||||
|
|
||||||
### Decorators
|
### Decorators
|
||||||
@@ -166,10 +180,48 @@ Most validation decorators accept an optional `ValidationOptions` object:
|
|||||||
|
|
||||||
### Utilities
|
### Utilities
|
||||||
|
|
||||||
- `JsonMapper.toJson(obj: any, options?)`: Validates and serializes an instance to a JSON string.
|
- `toJson(obj: any)`: Validates and serializes an instance to a JSON string (Returns `Promise<string>`).
|
||||||
- `JsonMapper.toPlain(obj: any, options?)`: Validates and transforms an instance to a plain object.
|
- `toPlain(obj: any)`: Validates and transforms an instance to a plain object (Returns `Promise<any>`).
|
||||||
- `JsonMapper.fromJson(clazz: ClassConstructor, json: string, options?)`: Parses JSON and transforms it to a validated class instance.
|
- `fromJson(clazz: ClassConstructor, json: string)`: Parses JSON and transforms it to a validated class instance (Returns `Promise<T>`).
|
||||||
- `JsonMapper.toInstance(clazz: ClassConstructor, plain: any, options?)`: Transforms a plain object to a validated class instance.
|
- `toInstance(clazz: ClassConstructor, plain: any)`: Transforms a plain object to a validated class instance (Returns `Promise<T>`).
|
||||||
|
- `fromRequest(clazz: ClassConstructor, request: Request)`: Extracts JSON from a Fetch `Request` and transforms it to a validated instance (Returns `Promise<T>`).
|
||||||
|
- `validate(obj: any)`: Performs full validation on an object/instance (Returns `Promise<ValidationError[]>`).
|
||||||
|
|
||||||
|
## Framework Integrations
|
||||||
|
|
||||||
|
Cereale is designed to be compatible with all trending web frameworks.
|
||||||
|
|
||||||
|
### Hono / Next.js / Cloudflare Workers
|
||||||
|
Use `fromRequest` for seamless integration with the Fetch `Request` API.
|
||||||
|
|
||||||
|
### NestJS
|
||||||
|
You can use Cereale inside your controllers for explicit mapping and validation without needing `reflect-metadata`.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { toInstance } from 'cereale';
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
async create(@Body() body: any) {
|
||||||
|
const user = await toInstance(User, body);
|
||||||
|
return this.userService.create(user);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Express / Fastify
|
||||||
|
Easily integrate with traditional Node.js frameworks.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { toInstance, toPlain } from 'cereale';
|
||||||
|
|
||||||
|
app.post('/user', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const user = await toInstance(User, req.body);
|
||||||
|
res.json(await toPlain(user));
|
||||||
|
} catch (err) {
|
||||||
|
res.status(400).json(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
@@ -177,4 +229,4 @@ Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute t
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Optimus is licensed under the [ISC License](LICENSE).
|
Cereale is licensed under the [MIT License](LICENSE).
|
||||||
File diff suppressed because one or more lines are too long
+9
-9
@@ -3,14 +3,14 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Optimus - Spring-like JSON Mapping & Validation for TypeScript</title>
|
<title>Cereale - Spring-like JSON Mapping & Validation for TypeScript</title>
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.css">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/theme/dracula.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/theme/dracula.min.css">
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/codemirror.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/javascript/javascript.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.2/mode/javascript/javascript.min.js"></script>
|
||||||
<script src="optimus.js"></script>
|
<script src="cereale.js"></script>
|
||||||
<style>
|
<style>
|
||||||
.CodeMirror {
|
.CodeMirror {
|
||||||
height: 400px;
|
height: 400px;
|
||||||
@@ -29,14 +29,14 @@
|
|||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div class="flex justify-between h-16 items-center">
|
<div class="flex justify-between h-16 items-center">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="text-2xl font-bold gradient-text">Optimus</span>
|
<span class="text-2xl font-bold gradient-text">Cereale</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden md:block">
|
<div class="hidden md:block">
|
||||||
<div class="ml-10 flex items-baseline space-x-4">
|
<div class="ml-10 flex items-baseline space-x-4">
|
||||||
<a href="#features" class="text-slate-600 hover:text-indigo-600 px-3 py-2 font-medium">Features</a>
|
<a href="#features" class="text-slate-600 hover:text-indigo-600 px-3 py-2 font-medium">Features</a>
|
||||||
<a href="#playground" class="text-slate-600 hover:text-indigo-600 px-3 py-2 font-medium">Playground</a>
|
<a href="#playground" class="text-slate-600 hover:text-indigo-600 px-3 py-2 font-medium">Playground</a>
|
||||||
<a href="#docs" class="text-slate-600 hover:text-indigo-600 px-3 py-2 font-medium">Docs</a>
|
<a href="#docs" class="text-slate-600 hover:text-indigo-600 px-3 py-2 font-medium">Docs</a>
|
||||||
<a href="https://github.com/Avalon-Vanguard/optimus" class="bg-indigo-600 text-white px-4 py-2 rounded-md font-medium hover:bg-indigo-700 transition">GitHub</a>
|
<a href="https://github.com/Avalon-Vanguard/cereale" class="bg-indigo-600 text-white px-4 py-2 rounded-md font-medium hover:bg-indigo-700 transition">GitHub</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
<div class="flex justify-center gap-4">
|
<div class="flex justify-center gap-4">
|
||||||
<a href="#playground" class="bg-indigo-600 text-white px-8 py-3 rounded-lg text-lg font-semibold hover:bg-indigo-700 shadow-lg shadow-indigo-200 transition">Try on the fly</a>
|
<a href="#playground" class="bg-indigo-600 text-white px-8 py-3 rounded-lg text-lg font-semibold hover:bg-indigo-700 shadow-lg shadow-indigo-200 transition">Try on the fly</a>
|
||||||
<code class="bg-slate-800 text-slate-100 px-6 py-3 rounded-lg text-lg font-mono flex items-center">
|
<code class="bg-slate-800 text-slate-100 px-6 py-3 rounded-lg text-lg font-mono flex items-center">
|
||||||
npm install optimus
|
npm install cereale
|
||||||
</code>
|
</code>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
<!-- Features -->
|
<!-- Features -->
|
||||||
<section id="features" class="py-20 bg-white">
|
<section id="features" class="py-20 bg-white">
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<h2 class="text-3xl font-bold text-center mb-16">Why Optimus?</h2>
|
<h2 class="text-3xl font-bold text-center mb-16">Why Cereale?</h2>
|
||||||
<div class="grid md:grid-cols-3 gap-8">
|
<div class="grid md:grid-cols-3 gap-8">
|
||||||
<div class="p-6 rounded-xl bg-slate-50 border border-slate-100">
|
<div class="p-6 rounded-xl bg-slate-50 border border-slate-100">
|
||||||
<div class="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center mb-4 text-indigo-600">
|
<div class="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center mb-4 text-indigo-600">
|
||||||
@@ -117,7 +117,7 @@
|
|||||||
|
|
||||||
<div class="grid lg:grid-cols-2 gap-6">
|
<div class="grid lg:grid-cols-2 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<div class="bg-slate-800 rounded-t-lg px-4 py-2 text-sm font-mono text-slate-400 border-b border-slate-700">TypeScript / Optimus</div>
|
<div class="bg-slate-800 rounded-t-lg px-4 py-2 text-sm font-mono text-slate-400 border-b border-slate-700">TypeScript / Cereale</div>
|
||||||
<textarea id="editor"></textarea>
|
<textarea id="editor"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
@@ -168,7 +168,7 @@
|
|||||||
|
|
||||||
<footer class="bg-slate-50 border-t border-slate-200 py-12">
|
<footer class="bg-slate-50 border-t border-slate-200 py-12">
|
||||||
<div class="max-w-7xl mx-auto px-4 text-center text-slate-500">
|
<div class="max-w-7xl mx-auto px-4 text-center text-slate-500">
|
||||||
<p>© 2026 Optimus Library. Licensed under MIT.</p>
|
<p>© 2026 Cereale Library. Licensed under MIT.</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
@@ -255,7 +255,7 @@ demo();`;
|
|||||||
Positive, Negative,
|
Positive, Negative,
|
||||||
JsonSerialize, JsonDeserialize, JsonPolymorphic, JsonType, JsonMapper,
|
JsonSerialize, JsonDeserialize, JsonPolymorphic, JsonType, JsonMapper,
|
||||||
JsonValidationError
|
JsonValidationError
|
||||||
} = Optimus;
|
} = Cereale;
|
||||||
|
|
||||||
const run = new Function(
|
const run = new Function(
|
||||||
'console',
|
'console',
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import eslint from '@eslint/js';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
import globals from 'globals';
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{
|
||||||
|
ignores: ['dist/**', 'node_modules/**', 'coverage/**', 'docs/**'],
|
||||||
|
},
|
||||||
|
eslint.configs.recommended,
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.node,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
||||||
|
'no-console': 'warn',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.test.ts', 'src/example.ts'],
|
||||||
|
rules: {
|
||||||
|
'no-console': 'off',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
+27
-9
@@ -1,22 +1,35 @@
|
|||||||
{
|
{
|
||||||
"name": "optimus",
|
"name": "cereale",
|
||||||
"version": "1.0.0",
|
"version": "0.0.1",
|
||||||
"description": "Spring-like decorators for JSON mapping and validation in TypeScript",
|
"description": "Spring-like decorators for JSON mapping and validation in TypeScript",
|
||||||
"main": "dist/index.js",
|
"type": "module",
|
||||||
"types": "dist/index.d.ts",
|
"main": "./dist/cjs/index.js",
|
||||||
|
"module": "./dist/esm/index.js",
|
||||||
|
"types": "./dist/esm/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/esm/index.d.ts",
|
||||||
|
"import": "./dist/esm/index.js",
|
||||||
|
"require": "./dist/cjs/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sideEffects": false,
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "rm -rf dist && tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json && echo '{\"type\": \"commonjs\"}' > dist/cjs/package.json",
|
||||||
"demo": "ts-node src/example.ts",
|
"demo": "node --no-warnings=ExperimentalWarning --loader ts-node/esm src/example.ts",
|
||||||
"type-check": "tsc --noEmit",
|
"type-check": "tsc --noEmit",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
|
"test:coverage": "vitest run --coverage",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"lint:fix": "eslint . --fix",
|
||||||
"prepublishOnly": "npm run build"
|
"prepublishOnly": "npm run build"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/Avalon-Vanguard/optimus.git"
|
"url": "git+https://github.com/Avalon-Vanguard/cereale.git"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"json",
|
"json",
|
||||||
@@ -29,13 +42,18 @@
|
|||||||
"author": "Avalon Vanguard",
|
"author": "Avalon Vanguard",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/Avalon-Vanguard/optimus/issues"
|
"url": "https://github.com/Avalon-Vanguard/cereale/issues"
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/Avalon-Vanguard/optimus#readme",
|
"homepage": "https://github.com/Avalon-Vanguard/cereale#readme",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^10.0.1",
|
||||||
"@types/node": "^25.6.0",
|
"@types/node": "^25.6.0",
|
||||||
|
"@vitest/coverage-v8": "^4.1.4",
|
||||||
|
"eslint": "^10.2.1",
|
||||||
|
"globals": "^17.5.0",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^6.0.2",
|
"typescript": "^6.0.2",
|
||||||
|
"typescript-eslint": "^8.58.2",
|
||||||
"vitest": "^4.1.4"
|
"vitest": "^4.1.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,382 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import {
|
||||||
|
IsNumber,
|
||||||
|
IsObject,
|
||||||
|
IsDefined,
|
||||||
|
IsNotEmpty,
|
||||||
|
Positive,
|
||||||
|
Negative,
|
||||||
|
IsUrl,
|
||||||
|
Max,
|
||||||
|
Matches,
|
||||||
|
ArrayMinSize,
|
||||||
|
ArrayMaxSize,
|
||||||
|
IsNotIn,
|
||||||
|
Validate,
|
||||||
|
registerDecorator,
|
||||||
|
JsonType,
|
||||||
|
JsonPolymorphic,
|
||||||
|
JsonMapper,
|
||||||
|
JsonValidationError,
|
||||||
|
ValidationArguments,
|
||||||
|
ValidatorConstraintInterface,
|
||||||
|
IsString
|
||||||
|
} from './index.js';
|
||||||
|
|
||||||
|
describe('Additional Decorators', () => {
|
||||||
|
describe('IsNumber', () => {
|
||||||
|
class Test {
|
||||||
|
@IsNumber()
|
||||||
|
val: any;
|
||||||
|
}
|
||||||
|
it('should validate numbers', async () => {
|
||||||
|
const t = new Test();
|
||||||
|
t.val = 123;
|
||||||
|
expect(await JsonMapper.validate(t)).toHaveLength(0);
|
||||||
|
t.val = '123';
|
||||||
|
expect(await JsonMapper.validate(t)).toHaveLength(1);
|
||||||
|
t.val = NaN;
|
||||||
|
expect(await JsonMapper.validate(t)).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('IsObject', () => {
|
||||||
|
class Test {
|
||||||
|
@IsObject()
|
||||||
|
val: any;
|
||||||
|
}
|
||||||
|
it('should validate objects', async () => {
|
||||||
|
const t = new Test();
|
||||||
|
t.val = { a: 1 };
|
||||||
|
expect(await JsonMapper.validate(t)).toHaveLength(0);
|
||||||
|
t.val = 123;
|
||||||
|
expect(await JsonMapper.validate(t)).toHaveLength(1);
|
||||||
|
t.val = null;
|
||||||
|
expect(await JsonMapper.validate(t)).toHaveLength(1);
|
||||||
|
t.val = [];
|
||||||
|
expect(await JsonMapper.validate(t)).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('IsDefined', () => {
|
||||||
|
class Test {
|
||||||
|
@IsDefined()
|
||||||
|
val: any;
|
||||||
|
}
|
||||||
|
it('should validate defined values', async () => {
|
||||||
|
const t = new Test();
|
||||||
|
t.val = 0;
|
||||||
|
expect(await JsonMapper.validate(t)).toHaveLength(0);
|
||||||
|
t.val = null;
|
||||||
|
expect(await JsonMapper.validate(t)).toHaveLength(1);
|
||||||
|
t.val = undefined;
|
||||||
|
expect(await JsonMapper.validate(t)).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('IsNotEmpty', () => {
|
||||||
|
class Test {
|
||||||
|
@IsNotEmpty()
|
||||||
|
val: any;
|
||||||
|
}
|
||||||
|
it('should validate non-empty values', async () => {
|
||||||
|
const t = new Test();
|
||||||
|
t.val = 'abc';
|
||||||
|
expect(await JsonMapper.validate(t)).toHaveLength(0);
|
||||||
|
t.val = '';
|
||||||
|
expect(await JsonMapper.validate(t)).toHaveLength(1);
|
||||||
|
t.val = null;
|
||||||
|
expect(await JsonMapper.validate(t)).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Positive and Negative', () => {
|
||||||
|
class Test {
|
||||||
|
@Positive()
|
||||||
|
pos: number;
|
||||||
|
@Negative()
|
||||||
|
neg: number;
|
||||||
|
}
|
||||||
|
it('should validate positive and negative numbers', async () => {
|
||||||
|
const t = new Test();
|
||||||
|
t.pos = 5;
|
||||||
|
t.neg = -5;
|
||||||
|
expect(await JsonMapper.validate(t)).toHaveLength(0);
|
||||||
|
t.pos = -1;
|
||||||
|
t.neg = 1;
|
||||||
|
expect(await JsonMapper.validate(t)).toHaveLength(2);
|
||||||
|
const errors = await JsonMapper.validate(t);
|
||||||
|
expect(errors.find(e => e.property === 'pos')).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Matches', () => {
|
||||||
|
class Test {
|
||||||
|
@Matches(/^[a-z]+$/)
|
||||||
|
val: string;
|
||||||
|
@Max(10)
|
||||||
|
num: number;
|
||||||
|
}
|
||||||
|
it('should validate regex matches and Max', async () => {
|
||||||
|
const t = new Test();
|
||||||
|
t.val = 'abc';
|
||||||
|
t.num = 5;
|
||||||
|
expect(await JsonMapper.validate(t)).toHaveLength(0);
|
||||||
|
t.val = '123';
|
||||||
|
t.num = 15;
|
||||||
|
expect(await JsonMapper.validate(t)).toHaveLength(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('IsUrl invalid', () => {
|
||||||
|
class Test {
|
||||||
|
@IsUrl()
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
it('should fail on invalid URL', async () => {
|
||||||
|
const t = new Test();
|
||||||
|
t.url = 'not-a-url';
|
||||||
|
expect(await JsonMapper.validate(t)).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('JsonValidationError and toPlain error', () => {
|
||||||
|
it('should have a working toString on JsonValidationError', () => {
|
||||||
|
const err = new JsonValidationError('fail', [{ property: 'x', value: 1, constraints: { c: 'm' } }]);
|
||||||
|
expect(err.toString()).toContain('fail');
|
||||||
|
expect(err.toString()).toContain('x');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw during serialization if invalid', async () => {
|
||||||
|
class Test {
|
||||||
|
@IsString()
|
||||||
|
val: any;
|
||||||
|
}
|
||||||
|
const t = new Test();
|
||||||
|
t.val = 123;
|
||||||
|
await expect(JsonMapper.toPlain(t)).rejects.toThrow(JsonValidationError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Array Size', () => {
|
||||||
|
class Test {
|
||||||
|
@ArrayMinSize(2)
|
||||||
|
@ArrayMaxSize(4)
|
||||||
|
vals: any[];
|
||||||
|
}
|
||||||
|
it('should validate array size', async () => {
|
||||||
|
const t = new Test();
|
||||||
|
t.vals = [1, 2];
|
||||||
|
expect(await JsonMapper.validate(t)).toHaveLength(0);
|
||||||
|
t.vals = [1];
|
||||||
|
expect(await JsonMapper.validate(t)).toHaveLength(1);
|
||||||
|
t.vals = [1, 2, 3, 4, 5];
|
||||||
|
expect(await JsonMapper.validate(t)).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('IsNotIn', () => {
|
||||||
|
class Test {
|
||||||
|
@IsNotIn(['black', 'white'])
|
||||||
|
color: string;
|
||||||
|
}
|
||||||
|
it('should validate value is NOT in list', async () => {
|
||||||
|
const t = new Test();
|
||||||
|
t.color = 'red';
|
||||||
|
expect(await JsonMapper.validate(t)).toHaveLength(0);
|
||||||
|
t.color = 'black';
|
||||||
|
expect(await JsonMapper.validate(t)).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('JsonType', () => {
|
||||||
|
class Child {
|
||||||
|
@IsString()
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
class Parent {
|
||||||
|
@JsonType(() => Child)
|
||||||
|
child: Child;
|
||||||
|
}
|
||||||
|
it('should deserialize nested type using JsonType', async () => {
|
||||||
|
const plain = { child: { name: 'Junior' } };
|
||||||
|
const parent = await JsonMapper.toInstance(Parent, plain);
|
||||||
|
expect(parent.child).toBeInstanceOf(Child);
|
||||||
|
expect(parent.child.name).toBe('Junior');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Custom Validate', () => {
|
||||||
|
it('should use functional validator', async () => {
|
||||||
|
class Test {
|
||||||
|
@Validate((v) => v === 'secret')
|
||||||
|
val: string;
|
||||||
|
}
|
||||||
|
const t = new Test();
|
||||||
|
t.val = 'secret';
|
||||||
|
expect(await JsonMapper.validate(t)).toHaveLength(0);
|
||||||
|
t.val = 'wrong';
|
||||||
|
expect(await JsonMapper.validate(t)).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use class-based validator', async () => {
|
||||||
|
class CustomValidator implements ValidatorConstraintInterface {
|
||||||
|
validate(value: any) {
|
||||||
|
return value === 'correct';
|
||||||
|
}
|
||||||
|
defaultMessage(args: ValidationArguments) {
|
||||||
|
return `${args.property} must be correct`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Test {
|
||||||
|
@Validate(CustomValidator)
|
||||||
|
val: string;
|
||||||
|
}
|
||||||
|
const t = new Test();
|
||||||
|
t.val = 'correct';
|
||||||
|
expect(await JsonMapper.validate(t)).toHaveLength(0);
|
||||||
|
t.val = 'wrong';
|
||||||
|
const errors = await JsonMapper.validate(t);
|
||||||
|
expect(errors).toHaveLength(1);
|
||||||
|
expect(errors[0].constraints['CustomValidator']).toBe('val must be correct');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('registerDecorator', () => {
|
||||||
|
it('should register a custom decorator with functional validator', async () => {
|
||||||
|
function IsEven() {
|
||||||
|
return function (object: any, propertyName: string) {
|
||||||
|
registerDecorator({
|
||||||
|
name: 'isEven',
|
||||||
|
target: object.constructor,
|
||||||
|
propertyName: propertyName,
|
||||||
|
validator: (value: any) => typeof value === 'number' && value % 2 === 0,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class Test {
|
||||||
|
@IsEven()
|
||||||
|
val: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const t = new Test();
|
||||||
|
t.val = 2;
|
||||||
|
expect(await JsonMapper.validate(t)).toHaveLength(0);
|
||||||
|
t.val = 3;
|
||||||
|
expect(await JsonMapper.validate(t)).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should register a custom decorator with class validator', async () => {
|
||||||
|
class MyValidator implements ValidatorConstraintInterface {
|
||||||
|
validate(v: any) { return v === 'ok'; }
|
||||||
|
}
|
||||||
|
function IsOk() {
|
||||||
|
return function (object: any, propertyName: string) {
|
||||||
|
registerDecorator({
|
||||||
|
name: 'isOk',
|
||||||
|
target: object.constructor,
|
||||||
|
propertyName: propertyName,
|
||||||
|
validator: MyValidator,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
class Test {
|
||||||
|
@IsOk() val: string;
|
||||||
|
}
|
||||||
|
const t = new Test();
|
||||||
|
t.val = 'ok';
|
||||||
|
expect(await JsonMapper.validate(t)).toHaveLength(0);
|
||||||
|
t.val = 'not ok';
|
||||||
|
expect(await JsonMapper.validate(t)).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Validate with constraints and options', () => {
|
||||||
|
it('should handle constraints and options', async () => {
|
||||||
|
class Test {
|
||||||
|
@Validate((v, a) => v === a.constraints[0], [10], { message: 'must be ten' })
|
||||||
|
val: number;
|
||||||
|
}
|
||||||
|
const t = new Test();
|
||||||
|
t.val = 10;
|
||||||
|
expect(await JsonMapper.validate(t)).toHaveLength(0);
|
||||||
|
t.val = 5;
|
||||||
|
const errors = await JsonMapper.validate(t);
|
||||||
|
expect(errors).toHaveLength(1);
|
||||||
|
expect(errors[0].constraints['custom']).toBe('must be ten');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle options as second argument', async () => {
|
||||||
|
class Test {
|
||||||
|
@Validate((v) => v === 1, { message: 'must be one' })
|
||||||
|
val: number;
|
||||||
|
}
|
||||||
|
const t = new Test();
|
||||||
|
t.val = 1;
|
||||||
|
expect(await JsonMapper.validate(t)).toHaveLength(0);
|
||||||
|
t.val = 2;
|
||||||
|
const errors = await JsonMapper.validate(t);
|
||||||
|
expect(errors).toHaveLength(1);
|
||||||
|
expect(errors[0].constraints['custom']).toBe('must be one');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('JsonMapper extra cases', () => {
|
||||||
|
it('should serialize Date with default toISOString', async () => {
|
||||||
|
class Test {
|
||||||
|
date: Date;
|
||||||
|
}
|
||||||
|
const t = new Test();
|
||||||
|
t.date = new Date("2026-01-01T00:00:00Z");
|
||||||
|
const plain = await JsonMapper.toPlain(t);
|
||||||
|
expect(plain.date).toBe(t.date.toISOString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should deserialize top-level array', async () => {
|
||||||
|
class Item {
|
||||||
|
@IsString()
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
const json = '[{"name": "a"}, {"name": "b"}]';
|
||||||
|
const items = await JsonMapper.fromJson(Item, json);
|
||||||
|
expect(Array.isArray(items)).toBe(true);
|
||||||
|
expect(items[0]).toBeInstanceOf(Item);
|
||||||
|
expect(items[0].name).toBe('a');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle single polymorphic object', async () => {
|
||||||
|
abstract class Animal {
|
||||||
|
@IsString() type: string;
|
||||||
|
}
|
||||||
|
class Dog extends Animal {
|
||||||
|
type = 'dog';
|
||||||
|
@IsString() breed: string;
|
||||||
|
}
|
||||||
|
class Test {
|
||||||
|
@JsonPolymorphic('type', [{ value: Dog, name: 'dog' }])
|
||||||
|
pet: Animal;
|
||||||
|
}
|
||||||
|
const plain = { pet: { type: 'dog', breed: 'Labrador' } };
|
||||||
|
const t = await JsonMapper.toInstance(Test, plain);
|
||||||
|
expect(t.pet).toBeInstanceOf(Dog);
|
||||||
|
expect((t.pet as Dog).breed).toBe('Labrador');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ValidationOptions each', () => {
|
||||||
|
class Test {
|
||||||
|
@IsString({ each: true })
|
||||||
|
tags: string[];
|
||||||
|
}
|
||||||
|
it('should validate each element in array', async () => {
|
||||||
|
const t = new Test();
|
||||||
|
t.tags = ['a', 'b'];
|
||||||
|
expect(await JsonMapper.validate(t)).toHaveLength(0);
|
||||||
|
t.tags = ['a', 1 as any];
|
||||||
|
const errors = await JsonMapper.validate(t);
|
||||||
|
expect(errors).toHaveLength(1);
|
||||||
|
expect(errors[0].constraints['isString']).toContain('each element');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
+13
-13
@@ -1,14 +1,14 @@
|
|||||||
import { JsonSerializer, JsonDeserializer, ClassConstructor } from './interfaces';
|
import { JsonSerializer, JsonDeserializer, ClassConstructor } from './interfaces.js';
|
||||||
import { metadataStorage } from './metadata-storage';
|
import { metadataStorage } from './metadata-storage.js';
|
||||||
|
|
||||||
export const METADATA_KEYS = {
|
export const METADATA_KEYS = {
|
||||||
PROPERTIES: 'optimus:properties',
|
PROPERTIES: 'cereale:properties',
|
||||||
TYPE: 'optimus:type',
|
TYPE: 'cereale:type',
|
||||||
VALIDATION: 'optimus:validation',
|
VALIDATION: 'cereale:validation',
|
||||||
SERIALIZER: 'optimus:serializer',
|
SERIALIZER: 'cereale:serializer',
|
||||||
DESERIALIZER: 'optimus:deserializer',
|
DESERIALIZER: 'cereale:deserializer',
|
||||||
POLYMORPHIC: 'optimus:polymorphic',
|
POLYMORPHIC: 'cereale:polymorphic',
|
||||||
IS_OPTIONAL: 'optimus:optional',
|
IS_OPTIONAL: 'cereale:optional',
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ValidationArguments {
|
export interface ValidationArguments {
|
||||||
@@ -445,7 +445,7 @@ export function ValidateNested() {
|
|||||||
return (target: any, propertyKey: string) => {
|
return (target: any, propertyKey: string) => {
|
||||||
registerProperty(target, propertyKey);
|
registerProperty(target, propertyKey);
|
||||||
// This is a marker for recursive validation
|
// This is a marker for recursive validation
|
||||||
metadataStorage.defineMetadata('optimus:nested', true, target, propertyKey);
|
metadataStorage.defineMetadata('cereale:nested', true, target, propertyKey);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -472,7 +472,7 @@ export function Validate(
|
|||||||
// Functional validator
|
// Functional validator
|
||||||
addValidation(target, propertyKey, {
|
addValidation(target, propertyKey, {
|
||||||
name: 'custom',
|
name: 'custom',
|
||||||
validate: validator as (value: any, args: ValidationArguments) => boolean | Promise<boolean>,
|
validate: validator as (value: any, args: ValidationArguments) => boolean,
|
||||||
message: (args) => `${args.property} is invalid`,
|
message: (args) => `${args.property} is invalid`,
|
||||||
constraints
|
constraints
|
||||||
}, validationOptions);
|
}, validationOptions);
|
||||||
@@ -494,7 +494,7 @@ export function Validate(
|
|||||||
*/
|
*/
|
||||||
export function registerDecorator(options: {
|
export function registerDecorator(options: {
|
||||||
name: string;
|
name: string;
|
||||||
target: Function;
|
target: any;
|
||||||
propertyName: string;
|
propertyName: string;
|
||||||
options?: ValidationOptions;
|
options?: ValidationOptions;
|
||||||
constraints?: any[];
|
constraints?: any[];
|
||||||
@@ -507,7 +507,7 @@ export function registerDecorator(options: {
|
|||||||
if (typeof validator === 'function' && !validator.prototype?.validate) {
|
if (typeof validator === 'function' && !validator.prototype?.validate) {
|
||||||
validationConstraint = {
|
validationConstraint = {
|
||||||
name,
|
name,
|
||||||
validate: validator as (value: any, args: ValidationArguments) => boolean | Promise<boolean>,
|
validate: validator as (value: any, args: ValidationArguments) => boolean,
|
||||||
message: (args) => `${args.property} is invalid`,
|
message: (args) => `${args.property} is invalid`,
|
||||||
...(constraints ? { constraints } : {})
|
...(constraints ? { constraints } : {})
|
||||||
};
|
};
|
||||||
|
|||||||
+7
-6
@@ -8,7 +8,8 @@ import {
|
|||||||
JsonSerialize,
|
JsonSerialize,
|
||||||
JsonDeserialize,
|
JsonDeserialize,
|
||||||
JsonPolymorphic,
|
JsonPolymorphic,
|
||||||
JsonMapper,
|
toJson,
|
||||||
|
fromJson,
|
||||||
JsonSerializer,
|
JsonSerializer,
|
||||||
JsonDeserializer,
|
JsonDeserializer,
|
||||||
Validate,
|
Validate,
|
||||||
@@ -16,7 +17,7 @@ import {
|
|||||||
ValidationArguments,
|
ValidationArguments,
|
||||||
registerDecorator,
|
registerDecorator,
|
||||||
ValidationOptions
|
ValidationOptions
|
||||||
} from './index';
|
} from './index.js';
|
||||||
|
|
||||||
// --- Custom Validators ---
|
// --- Custom Validators ---
|
||||||
|
|
||||||
@@ -76,7 +77,7 @@ class Book extends Media {
|
|||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsUsername({ message: 'Title must be a valid alphanumeric username' })
|
@IsUsername({ message: 'Title must be a valid alphanumeric username' })
|
||||||
override title: string;
|
declare title: string;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@Validate(IsLongerThan, [5])
|
@Validate(IsLongerThan, [5])
|
||||||
@@ -133,12 +134,12 @@ async function runExample() {
|
|||||||
try {
|
try {
|
||||||
// 2. Serialize to JSON
|
// 2. Serialize to JSON
|
||||||
console.log("\n[1] Serializing Library to JSON...");
|
console.log("\n[1] Serializing Library to JSON...");
|
||||||
const json = await JsonMapper.toJson(library);
|
const json = await toJson(library);
|
||||||
console.log("JSON Output:", json);
|
console.log("JSON Output:", json);
|
||||||
|
|
||||||
// 3. Deserialize back to Instance
|
// 3. Deserialize back to Instance
|
||||||
console.log("\n[2] Deserializing JSON back to Library instance...");
|
console.log("\n[2] Deserializing JSON back to Library instance...");
|
||||||
const deserializedLibrary = await JsonMapper.fromJson(Library, json);
|
const deserializedLibrary = await fromJson(Library, json);
|
||||||
console.log("Deserialized Library Name:", deserializedLibrary.name);
|
console.log("Deserialized Library Name:", deserializedLibrary.name);
|
||||||
console.log("Items count:", deserializedLibrary.items.length);
|
console.log("Items count:", deserializedLibrary.items.length);
|
||||||
|
|
||||||
@@ -162,7 +163,7 @@ async function runExample() {
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
await JsonMapper.fromJson(Library, invalidJson);
|
await fromJson(Library, invalidJson);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
console.log("Caught expected error:", error.message);
|
console.log("Caught expected error:", error.message);
|
||||||
|
|||||||
+1
-2
@@ -4,7 +4,6 @@ import {
|
|||||||
IsBoolean,
|
IsBoolean,
|
||||||
IsInt,
|
IsInt,
|
||||||
Min,
|
Min,
|
||||||
Max,
|
|
||||||
MinLength,
|
MinLength,
|
||||||
MaxLength,
|
MaxLength,
|
||||||
Email,
|
Email,
|
||||||
@@ -22,7 +21,7 @@ import {
|
|||||||
JsonSerializer,
|
JsonSerializer,
|
||||||
JsonDeserializer,
|
JsonDeserializer,
|
||||||
JsonValidationError
|
JsonValidationError
|
||||||
} from './index';
|
} from './index.js';
|
||||||
|
|
||||||
// --- Custom Serializers ---
|
// --- Custom Serializers ---
|
||||||
class DateSerializer implements JsonSerializer<Date, string> {
|
class DateSerializer implements JsonSerializer<Date, string> {
|
||||||
|
|||||||
+3
-3
@@ -1,3 +1,3 @@
|
|||||||
export * from './interfaces';
|
export * from './interfaces.js';
|
||||||
export * from './decorators';
|
export * from './decorators.js';
|
||||||
export * from './utils';
|
export * from './utils.js';
|
||||||
|
|||||||
+4
-4
@@ -9,9 +9,9 @@ export interface JsonSerializer<T = any, R = any> {
|
|||||||
* Serializes the value into a representation suitable for JSON output.
|
* Serializes the value into a representation suitable for JSON output.
|
||||||
*
|
*
|
||||||
* @param value - The value to be serialized.
|
* @param value - The value to be serialized.
|
||||||
* @returns The serialized value.
|
* @returns The serialized value or a promise resolving to it.
|
||||||
*/
|
*/
|
||||||
serialize(value: T): R;
|
serialize(value: T): R | Promise<R>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,9 +25,9 @@ export interface JsonDeserializer<T = any, R = any> {
|
|||||||
* Deserializes the value from a JSON-like representation back to its original type.
|
* Deserializes the value from a JSON-like representation back to its original type.
|
||||||
*
|
*
|
||||||
* @param value - The value to be deserialized.
|
* @param value - The value to be deserialized.
|
||||||
* @returns The deserialized value.
|
* @returns The deserialized value or a promise resolving to it.
|
||||||
*/
|
*/
|
||||||
deserialize(value: T): R;
|
deserialize(value: T): R | Promise<R>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import {
|
||||||
|
toPlain,
|
||||||
|
toInstance,
|
||||||
|
toJson,
|
||||||
|
fromJson,
|
||||||
|
fromRequest,
|
||||||
|
validate,
|
||||||
|
IsString,
|
||||||
|
IsNumber,
|
||||||
|
JsonValidationError
|
||||||
|
} from './index.js';
|
||||||
|
|
||||||
|
describe('Standalone Utility Functions', () => {
|
||||||
|
class User {
|
||||||
|
@IsString()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
age: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should validate an object directly', async () => {
|
||||||
|
const user = new User();
|
||||||
|
user.name = 'John';
|
||||||
|
user.age = 30;
|
||||||
|
const errors = await validate(user);
|
||||||
|
expect(errors).toHaveLength(0);
|
||||||
|
|
||||||
|
user.age = '30' as any;
|
||||||
|
const errors2 = await validate(user);
|
||||||
|
expect(errors2).toHaveLength(1);
|
||||||
|
expect(errors2[0].property).toBe('age');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should transform to plain object directly', async () => {
|
||||||
|
const user = new User();
|
||||||
|
user.name = 'John';
|
||||||
|
user.age = 30;
|
||||||
|
const plain = await toPlain(user);
|
||||||
|
expect(plain).toEqual({ name: 'John', age: 30 });
|
||||||
|
expect(plain).not.toBeInstanceOf(User);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw JsonValidationError in toPlain if invalid', async () => {
|
||||||
|
const user = new User();
|
||||||
|
user.name = 'John';
|
||||||
|
user.age = '30' as any;
|
||||||
|
await expect(toPlain(user)).rejects.toThrow(JsonValidationError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should transform to instance directly', async () => {
|
||||||
|
const plain = { name: 'John', age: 30 };
|
||||||
|
const user = await toInstance(User, plain);
|
||||||
|
expect(user).toBeInstanceOf(User);
|
||||||
|
expect(user.name).toBe('John');
|
||||||
|
expect(user.age).toBe(30);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw JsonValidationError in toInstance if invalid', async () => {
|
||||||
|
const plain = { name: 'John', age: '30' };
|
||||||
|
await expect(toInstance(User, plain)).rejects.toThrow(JsonValidationError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should transform to JSON directly', async () => {
|
||||||
|
const user = new User();
|
||||||
|
user.name = 'John';
|
||||||
|
user.age = 30;
|
||||||
|
const json = await toJson(user);
|
||||||
|
expect(json).toBe('{"name":"John","age":30}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should transform from JSON directly', async () => {
|
||||||
|
const json = '{"name":"John","age":30}';
|
||||||
|
const user = await fromJson(User, json);
|
||||||
|
expect(user).toBeInstanceOf(User);
|
||||||
|
expect(user.name).toBe('John');
|
||||||
|
expect(user.age).toBe(30);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should transform from Request directly', async () => {
|
||||||
|
const json = '{"name":"John","age":30}';
|
||||||
|
const request = new Request('https://example.com', {
|
||||||
|
method: 'POST',
|
||||||
|
body: json,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
const user = await fromRequest(User, request);
|
||||||
|
expect(user).toBeInstanceOf(User);
|
||||||
|
expect(user.name).toBe('John');
|
||||||
|
expect(user.age).toBe(30);
|
||||||
|
});
|
||||||
|
});
|
||||||
+246
-211
@@ -1,6 +1,6 @@
|
|||||||
import { ClassConstructor } from './interfaces';
|
import { ClassConstructor } from './interfaces.js';
|
||||||
import { METADATA_KEYS, ValidationConstraint, ValidationArguments } from './decorators';
|
import { METADATA_KEYS, ValidationConstraint, ValidationArguments } from './decorators.js';
|
||||||
import { metadataStorage } from './metadata-storage';
|
import { metadataStorage } from './metadata-storage.js';
|
||||||
|
|
||||||
export interface ValidationError {
|
export interface ValidationError {
|
||||||
property: string;
|
property: string;
|
||||||
@@ -20,237 +20,272 @@ export class JsonValidationError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class JsonMapper {
|
// --- Internal Engine ---
|
||||||
/**
|
|
||||||
* Converts a class instance to a plain object with validation.
|
|
||||||
*/
|
|
||||||
static async toPlain<T>(obj: T): Promise<any> {
|
|
||||||
if (obj === null || obj === undefined) return obj;
|
|
||||||
|
|
||||||
// Validate first
|
async function serialize(obj: any): Promise<any> {
|
||||||
const errors = await this.validate(obj);
|
if (obj === null || obj === undefined || typeof obj !== 'object') {
|
||||||
if (errors.length > 0) {
|
return obj;
|
||||||
throw new JsonValidationError('Validation failed during serialization', errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.serialize(obj);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
if (Array.isArray(obj)) {
|
||||||
* Converts a class instance to a JSON string with validation.
|
return Promise.all(obj.map(item => serialize(item)));
|
||||||
*/
|
|
||||||
static async toJson<T>(obj: T): Promise<string> {
|
|
||||||
const plain = await this.toPlain(obj);
|
|
||||||
return JSON.stringify(plain);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
if (obj instanceof Date) {
|
||||||
* Converts a plain object to a class instance with validation.
|
return obj.toISOString();
|
||||||
*/
|
|
||||||
static async toInstance<T>(clazz: ClassConstructor<T>, plain: any): Promise<T> {
|
|
||||||
const instance = this.deserialize(clazz, plain);
|
|
||||||
|
|
||||||
const errors = await this.validate(instance);
|
|
||||||
if (errors.length > 0) {
|
|
||||||
throw new JsonValidationError('Validation failed during deserialization', errors);
|
|
||||||
}
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const target = obj.constructor.prototype;
|
||||||
* Parses a JSON string to a class instance with validation.
|
|
||||||
*/
|
const result: any = {};
|
||||||
static async fromJson<T>(clazz: ClassConstructor<T>, json: string): Promise<T> {
|
const allKeys = Object.keys(obj);
|
||||||
const plain = JSON.parse(json);
|
|
||||||
return this.toInstance(clazz, plain);
|
for (const key of allKeys) {
|
||||||
|
const value = obj[key];
|
||||||
|
|
||||||
|
// Check for custom serializer
|
||||||
|
const serializerCls = metadataStorage.getMetadata(METADATA_KEYS.SERIALIZER, target, key);
|
||||||
|
if (serializerCls) {
|
||||||
|
const serializer = new serializerCls();
|
||||||
|
result[key] = await serializer.serialize(value);
|
||||||
|
} else {
|
||||||
|
result[key] = await serialize(value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Internal Engine ---
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private static serialize(obj: any): any {
|
async function deserialize<T>(clazz: ClassConstructor<T>, plain: any): Promise<T> {
|
||||||
if (obj === null || obj === undefined || typeof obj !== 'object') {
|
if (plain === null || plain === undefined) return plain;
|
||||||
return obj;
|
|
||||||
|
if (Array.isArray(plain)) {
|
||||||
|
const results = await Promise.all(plain.map(item => deserialize(clazz, item)));
|
||||||
|
return results as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = new clazz();
|
||||||
|
const target = clazz.prototype;
|
||||||
|
|
||||||
|
// Copy all properties from plain to instance
|
||||||
|
for (const key of Object.keys(plain)) {
|
||||||
|
const value = plain[key];
|
||||||
|
|
||||||
|
// Custom Deserializer
|
||||||
|
const deserializerCls = metadataStorage.getMetadata(METADATA_KEYS.DESERIALIZER, target, key);
|
||||||
|
if (deserializerCls) {
|
||||||
|
const deserializer = new deserializerCls();
|
||||||
|
instance[key as keyof T] = await deserializer.deserialize(value);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(obj)) {
|
// Polymorphic
|
||||||
return obj.map(item => this.serialize(item));
|
const poly = metadataStorage.getMetadata(METADATA_KEYS.POLYMORPHIC, target, key);
|
||||||
}
|
if (poly && value !== null && value !== undefined) {
|
||||||
|
const { discriminator, subTypes } = poly;
|
||||||
if (obj instanceof Date) {
|
if (Array.isArray(value)) {
|
||||||
return obj.toISOString();
|
instance[key as keyof T] = await Promise.all(value.map(async item => {
|
||||||
}
|
const subTypeInfo = subTypes.find((s: any) => item[discriminator] === s.name);
|
||||||
|
return subTypeInfo ? deserialize(subTypeInfo.value, item) : item;
|
||||||
const target = obj.constructor.prototype;
|
})) as any;
|
||||||
|
|
||||||
// If no properties are registered with decorators, we might want to serialize everything
|
|
||||||
// But for a "lightweight lib" based on decorators, we only serialize registered properties?
|
|
||||||
// Actually, usually we serialize everything and only apply special logic to registered ones.
|
|
||||||
// Let's take all keys of the object.
|
|
||||||
const result: any = {};
|
|
||||||
const allKeys = Object.keys(obj);
|
|
||||||
|
|
||||||
for (const key of allKeys) {
|
|
||||||
const value = obj[key];
|
|
||||||
|
|
||||||
// Check for custom serializer
|
|
||||||
const serializerCls = metadataStorage.getMetadata(METADATA_KEYS.SERIALIZER, target, key);
|
|
||||||
if (serializerCls) {
|
|
||||||
const serializer = new serializerCls();
|
|
||||||
result[key] = serializer.serialize(value);
|
|
||||||
} else {
|
} else {
|
||||||
result[key] = this.serialize(value);
|
const subTypeInfo = subTypes.find((s: any) => value[discriminator] === s.name);
|
||||||
|
if (subTypeInfo) {
|
||||||
|
instance[key as keyof T] = await deserialize(subTypeInfo.value, value);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
// Nested Type
|
||||||
|
const typeFn = metadataStorage.getMetadata(METADATA_KEYS.TYPE, target, key);
|
||||||
|
if (typeFn && value !== null && value !== undefined) {
|
||||||
|
const type = typeFn();
|
||||||
|
instance[key as keyof T] = await deserialize(type, value);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
instance[key as keyof T] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static deserialize<T>(clazz: ClassConstructor<T>, plain: any): T {
|
return instance;
|
||||||
if (plain === null || plain === undefined) return plain;
|
}
|
||||||
|
|
||||||
if (Array.isArray(plain)) {
|
// --- Public API Functions ---
|
||||||
return plain.map(item => this.deserialize(clazz, item)) as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const instance = new clazz();
|
/**
|
||||||
const target = clazz.prototype;
|
* Validates a class instance or object against its decorators.
|
||||||
|
* @param obj The object to validate
|
||||||
|
* @returns Array of validation errors
|
||||||
|
*/
|
||||||
|
export async function validate(obj: any): Promise<ValidationError[]> {
|
||||||
|
const errors: ValidationError[] = [];
|
||||||
|
if (obj === null || obj === undefined || typeof obj !== 'object') return errors;
|
||||||
|
|
||||||
// Copy all properties from plain to instance
|
if (Array.isArray(obj)) {
|
||||||
for (const key of Object.keys(plain)) {
|
for (let i = 0; i < obj.length; i++) {
|
||||||
let value = plain[key];
|
const childErrors = await validate(obj[i]);
|
||||||
|
if (childErrors.length > 0) {
|
||||||
// Custom Deserializer
|
errors.push({
|
||||||
const deserializerCls = metadataStorage.getMetadata(METADATA_KEYS.DESERIALIZER, target, key);
|
property: `[${i}]`,
|
||||||
if (deserializerCls) {
|
value: obj[i],
|
||||||
const deserializer = new deserializerCls();
|
constraints: {},
|
||||||
instance[key as keyof T] = deserializer.deserialize(value);
|
children: childErrors
|
||||||
continue;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// Polymorphic
|
|
||||||
const poly = metadataStorage.getMetadata(METADATA_KEYS.POLYMORPHIC, target, key);
|
|
||||||
if (poly && value !== null && value !== undefined) {
|
|
||||||
const { discriminator, subTypes } = poly;
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
instance[key as keyof T] = value.map(item => {
|
|
||||||
const subTypeInfo = subTypes.find((s: any) => item[discriminator] === s.name);
|
|
||||||
return subTypeInfo ? this.deserialize(subTypeInfo.value, item) : item;
|
|
||||||
}) as any;
|
|
||||||
} else {
|
|
||||||
const subTypeInfo = subTypes.find((s: any) => value[discriminator] === s.name);
|
|
||||||
if (subTypeInfo) {
|
|
||||||
instance[key as keyof T] = this.deserialize(subTypeInfo.value, value);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nested Type
|
|
||||||
const typeFn = metadataStorage.getMetadata(METADATA_KEYS.TYPE, target, key);
|
|
||||||
if (typeFn && value !== null && value !== undefined) {
|
|
||||||
const type = typeFn();
|
|
||||||
instance[key as keyof T] = this.deserialize(type, value);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
instance[key as keyof T] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
static async validate(obj: any): Promise<ValidationError[]> {
|
|
||||||
const errors: ValidationError[] = [];
|
|
||||||
if (obj === null || obj === undefined || typeof obj !== 'object') return errors;
|
|
||||||
|
|
||||||
if (Array.isArray(obj)) {
|
|
||||||
for (let i = 0; i < obj.length; i++) {
|
|
||||||
const childErrors = await this.validate(obj[i]);
|
|
||||||
if (childErrors.length > 0) {
|
|
||||||
errors.push({
|
|
||||||
property: `[${i}]`,
|
|
||||||
value: obj[i],
|
|
||||||
constraints: {},
|
|
||||||
children: childErrors
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return errors;
|
|
||||||
}
|
|
||||||
|
|
||||||
const target = Object.getPrototypeOf(obj);
|
|
||||||
const properties: string[] = metadataStorage.getProperties(target);
|
|
||||||
|
|
||||||
for (const key of properties) {
|
|
||||||
const value = obj[key];
|
|
||||||
const propertyErrors: ValidationError = {
|
|
||||||
property: key,
|
|
||||||
value: value,
|
|
||||||
constraints: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Handle IsOptional
|
|
||||||
const isOptional = metadataStorage.getMetadata(METADATA_KEYS.IS_OPTIONAL, target, key);
|
|
||||||
const isNullOrUndefined = value === null || value === undefined;
|
|
||||||
|
|
||||||
if (isOptional && isNullOrUndefined) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check validation constraints
|
|
||||||
const constraints: ValidationConstraint[] = metadataStorage.getMetadata(METADATA_KEYS.VALIDATION, target, key) || [];
|
|
||||||
const validationArgs: ValidationArguments = {
|
|
||||||
value: value,
|
|
||||||
object: obj,
|
|
||||||
property: key,
|
|
||||||
constraints: []
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const constraint of constraints) {
|
|
||||||
validationArgs.constraints = constraint.constraints || [];
|
|
||||||
|
|
||||||
let isValid = true;
|
|
||||||
if (constraint.each && Array.isArray(value)) {
|
|
||||||
for (const item of value) {
|
|
||||||
const itemArgs = { ...validationArgs, value: item };
|
|
||||||
if (!(await constraint.validate(item, itemArgs))) {
|
|
||||||
isValid = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
isValid = await constraint.validate(value, validationArgs);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isValid) {
|
|
||||||
let message = typeof constraint.message === 'function'
|
|
||||||
? constraint.message(validationArgs)
|
|
||||||
: constraint.message;
|
|
||||||
|
|
||||||
if (constraint.each) {
|
|
||||||
message = `each element in ${message}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
propertyErrors.constraints[constraint.name] = message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recursive validation
|
|
||||||
const isNested = metadataStorage.getMetadata('optimus:nested', target, key);
|
|
||||||
if (isNested && value !== null && value !== undefined) {
|
|
||||||
const nestedErrors = await this.validate(value);
|
|
||||||
if (nestedErrors.length > 0) {
|
|
||||||
propertyErrors.children = nestedErrors;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(propertyErrors.constraints).length > 0 || propertyErrors.children) {
|
|
||||||
errors.push(propertyErrors);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const target = Object.getPrototypeOf(obj);
|
||||||
|
const properties: string[] = metadataStorage.getProperties(target);
|
||||||
|
|
||||||
|
for (const key of properties) {
|
||||||
|
const value = obj[key];
|
||||||
|
const propertyErrors: ValidationError = {
|
||||||
|
property: key,
|
||||||
|
value: value,
|
||||||
|
constraints: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle IsOptional
|
||||||
|
const isOptional = metadataStorage.getMetadata(METADATA_KEYS.IS_OPTIONAL, target, key);
|
||||||
|
const isNullOrUndefined = value === null || value === undefined;
|
||||||
|
|
||||||
|
if (isOptional && isNullOrUndefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check validation constraints
|
||||||
|
const constraints: ValidationConstraint[] = metadataStorage.getMetadata(METADATA_KEYS.VALIDATION, target, key) || [];
|
||||||
|
const validationArgs: ValidationArguments = {
|
||||||
|
value: value,
|
||||||
|
object: obj,
|
||||||
|
property: key,
|
||||||
|
constraints: []
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const constraint of constraints) {
|
||||||
|
validationArgs.constraints = constraint.constraints || [];
|
||||||
|
|
||||||
|
let isValid = true;
|
||||||
|
if (constraint.each && Array.isArray(value)) {
|
||||||
|
for (const item of value) {
|
||||||
|
const itemArgs = { ...validationArgs, value: item };
|
||||||
|
if (!(await constraint.validate(item, itemArgs))) {
|
||||||
|
isValid = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
isValid = await constraint.validate(value, validationArgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
let message = typeof constraint.message === 'function'
|
||||||
|
? constraint.message(validationArgs)
|
||||||
|
: constraint.message;
|
||||||
|
|
||||||
|
if (constraint.each) {
|
||||||
|
message = `each element in ${message}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
propertyErrors.constraints[constraint.name] = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursive validation
|
||||||
|
const isNested = metadataStorage.getMetadata('cereale:nested', target, key);
|
||||||
|
if (isNested && value !== null && value !== undefined) {
|
||||||
|
const nestedErrors = await validate(value);
|
||||||
|
if (nestedErrors.length > 0) {
|
||||||
|
propertyErrors.children = nestedErrors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(propertyErrors.constraints).length > 0 || propertyErrors.children) {
|
||||||
|
errors.push(propertyErrors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a class instance to a plain object with validation.
|
||||||
|
* @param obj The class instance to transform
|
||||||
|
* @returns Plain object
|
||||||
|
*/
|
||||||
|
export async function toPlain<T>(obj: T): Promise<any> {
|
||||||
|
if (obj === null || obj === undefined) return obj;
|
||||||
|
|
||||||
|
const errors = await validate(obj);
|
||||||
|
if (errors.length > 0) {
|
||||||
|
throw new JsonValidationError('Validation failed during serialization', errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
return serialize(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a class instance to a JSON string with validation.
|
||||||
|
* @param obj The class instance to transform
|
||||||
|
* @returns JSON string
|
||||||
|
*/
|
||||||
|
export async function toJson<T>(obj: T): Promise<string> {
|
||||||
|
const plain = await toPlain(obj);
|
||||||
|
return JSON.stringify(plain);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a plain object to a class instance with validation.
|
||||||
|
* @param clazz The class constructor
|
||||||
|
* @param plain The plain object to transform
|
||||||
|
* @returns Validated class instance
|
||||||
|
*/
|
||||||
|
export async function toInstance<T>(clazz: ClassConstructor<T>, plain: any): Promise<T> {
|
||||||
|
const instance = await deserialize(clazz, plain);
|
||||||
|
|
||||||
|
const errors = await validate(instance);
|
||||||
|
if (errors.length > 0) {
|
||||||
|
throw new JsonValidationError('Validation failed during deserialization', errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a JSON string to a class instance with validation.
|
||||||
|
* @param clazz The class constructor
|
||||||
|
* @param json JSON string
|
||||||
|
* @returns Validated class instance
|
||||||
|
*/
|
||||||
|
export async function fromJson<T>(clazz: ClassConstructor<T>, json: string): Promise<T> {
|
||||||
|
const plain = JSON.parse(json);
|
||||||
|
return toInstance(clazz, plain);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper for Fetch-based frameworks (Next.js, Hono, etc.)
|
||||||
|
* Extracts JSON from a Request and transforms it to a validated instance.
|
||||||
|
* @param clazz The class constructor
|
||||||
|
* @param request Web Request object
|
||||||
|
* @returns Validated class instance
|
||||||
|
*/
|
||||||
|
export async function fromRequest<T>(clazz: ClassConstructor<T>, request: Request): Promise<T> {
|
||||||
|
const plain = await request.json();
|
||||||
|
return toInstance(clazz, plain);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use standalone functions like toPlain, toInstance, etc.
|
||||||
|
*/
|
||||||
|
export class JsonMapper {
|
||||||
|
static toPlain = toPlain;
|
||||||
|
static toJson = toJson;
|
||||||
|
static toInstance = toInstance;
|
||||||
|
static fromJson = fromJson;
|
||||||
|
static fromRequest = fromRequest;
|
||||||
|
static validate = validate;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "CommonJS",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"outDir": "dist/cjs",
|
||||||
|
"declaration": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "NodeNext",
|
||||||
|
"outDir": "dist/esm",
|
||||||
|
"declaration": true
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-3
@@ -6,8 +6,8 @@
|
|||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
|
|
||||||
// Environment Settings
|
// Environment Settings
|
||||||
"module": "CommonJS",
|
"module": "NodeNext",
|
||||||
"target": "ES2020",
|
"target": "ES2025",
|
||||||
"lib": ["ESNext"],
|
"lib": ["ESNext"],
|
||||||
"types": ["node"],
|
"types": ["node"],
|
||||||
|
|
||||||
@@ -36,5 +36,5 @@
|
|||||||
"experimentalDecorators": true
|
"experimentalDecorators": true
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"],
|
||||||
"exclude": ["node_modules", "dist", "src/**/*.test.ts", "src/example.ts"]
|
"exclude": ["node_modules", "dist", "src/**/*.test.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user