ata-validator 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -49,6 +49,131 @@ function collectDefaults(schema, actions, path) {
49
49
  }
50
50
  }
51
51
 
52
+ // Build a function that coerces property values to match schema types in-place.
53
+ // Handles string→number, string→integer, string→boolean, number→string, boolean→string.
54
+ function buildCoercer(schema) {
55
+ if (typeof schema !== 'object' || schema === null) return null;
56
+ const actions = [];
57
+ collectCoercions(schema, actions);
58
+ if (actions.length === 0) return null;
59
+ return (data) => {
60
+ for (let i = 0; i < actions.length; i++) actions[i](data);
61
+ };
62
+ }
63
+
64
+ function collectCoercions(schema, actions, path) {
65
+ if (typeof schema !== 'object' || schema === null) return;
66
+ const props = schema.properties;
67
+ if (!props) return;
68
+ for (const [key, prop] of Object.entries(props)) {
69
+ if (!prop || typeof prop !== 'object' || !prop.type) continue;
70
+ const targetType = Array.isArray(prop.type) ? null : prop.type;
71
+ if (!targetType) continue;
72
+
73
+ const coerce = buildSingleCoercion(targetType);
74
+ if (!coerce) continue;
75
+
76
+ if (!path) {
77
+ actions.push((data) => {
78
+ if (typeof data === 'object' && data !== null && key in data) {
79
+ const coerced = coerce(data[key]);
80
+ if (coerced !== undefined) data[key] = coerced;
81
+ }
82
+ });
83
+ } else {
84
+ const parentPath = path;
85
+ actions.push((data) => {
86
+ let target = data;
87
+ for (let j = 0; j < parentPath.length; j++) {
88
+ if (typeof target !== 'object' || target === null) return;
89
+ target = target[parentPath[j]];
90
+ }
91
+ if (typeof target === 'object' && target !== null && key in target) {
92
+ const coerced = coerce(target[key]);
93
+ if (coerced !== undefined) target[key] = coerced;
94
+ }
95
+ });
96
+ }
97
+
98
+ // Recurse into nested object properties
99
+ if (prop.properties) {
100
+ collectCoercions(prop, actions, (path || []).concat(key));
101
+ }
102
+ }
103
+ }
104
+
105
+ function buildSingleCoercion(targetType) {
106
+ switch (targetType) {
107
+ case 'number': return (v) => {
108
+ if (typeof v === 'string') { const n = Number(v); if (v !== '' && !isNaN(n)) return n; }
109
+ if (typeof v === 'boolean') return v ? 1 : 0;
110
+ };
111
+ case 'integer': return (v) => {
112
+ if (typeof v === 'string') { const n = Number(v); if (v !== '' && Number.isInteger(n)) return n; }
113
+ if (typeof v === 'boolean') return v ? 1 : 0;
114
+ };
115
+ case 'string': return (v) => {
116
+ if (typeof v === 'number' || typeof v === 'boolean') return String(v);
117
+ };
118
+ case 'boolean': return (v) => {
119
+ if (v === 'true' || v === '1') return true;
120
+ if (v === 'false' || v === '0') return false;
121
+ };
122
+ default: return null;
123
+ }
124
+ }
125
+
126
+ // Build a function that removes properties not defined in schema.properties.
127
+ // Walks nested objects recursively.
128
+ function buildRemover(schema) {
129
+ if (typeof schema !== 'object' || schema === null) return null;
130
+ const actions = [];
131
+ collectRemovals(schema, actions);
132
+ if (actions.length === 0) return null;
133
+ return (data) => {
134
+ for (let i = 0; i < actions.length; i++) actions[i](data);
135
+ };
136
+ }
137
+
138
+ function collectRemovals(schema, actions, path) {
139
+ if (typeof schema !== 'object' || schema === null || !schema.properties) return;
140
+
141
+ // If this level has additionalProperties: false, add a removal action
142
+ if (schema.additionalProperties === false) {
143
+ const allowed = new Set(Object.keys(schema.properties));
144
+ if (!path) {
145
+ actions.push((data) => {
146
+ if (typeof data !== 'object' || data === null || Array.isArray(data)) return;
147
+ const keys = Object.keys(data);
148
+ for (let i = 0; i < keys.length; i++) {
149
+ if (!allowed.has(keys[i])) delete data[keys[i]];
150
+ }
151
+ });
152
+ } else {
153
+ const parentPath = path;
154
+ actions.push((data) => {
155
+ let target = data;
156
+ for (let j = 0; j < parentPath.length; j++) {
157
+ if (typeof target !== 'object' || target === null) return;
158
+ target = target[parentPath[j]];
159
+ }
160
+ if (typeof target !== 'object' || target === null || Array.isArray(target)) return;
161
+ const keys = Object.keys(target);
162
+ for (let i = 0; i < keys.length; i++) {
163
+ if (!allowed.has(keys[i])) delete target[keys[i]];
164
+ }
165
+ });
166
+ }
167
+ }
168
+
169
+ // Always recurse into nested properties (they may have their own additionalProperties: false)
170
+ for (const [key, prop] of Object.entries(schema.properties)) {
171
+ if (prop && typeof prop === 'object' && prop.properties) {
172
+ collectRemovals(prop, actions, (path || []).concat(key));
173
+ }
174
+ }
175
+ }
176
+
52
177
  const SIMDJSON_PADDING = 64;
