ata-validator 0.7.3 → 0.9.0
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/CMakeLists.txt +41 -23
- package/README.md +22 -9
- package/binding/ata_napi.cpp +206 -5
- package/include/ata.h +11 -3
- package/index.js +121 -27
- package/lib/js-compiler.js +692 -83
- package/package.json +3 -2
- package/prebuilds/ata-darwin-arm64/node-napi-v10.node +0 -0
- package/prebuilds/darwin-arm64/ata-validator.node +0 -0
- package/src/ata.cpp +607 -154
- package/prebuilds/ata-linux-arm64/node-napi-v10.node +0 -0
- package/prebuilds/ata-linux-x64/node-napi-v10.node +0 -0
package/CMakeLists.txt
CHANGED
|
@@ -19,43 +19,48 @@ endif()
|
|
|
19
19
|
option(ATA_TESTING "Build test suite" ON)
|
|
20
20
|
option(ATA_BENCHMARKS "Build benchmarks" OFF)
|
|
21
21
|
option(ATA_SANITIZE "Enable address sanitizer" OFF)
|
|
22
|
+
option(ATA_NO_RE2 "Build without RE2 regex engine (disables pattern keyword)" OFF)
|
|
22
23
|
|
|
23
24
|
# Fetch simdjson
|
|
24
25
|
include(FetchContent)
|
|
25
26
|
FetchContent_Declare(
|
|
26
27
|
simdjson
|
|
27
28
|
GIT_REPOSITORY https://github.com/simdjson/simdjson.git
|
|
28
|
-
GIT_TAG
|
|
29
|
+
GIT_TAG v4.5.0
|
|
29
30
|
GIT_SHALLOW TRUE
|
|
30
31
|
)
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
if(NOT ATA_NO_RE2)
|
|
34
|
+
# RE2 — fast regex engine (replaces std::regex)
|
|
35
|
+
set(RE2_BUILD_TESTING OFF CACHE BOOL "" FORCE)
|
|
36
|
+
FetchContent_Declare(
|
|
37
|
+
re2
|
|
38
|
+
GIT_REPOSITORY https://github.com/google/re2.git
|
|
39
|
+
GIT_TAG 2024-07-02
|
|
40
|
+
GIT_SHALLOW TRUE
|
|
41
|
+
EXCLUDE_FROM_ALL
|
|
42
|
+
)
|
|
41
43
|
|
|
42
|
-
# Abseil — required by RE2
|
|
43
|
-
set(ABSL_PROPAGATE_CXX_STD ON CACHE BOOL "" FORCE)
|
|
44
|
-
set(ABSL_BUILD_TESTING OFF CACHE BOOL "" FORCE)
|
|
45
|
-
set(ABSL_ENABLE_INSTALL ON CACHE BOOL "" FORCE)
|
|
46
|
-
FetchContent_Declare(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
)
|
|
44
|
+
# Abseil — required by RE2
|
|
45
|
+
set(ABSL_PROPAGATE_CXX_STD ON CACHE BOOL "" FORCE)
|
|
46
|
+
set(ABSL_BUILD_TESTING OFF CACHE BOOL "" FORCE)
|
|
47
|
+
set(ABSL_ENABLE_INSTALL ON CACHE BOOL "" FORCE)
|
|
48
|
+
FetchContent_Declare(
|
|
49
|
+
abseil-cpp
|
|
50
|
+
GIT_REPOSITORY https://github.com/abseil/abseil-cpp.git
|
|
51
|
+
GIT_TAG 20240722.0
|
|
52
|
+
GIT_SHALLOW TRUE
|
|
53
|
+
)
|
|
52
54
|
|
|
53
|
-
FetchContent_MakeAvailable(abseil-cpp simdjson re2)
|
|
55
|
+
FetchContent_MakeAvailable(abseil-cpp simdjson re2)
|
|
56
|
+
else()
|
|
57
|
+
FetchContent_MakeAvailable(simdjson)
|
|
58
|
+
endif()
|
|
54
59
|
|
|
55
60
|
if (CMAKE_JS_VERSION)
|
|
56
61
|
# add_definitions(-DNAPI_VERSION=10)
|
|
57
62
|
include_directories(${CMAKE_JS_INC})
|
|
58
|
-
file(GLOB SOURCE_FILES "binding/*.cpp" "src/*.cpp"
|
|
63
|
+
file(GLOB SOURCE_FILES "binding/*.cpp" "src/*.cpp")
|
|
59
64
|
else()
|
|
60
65
|
file(GLOB SOURCE_FILES "src/*.cpp")
|
|
61
66
|
endif()
|
|
@@ -88,7 +93,12 @@ else()
|
|
|
88
93
|
endif()
|
|
89
94
|
|
|
90
95
|
target_include_directories(${PROJECT_NAME} PUBLIC include)
|
|
91
|
-
|
|
96
|
+
if(ATA_NO_RE2)
|
|
97
|
+
target_link_libraries(${PROJECT_NAME} PRIVATE simdjson)
|
|
98
|
+
target_compile_definitions(${PROJECT_NAME} PRIVATE ATA_NO_RE2)
|
|
99
|
+
else()
|
|
100
|
+
target_link_libraries(${PROJECT_NAME} PRIVATE simdjson re2)
|
|
101
|
+
endif()
|
|
92
102
|
|
|
93
103
|
if (CMAKE_JS_VERSION)
|
|
94
104
|
target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_JS_INC})
|
|
@@ -130,3 +140,11 @@ if(ATA_BENCHMARKS)
|
|
|
130
140
|
add_executable(ata_bench benchmark/bench.cpp)
|
|
131
141
|
target_link_libraries(ata_bench PRIVATE ata simdjson)
|
|
132
142
|
endif()
|
|
143
|
+
|
|
144
|
+
# Fuzz targets (for OSS-Fuzz / libFuzzer)
|
|
145
|
+
if(ATA_FUZZING)
|
|
146
|
+
foreach(fuzzer compile_fuzzer validate_fuzzer roundtrip_fuzzer)
|
|
147
|
+
add_executable(${fuzzer} fuzz/${fuzzer}.cpp)
|
|
148
|
+
target_link_libraries(${fuzzer} PRIVATE ata simdjson ${LIB_FUZZING_ENGINE})
|
|
149
|
+
endforeach()
|
|
150
|
+
endif()
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ Ultra-fast JSON Schema validator powered by [simdjson](https://github.com/simdjs
|
|
|
13
13
|
| **validate(obj)** valid | 22ns | 102ns | **ata 4.6x faster** |
|
|
14
14
|
| **validate(obj)** invalid | 87ns | 182ns | **ata 2.1x faster** |
|
|
15
15
|
| **isValidObject(obj)** | 21ns | 100ns | **ata 4.7x faster** |
|
|
16
|
-
| **Schema compilation** |
|
|
16
|
+
| **Schema compilation** | 453ns | 1.24ms | **ata 2,729x faster** |
|
|
17
17
|
| **First validation** | 2.07μs | 1.11ms | **ata 534x faster** |
|
|
18
18
|
|
|
19
19
|
### Complex Schema (patternProperties + dependentSchemas + propertyNames + additionalProperties)
|
|
@@ -54,7 +54,7 @@ Three-tier hybrid codegen: static schemas compile to zero-overhead key checks, d
|
|
|
54
54
|
|---|---|---|---|---|---|
|
|
55
55
|
| **validate (valid)** | **9ns** | 38ns | 50ns | 334ns | 326ns |
|
|
56
56
|
| **validate (invalid)** | **37ns** | 103ns | 4ns | 11.8μs | 842ns |
|
|
57
|
-
| **compilation** | **
|
|
57
|
+
| **compilation** | **453ns** | 1.24ms | 52μs | — | — |
|
|
58
58
|
| **first validation** | **2.1μs** | 1.11ms | 54μs | — | — |
|
|
59
59
|
|
|
60
60
|
> Different categories: ata/ajv/typebox are JSON Schema validators, zod/valibot are schema-builder DSLs. [Benchmark code](benchmark/bench_all_mitata.mjs)
|
|
@@ -71,7 +71,7 @@ Three-tier hybrid codegen: static schemas compile to zero-overhead key checks, d
|
|
|
71
71
|
|
|
72
72
|
| Scenario | ata | ajv | |
|
|
73
73
|
|---|---|---|---|
|
|
74
|
-
| **Serverless cold start** (50 schemas) | 0.
|
|
74
|
+
| **Serverless cold start** (50 schemas) | 0.087ms | 3.67ms | **ata 42x faster** |
|
|
75
75
|
| **ReDoS protection** (`^(a+)+$`) | 0.3ms | 765ms | **ata immune (RE2)** |
|
|
76
76
|
| **Batch NDJSON** (10K items, multi-core) | 13.4M/sec | 5.1M/sec | **ata 2.6x faster** |
|
|
77
77
|
| **Fastify startup** (5 routes) | 0.5ms | 6.0ms | **ata 12x faster** |
|
|
@@ -88,14 +88,26 @@ Three-tier hybrid codegen: static schemas compile to zero-overhead key checks, d
|
|
|
88
88
|
|
|
89
89
|
**Adaptive simdjson**: For large documents (>8KB) with selective schemas, simdjson On Demand seeks only the needed fields - skipping irrelevant data at GB/s speeds.
|
|
90
90
|
|
|
91
|
+
### $dynamicRef / $dynamicAnchor / $anchor
|
|
92
|
+
|
|
93
|
+
| Scenario | ata | ajv | |
|
|
94
|
+
|---|---|---|---|
|
|
95
|
+
| **$dynamicRef tree** valid | 22ns | 54ns | **ata 2.4x faster** |
|
|
96
|
+
| **$dynamicRef tree** invalid | 70ns | 76ns | **ata 1.1x faster** |
|
|
97
|
+
| **$dynamicRef override** valid | 2.6ns | 183ns | **ata 70x faster** |
|
|
98
|
+
| **$dynamicRef override** invalid | 48ns | 185ns | **ata 3.8x faster** |
|
|
99
|
+
| **$anchor array** valid | 2.3ns | 3.1ns | **ata 1.4x faster** |
|
|
100
|
+
|
|
101
|
+
Self-recursive named functions for $dynamicRef, compile-time cross-schema resolution, zero-wrapper hybrid path. [Benchmark code](benchmark/bench_dynamicref_vs_ajv.mjs)
|
|
102
|
+
|
|
91
103
|
### JSON Schema Test Suite
|
|
92
104
|
|
|
93
|
-
**
|
|
105
|
+
**95.3%** pass rate (1170/1227) on official [JSON Schema Test Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite) (Draft 2020-12). **95.3%** on [@exodus/schemasafe](https://github.com/ExodusMovement/schemasafe) test suite.
|
|
94
106
|
|
|
95
107
|
## When to use ata
|
|
96
108
|
|
|
97
|
-
- **High-throughput `validate(obj)`** -
|
|
98
|
-
- **Complex schemas** - `patternProperties`, `dependentSchemas`, `propertyNames`, `unevaluatedProperties` all inline JS codegen
|
|
109
|
+
- **High-throughput `validate(obj)`** - 3.1x faster than ajv, 38x faster than zod
|
|
110
|
+
- **Complex schemas** - `patternProperties`, `dependentSchemas`, `propertyNames`, `unevaluatedProperties` all inline JS codegen
|
|
99
111
|
- **Multi-schema projects** - cross-schema `$ref` with `$id` registry, `addSchema()` API
|
|
100
112
|
- **Draft 7 migration** - auto-detects `$schema`, normalizes Draft 7 keywords transparently
|
|
101
113
|
- **Serverless / cold starts** - 6,904x faster compilation, 5,148x faster first validation
|
|
@@ -106,12 +118,13 @@ Three-tier hybrid codegen: static schemas compile to zero-overhead key checks, d
|
|
|
106
118
|
|
|
107
119
|
## When to use ajv
|
|
108
120
|
|
|
109
|
-
- **
|
|
110
|
-
-
|
|
121
|
+
- **Existing ajv ecosystem** - plugins, custom keywords, large community
|
|
122
|
+
- **Full unevaluatedProperties/Items** - ata covers most cases but some edge cases remain
|
|
111
123
|
|
|
112
124
|
## Features
|
|
113
125
|
|
|
114
|
-
- **Hybrid validator**:
|
|
126
|
+
- **Hybrid validator**: 4.1x faster than ajv, up to 70x faster on $dynamicRef - zero-wrapper hybrid path for valid data (no allocation), combined codegen for error collection. Schema compilation cache for repeated schemas
|
|
127
|
+
- **$dynamicRef / $dynamicAnchor / $anchor**: Full Draft 2020-12 dynamic reference support. Self-recursive named functions, compile-time cross-schema resolution (42/42 spec tests)
|
|
115
128
|
- **Cross-schema `$ref`**: `schemas` option and `addSchema()` API. Compile-time resolution with `$id` registry, zero runtime overhead
|
|
116
129
|
- **Draft 7 support**: Auto-detects `$schema` field, normalizes `dependencies`/`additionalItems`/`definitions` transparently
|
|
117
130
|
- **Multi-core**: Parallel validation across all CPU cores - 13.4M validations/sec
|
package/binding/ata_napi.cpp
CHANGED
|
@@ -9,12 +9,15 @@
|
|
|
9
9
|
#include <functional>
|
|
10
10
|
#include <queue>
|
|
11
11
|
#include <atomic>
|
|
12
|
+
#ifndef ATA_NO_RE2
|
|
12
13
|
#include <re2/re2.h>
|
|
14
|
+
#endif
|
|
13
15
|
#include <set>
|
|
14
16
|
#include <string>
|
|
15
17
|
#include <vector>
|
|
16
18
|
|
|
17
19
|
#include "ata.h"
|
|
20
|
+
#include <simdjson.h>
|
|
18
21
|
|
|
19
22
|
// ============================================================================
|
|
20
23
|
// V8 Direct Object Traversal Engine
|
|
@@ -39,7 +42,9 @@ struct schema_node {
|
|
|
39
42
|
std::optional<uint64_t> min_length;
|
|
40
43
|
std::optional<uint64_t> max_length;
|
|
41
44
|
std::optional<std::string> pattern;
|
|
45
|
+
#ifndef ATA_NO_RE2
|
|
42
46
|
std::shared_ptr<re2::RE2> compiled_pattern;
|
|
47
|
+
#endif
|
|
43
48
|
|
|
44
49
|
std::optional<uint64_t> min_items;
|
|
45
50
|
std::optional<uint64_t> max_items;
|
|
@@ -63,7 +68,9 @@ struct schema_node {
|
|
|
63
68
|
struct pattern_prop {
|
|
64
69
|
std::string pattern;
|
|
65
70
|
schema_node_ptr schema;
|
|
71
|
+
#ifndef ATA_NO_RE2
|
|
66
72
|
std::shared_ptr<re2::RE2> compiled;
|
|
73
|
+
#endif
|
|
67
74
|
};
|
|
68
75
|
std::vector<pattern_prop> pattern_properties;
|
|
69
76
|
|
|
@@ -83,6 +90,8 @@ struct schema_node {
|
|
|
83
90
|
schema_node_ptr else_schema;
|
|
84
91
|
|
|
85
92
|
std::string ref;
|
|
93
|
+
std::string dynamic_ref;
|
|
94
|
+
std::string id;
|
|
86
95
|
|
|
87
96
|
std::unordered_map<std::string, schema_node_ptr> defs;
|
|
88
97
|
|
|
@@ -114,10 +123,14 @@ static bool napi_check_format(const std::string& sv, const std::string& fmt) {
|
|
|
114
123
|
(sv.size() - dot - 1) >= 2;
|
|
115
124
|
}
|
|
116
125
|
if (fmt == "date") {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
126
|
+
if (sv.size() != 10 || !nb_is_digit(sv[0]) || !nb_is_digit(sv[1]) ||
|
|
127
|
+
!nb_is_digit(sv[2]) || !nb_is_digit(sv[3]) || sv[4] != '-' ||
|
|
128
|
+
!nb_is_digit(sv[5]) || !nb_is_digit(sv[6]) || sv[7] != '-' ||
|
|
129
|
+
!nb_is_digit(sv[8]) || !nb_is_digit(sv[9]))
|
|
130
|
+
return false;
|
|
131
|
+
int month = (sv[5] - '0') * 10 + (sv[6] - '0');
|
|
132
|
+
int day = (sv[8] - '0') * 10 + (sv[9] - '0');
|
|
133
|
+
return month >= 1 && month <= 12 && day >= 1 && day <= 31;
|
|
121
134
|
}
|
|
122
135
|
if (fmt == "time") {
|
|
123
136
|
if (sv.size() < 8) return false;
|
|
@@ -286,6 +299,15 @@ static void validate_napi(const schema_node_ptr& node,
|
|
|
286
299
|
const compiled_schema_internal& ctx,
|
|
287
300
|
std::vector<ata::validation_error>& errors);
|
|
288
301
|
|
|
302
|
+
// Recursion depth guard — prevents stack overflow on self-referencing schemas
|
|
303
|
+
struct NapiDepthGuard {
|
|
304
|
+
static thread_local int depth;
|
|
305
|
+
bool overflow;
|
|
306
|
+
NapiDepthGuard() : overflow(++depth > 100) {}
|
|
307
|
+
~NapiDepthGuard() { --depth; }
|
|
308
|
+
};
|
|
309
|
+
thread_local int NapiDepthGuard::depth = 0;
|
|
310
|
+
|
|
289
311
|
static void validate_napi(const schema_node_ptr& node,
|
|
290
312
|
Napi::Value value,
|
|
291
313
|
Napi::Env env,
|
|
@@ -294,6 +316,9 @@ static void validate_napi(const schema_node_ptr& node,
|
|
|
294
316
|
std::vector<ata::validation_error>& errors) {
|
|
295
317
|
if (!node) return;
|
|
296
318
|
|
|
319
|
+
NapiDepthGuard guard;
|
|
320
|
+
if (guard.overflow) return;
|
|
321
|
+
|
|
297
322
|
// Boolean schema
|
|
298
323
|
if (node->boolean_schema.has_value()) {
|
|
299
324
|
if (!node->boolean_schema.value()) {
|
|
@@ -516,6 +541,7 @@ static void validate_napi(const schema_node_ptr& node,
|
|
|
516
541
|
" > maxLength " +
|
|
517
542
|
std::to_string(node->max_length.value())});
|
|
518
543
|
}
|
|
544
|
+
#ifndef ATA_NO_RE2
|
|
519
545
|
if (node->compiled_pattern) {
|
|
520
546
|
if (!re2::RE2::PartialMatch(sv, *node->compiled_pattern)) {
|
|
521
547
|
errors.push_back({ata::error_code::pattern_mismatch, path,
|
|
@@ -523,6 +549,7 @@ static void validate_napi(const schema_node_ptr& node,
|
|
|
523
549
|
node->pattern.value()});
|
|
524
550
|
}
|
|
525
551
|
}
|
|
552
|
+
#endif
|
|
526
553
|
if (node->format.has_value()) {
|
|
527
554
|
const auto& fmt = node->format.value();
|
|
528
555
|
bool format_ok = napi_check_format(sv, fmt);
|
|
@@ -644,11 +671,13 @@ static void validate_napi(const schema_node_ptr& node,
|
|
|
644
671
|
}
|
|
645
672
|
|
|
646
673
|
for (const auto& pp : node->pattern_properties) {
|
|
674
|
+
#ifndef ATA_NO_RE2
|
|
647
675
|
if (pp.compiled && re2::RE2::PartialMatch(key_str, *pp.compiled)) {
|
|
648
676
|
validate_napi(pp.schema, val, env, path + "/" + key_str, ctx,
|
|
649
677
|
errors);
|
|
650
678
|
matched = true;
|
|
651
679
|
}
|
|
680
|
+
#endif
|
|
652
681
|
}
|
|
653
682
|
|
|
654
683
|
if (!matched) {
|
|
@@ -769,6 +798,67 @@ static void validate_napi(const schema_node_ptr& node,
|
|
|
769
798
|
// N-API Binding
|
|
770
799
|
// ============================================================================
|
|
771
800
|
|
|
801
|
+
// ============================================================================
|
|
802
|
+
// simdjson DOM to V8 JS Object conversion
|
|
803
|
+
// ============================================================================
|
|
804
|
+
|
|
805
|
+
static Napi::Value dom_to_napi(Napi::Env env, simdjson::dom::element el) {
|
|
806
|
+
using namespace simdjson;
|
|
807
|
+
switch (el.type()) {
|
|
808
|
+
case dom::element_type::OBJECT: {
|
|
809
|
+
auto obj = Napi::Object::New(env);
|
|
810
|
+
for (auto [key, val] : dom::object(el)) {
|
|
811
|
+
obj.Set(std::string(key), dom_to_napi(env, val));
|
|
812
|
+
}
|
|
813
|
+
return obj;
|
|
814
|
+
}
|
|
815
|
+
case dom::element_type::ARRAY: {
|
|
816
|
+
dom::array arr = el;
|
|
817
|
+
auto jsArr = Napi::Array::New(env, arr.size());
|
|
818
|
+
uint32_t i = 0;
|
|
819
|
+
for (auto val : arr) {
|
|
820
|
+
jsArr.Set(i++, dom_to_napi(env, val));
|
|
821
|
+
}
|
|
822
|
+
return jsArr;
|
|
823
|
+
}
|
|
824
|
+
case dom::element_type::STRING: {
|
|
825
|
+
std::string_view sv;
|
|
826
|
+
el.get(sv);
|
|
827
|
+
return Napi::String::New(env, sv.data(), sv.length());
|
|
828
|
+
}
|
|
829
|
+
case dom::element_type::INT64: {
|
|
830
|
+
int64_t v;
|
|
831
|
+
el.get(v);
|
|
832
|
+
return Napi::Number::New(env, static_cast<double>(v));
|
|
833
|
+
}
|
|
834
|
+
case dom::element_type::UINT64: {
|
|
835
|
+
uint64_t v;
|
|
836
|
+
el.get(v);
|
|
837
|
+
return Napi::Number::New(env, static_cast<double>(v));
|
|
838
|
+
}
|
|
839
|
+
case dom::element_type::DOUBLE: {
|
|
840
|
+
double v;
|
|
841
|
+
el.get(v);
|
|
842
|
+
return Napi::Number::New(env, v);
|
|
843
|
+
}
|
|
844
|
+
case dom::element_type::BOOL: {
|
|
845
|
+
bool v;
|
|
846
|
+
el.get(v);
|
|
847
|
+
return Napi::Boolean::New(env, v);
|
|
848
|
+
}
|
|
849
|
+
case dom::element_type::NULL_VALUE:
|
|
850
|
+
return env.Null();
|
|
851
|
+
default:
|
|
852
|
+
return env.Undefined();
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// Thread-local simdjson DOM parser for parseJSON / validateAndParse
|
|
857
|
+
static simdjson::dom::parser& tl_dom_parser() {
|
|
858
|
+
thread_local simdjson::dom::parser parser;
|
|
859
|
+
return parser;
|
|
860
|
+
}
|
|
861
|
+
|
|
772
862
|
static Napi::Object make_result(Napi::Env env,
|
|
773
863
|
const ata::validation_result& result) {
|
|
774
864
|
Napi::Object obj = Napi::Object::New(env);
|
|
@@ -794,7 +884,8 @@ class CompiledSchema : public Napi::ObjectWrap<CompiledSchema> {
|
|
|
794
884
|
{InstanceMethod("validate", &CompiledSchema::Validate),
|
|
795
885
|
InstanceMethod("validateJSON", &CompiledSchema::ValidateJSON),
|
|
796
886
|
InstanceMethod("validateDirect", &CompiledSchema::ValidateDirect),
|
|
797
|
-
InstanceMethod("isValidJSON", &CompiledSchema::IsValidJSON)
|
|
887
|
+
InstanceMethod("isValidJSON", &CompiledSchema::IsValidJSON),
|
|
888
|
+
InstanceMethod("validateAndParse", &CompiledSchema::ValidateAndParse)});
|
|
798
889
|
auto* constructor = new Napi::FunctionReference();
|
|
799
890
|
*constructor = Napi::Persistent(func);
|
|
800
891
|
env.SetInstanceData(constructor);
|
|
@@ -916,6 +1007,77 @@ class CompiledSchema : public Napi::ObjectWrap<CompiledSchema> {
|
|
|
916
1007
|
return ValidateDirectImpl(env, info[0]);
|
|
917
1008
|
}
|
|
918
1009
|
|
|
1010
|
+
// Parse JSON with simdjson, validate against schema, return parsed JS object
|
|
1011
|
+
Napi::Value ValidateAndParse(const Napi::CallbackInfo& info) {
|
|
1012
|
+
Napi::Env env = info.Env();
|
|
1013
|
+
if (info.Length() < 1) {
|
|
1014
|
+
Napi::TypeError::New(env, "JSON string or Buffer expected")
|
|
1015
|
+
.ThrowAsJavaScriptException();
|
|
1016
|
+
return env.Undefined();
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
const char* data;
|
|
1020
|
+
size_t len;
|
|
1021
|
+
|
|
1022
|
+
if (info[0].IsBuffer()) {
|
|
1023
|
+
auto buf = info[0].As<Napi::Buffer<char>>();
|
|
1024
|
+
data = buf.Data();
|
|
1025
|
+
len = buf.Length();
|
|
1026
|
+
} else if (info[0].IsString()) {
|
|
1027
|
+
auto [d, l] = extract_string(env, info[0]);
|
|
1028
|
+
data = d;
|
|
1029
|
+
len = l;
|
|
1030
|
+
} else {
|
|
1031
|
+
Napi::TypeError::New(env, "JSON string or Buffer expected")
|
|
1032
|
+
.ThrowAsJavaScriptException();
|
|
1033
|
+
return env.Undefined();
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// Parse with simdjson
|
|
1037
|
+
simdjson::padded_string padded(data, len);
|
|
1038
|
+
auto& parser = tl_dom_parser();
|
|
1039
|
+
auto doc_result = parser.parse(padded);
|
|
1040
|
+
if (doc_result.error()) {
|
|
1041
|
+
auto obj = Napi::Object::New(env);
|
|
1042
|
+
obj.Set("valid", false);
|
|
1043
|
+
obj.Set("value", env.Null());
|
|
1044
|
+
auto errors = Napi::Array::New(env, 1);
|
|
1045
|
+
auto err = Napi::Object::New(env);
|
|
1046
|
+
err.Set("code", Napi::Number::New(env, static_cast<int>(ata::error_code::invalid_json)));
|
|
1047
|
+
err.Set("path", Napi::String::New(env, ""));
|
|
1048
|
+
err.Set("message", Napi::String::New(env, "Invalid JSON"));
|
|
1049
|
+
errors[0u] = err;
|
|
1050
|
+
obj.Set("errors", errors);
|
|
1051
|
+
return obj;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// Validate
|
|
1055
|
+
auto valResult = ata::validate(schema_, std::string_view(data, len));
|
|
1056
|
+
|
|
1057
|
+
// Convert DOM to JS object
|
|
1058
|
+
Napi::Value jsValue = dom_to_napi(env, doc_result.value());
|
|
1059
|
+
|
|
1060
|
+
// Build result
|
|
1061
|
+
auto obj = Napi::Object::New(env);
|
|
1062
|
+
obj.Set("valid", valResult.valid);
|
|
1063
|
+
obj.Set("value", jsValue);
|
|
1064
|
+
if (valResult.valid) {
|
|
1065
|
+
obj.Set("errors", Napi::Array::New(env, 0));
|
|
1066
|
+
} else {
|
|
1067
|
+
Napi::Array errors = Napi::Array::New(env, valResult.errors.size());
|
|
1068
|
+
for (size_t i = 0; i < valResult.errors.size(); ++i) {
|
|
1069
|
+
Napi::Object err = Napi::Object::New(env);
|
|
1070
|
+
err.Set("code",
|
|
1071
|
+
Napi::Number::New(env, static_cast<int>(valResult.errors[i].code)));
|
|
1072
|
+
err.Set("path", Napi::String::New(env, valResult.errors[i].path));
|
|
1073
|
+
err.Set("message", Napi::String::New(env, valResult.errors[i].message));
|
|
1074
|
+
errors[i] = err;
|
|
1075
|
+
}
|
|
1076
|
+
obj.Set("errors", errors);
|
|
1077
|
+
}
|
|
1078
|
+
return obj;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
919
1081
|
private:
|
|
920
1082
|
Napi::Value ValidateDirectImpl(Napi::Env env, Napi::Value value) {
|
|
921
1083
|
compiled_schema_internal ctx;
|
|
@@ -968,6 +1130,44 @@ Napi::Value GetVersion(const Napi::CallbackInfo& info) {
|
|
|
968
1130
|
return Napi::String::New(info.Env(), std::string(ata::version()));
|
|
969
1131
|
}
|
|
970
1132
|
|
|
1133
|
+
// Standalone JSON parser using simdjson — returns parsed JS object
|
|
1134
|
+
Napi::Value ParseJSON(const Napi::CallbackInfo& info) {
|
|
1135
|
+
Napi::Env env = info.Env();
|
|
1136
|
+
if (info.Length() < 1) {
|
|
1137
|
+
Napi::TypeError::New(env, "JSON string or Buffer expected")
|
|
1138
|
+
.ThrowAsJavaScriptException();
|
|
1139
|
+
return env.Undefined();
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
const char* data;
|
|
1143
|
+
size_t len;
|
|
1144
|
+
|
|
1145
|
+
if (info[0].IsBuffer()) {
|
|
1146
|
+
auto buf = info[0].As<Napi::Buffer<char>>();
|
|
1147
|
+
data = buf.Data();
|
|
1148
|
+
len = buf.Length();
|
|
1149
|
+
} else if (info[0].IsString()) {
|
|
1150
|
+
auto [d, l] = CompiledSchema::extract_string(env, info[0]);
|
|
1151
|
+
data = d;
|
|
1152
|
+
len = l;
|
|
1153
|
+
} else {
|
|
1154
|
+
Napi::TypeError::New(env, "JSON string or Buffer expected")
|
|
1155
|
+
.ThrowAsJavaScriptException();
|
|
1156
|
+
return env.Undefined();
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// Parse with simdjson using thread-local parser
|
|
1160
|
+
simdjson::padded_string padded(data, len);
|
|
1161
|
+
auto& parser = tl_dom_parser();
|
|
1162
|
+
auto result = parser.parse(padded);
|
|
1163
|
+
if (result.error()) {
|
|
1164
|
+
Napi::Error::New(env, "Invalid JSON").ThrowAsJavaScriptException();
|
|
1165
|
+
return env.Undefined();
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
return dom_to_napi(env, result.value());
|
|
1169
|
+
}
|
|
1170
|
+
|
|
971
1171
|
// --- Thread Pool ---
|
|
972
1172
|
class ThreadPool {
|
|
973
1173
|
public:
|
|
@@ -1503,6 +1703,7 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
|
1503
1703
|
CompiledSchema::Init(env, exports);
|
|
1504
1704
|
exports.Set("validate", Napi::Function::New(env, ValidateOneShot));
|
|
1505
1705
|
exports.Set("version", Napi::Function::New(env, GetVersion));
|
|
1706
|
+
exports.Set("parseJSON", Napi::Function::New(env, ParseJSON));
|
|
1506
1707
|
exports.Set("fastRegister", Napi::Function::New(env, FastRegister));
|
|
1507
1708
|
exports.Set("fastValidate", Napi::Function::New(env, FastValidateSlow));
|
|
1508
1709
|
|
package/include/ata.h
CHANGED
|
@@ -8,14 +8,16 @@
|
|
|
8
8
|
#include <variant>
|
|
9
9
|
#include <vector>
|
|
10
10
|
|
|
11
|
+
#define ATA_VERSION "0.9.0"
|
|
12
|
+
|
|
11
13
|
namespace ata {
|
|
12
14
|
|
|
13
15
|
inline constexpr uint32_t VERSION_MAJOR = 0;
|
|
14
|
-
inline constexpr uint32_t VERSION_MINOR =
|
|
15
|
-
inline constexpr uint32_t VERSION_REVISION =
|
|
16
|
+
inline constexpr uint32_t VERSION_MINOR = 9;
|
|
17
|
+
inline constexpr uint32_t VERSION_REVISION = 0;
|
|
16
18
|
|
|
17
19
|
inline constexpr std::string_view version() noexcept {
|
|
18
|
-
return "0.
|
|
20
|
+
return "0.9.0";
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
enum class error_code : uint8_t {
|
|
@@ -64,8 +66,14 @@ struct validation_result {
|
|
|
64
66
|
|
|
65
67
|
struct compiled_schema;
|
|
66
68
|
|
|
69
|
+
struct schema_warning {
|
|
70
|
+
std::string path;
|
|
71
|
+
std::string message;
|
|
72
|
+
};
|
|
73
|
+
|
|
67
74
|
struct schema_ref {
|
|
68
75
|
std::shared_ptr<compiled_schema> impl;
|
|
76
|
+
std::vector<schema_warning> warnings;
|
|
69
77
|
|
|
70
78
|
explicit operator bool() const noexcept { return impl != nullptr; }
|
|
71
79
|
};
|