From 568942e6cc0f802e6438d2fe4e3f72d0ffed109a Mon Sep 17 00:00:00 2001 From: Jonathan Tatum Date: Fri, 13 Nov 2026 11:23:08 -0800 Subject: [PATCH] Report unhandled fields from the env YAML parser and add a strict mode. `EnvConfigFromYamlOptions::warnings` receives a message for each field the parser does not handle, including the unimplemented `imports`, `limits`, `validators` and `features`. `EnvConfigFromYamlOptions::strict` returns those messages as an error. PiperOrigin-RevId: 1021187543 --- env/BUILD | 1 + env/env_yaml.cc | 167 ++++++++++++++++++++++++++++++++++++------- env/env_yaml.h | 16 +++++ env/env_yaml_test.cc | 146 +++++++++++++++++++++++++++++++++++++ 4 files changed, 305 insertions(+), 25 deletions(-) diff --git a/env/BUILD b/env/BUILD index 0c17d6305..fd39f9997 100644 --- a/env/BUILD +++ b/env/BUILD @@ -137,6 +137,7 @@ cc_library( "@com_google_absl//absl/strings", "@com_google_absl//absl/strings:str_format", "@com_google_absl//absl/time", + "@com_google_absl//absl/types:span", "@yaml-cpp", ], ) diff --git a/env/env_yaml.cc b/env/env_yaml.cc index 5b6216e05..a0109c158 100644 --- a/env/env_yaml.cc +++ b/env/env_yaml.cc @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -34,8 +35,10 @@ #include "absl/strings/numbers.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_format.h" +#include "absl/strings/str_join.h" #include "absl/strings/string_view.h" #include "absl/time/time.h" +#include "absl/types/span.h" #include "common/ast.h" #include "common/constant.h" #include "common/signature.h" @@ -151,6 +154,73 @@ YAML::Node GetContextNodeForKeyValue(const YAML::Node& node, return value_node; } +// Fields handled by the parser, for each kind of map node in the config. +constexpr absl::string_view kConfigFields[] = { + "name", "description", "container", "extensions", + "stdlib", "context_variable", "variables", "functions"}; +// Fields that other CEL implementations support but cel-cpp does not. +constexpr absl::string_view kUnsupportedConfigFields[] = { + "imports", "limits", "validators", "features"}; +constexpr absl::string_view kContainerFields[] = {"name", "abbreviations", + "aliases"}; +constexpr absl::string_view kAliasFields[] = {"alias", "qualified_name"}; +constexpr absl::string_view kExtensionFields[] = {"name", "version"}; +constexpr absl::string_view kStandardLibraryFields[] = { + "disabled", "disable", "disable_macros", "include_macros", + "exclude_macros", "include_functions", "exclude_functions"}; +constexpr absl::string_view kFunctionSelectorFields[] = {"name", "overloads"}; +constexpr absl::string_view kOverloadSelectorFields[] = {"id"}; +constexpr absl::string_view kContextVariableFields[] = {"type_name", "type"}; +constexpr absl::string_view kTypeFields[] = {"type", "type_name", + "is_type_param", "params"}; +// A variable may specify its type inline, so the type fields are included. +constexpr absl::string_view kVariableFields[] = { + "name", "description", "value", "type", + "type_name", "is_type_param", "params"}; +constexpr absl::string_view kFunctionFields[] = {"name", "description", + "overloads"}; +constexpr absl::string_view kOverloadFields[] = { + "id", "examples", "signature", "target", "args", "return"}; + +// Collects a warning for each field that the parser does not handle. +class UnhandledFieldReporter { + public: + // Adds a warning for each key of the map `node` that is not in `handled`. + // Keys in `unsupported` are reported as recognized but not supported. + // Does nothing if `node` is not a map. Child nodes are not visited. + void Check(const YAML::Node& node, absl::string_view context, + absl::Span handled, + absl::Span unsupported = {}) { + if (!node.IsDefined() || !node.IsMap()) { + return; + } + for (const auto& kv : node) { + if (!kv.first.IsScalar()) { + continue; + } + const std::string& key = kv.first.Scalar(); + if (absl::c_linear_search(handled, key)) { + continue; + } + const YAML::Mark mark = kv.first.Mark(); + if (absl::c_linear_search(unsupported, key)) { + warnings_.push_back(absl::StrCat(mark.line + 1, ":", mark.column + 1, + ": Field '", key, + "' is not supported")); + } else { + warnings_.push_back(absl::StrCat(mark.line + 1, ":", mark.column + 1, + ": Unknown field '", key, "' in ", + context)); + } + } + } + + std::vector& warnings() { return warnings_; } + + private: + std::vector warnings_; +}; + absl::Status ParseName(Config& config, absl::string_view yaml, const YAML::Node& root) { const YAML::Node name = root["name"]; @@ -164,7 +234,8 @@ absl::Status ParseName(Config& config, absl::string_view yaml, } absl::Status ParseContainerConfig(Config& config, absl::string_view yaml, - const YAML::Node& root) { + const YAML::Node& root, + UnhandledFieldReporter& reporter) { const YAML::Node container = root["container"]; if (!container.IsDefined()) { return absl::OkStatus(); @@ -180,6 +251,7 @@ absl::Status ParseContainerConfig(Config& config, absl::string_view yaml, "Node 'container' is neither a string nor a map"); } + reporter.Check(container, "container", kContainerFields); Config::ContainerConfig container_config; const YAML::Node name = container["name"]; @@ -213,6 +285,7 @@ absl::Status ParseContainerConfig(Config& config, absl::string_view yaml, if (!alias_node.IsMap()) { return YamlError(yaml, alias_node, "Alias entry is not a map"); } + reporter.Check(alias_node, "alias", kAliasFields); const YAML::Node alias_key = alias_node["alias"]; const YAML::Node qualified_name_key = alias_node["qualified_name"]; @@ -237,7 +310,8 @@ absl::Status ParseContainerConfig(Config& config, absl::string_view yaml, absl::Status ParseExtensionConfigs(Config& config, absl::string_view yaml, const YAML::Node& root, - const EnvConfigFromYamlOptions& options) { + const EnvConfigFromYamlOptions& options, + UnhandledFieldReporter& reporter) { const YAML::Node extensions = root["extensions"]; if (!extensions.IsDefined()) { return absl::OkStatus(); @@ -250,6 +324,7 @@ absl::Status ParseExtensionConfigs(Config& config, absl::string_view yaml, if (!extension || !extension.IsMap()) { return YamlError(yaml, extension, "Extension is not a map"); } + reporter.Check(extension, "extension", kExtensionFields); const YAML::Node name = extension["name"]; if (!name || !name.IsScalar()) { return YamlError(yaml, name, "Extension name is not a string"); @@ -314,7 +389,7 @@ absl::StatusOr> ParseMacroList( absl::StatusOr>> ParseFunctionList(absl::string_view yaml, const YAML::Node& standard_library, - absl::string_view key) { + absl::string_view key, UnhandledFieldReporter& reporter) { absl::flat_hash_set> function_set; const YAML::Node functions = standard_library[std::string(key)]; if (!functions.IsDefined()) { @@ -329,6 +404,7 @@ ParseFunctionList(absl::string_view yaml, const YAML::Node& standard_library, return YamlError(yaml, function, absl::StrCat("Entry in '", key, "' is not a map")); } + reporter.Check(function, "function selector", kFunctionSelectorFields); const YAML::Node name = function["name"]; if (!name.IsDefined()) { return YamlError( @@ -356,6 +432,7 @@ ParseFunctionList(absl::string_view yaml, const YAML::Node& standard_library, yaml, overload, absl::StrCat("Overload in '", key, "' entry is not a map")); } + reporter.Check(overload, "overload selector", kOverloadSelectorFields); const YAML::Node id = overload["id"]; if (!id || !id.IsScalar()) { return YamlError( @@ -370,7 +447,8 @@ ParseFunctionList(absl::string_view yaml, const YAML::Node& standard_library, } absl::Status ParseStandardLibraryConfig(Config& config, absl::string_view yaml, - const YAML::Node& root) { + const YAML::Node& root, + UnhandledFieldReporter& reporter) { const YAML::Node standard_library = root["stdlib"]; if (!standard_library.IsDefined()) { return absl::OkStatus(); @@ -381,6 +459,7 @@ absl::Status ParseStandardLibraryConfig(Config& config, absl::string_view yaml, "Standard library config ('stdlib') is not a map"); } + reporter.Check(standard_library, "stdlib", kStandardLibraryFields); Config::StandardLibraryConfig standard_library_config; // `disabled` is the key used by the other CEL implementations. @@ -430,17 +509,23 @@ absl::Status ParseStandardLibraryConfig(Config& config, absl::string_view yaml, CEL_ASSIGN_OR_RETURN( standard_library_config.included_functions, - ParseFunctionList(yaml, standard_library, "include_functions")); + ParseFunctionList(yaml, standard_library, "include_functions", reporter)); CEL_ASSIGN_OR_RETURN( standard_library_config.excluded_functions, - ParseFunctionList(yaml, standard_library, "exclude_functions")); + ParseFunctionList(yaml, standard_library, "exclude_functions", reporter)); return config.SetStandardLibraryConfig(standard_library_config); } -absl::StatusOr ParseTypeInfo(const YAML::Node& node, - absl::string_view yaml) { +absl::StatusOr ParseTypeInfo( + const YAML::Node& node, absl::string_view yaml, + UnhandledFieldReporter& reporter); + +// Parses the type fields of `node` without checking for unhandled fields. +absl::StatusOr ParseTypeInfoFields( + const YAML::Node& node, absl::string_view yaml, + UnhandledFieldReporter& reporter) { Config::TypeInfo type_config; const YAML::Node type = node["type"]; const YAML::Node type_name = node["type_name"]; @@ -485,13 +570,20 @@ absl::StatusOr ParseTypeInfo(const YAML::Node& node, } for (const YAML::Node& param : params) { CEL_ASSIGN_OR_RETURN(Config::TypeInfo param_config, - ParseTypeInfo(param, yaml)); + ParseTypeInfo(param, yaml, reporter)); type_config.params.push_back(param_config); } return type_config; } +absl::StatusOr ParseTypeInfo( + const YAML::Node& node, absl::string_view yaml, + UnhandledFieldReporter& reporter) { + reporter.Check(node, "type", kTypeFields); + return ParseTypeInfoFields(node, yaml, reporter); +} + bool CompareTypeInfo(const Config::TypeInfo& a, const Config::TypeInfo& b) { if (a.name != b.name) { return a.name < b.name; @@ -612,7 +704,8 @@ absl::StatusOr ParseConstantValue(absl::string_view yaml, } absl::Status ParseVariableConfigs(Config& config, absl::string_view yaml, - const YAML::Node& root) { + const YAML::Node& root, + UnhandledFieldReporter& reporter) { const YAML::Node variables = root["variables"]; if (!variables.IsDefined()) { return absl::OkStatus(); @@ -626,6 +719,7 @@ absl::Status ParseVariableConfigs(Config& config, absl::string_view yaml, if (!variable || !variable.IsMap()) { return YamlError(yaml, variable, "Variable is not a map"); } + reporter.Check(variable, "variable", kVariableFields); const YAML::Node name = variable["name"]; if (!name || !name.IsScalar()) { return YamlError(yaml, name, "Variable name is not a string"); @@ -643,9 +737,11 @@ absl::Status ParseVariableConfigs(Config& config, absl::string_view yaml, Config::TypeInfo type_info; if (type.IsDefined() && !type.IsScalar()) { // Old format, type spec is in 'type' instead of directly embedded. - CEL_ASSIGN_OR_RETURN(type_info, ParseTypeInfo(variable["type"], yaml)); + CEL_ASSIGN_OR_RETURN(type_info, + ParseTypeInfo(variable["type"], yaml, reporter)); } else { - CEL_ASSIGN_OR_RETURN(type_info, ParseTypeInfo(variable, yaml)); + CEL_ASSIGN_OR_RETURN(type_info, + ParseTypeInfoFields(variable, yaml, reporter)); } ConstantKindCase constant_kind_case = GetConstantKindCase(type_info.name); std::string value_str; @@ -684,11 +780,12 @@ absl::Status ParseVariableConfigs(Config& config, absl::string_view yaml, absl::StatusOr ParseFunctionOverloadConfig( absl::string_view yaml, const YAML::Node& overload, - absl::string_view function_name) { + absl::string_view function_name, UnhandledFieldReporter& reporter) { Config::FunctionOverloadConfig overload_config; if (!overload || !overload.IsMap()) { return YamlError(yaml, overload, "Function overload is not a map"); } + reporter.Check(overload, "overload", kOverloadFields); const YAML::Node id = overload["id"]; if (id.IsDefined()) { if (!id.IsScalar()) { @@ -761,7 +858,7 @@ absl::StatusOr ParseFunctionOverloadConfig( return YamlError(yaml, target, "Function overload target is not a map"); } CEL_ASSIGN_OR_RETURN(Config::TypeInfo type_info, - ParseTypeInfo(target, yaml)); + ParseTypeInfo(target, yaml, reporter)); overload_config.is_member_function = true; overload_config.parameters.push_back(type_info); } @@ -776,7 +873,7 @@ absl::StatusOr ParseFunctionOverloadConfig( return YamlError(yaml, arg, "Function overload arg is not a map"); } CEL_ASSIGN_OR_RETURN(Config::TypeInfo type_info, - ParseTypeInfo(arg, yaml)); + ParseTypeInfo(arg, yaml, reporter)); overload_config.parameters.push_back(type_info); } } @@ -790,7 +887,7 @@ absl::StatusOr ParseFunctionOverloadConfig( TypeSpecToTypeInfo(type_spec)); } else if (return_type.IsMap()) { CEL_ASSIGN_OR_RETURN(overload_config.return_type, - ParseTypeInfo(return_type, yaml)); + ParseTypeInfo(return_type, yaml, reporter)); } else { return YamlError( yaml, return_type, @@ -801,7 +898,8 @@ absl::StatusOr ParseFunctionOverloadConfig( } absl::Status ParseFunctionConfigs(Config& config, absl::string_view yaml, - const YAML::Node& root) { + const YAML::Node& root, + UnhandledFieldReporter& reporter) { const YAML::Node functions = root["functions"]; if (!functions.IsDefined()) { return absl::OkStatus(); @@ -815,6 +913,7 @@ absl::Status ParseFunctionConfigs(Config& config, absl::string_view yaml, if (!function || !function.IsMap()) { return YamlError(yaml, function, "Function is not a map"); } + reporter.Check(function, "function", kFunctionFields); const YAML::Node name = function["name"]; if (!name || !name.IsScalar()) { return YamlError(yaml, name, "Function name is not a string"); @@ -838,7 +937,8 @@ absl::Status ParseFunctionConfigs(Config& config, absl::string_view yaml, for (const YAML::Node& overload : overloads) { CEL_ASSIGN_OR_RETURN( Config::FunctionOverloadConfig overload_config, - ParseFunctionOverloadConfig(yaml, overload, function_config.name)); + ParseFunctionOverloadConfig(yaml, overload, function_config.name, + reporter)); function_config.overload_configs.push_back(std::move(overload_config)); } } @@ -1280,7 +1380,8 @@ void EmitFunctionConfigs(const Config& env_config, YAML::Emitter& out, } absl::Status ParseContextVariableConfig(Config& config, absl::string_view yaml, - const YAML::Node& root) { + const YAML::Node& root, + UnhandledFieldReporter& reporter) { const YAML::Node context_variable = root["context_variable"]; if (!context_variable.IsDefined()) { return absl::OkStatus(); @@ -1290,6 +1391,7 @@ absl::Status ParseContextVariableConfig(Config& config, absl::string_view yaml, "Node 'context_variable' is not a map"); } + reporter.Check(context_variable, "context_variable", kContextVariableFields); const YAML::Node type_name = context_variable["type_name"]; const YAML::Node type = context_variable["type"]; const YAML::Node* type_node = nullptr; @@ -1321,13 +1423,28 @@ absl::StatusOr EnvConfigFromYaml( yaml, "Invalid CEL environment config YAML", root.Mark())); } + UnhandledFieldReporter reporter; + reporter.Check(root, "config", kConfigFields, kUnsupportedConfigFields); CEL_RETURN_IF_ERROR(ParseName(config, yaml, root)); - CEL_RETURN_IF_ERROR(ParseContainerConfig(config, yaml, root)); - CEL_RETURN_IF_ERROR(ParseExtensionConfigs(config, yaml, root, options)); - CEL_RETURN_IF_ERROR(ParseStandardLibraryConfig(config, yaml, root)); - CEL_RETURN_IF_ERROR(ParseContextVariableConfig(config, yaml, root)); - CEL_RETURN_IF_ERROR(ParseVariableConfigs(config, yaml, root)); - CEL_RETURN_IF_ERROR(ParseFunctionConfigs(config, yaml, root)); + CEL_RETURN_IF_ERROR(ParseContainerConfig(config, yaml, root, reporter)); + CEL_RETURN_IF_ERROR( + ParseExtensionConfigs(config, yaml, root, options, reporter)); + CEL_RETURN_IF_ERROR(ParseStandardLibraryConfig(config, yaml, root, reporter)); + CEL_RETURN_IF_ERROR(ParseContextVariableConfig(config, yaml, root, reporter)); + CEL_RETURN_IF_ERROR(ParseVariableConfigs(config, yaml, root, reporter)); + CEL_RETURN_IF_ERROR(ParseFunctionConfigs(config, yaml, root, reporter)); + + std::vector& warnings = reporter.warnings(); + if (options.strict && !warnings.empty()) { + return absl::InvalidArgumentError( + absl::StrCat("Unhandled fields in CEL environment config:\n", + absl::StrJoin(warnings, "\n"))); + } + if (options.warnings != nullptr) { + options.warnings->insert(options.warnings->end(), + std::make_move_iterator(warnings.begin()), + std::make_move_iterator(warnings.end())); + } return config; } diff --git a/env/env_yaml.h b/env/env_yaml.h index 86ef5cd2e..b6bae6c65 100644 --- a/env/env_yaml.h +++ b/env/env_yaml.h @@ -17,6 +17,7 @@ #include #include +#include #include "absl/status/statusor.h" #include "env/config.h" @@ -28,6 +29,21 @@ struct EnvConfigFromYamlOptions { // `Config::ExtensionConfig::kLatest` instead of version 0. // Temporary rollout option; will be removed once callers migrate. bool legacy_default_extension_version_latest = false; + + // If not null, receives one message for each field in the config that the + // parser does not handle. Such fields are ignored. They are either unknown + // (for example, misspelled) or are supported by other CEL implementations + // but not by cel-cpp: `imports`, `limits`, `validators` and `features`. In + // the latter case the resulting environment may differ from the one that + // cel-go or cel-java builds from the same config. + // + // Not written to if parsing fails. The pointer must remain valid for the + // duration of the call. + std::vector* warnings = nullptr; + + // Whether to fail if the config has any field that the parser does not + // handle. The error lists every such field. + bool strict = false; }; // EnvConfigFromYaml creates an environment configuration from a YAML string. diff --git a/env/env_yaml_test.cc b/env/env_yaml_test.cc index bf25480d5..10c473b31 100644 --- a/env/env_yaml_test.cc +++ b/env/env_yaml_test.cc @@ -37,8 +37,10 @@ namespace cel { namespace { +using ::absl_testing::IsOk; using ::absl_testing::StatusIs; using ::testing::AllOf; +using ::testing::ElementsAre; using ::testing::ElementsAreArray; using ::testing::Field; using ::testing::HasSubstr; @@ -2053,5 +2055,149 @@ INSTANTIATE_TEST_SUITE_P(EnvYamlSignatureRoundTripTest, EnvYamlSignatureRoundTripTest, ::testing::ValuesIn(GetSignatureRoundTripTestCases())); +TEST(EnvYamlTest, UnhandledFieldsAreIgnoredWithWarnings) { + std::vector warnings; + ASSERT_OK_AND_ASSIGN( + Config config, + EnvConfigFromYaml("name: test\n" + "imports:\n" + " - name: google.protobuf.Timestamp\n" + "limits:\n" + " - name: cel.limit.expression_code_points\n" + " value: 10\n" + "validators:\n" + " - name: cel.validator.duration\n" + "features:\n" + " - name: cel.feature.macro_call_tracking\n" + " enabled: true\n" + "not_a_field: true\n" + "variables:\n" + " - name: x\n" + " type_name: int\n" + " typ: int\n", + {.warnings = &warnings})); + + EXPECT_EQ(config.GetName(), "test"); + EXPECT_THAT(config.GetVariableConfigs(), SizeIs(1)); + EXPECT_THAT(warnings, + ElementsAre("2:1: Field 'imports' is not supported", + "4:1: Field 'limits' is not supported", + "7:1: Field 'validators' is not supported", + "9:1: Field 'features' is not supported", + "12:1: Unknown field 'not_a_field' in config", + "16:5: Unknown field 'typ' in variable")); +} + +TEST(EnvYamlTest, UnhandledFieldsAreIgnoredWithoutWarningsOutput) { + EXPECT_THAT(EnvConfigFromYaml("limits: []\nnot_a_field: true\n"), IsOk()); +} + +TEST(EnvYamlTest, NoWarningsWhenAllFieldsAreHandled) { + std::vector warnings; + EXPECT_THAT(EnvConfigFromYaml("name: test\n" + "description: a description\n" + "stdlib:\n" + " disable: true\n", + {.warnings = &warnings}), + IsOk()); + EXPECT_THAT(warnings, IsEmpty()); +} + +TEST(EnvYamlTest, StrictReturnsAllWarningsAsOneError) { + std::vector warnings; + absl::StatusOr config = EnvConfigFromYaml( + "limits: []\n" + "not_a_field: true\n" + "extensions:\n" + " - name: math\n" + " versions: 1\n", + {.warnings = &warnings, .strict = true}); + + EXPECT_THAT(config, StatusIs(absl::StatusCode::kInvalidArgument, + "Unhandled fields in CEL environment config:\n" + "1:1: Field 'limits' is not supported\n" + "2:1: Unknown field 'not_a_field' in config\n" + "5:5: Unknown field 'versions' in extension")); + EXPECT_THAT(warnings, IsEmpty()); +} + +TEST(EnvYamlTest, StrictReportsParseErrorsBeforeUnhandledFields) { + EXPECT_THAT( + EnvConfigFromYaml("not_a_field: true\nvariables: 1\n", {.strict = true}), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("Node 'variables' is not a sequence"))); +} + +struct StrictTestCase { + std::string yaml; + std::string expected_error; +}; + +class EnvYamlStrictTest : public testing::TestWithParam {}; + +TEST_P(EnvYamlStrictTest, RejectsUnhandledField) { + const StrictTestCase& param = GetParam(); + // The config is accepted unless strict is set. + EXPECT_THAT(EnvConfigFromYaml(param.yaml), IsOk()); + EXPECT_THAT(EnvConfigFromYaml(param.yaml, {.strict = true}), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr(param.expected_error))); +} + +INSTANTIATE_TEST_SUITE_P( + EnvYamlStrictTest, EnvYamlStrictTest, + testing::ValuesIn(std::vector{ + {"imports:\n - name: a.B\n", "1:1: Field 'imports' is not supported"}, + {"name: test\nlimits: []\n", "2:1: Field 'limits' is not supported"}, + {"validators: []\n", "1:1: Field 'validators' is not supported"}, + {"features: []\n", "1:1: Field 'features' is not supported"}, + {"not_a_field: true\n", "1:1: Unknown field 'not_a_field' in config"}, + {"container:\n name: a\n alias: b\n", + "3:3: Unknown field 'alias' in container"}, + {"container:\n aliases:\n - alias: a\n qualified_name: b\n" + " name: c\n", + "5:7: Unknown field 'name' in alias"}, + {"extensions:\n - name: math\n versions: 1\n", + "3:5: Unknown field 'versions' in extension"}, + {"stdlib:\n disable_macro: true\n", + "2:3: Unknown field 'disable_macro' in stdlib"}, + {"stdlib:\n exclude_functions:\n - name: matches\n" + " overload: a\n", + "4:7: Unknown field 'overload' in function selector"}, + {"stdlib:\n exclude_functions:\n - name: matches\n" + " overloads:\n - id: a\n name: b\n", + "6:11: Unknown field 'name' in overload selector"}, + {"context_variable:\n type_name: a.B\n name: c\n", + "3:3: Unknown field 'name' in context_variable"}, + {"variables:\n - name: x\n type_name: int\n typ: int\n", + "4:5: Unknown field 'typ' in variable"}, + {"variables:\n - name: x\n type:\n type_name: list\n" + " params:\n - type_name: int\n name: y\n", + "7:11: Unknown field 'name' in type"}, + {"functions:\n - name: f\n overload: []\n", + "3:5: Unknown field 'overload' in function"}, + {"functions:\n - name: f\n overloads:\n - id: f_int\n" + " arg: []\n return:\n type_name: int\n", + "5:9: Unknown field 'arg' in overload"}, + {"functions:\n - name: f\n overloads:\n - id: f_int\n" + " args:\n - type_name: int\n name: x\n" + " return:\n type_name: int\n", + "7:13: Unknown field 'name' in type"}, + })); + +TEST(EnvYamlTest, StrictAcceptsSupportedFields) { + for (const std::string& yaml : GetStructuredRoundTripTestCases()) { + EXPECT_THAT(EnvConfigFromYaml(yaml, {.strict = true}), IsOk()) << yaml; + } + for (const std::string& yaml : GetSignatureRoundTripTestCases()) { + EXPECT_THAT(EnvConfigFromYaml(yaml, {.strict = true}), IsOk()) << yaml; + } + EXPECT_THAT(EnvConfigFromYaml("description: a description\n" + "stdlib:\n" + " disable: true\n", + {.strict = true}), + IsOk()); +} + } // namespace } // namespace cel