53
178
  const VALID_RESULT = Object.freeze({ valid: true, errors: Object.freeze([]) });
54
179
 
@@ -75,7 +200,8 @@ function createPaddedBuffer(jsonStr) {
75
200
  }
76
201
 
77
202
  class Validator {
78
- constructor(schema) {
203
+ constructor(schema, opts) {
204
+ const options = opts || {};
79
205
  const schemaStr =
80
206
  typeof schema === "string" ? schema : JSON.stringify(schema);
81
207
  const compiled = new native.CompiledSchema(schemaStr);
@@ -90,10 +216,19 @@ class Validator {
90
216
  : (compileToJSCodegen(schemaObj) || compileToJS(schemaObj));
91
217
  this._jsFn = jsFn;
92
218
 
93
- // Default value applier applies schema defaults to objects in-place
219
+ // Data mutatorsapplied in-place before validation
94
220
  const applyDefaults = buildDefaultsApplier(schemaObj);
221
+ const applyCoerce = options.coerceTypes ? buildCoercer(schemaObj) : null;
222
+ const applyRemove = options.removeAdditional ? buildRemover(schemaObj) : null;
95
223
  this._applyDefaults = applyDefaults;
96
224
 
225
+ // Combine all mutators into a single pre-validation step
226
+ const mutators = [applyRemove, applyCoerce, applyDefaults].filter(Boolean);
227
+ const preprocess = mutators.length === 0 ? null
228
+ : mutators.length === 1 ? mutators[0]
229
+ : (data) => { for (let i = 0; i < mutators.length; i++) mutators[i](data); };
230
+ this._preprocess = preprocess;
231
+
97
232
  // Closure-capture: avoid `this` property lookup on every call.
98
233
  // V8 keeps closure vars in registers — no hidden class traversal.
99
234
  const fastSlot = this._fastSlot;
@@ -107,8 +242,8 @@ class Validator {
107
242
  const useSimdjsonForLarge = !hasArrayTraversal;
108
243
 
109
244
  if (jsFn) {
110
- this.validate = applyDefaults
111
- ? (data) => { applyDefaults(data); return jsFn(data) ? VALID_RESULT : compiled.validate(data); }
245
+ this.validate = preprocess
246
+ ? (data) => { preprocess(data); return jsFn(data) ? VALID_RESULT : compiled.validate(data); }
112
247
  : (data) => jsFn(data) ? VALID_RESULT : compiled.validate(data);
113
248
  this.isValidObject = jsFn;
114
249
  this.validateJSON = useSimdjsonForLarge
@@ -177,7 +312,7 @@ class Validator {
177
312
 
178
313
  // Fallback methods — only used when JS codegen is unavailable
179
314
  validate(data) {
180
- if (this._applyDefaults) this._applyDefaults(data);
315
+ if (this._preprocess) this._preprocess(data);
181
316
  return this._compiled.validate(data);
182
317
  }
183
318
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Ultra-fast JSON Schema validator. Beats ajv on every valid-path benchmark: 1.1x–2.7x faster validate(obj), 151x faster compilation, 5.9x faster parallel batch. Speculative validation with V8-optimized JS codegen, simdjson, multi-core. Standard Schema V1 compatible.",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",