diff --git a/lib/internal/bootstrap/realm.js b/lib/internal/bootstrap/realm.js index f49f0814bbc687..b552121dc291a4 100644 --- a/lib/internal/bootstrap/realm.js +++ b/lib/internal/bootstrap/realm.js @@ -129,6 +129,7 @@ const schemelessBlockList = new SafeSet([ 'quic', 'test', 'test/reporters', + 'json_parser', ]); // Modules that will only be enabled at run time. const experimentalModuleList = new SafeSet(['sqlite', 'quic']); diff --git a/lib/json_parser.js b/lib/json_parser.js new file mode 100644 index 00000000000000..6107210ac0cc48 --- /dev/null +++ b/lib/json_parser.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = internalBinding('json_parser'); diff --git a/node.gyp b/node.gyp index 83351d1d82627e..01633229926132 100644 --- a/node.gyp +++ b/node.gyp @@ -195,6 +195,7 @@ 'src/udp_wrap.cc', 'src/util.cc', 'src/uv.cc', + 'src/node_json_parser.cc', # headers to make for a more pleasant IDE experience 'src/aliased_buffer.h', 'src/aliased_buffer-inl.h', diff --git a/src/env_properties.h b/src/env_properties.h index 903158ebbdc2b7..9d90855a9ea9a3 100644 --- a/src/env_properties.h +++ b/src/env_properties.h @@ -441,6 +441,9 @@ V(streambaseoutputstream_constructor_template, v8::ObjectTemplate) \ V(tcp_constructor_template, v8::FunctionTemplate) \ V(tlsa_record_template, v8::DictionaryTemplate) \ + V(json_document_constructor_template, v8::FunctionTemplate) \ + V(json_value_constructor_template, v8::FunctionTemplate) \ + V(json_array_iterator_constructor_template, v8::FunctionTemplate) \ V(tty_constructor_template, v8::FunctionTemplate) \ V(txt_record_template, v8::DictionaryTemplate) \ V(urlpatterncomponentresult_template, v8::DictionaryTemplate) \ diff --git a/src/node_binding.cc b/src/node_binding.cc index 740706e917b7d2..bc3a27a0545290 100644 --- a/src/node_binding.cc +++ b/src/node_binding.cc @@ -76,6 +76,7 @@ V(sea) \ V(serdes) \ V(signal_wrap) \ + V(json_parser) \ V(spawn_sync) \ V(stream_pipe) \ V(stream_wrap) \ diff --git a/src/node_json_parser.cc b/src/node_json_parser.cc new file mode 100644 index 00000000000000..e7c584e3460ad2 --- /dev/null +++ b/src/node_json_parser.cc @@ -0,0 +1,655 @@ +#include "env-inl.h" +#include "node_errors.h" +#include "node_external_reference.h" +#include "simdjson.h" +#include "util-inl.h" + +#define THROW_AND_RETURN_EMPTY_IF_SIMDJSON_ERROR(isolate, error) \ + do { \ + if ((error)) { \ + THROW_ERR_MODULE_NOT_INSTANTIATED((isolate)); \ + return MaybeLocal(); \ + } \ + } while (0) + +namespace node { +namespace json_parser { + +using v8::Boolean; +using v8::Context; +using v8::FunctionCallbackInfo; +using v8::FunctionTemplate; +using v8::Isolate; +using v8::Local; +using v8::MaybeLocal; +using v8::Number; +using v8::Object; +using v8::String; +using v8::Value; + +void IllegalConstructor(const FunctionCallbackInfo& args) { + THROW_ERR_ILLEGAL_CONSTRUCTOR(Environment::GetCurrent(args)); +} + +Local JsonTypeToV8String(Isolate* isolate, + simdjson::ondemand::json_type type) { + switch (type) { + case simdjson::ondemand::json_type::object: + return FIXED_ONE_BYTE_STRING(isolate, "object"); + case simdjson::ondemand::json_type::array: + return FIXED_ONE_BYTE_STRING(isolate, "array"); + case simdjson::ondemand::json_type::number: + return FIXED_ONE_BYTE_STRING(isolate, "number"); + case simdjson::ondemand::json_type::string: + return FIXED_ONE_BYTE_STRING(isolate, "string"); + case simdjson::ondemand::json_type::boolean: + return FIXED_ONE_BYTE_STRING(isolate, "boolean"); + case simdjson::ondemand::json_type::null: + return FIXED_ONE_BYTE_STRING(isolate, "null"); + case simdjson::ondemand::json_type::unknown: + // todo(araujogui): should throw error? + return FIXED_ONE_BYTE_STRING(isolate, "unknown"); + } + UNREACHABLE(); +} + +class JsonValue; + +class JsonDocument : public BaseObject { + public: + JsonDocument(Environment* env, + Local object, + simdjson::padded_string padded, + simdjson::ondemand::document document) + : BaseObject(env, object), + padded_(std::move(padded)), + document_(std::move(document)) { + MakeWeak(); + } + + static Local GetConstructorTemplate(Environment* env); + + static BaseObjectPtr Create( + Environment* env, + simdjson::padded_string padded, + simdjson::ondemand::document document); + + static void Type(const FunctionCallbackInfo& args); + static void Root(const FunctionCallbackInfo& args); + static void GetArray(const FunctionCallbackInfo& args); + + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(JsonDocument) + SET_SELF_SIZE(JsonDocument) + + private: + simdjson::padded_string padded_; + simdjson::ondemand::document document_; + + friend class JsonValue; +}; + +Local JsonDocument::GetConstructorTemplate(Environment* env) { + Local tmpl = env->json_document_constructor_template(); + if (tmpl.IsEmpty()) { + Isolate* isolate = env->isolate(); + tmpl = NewFunctionTemplate(isolate, nullptr); + tmpl->SetClassName(FIXED_ONE_BYTE_STRING(env->isolate(), "JsonDocument")); + SetProtoMethod(isolate, tmpl, "type", JsonDocument::Type); + SetProtoMethod(isolate, tmpl, "root", JsonDocument::Root); + SetProtoMethod(isolate, tmpl, "getArray", JsonDocument::GetArray); + tmpl->InstanceTemplate()->SetInternalFieldCount( + JsonDocument::kInternalFieldCount); + env->set_json_document_constructor_template(tmpl); + } + return tmpl; +} + +BaseObjectPtr JsonDocument::Create( + Environment* env, + simdjson::padded_string padded, + simdjson::ondemand::document document) { + Local obj; + + if (!GetConstructorTemplate(env) + ->InstanceTemplate() + ->NewInstance(env->context()) + .ToLocal(&obj)) { + return nullptr; + } + + return MakeBaseObject( + env, obj, std::move(padded), std::move(document)); +} + +class JsonValue : public BaseObject { + public: + JsonValue(Environment* env, + Local object, + BaseObjectPtr document, + simdjson::ondemand::value value) + : BaseObject(env, object), + document_(std::move(document)), + value_(std::move(value)) { + MakeWeak(); + } + + static Local GetConstructorTemplate(Environment* env); + + static BaseObjectPtr Create( + Environment* env, + BaseObjectPtr document, + simdjson::ondemand::value value); + + static void Type(const FunctionCallbackInfo& args); + static void GetString(const FunctionCallbackInfo& args); + static void GetNumber(const FunctionCallbackInfo& args); + static void GetBoolean(const FunctionCallbackInfo& args); + static void IsNull(const FunctionCallbackInfo& args); + + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(JsonValue) + SET_SELF_SIZE(JsonValue) + + private: + BaseObjectPtr document_; + simdjson::ondemand::value value_; +}; + +Local JsonValue::GetConstructorTemplate(Environment* env) { + Local tmpl = env->json_value_constructor_template(); + if (tmpl.IsEmpty()) { + Isolate* isolate = env->isolate(); + tmpl = NewFunctionTemplate(isolate, nullptr); + tmpl->SetClassName(FIXED_ONE_BYTE_STRING(isolate, "JsonValue")); + SetProtoMethod(isolate, tmpl, "type", JsonValue::Type); + SetProtoMethod(isolate, tmpl, "getString", JsonValue::GetString); + SetProtoMethod(isolate, tmpl, "getNumber", JsonValue::GetNumber); + SetProtoMethod(isolate, tmpl, "getBoolean", JsonValue::GetBoolean); + SetProtoMethod(isolate, tmpl, "isNull", JsonValue::IsNull); + tmpl->InstanceTemplate()->SetInternalFieldCount( + JsonValue::kInternalFieldCount); + env->set_json_value_constructor_template(tmpl); + } + return tmpl; +} + +BaseObjectPtr JsonValue::Create( + Environment* env, + BaseObjectPtr document, + simdjson::ondemand::value value) { + Local obj; + + if (!GetConstructorTemplate(env) + ->InstanceTemplate() + ->NewInstance(env->context()) + .ToLocal(&obj)) { + return nullptr; + } + + return MakeBaseObject( + env, obj, std::move(document), std::move(value)); +} + +void JsonValue::Type(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + JsonValue* json_value; + ASSIGN_OR_RETURN_UNWRAP(&json_value, args.This()); + + simdjson::ondemand::json_type type; + auto error = json_value->value_.type().get(type); + if (error) { + THROW_ERR_INVALID_STATE(env, "Failed to get value type."); + return; + } + + args.GetReturnValue().Set(JsonTypeToV8String(args.GetIsolate(), type)); +} + +void JsonValue::GetString(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + JsonValue* json_value; + ASSIGN_OR_RETURN_UNWRAP(&json_value, args.This()); + + std::string_view str; + auto error = json_value->value_.get_string().get(str); + if (error) { + THROW_ERR_INVALID_STATE(env, "Value is not a string."); + return; + } + + args.GetReturnValue().Set( + String::NewFromUtf8( + args.GetIsolate(), str.data(), v8::NewStringType::kNormal, str.size()) + .ToLocalChecked()); +} + +void JsonValue::GetNumber(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + JsonValue* json_value; + ASSIGN_OR_RETURN_UNWRAP(&json_value, args.This()); + + double num; + auto error = json_value->value_.get_double().get(num); + if (error) { + THROW_ERR_INVALID_STATE(env, "Value is not a number."); + return; + } + + args.GetReturnValue().Set(Number::New(args.GetIsolate(), num)); +} + +void JsonValue::GetBoolean(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + JsonValue* json_value; + ASSIGN_OR_RETURN_UNWRAP(&json_value, args.This()); + + bool val; + auto error = json_value->value_.get_bool().get(val); + if (error) { + THROW_ERR_INVALID_STATE(env, "Value is not a boolean."); + return; + } + + args.GetReturnValue().Set(Boolean::New(args.GetIsolate(), val)); +} + +void JsonValue::IsNull(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + JsonValue* json_value; + ASSIGN_OR_RETURN_UNWRAP(&json_value, args.This()); + + bool val; + auto error = json_value->value_.is_null().get(val); + if (error) { + THROW_ERR_INVALID_STATE(env, "Failed to check if value is null."); + return; + } + args.GetReturnValue().Set(val); +} + +void JsonDocument::Type(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + JsonDocument* doc; + ASSIGN_OR_RETURN_UNWRAP(&doc, args.This()); + + simdjson::ondemand::json_type type; + auto error = doc->document_.type().get(type); + if (error) { + THROW_ERR_INVALID_STATE(env, "Failed to get document type."); + return; + } + + args.GetReturnValue().Set(JsonTypeToV8String(args.GetIsolate(), type)); +} + +void JsonDocument::Root(const FunctionCallbackInfo& args) { + JsonDocument* doc; + ASSIGN_OR_RETURN_UNWRAP(&doc, args.This()); + Environment* env = Environment::GetCurrent(args); + Isolate* isolate = env->isolate(); + + bool is_scalar = false; + auto error = doc->document_.is_scalar().get(is_scalar); + if (error) { + THROW_ERR_INVALID_STATE(env, "Failed to check if document is scalar."); + return; + } + + if (is_scalar) { + simdjson::ondemand::json_type type; + if (doc->document_.type().get(type)) { + THROW_ERR_INVALID_STATE(env, "Failed to get document type."); + return; + } + + switch (type) { + case simdjson::ondemand::json_type::string: { + std::string_view str; + if (doc->document_.get_string().get(str)) { + THROW_ERR_INVALID_STATE(env, "Failed to get root string."); + return; + } + args.GetReturnValue().Set( + String::NewFromUtf8( + isolate, str.data(), v8::NewStringType::kNormal, str.size()) + .ToLocalChecked()); + return; + } + case simdjson::ondemand::json_type::number: { + double num; + if (doc->document_.get_double().get(num)) { + THROW_ERR_INVALID_STATE(env, "Failed to get root number."); + return; + } + args.GetReturnValue().Set(Number::New(isolate, num)); + return; + } + case simdjson::ondemand::json_type::boolean: { + bool val; + if (doc->document_.get_bool().get(val)) { + THROW_ERR_INVALID_STATE(env, "Failed to get root boolean."); + return; + } + args.GetReturnValue().Set(Boolean::New(isolate, val)); + return; + } + case simdjson::ondemand::json_type::null: { + bool is_null; + if (doc->document_.is_null().get(is_null)) { + THROW_ERR_INVALID_STATE(env, "Failed to check root null."); + return; + } + args.GetReturnValue().SetNull(); + return; + } + default: + THROW_ERR_INVALID_STATE(env, "Unsupported scalar type."); + return; + } + } + + args.GetReturnValue().Set(args.This()); +} + +namespace { +Local GetIterResultTemplate(Environment* env) { + auto tmpl = env->iter_template(); + if (tmpl.IsEmpty()) { + static constexpr std::string_view keys[] = {"done", "value"}; + tmpl = v8::DictionaryTemplate::New(env->isolate(), keys); + env->set_iter_template(tmpl); + } + return tmpl; +} +} // namespace + +class JsonArrayIterator : public BaseObject { + public: + JsonArrayIterator(Environment* env, + Local object, + BaseObjectPtr document, + simdjson::ondemand::array array, + simdjson::ondemand::array_iterator iter, + simdjson::ondemand::array_iterator end) + : BaseObject(env, object), + document_(std::move(document)), + array_(std::move(array)), + iter_(std::move(iter)), + end_(std::move(end)), + done_(false) { + MakeWeak(); + } + + static Local GetConstructorTemplate(Environment* env); + + static BaseObjectPtr Create( + Environment* env, BaseObjectPtr document, + simdjson::ondemand::array array); + + static void Next(const FunctionCallbackInfo& args); + static void Return(const FunctionCallbackInfo& args); + + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(JsonArrayIterator) + SET_SELF_SIZE(JsonArrayIterator) + + private: + BaseObjectPtr document_; + simdjson::ondemand::array array_; + simdjson::ondemand::array_iterator iter_; + simdjson::ondemand::array_iterator end_; + bool done_; +}; + +Local JsonArrayIterator::GetConstructorTemplate( + Environment* env) { + Local tmpl = + env->json_array_iterator_constructor_template(); + if (tmpl.IsEmpty()) { + Isolate* isolate = env->isolate(); + tmpl = NewFunctionTemplate(isolate, IllegalConstructor); + tmpl->SetClassName( + FIXED_ONE_BYTE_STRING(isolate, "JsonArrayIterator")); + tmpl->InstanceTemplate()->SetInternalFieldCount( + JsonArrayIterator::kInternalFieldCount); + SetProtoMethod(isolate, tmpl, "next", JsonArrayIterator::Next); + SetProtoMethod(isolate, tmpl, "return", JsonArrayIterator::Return); + env->set_json_array_iterator_constructor_template(tmpl); + } + return tmpl; +} + +BaseObjectPtr JsonArrayIterator::Create( + Environment* env, + BaseObjectPtr document, + simdjson::ondemand::array array) { + Local obj; + + if (!GetConstructorTemplate(env) + ->InstanceTemplate() + ->NewInstance(env->context()) + .ToLocal(&obj)) { + return nullptr; + } + + simdjson::ondemand::array_iterator begin; + auto begin_err = array.begin().get(begin); + if (begin_err) { + THROW_ERR_INVALID_STATE(env, "Failed to begin array iteration."); + return nullptr; + } + + simdjson::ondemand::array_iterator end; + auto end_err = array.end().get(end); + if (end_err) { + THROW_ERR_INVALID_STATE(env, "Failed to get array end iterator."); + return nullptr; + } + + return MakeBaseObject( + env, obj, std::move(document), std::move(array), + std::move(begin), std::move(end)); +} + +void JsonArrayIterator::Next(const FunctionCallbackInfo& args) { + JsonArrayIterator* iter; + ASSIGN_OR_RETURN_UNWRAP(&iter, args.This()); + Environment* env = Environment::GetCurrent(args); + Isolate* isolate = env->isolate(); + + auto iter_template = GetIterResultTemplate(env); + + if (iter->done_ || iter->iter_ == iter->end_) { + iter->done_ = true; + MaybeLocal values[] = { + Boolean::New(isolate, true), + v8::Null(isolate), + }; + Local result; + if (NewDictionaryInstanceNullProto(env->context(), iter_template, values) + .ToLocal(&result)) { + args.GetReturnValue().Set(result); + } + return; + } + + simdjson::ondemand::value element; + auto elem_error = (*iter->iter_).get(element); + ++(iter->iter_); + if (elem_error) { + THROW_ERR_INVALID_STATE(env, "Failed to get array element."); + return; + } + + auto json_value = + JsonValue::Create(env, iter->document_, std::move(element)); + if (!json_value) return; + + MaybeLocal values[] = { + Boolean::New(isolate, false), + json_value->object(), + }; + Local result; + if (NewDictionaryInstanceNullProto(env->context(), iter_template, values) + .ToLocal(&result)) { + args.GetReturnValue().Set(result); + } +} + +void JsonArrayIterator::Return(const FunctionCallbackInfo& args) { + JsonArrayIterator* iter; + ASSIGN_OR_RETURN_UNWRAP(&iter, args.This()); + Environment* env = Environment::GetCurrent(args); + Isolate* isolate = env->isolate(); + + iter->done_ = true; + + auto iter_template = GetIterResultTemplate(env); + MaybeLocal values[] = { + Boolean::New(isolate, true), + v8::Null(isolate), + }; + Local result; + if (NewDictionaryInstanceNullProto(env->context(), iter_template, values) + .ToLocal(&result)) { + args.GetReturnValue().Set(result); + } +} + +void JsonDocument::GetArray(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + JsonDocument* doc; + ASSIGN_OR_RETURN_UNWRAP(&doc, args.This()); + + simdjson::ondemand::array arr; + auto error = doc->document_.get_array().get(arr); + if (error) { + THROW_ERR_INVALID_STATE(env, "Document root is not an array."); + return; + } + + auto iter = JsonArrayIterator::Create( + env, BaseObjectPtr(doc), std::move(arr)); + if (!iter) return; + + Local context = env->context(); + Local global = context->Global(); + Local js_iterator; + Local js_iterator_prototype; + + if (!global->Get(context, env->iterator_string()).ToLocal(&js_iterator) || + !js_iterator.As() + ->Get(context, env->prototype_string()) + .ToLocal(&js_iterator_prototype)) { + return; + } + + if (iter->object() + ->GetPrototypeV2() + .As() + ->SetPrototypeV2(context, js_iterator_prototype) + .IsNothing()) { + return; + } + + args.GetReturnValue().Set(iter->object()); +} + +simdjson::padded_string ToPaddedString(Isolate* isolate, Local value) { + Utf8Value utf8_value(isolate, value); + return simdjson::padded_string(utf8_value.ToStringView()); +} + +class LazyParser : public BaseObject { + public: + LazyParser(Environment* env, Local obj); + ~LazyParser(); + + static void New(const FunctionCallbackInfo& args); + static void Parse(const FunctionCallbackInfo& args); + + SET_NO_MEMORY_INFO() + SET_MEMORY_INFO_NAME(JsonParser) + SET_SELF_SIZE(LazyParser) + + private: + simdjson::ondemand::parser parser_; +}; + +LazyParser::LazyParser(Environment* env, Local obj) + : BaseObject(env, obj) { + MakeWeak(); + + parser_ = simdjson::ondemand::parser(); +} + +LazyParser::~LazyParser() = default; + +void LazyParser::Parse(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + + LazyParser* parser; + ASSIGN_OR_RETURN_UNWRAP(&parser, args.This()); + + Isolate* isolate = env->isolate(); + + if (args.Length() < 1 || !args[0]->IsString()) { + THROW_ERR_INVALID_ARG_TYPE(isolate, + "The \"text\" argument must be a string."); + return; + } + + Local text = args[0].As(); + + simdjson::padded_string padded_string = ToPaddedString(isolate, text); + + simdjson::ondemand::document document; + + auto error = parser->parser_.iterate(padded_string).get(document); + + if (error) { + // TODO(araujogui): create a ERR_INVALID_JSON macro + THROW_ERR_SOURCE_PHASE_NOT_DEFINED( + isolate, "The \"json\" argument must be valid JSON."); + return; + } + + BaseObjectPtr json_document = + JsonDocument::Create(env, std::move(padded_string), std::move(document)); + + args.GetReturnValue().Set(json_document->object()); +} + +void LazyParser::New(const FunctionCallbackInfo& args) { + Environment* env = Environment::GetCurrent(args); + if (!args.IsConstructCall()) { + THROW_ERR_CONSTRUCT_CALL_REQUIRED(env); + return; + } + + new LazyParser(env, args.This()); +} + +void Initialize(Local target, + Local unused, + Local context, + void* priv) { + Environment* env = Environment::GetCurrent(context); + Isolate* isolate = env->isolate(); + + Local parser_tmpl = + NewFunctionTemplate(isolate, LazyParser::New); + + parser_tmpl->InstanceTemplate()->SetInternalFieldCount( + LazyParser::kInternalFieldCount); + + SetProtoMethodNoSideEffect(isolate, parser_tmpl, "parse", LazyParser::Parse); + + SetConstructorFunction(context, target, "Parser", parser_tmpl); +} + +} // namespace json_parser +} // namespace node + +NODE_BINDING_CONTEXT_AWARE_INTERNAL(json_parser, node::json_parser::Initialize) diff --git a/test/parallel/test-json-parser.js b/test/parallel/test-json-parser.js new file mode 100644 index 00000000000000..9d4d7d4a07eac2 --- /dev/null +++ b/test/parallel/test-json-parser.js @@ -0,0 +1,90 @@ +'use strict'; +require('../common'); + +const { test, describe } = require('node:test'); +const { Parser } = require('node:json_parser'); + +describe('LazyParser', () => { + test('scalar root string returns plain JS string', (t) => { + const parser = new Parser(); + t.assert.strictEqual(parser.parse('"hello"').root(), 'hello'); + }); + + test('scalar root number returns plain JS number', (t) => { + const parser = new Parser(); + t.assert.strictEqual(parser.parse('42').root(), 42); + }); + + test('scalar root boolean returns plain JS boolean', (t) => { + const parser = new Parser(); + t.assert.strictEqual(parser.parse('true').root(), true); + t.assert.strictEqual(parser.parse('false').root(), false); + }); + + test('scalar root null returns null', (t) => { + const parser = new Parser(); + t.assert.strictEqual(parser.parse('null').root(), null); + }); + + test('compound root array returns document with type "array"', (t) => { + const parser = new Parser(); + const doc = parser.parse('[1, 2, 3]'); + t.assert.strictEqual(doc.root().type(), 'array'); + }); + + test('compound root object returns document with type "object"', (t) => { + const parser = new Parser(); + const doc = parser.parse('{"a": 1}'); + t.assert.strictEqual(doc.root().type(), 'object'); + }); + + test('array iteration returns JsonValue objects', (t) => { + const parser = new Parser(); + const doc = parser.parse('[1, "hi", true, null]'); + const types = []; + for (const val of doc.root().getArray()) { + types.push(val.type()); + } + t.assert.deepStrictEqual(types, ['number', 'string', 'boolean', 'null']); + }); + + test('JsonValue getString returns string', (t) => { + const parser = new Parser(); + const doc = parser.parse('["hello"]'); + const iter = doc.root().getArray(); + const val = iter.next().value; + t.assert.strictEqual(val.getString(), 'hello'); + }); + + test('JsonValue getNumber returns number', (t) => { + const parser = new Parser(); + const doc = parser.parse('[99.5]'); + const iter = doc.root().getArray(); + const val = iter.next().value; + t.assert.strictEqual(val.getNumber(), 99.5); + }); + + test('JsonValue getBoolean returns boolean', (t) => { + const parser = new Parser(); + const doc = parser.parse('[true]'); + const iter = doc.root().getArray(); + const val = iter.next().value; + t.assert.strictEqual(val.getBoolean(), true); + }); + + test('JsonValue isNull returns true for null', (t) => { + const parser = new Parser(); + const doc = parser.parse('[null]'); + const iter = doc.root().getArray(); + const val = iter.next().value; + t.assert.strictEqual(val.isNull(), true); + }); + + test('array iterator done after exhaustion', (t) => { + const parser = new Parser(); + const doc = parser.parse('[1]'); + const iter = doc.root().getArray(); + t.assert.strictEqual(iter.next().done, false); + t.assert.strictEqual(iter.next().done, true); + }); +});