Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file removed integration/.gitkeep
Empty file.
3 changes: 2 additions & 1 deletion integration/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"name": "integrations",
"type": "module",
"scripts": {
"test:int:to-ts": "tsx ./toTypescript/index.ts"
"test:int:to-ts": "tsx ./toTypescript/index.ts",
"test:int:to-json": "tsx ./toJsonSchema/index.ts"
},
"dependencies": {
"@duplojs/data-parser-tools": "file:.."
Expand Down
44 changes: 44 additions & 0 deletions integration/toJsonSchema/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { render, defaultTransformers } from "@duplojs/data-parser-tools/toJsonSchema";
import { DPE } from "@duplojs/utils";

const userSchema = DPE.object({
id: DPE.templateLiteral(["user-", DPE.number(), "-db1"]),
name: DPE.string().min(2),
email: DPE.email(),
role: DPE.literal(["admin", "editor", "viewer"]),
age: DPE.coerce.number().min(0).optional(),
contact: DPE.union([
DPE.object({
phone: DPE.string().regex(/^[+\\d][\\d\\s-]{5,}$/),
}),
DPE.object({
email: DPE.email(),
}),
]),
address: DPE.object({
street: DPE.string(),
city: DPE.string(),
zip: DPE.string().regex(/^\\d{5}$/),
country: DPE.literal("FR"),
}),
roles: DPE.literal(["admin", "editor", "viewer"]).array().min(1),
preferences: DPE.object({
theme: DPE.literal(["light", "dark"]),
newsletter: DPE.boolean(),
}).nullable(),
metadata: DPE.record(DPE.string(), DPE.string(), { requireKey: null }),
location: DPE.tuple([DPE.number(), DPE.number()], { rest: DPE.number() }),
createdAt: DPE.date({ coerce: true }),
}).addIdentifier("UserProfile");

const result = render(
userSchema,
{
identifier: "UserProfile",
mode: "in",
transformers: defaultTransformers,
version: "jsonSchema7",
},
);

console.log(result);

Check warning on line 44 in integration/toJsonSchema/index.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected console statement
11 changes: 6 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 9 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@
"import": "./dist/toTypescript/index.mjs",
"require": "./dist/toTypescript/index.cjs",
"types": "./dist/toTypescript/index.d.ts"
},
"./toJsonSchema": {
"import": "./dist/toJsonSchema/index.mjs",
"require": "./dist/toJsonSchema/index.cjs",
"types": "./dist/toJsonSchema/index.d.ts"
}
},
"files": [
Expand All @@ -61,7 +66,7 @@
"vitest": "3.2.4"
},
"peerDependencies": {
"@duplojs/utils": ">=1.3.19 <2.0.0"
"@duplojs/utils": ">=1.3.22 <2.0.0"
},
"dependencies": {
"typescript": "5.9.2"
Expand All @@ -75,7 +80,9 @@
"data-parser",
"tools",
"data-parser-to-typescript",
"dp2ts"
"dp2ts",
"data-parser-to-json-schema",
"dp2json"
],
"engines": {
"node": ">=22.15.1"
Expand Down
67 changes: 43 additions & 24 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,46 @@ import del from "rollup-plugin-delete";
import tscAlias from "rollup-plugin-tsc-alias";
import { defineConfig } from "rollup";

export default defineConfig({
input: "scripts/index.ts",
output: [
{
dir: "dist",
format: "esm",
preserveModules: true,
preserveModulesRoot: "scripts",
entryFileNames: "[name].mjs"
},
{
dir: "dist",
format: "cjs",
preserveModules: true,
preserveModulesRoot: "scripts",
entryFileNames: "[name].cjs"
},
],
plugins: [
del({ targets: "dist" }),
typescript({ tsconfig: "tsconfig.build.json" }),
tscAlias({ configFile: "tsconfig.build.json" }),
],
});
/**
* @type {import("rollup").RollupOptions[]}
*/
const outputs = [
{
dir: "dist",
format: "esm",
preserveModules: true,
preserveModulesRoot: "scripts",
entryFileNames: "[name].mjs",
},
{
dir: "dist",
format: "cjs",
preserveModules: true,
preserveModulesRoot: "scripts",
entryFileNames: "[name].cjs",
},
];

// mandatory because it removes the previously built tool
const delPlugin = del({ targets: "dist", runOnce: true })

/**
* @param {string} input
* @returns {import("rollup").RollupOptions}
*/
function createConfig(input) {
return {
input,
output: outputs,
plugins: [
delPlugin,
typescript({ tsconfig: "tsconfig.build.json" }),
tscAlias({ configFile: "tsconfig.build.json" }),
],
}
};

export default defineConfig([
createConfig("scripts/toTypescript/index.ts"),
createConfig("scripts/toJsonSchema/index.ts"),
]);
1 change: 0 additions & 1 deletion scripts/index.ts

This file was deleted.

4 changes: 4 additions & 0 deletions scripts/toJsonSchema/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./transformer";
export * from "./override";
export * from "./render";
export * from "./kind";
12 changes: 12 additions & 0 deletions scripts/toJsonSchema/kind.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { createKindNamespace } from "@duplojs/utils";

declare module "@duplojs/utils" {
interface ReservedKindNamespace {
DuplojsDataParserToolsToJsonSchema: true;
}
}

export const createToJsonSchemaKind = createKindNamespace(
// @ts-expect-error reserved kind namespace
"DuplojsDataParserToolsToJsonSchema",
);
36 changes: 36 additions & 0 deletions scripts/toJsonSchema/override.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { dataParserInit } from "@duplojs/utils/dataParser";

declare module "@duplojs/utils/dataParser" {
interface DataParser {

/**
* @deprecated this method mutated the dataParser by adding an identifier
*/
setIdentifier(input: string): this;
addIdentifier(input: string): this;
}

interface DataParserDefinition {
identifier?: string;
}
}

dataParserInit.overrideHandler.setMethod(
"setIdentifier",
(schema, identifier) => {
schema.definition.identifier = identifier;

return schema;
},
);

dataParserInit.overrideHandler.setMethod(
"addIdentifier",
(schema, identifier) => ({
...schema,
definition: {
...schema.definition,
identifier,
},
}),
);
136 changes: 136 additions & 0 deletions scripts/toJsonSchema/render.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { type DP, unwrap, E, kindHeritage, equal, or, G } from "@duplojs/utils";
import {
type DataParserErrorEither,
type DataParserNotSupportedEither,
transformer,
type MapContext,
type TransformerMode,
type TransformerHook,
type createTransformer,
supportedVersions,
type SupportedVersionsUrl,
type SupportedVersions,
} from "./transformer";
import { createToJsonSchemaKind } from "./kind";
import { getRecursiveDataParser } from "@scripts/utils/getRecursiveDataParser";

export interface RenderParams {
readonly identifier: string;
readonly transformers: readonly ReturnType<typeof createTransformer>[];
readonly context?: MapContext;
readonly mode?: TransformerMode;
readonly hooks?: readonly TransformerHook[];
readonly version: SupportedVersions;
}

export class DataParserToJsonSchemaRenderError extends kindHeritage(
"data-parser-to-json-schema-render-error",
createToJsonSchemaKind("data-parser-to-json-schema-render-error"),
Error,
) {
public constructor(
public schema: DP.DataParsers,
public error: DataParserNotSupportedEither | DataParserErrorEither,
) {
super({}, ["Error during the render of dataParser in jsonSchema."]);
}
}

function buildRef(
name: string,
version: SupportedVersionsUrl,
) {
if (version === supportedVersions.openApi3 || version === supportedVersions.openApi31) {
return { $ref: `#/components/schemas/${name}` };
}

if (version === supportedVersions.jsonSchema202012) {
return { $ref: `#/$defs/${name}` };
}

return { $ref: `#/definitions/${name}` };
}

function getDefinitionKey(version: SupportedVersionsUrl) {
if (version === supportedVersions.jsonSchema202012 || version === supportedVersions.openApi31) {
return "$defs";
}

return "definitions";
}

export function render(schema: DP.DataParsers, params: RenderParams) {
const context: MapContext = new Map(params.context);
const version = supportedVersions[params.version];

const result = transformer(
schema,
{
...params,
context,
mode: params.mode ?? "out",
hooks: params.hooks ?? [],
version,
recursiveDataParsers: getRecursiveDataParser(schema),
},
);

if (E.isLeft(result)) {
throw new DataParserToJsonSchemaRenderError(
schema,
result,
);
}

const built = unwrap(result);

const definitions = G.reduce(
context.values(),
G.reduceFrom<Record<string, unknown>>({}),
({ element, lastValue, next }) => element.schema
? next({
...lastValue,
[element.name]: element.schema,
})
: next(lastValue),
);

const definitionsWithIdentifier = definitions[params.identifier]
? definitions
: {
...definitions,
[params.identifier]: built.schema,
};

if (or(
version,
[
equal(supportedVersions.openApi3),
equal(supportedVersions.openApi31),
],
)) {
return JSON.stringify(
{
openapi: version,
components: {
schemas: definitionsWithIdentifier,
},
...buildRef(params.identifier, version),
},
null,
4,
);
}

const definitionKey = getDefinitionKey(version);

return JSON.stringify(
{
$schema: version,
...buildRef(params.identifier, version),
[definitionKey]: definitionsWithIdentifier,
},
null,
4,
);
}
Loading
Loading