diff --git a/README.markdown b/README.markdown index b901291..46949e8 100644 --- a/README.markdown +++ b/README.markdown @@ -32,7 +32,7 @@ to use a JSON Schema validator at runtime to enforce remaining constraints. | Applicator (2020-12) | `items` | Yes | | Applicator (2020-12) | `prefixItems` | Yes | | Applicator (2020-12) | `anyOf` | Yes | -| Applicator (2020-12) | `patternProperties` | **PARTIAL** (Prefix patterns only) | +| Applicator (2020-12) | `patternProperties` | **PARTIAL GIVEN LANGUAGE LIMITATIONS** | | Applicator (2020-12) | `propertyNames` | Ignored | | Applicator (2020-12) | `dependentSchemas` | Pending | | Applicator (2020-12) | `contains` | Ignored | diff --git a/src/generator/typescript.cc b/src/generator/typescript.cc index 2508f85..5105425 100644 --- a/src/generator/typescript.cc +++ b/src/generator/typescript.cc @@ -1,7 +1,8 @@ #include -#include // std::hex, std::setfill, std::setw -#include // std::ostringstream +#include // std::ranges::any_of +#include // std::hex, std::setfill, std::setw +#include // std::ostringstream namespace { @@ -137,7 +138,12 @@ auto TypeScript::operator()(const IRObject &entry) -> void { } for (const auto &pattern_property : entry.pattern) { - this->output << " [key: `" << pattern_property.prefix << "${string}`]: " + if (!pattern_property.prefix.has_value()) { + continue; + } + + this->output << " [key: `" << pattern_property.prefix.value() + << "${string}`]: " << mangle(this->prefix, pattern_property.pointer, pattern_property.symbol, this->cache); @@ -146,11 +152,11 @@ auto TypeScript::operator()(const IRObject &entry) -> void { // is a sub-prefix of another (i.e. "x-data-" starts with "x-"), // intersect the types so the constraint is satisfied for (const auto &other : entry.pattern) { - if (&other == &pattern_property) { + if (&other == &pattern_property || !other.prefix.has_value()) { continue; } - if (pattern_property.prefix.starts_with(other.prefix)) { + if (pattern_property.prefix.value().starts_with(other.prefix.value())) { this->output << " & " << mangle(this->prefix, other.pointer, other.symbol, this->cache); @@ -160,9 +166,14 @@ auto TypeScript::operator()(const IRObject &entry) -> void { this->output << ";\n"; } + const auto has_non_prefix_pattern{ + std::ranges::any_of(entry.pattern, [](const auto &pattern_property) { + return !pattern_property.prefix.has_value(); + })}; + if (allows_any_additional) { this->output << " [key: string]: unknown | undefined;\n"; - } else if (has_typed_additional) { + } else if (has_typed_additional || has_non_prefix_pattern) { // TypeScript index signatures must be a supertype of all property value // types. We use a union of all member types plus the additional properties // type plus undefined (for optional properties). @@ -186,11 +197,14 @@ auto TypeScript::operator()(const IRObject &entry) -> void { << " |\n"; } - const auto &additional_type{std::get(entry.additional)}; - this->output << " " - << mangle(this->prefix, additional_type.pointer, - additional_type.symbol, this->cache) - << " |\n"; + if (has_typed_additional) { + const auto &additional_type{std::get(entry.additional)}; + this->output << " " + << mangle(this->prefix, additional_type.pointer, + additional_type.symbol, this->cache) + << " |\n"; + } + this->output << " undefined;\n"; } diff --git a/src/ir/include/sourcemeta/codegen/ir.h b/src/ir/include/sourcemeta/codegen/ir.h index f7902f2..ba3dfc2 100644 --- a/src/ir/include/sourcemeta/codegen/ir.h +++ b/src/ir/include/sourcemeta/codegen/ir.h @@ -64,7 +64,7 @@ struct IRObjectValue : IRType { /// @ingroup ir struct IRObjectPatternProperty : IRType { - std::string prefix; + std::optional prefix; }; /// @ingroup ir diff --git a/src/ir/ir_default_compiler.h b/src/ir/ir_default_compiler.h index cb836bb..721e22f 100644 --- a/src/ir/ir_default_compiler.h +++ b/src/ir/ir_default_compiler.h @@ -146,19 +146,18 @@ auto handle_object(const sourcemeta::core::JSON &schema, frame.traverse(sourcemeta::core::to_weak_pointer(pattern_pointer))}; assert(pattern_location.has_value()); + std::optional prefix{std::nullopt}; const auto regex{sourcemeta::core::to_regex(entry.first)}; - if (!regex.has_value() || - !std::holds_alternative( + if (regex.has_value() && + std::holds_alternative( regex.value())) { - throw UnsupportedKeywordValueError( - schema, location.pointer, "patternProperties", - "Only prefix patterns are supported"); + prefix = std::get(regex.value()); } pattern.push_back(IRObjectPatternProperty{ {.pointer = std::move(pattern_pointer), .symbol = symbol(frame, pattern_location.value().get())}, - std::get(regex.value())}); + std::move(prefix)}); } } diff --git a/test/e2e/typescript/2020-12/pattern_properties_additional_false/test.ts b/test/e2e/typescript/2020-12/pattern_properties_additional_false/test.ts index a021119..62e67b0 100644 --- a/test/e2e/typescript/2020-12/pattern_properties_additional_false/test.ts +++ b/test/e2e/typescript/2020-12/pattern_properties_additional_false/test.ts @@ -22,3 +22,9 @@ const test4: StrictExt = { // @ts-expect-error other: "value" }; + +// Edge case: key is exactly the prefix with empty suffix +const test5: StrictExt = { + name: "hello", + "x-": "value" +}; diff --git a/test/e2e/typescript/2020-12/pattern_properties_empty_pattern/expected.d.ts b/test/e2e/typescript/2020-12/pattern_properties_empty_pattern/expected.d.ts new file mode 100644 index 0000000..8cc294d --- /dev/null +++ b/test/e2e/typescript/2020-12/pattern_properties_empty_pattern/expected.d.ts @@ -0,0 +1,16 @@ +export type EmptyPatternName = string; + +export type EmptyPattern = number; + +export type EmptyPatternAdditionalProperties = never; + +export interface _EmptyPattern { + "name"?: EmptyPatternName; + [key: string]: + // As a notable limitation, TypeScript requires index signatures + // to also include the types of all of its properties, so we must + // match a superset of what JSON Schema allows + EmptyPatternName | + EmptyPattern | + undefined; +} diff --git a/test/e2e/typescript/2020-12/pattern_properties_empty_pattern/options.json b/test/e2e/typescript/2020-12/pattern_properties_empty_pattern/options.json new file mode 100644 index 0000000..bd1a8ef --- /dev/null +++ b/test/e2e/typescript/2020-12/pattern_properties_empty_pattern/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "EmptyPattern" +} diff --git a/test/e2e/typescript/2020-12/pattern_properties_empty_pattern/schema.json b/test/e2e/typescript/2020-12/pattern_properties_empty_pattern/schema.json new file mode 100644 index 0000000..9b34767 --- /dev/null +++ b/test/e2e/typescript/2020-12/pattern_properties_empty_pattern/schema.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "patternProperties": { + "": { "type": "integer" } + }, + "additionalProperties": false +} diff --git a/test/e2e/typescript/2020-12/pattern_properties_empty_pattern/test.ts b/test/e2e/typescript/2020-12/pattern_properties_empty_pattern/test.ts new file mode 100644 index 0000000..8d1a36d --- /dev/null +++ b/test/e2e/typescript/2020-12/pattern_properties_empty_pattern/test.ts @@ -0,0 +1,20 @@ +import { _EmptyPattern } from "./expected"; + +// Empty regex matches everything, so all keys go through [key: string] +// union (string | number | undefined) +const test1: _EmptyPattern = { + name: "hello", + anything: 42 +}; + +const test2: _EmptyPattern = { + name: "hello", + anything: "also valid" +}; + +// Boolean is not in the union +const test3: _EmptyPattern = { + name: "hello", + // @ts-expect-error + flag: true +}; diff --git a/test/e2e/typescript/2020-12/pattern_properties_full_hybrid/expected.d.ts b/test/e2e/typescript/2020-12/pattern_properties_full_hybrid/expected.d.ts new file mode 100644 index 0000000..937029b --- /dev/null +++ b/test/e2e/typescript/2020-12/pattern_properties_full_hybrid/expected.d.ts @@ -0,0 +1,25 @@ +export type HybridName = string; + +export type HybridAge = number; + +export type HybridX = string; + +export type Hybrid_09_id = number; + +export type HybridAdditionalProperties = boolean; + +export interface Hybrid { + "name": HybridName; + "age"?: HybridAge; + [key: `x-${string}`]: HybridX; + [key: string]: + // As a notable limitation, TypeScript requires index signatures + // to also include the types of all of its properties, so we must + // match a superset of what JSON Schema allows + HybridName | + HybridAge | + HybridX | + Hybrid_09_id | + HybridAdditionalProperties | + undefined; +} diff --git a/test/e2e/typescript/2020-12/pattern_properties_full_hybrid/options.json b/test/e2e/typescript/2020-12/pattern_properties_full_hybrid/options.json new file mode 100644 index 0000000..e3f42df --- /dev/null +++ b/test/e2e/typescript/2020-12/pattern_properties_full_hybrid/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "Hybrid" +} diff --git a/test/e2e/typescript/2020-12/pattern_properties_full_hybrid/schema.json b/test/e2e/typescript/2020-12/pattern_properties_full_hybrid/schema.json new file mode 100644 index 0000000..e1424a7 --- /dev/null +++ b/test/e2e/typescript/2020-12/pattern_properties_full_hybrid/schema.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { "type": "string" }, + "age": { "type": "integer" } + }, + "required": [ "name" ], + "patternProperties": { + "^x-": { "type": "string" }, + "[0-9]+_id": { "type": "integer" } + }, + "additionalProperties": { "type": "boolean" } +} diff --git a/test/e2e/typescript/2020-12/pattern_properties_full_hybrid/test.ts b/test/e2e/typescript/2020-12/pattern_properties_full_hybrid/test.ts new file mode 100644 index 0000000..c712da2 --- /dev/null +++ b/test/e2e/typescript/2020-12/pattern_properties_full_hybrid/test.ts @@ -0,0 +1,59 @@ +import { Hybrid } from "./expected"; + +// All features together +const test1: Hybrid = { + name: "hello", + age: 30, + "x-custom": "extension", + "123_id": 42, + flag: true +}; + +// Required name only +const test2: Hybrid = { + name: "hello" +}; + +// Missing required name +// @ts-expect-error +const test3: Hybrid = { + age: 30 +}; + +// Prefix pattern enforced: x- must be string, not number +const test4: Hybrid = { + name: "hello", + // @ts-expect-error + "x-custom": 42 +}; + +// Array is not in the union +const test5: Hybrid = { + name: "hello", + // @ts-expect-error + extra: [ 1, 2, 3 ] +}; + +// Additional property with boolean (additionalProperties type) +const test6: Hybrid = { + name: "hello", + flag: true +}; + +// JSON Schema would reject this (additionalProperties is boolean, not +// number), but TypeScript allows it because the [key: string] union must +// include all member and pattern types (number from "age" and the non-prefix +// pattern). The generated types are always a superset of what JSON Schema +// allows, never a subset. +const test7: Hybrid = { + name: "hello", + extra: 42 +}; + +// Template literal takes priority over permissive [key: string] union: +// boolean is in the union but x- keys must be string +const test8: Hybrid = { + name: "hello", + // @ts-expect-error + "x-custom": true +}; diff --git a/test/e2e/typescript/2020-12/pattern_properties_mixed_prefix_and_fallback/expected.d.ts b/test/e2e/typescript/2020-12/pattern_properties_mixed_prefix_and_fallback/expected.d.ts new file mode 100644 index 0000000..e309551 --- /dev/null +++ b/test/e2e/typescript/2020-12/pattern_properties_mixed_prefix_and_fallback/expected.d.ts @@ -0,0 +1,8 @@ +export type MixedFallbackX = string; + +export type MixedFallback_09 = number; + +export interface MixedFallback { + [key: `x-${string}`]: MixedFallbackX; + [key: string]: unknown | undefined; +} diff --git a/test/e2e/typescript/2020-12/pattern_properties_mixed_prefix_and_fallback/options.json b/test/e2e/typescript/2020-12/pattern_properties_mixed_prefix_and_fallback/options.json new file mode 100644 index 0000000..84e0968 --- /dev/null +++ b/test/e2e/typescript/2020-12/pattern_properties_mixed_prefix_and_fallback/options.json @@ -0,0 +1 @@ +{ "defaultPrefix": "MixedFallback" } diff --git a/test/e2e/typescript/2020-12/pattern_properties_mixed_prefix_and_fallback/schema.json b/test/e2e/typescript/2020-12/pattern_properties_mixed_prefix_and_fallback/schema.json new file mode 100644 index 0000000..50fd805 --- /dev/null +++ b/test/e2e/typescript/2020-12/pattern_properties_mixed_prefix_and_fallback/schema.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "patternProperties": { + "^x-": { "type": "string" }, + "[0-9]+": { "type": "integer" } + } +} diff --git a/test/e2e/typescript/2020-12/pattern_properties_mixed_prefix_and_fallback/test.ts b/test/e2e/typescript/2020-12/pattern_properties_mixed_prefix_and_fallback/test.ts new file mode 100644 index 0000000..5d3c2cb --- /dev/null +++ b/test/e2e/typescript/2020-12/pattern_properties_mixed_prefix_and_fallback/test.ts @@ -0,0 +1,28 @@ +import { MixedFallback } from "./expected"; + +// Prefix pattern enforced: x- key must be string +const test1: MixedFallback = { + "x-foo": "hello" +}; + +// Prefix pattern enforced even with [key: string]: unknown fallback +const test2: MixedFallback = { + // @ts-expect-error + "x-foo": 123 +}; + +// Non-prefix pattern falls back to unknown, so any value works +const test3: MixedFallback = { + "123": 42 +}; + +const test4: MixedFallback = { + "123": "also fine" +}; + +// Mixed together +const test5: MixedFallback = { + "x-custom": "hello", + "456": 99, + other: true +}; diff --git a/test/e2e/typescript/2020-12/pattern_properties_non_prefix_additional_false/expected.d.ts b/test/e2e/typescript/2020-12/pattern_properties_non_prefix_additional_false/expected.d.ts new file mode 100644 index 0000000..96a1cd0 --- /dev/null +++ b/test/e2e/typescript/2020-12/pattern_properties_non_prefix_additional_false/expected.d.ts @@ -0,0 +1,16 @@ +export type StrictFallbackName = string; + +export type StrictFallbackAz_id = number; + +export type StrictFallbackAdditionalProperties = never; + +export interface StrictFallback { + "name"?: StrictFallbackName; + [key: string]: + // As a notable limitation, TypeScript requires index signatures + // to also include the types of all of its properties, so we must + // match a superset of what JSON Schema allows + StrictFallbackName | + StrictFallbackAz_id | + undefined; +} diff --git a/test/e2e/typescript/2020-12/pattern_properties_non_prefix_additional_false/options.json b/test/e2e/typescript/2020-12/pattern_properties_non_prefix_additional_false/options.json new file mode 100644 index 0000000..269af9e --- /dev/null +++ b/test/e2e/typescript/2020-12/pattern_properties_non_prefix_additional_false/options.json @@ -0,0 +1 @@ +{ "defaultPrefix": "StrictFallback" } diff --git a/test/e2e/typescript/2020-12/pattern_properties_non_prefix_additional_false/schema.json b/test/e2e/typescript/2020-12/pattern_properties_non_prefix_additional_false/schema.json new file mode 100644 index 0000000..66f81b6 --- /dev/null +++ b/test/e2e/typescript/2020-12/pattern_properties_non_prefix_additional_false/schema.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "patternProperties": { + "[a-z]+_id": { "type": "integer" } + }, + "additionalProperties": false +} diff --git a/test/e2e/typescript/2020-12/pattern_properties_non_prefix_additional_false/test.ts b/test/e2e/typescript/2020-12/pattern_properties_non_prefix_additional_false/test.ts new file mode 100644 index 0000000..42cb1ae --- /dev/null +++ b/test/e2e/typescript/2020-12/pattern_properties_non_prefix_additional_false/test.ts @@ -0,0 +1,30 @@ +import { StrictFallback } from "./expected"; + +const test1: StrictFallback = { + name: "hello", + "user_id": 42 +}; + +// JSON Schema would reject this (additionalProperties: false and "other" +// does not match the pattern), but TypeScript allows it because the +// [key: string] union must include all member types (string from "name"), +// so any string value passes. The generated types are always a superset +// of what JSON Schema allows, never a subset. +const test2: StrictFallback = { + name: "hello", + other: "also string" +}; + +// Boolean is not in the union (string | number | undefined) +const test3: StrictFallback = { + name: "hello", + // @ts-expect-error + flag: true +}; + +// Array is not in the union +const test4: StrictFallback = { + name: "hello", + // @ts-expect-error + items: [ 1, 2, 3 ] +}; diff --git a/test/e2e/typescript/2020-12/pattern_properties_non_prefix_fallback/expected.d.ts b/test/e2e/typescript/2020-12/pattern_properties_non_prefix_fallback/expected.d.ts new file mode 100644 index 0000000..ba14512 --- /dev/null +++ b/test/e2e/typescript/2020-12/pattern_properties_non_prefix_fallback/expected.d.ts @@ -0,0 +1,8 @@ +export type FallbackName = string; + +export type FallbackAz_id = number; + +export interface Fallback { + "name"?: FallbackName; + [key: string]: unknown | undefined; +} diff --git a/test/e2e/typescript/2020-12/pattern_properties_non_prefix_fallback/options.json b/test/e2e/typescript/2020-12/pattern_properties_non_prefix_fallback/options.json new file mode 100644 index 0000000..4975c35 --- /dev/null +++ b/test/e2e/typescript/2020-12/pattern_properties_non_prefix_fallback/options.json @@ -0,0 +1 @@ +{ "defaultPrefix": "Fallback" } diff --git a/test/e2e/typescript/2020-12/pattern_properties_non_prefix_fallback/schema.json b/test/e2e/typescript/2020-12/pattern_properties_non_prefix_fallback/schema.json new file mode 100644 index 0000000..1ac6a7e --- /dev/null +++ b/test/e2e/typescript/2020-12/pattern_properties_non_prefix_fallback/schema.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "patternProperties": { + "[a-z]+_id": { "type": "integer" } + } +} diff --git a/test/e2e/typescript/2020-12/pattern_properties_non_prefix_fallback/test.ts b/test/e2e/typescript/2020-12/pattern_properties_non_prefix_fallback/test.ts new file mode 100644 index 0000000..1441ff0 --- /dev/null +++ b/test/e2e/typescript/2020-12/pattern_properties_non_prefix_fallback/test.ts @@ -0,0 +1,18 @@ +import { Fallback } from "./expected"; + +const test1: Fallback = { + name: "hello", + "user_id": 42 +}; + +// Name wrong type +const test2: Fallback = { + // @ts-expect-error + name: 123 +}; + +// Any extra key is allowed (additionalProperties defaults to true) +const test3: Fallback = { + name: "hello", + extra: true +}; diff --git a/test/e2e/typescript/2020-12/pattern_properties_non_prefix_only/expected.d.ts b/test/e2e/typescript/2020-12/pattern_properties_non_prefix_only/expected.d.ts new file mode 100644 index 0000000..2b4d768 --- /dev/null +++ b/test/e2e/typescript/2020-12/pattern_properties_non_prefix_only/expected.d.ts @@ -0,0 +1,5 @@ +export type NonPrefixAz_id = number; + +export interface NonPrefix { + [key: string]: unknown | undefined; +} diff --git a/test/e2e/typescript/2020-12/pattern_properties_non_prefix_only/options.json b/test/e2e/typescript/2020-12/pattern_properties_non_prefix_only/options.json new file mode 100644 index 0000000..0b8f3c8 --- /dev/null +++ b/test/e2e/typescript/2020-12/pattern_properties_non_prefix_only/options.json @@ -0,0 +1 @@ +{ "defaultPrefix": "NonPrefix" } diff --git a/test/e2e/typescript/2020-12/pattern_properties_non_prefix_only/schema.json b/test/e2e/typescript/2020-12/pattern_properties_non_prefix_only/schema.json new file mode 100644 index 0000000..18bf6d9 --- /dev/null +++ b/test/e2e/typescript/2020-12/pattern_properties_non_prefix_only/schema.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "patternProperties": { + "[a-z]+_id": { "type": "integer" } + } +} diff --git a/test/e2e/typescript/2020-12/pattern_properties_non_prefix_only/test.ts b/test/e2e/typescript/2020-12/pattern_properties_non_prefix_only/test.ts new file mode 100644 index 0000000..73c2cec --- /dev/null +++ b/test/e2e/typescript/2020-12/pattern_properties_non_prefix_only/test.ts @@ -0,0 +1,13 @@ +import { NonPrefix } from "./expected"; + +// Any key, any value (falls back to [key: string]: unknown) +const test1: NonPrefix = { + "user_id": 42 +}; + +const test2: NonPrefix = { + "user_id": 42, + other: "hello" +}; + +const test3: NonPrefix = {}; diff --git a/test/ir/ir_2020_12_test.cc b/test/ir/ir_2020_12_test.cc index 50227f2..dbbfdfc 100644 --- a/test/ir/ir_2020_12_test.cc +++ b/test/ir/ir_2020_12_test.cc @@ -1259,7 +1259,7 @@ TEST(IR_2020_12, object_with_pattern_properties_and_additional_false) { EXPECT_FALSE(std::get(object.additional)); } -TEST(IR_2020_12, object_with_non_prefix_pattern_properties_throws) { +TEST(IR_2020_12, object_with_non_prefix_pattern_properties) { const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ "$schema": "https://json-schema.org/draft/2020-12/schema", "type": "object", @@ -1268,9 +1268,42 @@ TEST(IR_2020_12, object_with_non_prefix_pattern_properties_throws) { } })JSON")}; - EXPECT_THROW( + const auto result{ + sourcemeta::codegen::compile(schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, + sourcemeta::codegen::default_compiler)}; + + using namespace sourcemeta::codegen; + + ASSERT_FALSE(result.empty()); + EXPECT_TRUE(std::holds_alternative(result.back())); + const auto &object{std::get(result.back())}; + EXPECT_EQ(object.pattern.size(), 1); + EXPECT_FALSE(object.pattern.at(0).prefix.has_value()); +} + +TEST(IR_2020_12, object_with_mixed_prefix_and_non_prefix_patterns) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "patternProperties": { + "^x-": { "type": "string" }, + "[0-9]+": { "type": "integer" } + } + })JSON")}; + + const auto result{ sourcemeta::codegen::compile(schema, sourcemeta::core::schema_walker, sourcemeta::core::schema_resolver, - sourcemeta::codegen::default_compiler), - sourcemeta::codegen::UnsupportedKeywordValueError); + sourcemeta::codegen::default_compiler)}; + + using namespace sourcemeta::codegen; + + ASSERT_FALSE(result.empty()); + EXPECT_TRUE(std::holds_alternative(result.back())); + const auto &object{std::get(result.back())}; + EXPECT_EQ(object.pattern.size(), 2); + EXPECT_TRUE(object.pattern.at(0).prefix.has_value()); + EXPECT_EQ(object.pattern.at(0).prefix.value(), "x-"); + EXPECT_FALSE(object.pattern.at(1).prefix.has_value()); }