Skip to content
Open

Dev #14

Show file tree
Hide file tree
Changes from all commits
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

All notable changes to this project will be documented in this file.

## [0.2.5] - 2025-03-27

### Changed

- Improved error handling for oneOf, anyOf, allOf

## [0.2.4] - 2025-03-24

### Changed
Expand Down
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ Cabidela takes a JSON-Schema and optional configuration flags:
- `errorMessages`: boolean - If true, the validator will use custom `errorMessage` messages from the schema. Default is false.
- `fullErrors`: boolean - If true, the validator will be more verbose when throwing errors for complex schemas (example: anyOf, oneOf's), set to false for shorter exceptions. Default is true.
- `useMerge`: boolean - Set to true if you want to use the `$merge` keyword. Default is false. See below for more information.
- `usePatch`: boolean - Set to true if you want to use the `$patch` keyword. Default is false. See below for more information.
- `subSchemas`: any[] - An optional array of sub-schemas that can be used with `$id` and `$ref`. See below for more information.

Returns a validation object.
Expand Down Expand Up @@ -299,6 +300,52 @@ new Cabidela(schema, { useMerge: true });

You can combine `$merge` with `$id` and `$ref` keywords, which get resolved first, for even more flexibility.

## $patch

Use can use `$patch` to remove properties from an object.

Here's how it works:

```json
{
"$patch": {
"source": {
"type": "object",
"properties": {
"p": { "type": "string" },
"q": { "type": "number" }
},
"additionalProperties": false
},
"with": {
"properties": { "q": null }
}
}
}
```

Resolves to:

```json
{
"type": "object",
"properties": {
"p": {
"type": "string" }
},
},
"additionalProperties": false
}
```

To use `$patch` set the `usePatch` flag to true when creating the instance.

```js
new Cabidela(schema, { usePatch: true });
```

Like `$merge`, you can combine `$patch` with `$id` and `$ref` keywords, which get resolved first, for even more flexibility.

## Custom errors

If the new instance options has the `errorMessages` flag set to true, you can use the property `errorMessage` in the schema to define custom error messages.
Expand Down
29 changes: 28 additions & 1 deletion src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,26 @@ function deepMerge(target: any, source: any) {
return result;
}

function deepPatch(target: any, source: any) {
const result = { ...target, ...source };
console.log(result);
for (const key of Object.keys(result)) {
if (typeof target[key] == "object" && typeof source[key] == "object") {
result[key] = deepMerge(target[key], source[key]);
} else if (target[key] == null) {
delete result[key];
} else {
result[key] = structuredClone(result[key]);
}
}
console.log(result);
return result;
}

export const traverseSchema = (options: CabidelaOptions, definitions: any, obj: any) => {
const ts = (obj: any, cb?: any) => {
let hits: number;
if (!obj) return;
do {
hits = 0;
for (const key of Object.keys(obj)) {
Expand All @@ -57,6 +74,16 @@ export const traverseSchema = (options: CabidelaOptions, definitions: any, obj:
delete obj[key];
}
}
if (options.usePatch && key == "$patch") {
const merge = deepPatch(obj[key].source, obj[key].with);
if (cb) {
cb(merge);
} else {
// root level
Object.assign(obj, merge);
delete obj[key];
}
}
} else {
if (key == "$ref") {
const { $id, $path } = parse$ref(obj[key]);
Expand All @@ -76,7 +103,7 @@ export const traverseSchema = (options: CabidelaOptions, definitions: any, obj:
}
}
}
} while (hits > 0);
} while (obj && hits > 0);
};
ts(obj);
};
Expand Down
13 changes: 7 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { resolvePayload, pathToString, traverseSchema } from "./helpers";
export type CabidelaOptions = {
applyDefaults?: boolean;
useMerge?: boolean;
usePatch?: boolean;
errorMessages?: boolean;
fullErrors?: boolean;
subSchemas?: Array<any>;
Expand Down Expand Up @@ -30,6 +31,8 @@ export class Cabidela {
this.options = {
fullErrors: true,
subSchemas: [],
useMerge: false,
usePatch: false,
applyDefaults: false,
errorMessages: false,
...(options || {}),
Expand All @@ -43,7 +46,7 @@ export class Cabidela {
this.addSchema(subSchema, false);
}
}
if (this.options.useMerge || (this.options.subSchemas as []).length > 0) {
if (this.options.useMerge || this.options.usePatch || (this.options.subSchemas as []).length > 0) {
traverseSchema(this.options, this.definitions, this.schema);
}
}
Expand Down Expand Up @@ -139,7 +142,7 @@ export class Cabidela {
if (needle.schema.hasOwnProperty("maxProperties")) {
if (Object.keys(needle.payload).length > needle.schema.maxProperties) {
this.throw(
`maxProperties at '${pathToString(needle.path)}' is ${needle.schema.minProperties}, got ${Object.keys(needle.payload).length}`,
`maxProperties at '${pathToString(needle.path)}' is ${needle.schema.maxProperties}, got ${Object.keys(needle.payload).length}`,
needle,
);
}
Expand Down Expand Up @@ -207,7 +210,7 @@ export class Cabidela {
absorvErrors: true,
deferredApplyDefaults: true,
});
rounds += matches;
rounds++;
if (breakCondition && breakCondition(rounds)) break;
defaultsCallbacks.push(...needle.defaultsCallbacks);
needle.defaultsCallbacks = [];
Expand Down Expand Up @@ -247,9 +250,7 @@ export class Cabidela {
if (needle.schema.hasOwnProperty("oneOf")) {
const rounds = this.parseList(needle.schema.oneOf, needle, (r: number) => r !== 1);
if (rounds !== 1) {
if (needle.path.length == 0) {
this.throw(`oneOf at '${pathToString(needle.path)}' not met, ${rounds} matches`, needle);
}
this.throw(`oneOf at '${pathToString(needle.path)}' not met, ${rounds} matches found`, needle);
return 0;
}
return 1;
Expand Down
4 changes: 2 additions & 2 deletions tests/60-error-messages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,13 @@ describe("errorMessages oneOf", () => {
cabidela.validate({
missing: "property",
}),
).toThrowError(/oneOf at '.' not met, 0 matches: prompt required, messages required/);
).toThrowError(/oneOf at '\/' not met, 0 matches found: prompt required, messages required/);
});
test.skipIf(process.env.AJV)("messages need role and content", () => {
expect(() =>
cabidela.validate({
messages: [{ role: "user" }],
}),
).toThrowError(/oneOf at '.' not met, 0 matches: prompt required, messages need both role and content/);
).toThrowError(/oneOf at '\/' not met, 0 matches found: prompt required, messages need both role and content/);
});
});