From 55eca66f3921124436af6ac8b1e16ba7c4048060 Mon Sep 17 00:00:00 2001 From: Steve Hanson Date: Fri, 2 Dec 2022 16:53:27 +0000 Subject: [PATCH] code & tests for openapi 2.0 & 3.0 suppprt --- include/rapidjson/error/en.h | 15 +- include/rapidjson/error/error.h | 19 +- include/rapidjson/schema.h | 470 ++++++++++++++++++++++++------ test/unittest/schematest.cpp | 502 ++++++++++++++++++++++++++++++-- 4 files changed, 897 insertions(+), 109 deletions(-) diff --git a/include/rapidjson/error/en.h b/include/rapidjson/error/en.h index f27d62a34..c87b04eb1 100644 --- a/include/rapidjson/error/en.h +++ b/include/rapidjson/error/en.h @@ -1,5 +1,5 @@ // Tencent is pleased to support the open source community by making RapidJSON available. -// +// // Copyright (C) 2015 THL A29 Limited, a Tencent company, and Milo Yip. // // Licensed under the MIT License (the "License"); you may not use this file except @@ -7,9 +7,9 @@ // // http://opensource.org/licenses/MIT // -// Unless required by applicable law or agreed to in writing, software distributed -// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -// CONDITIONS OF ANY KIND, either express or implied. See the License for the +// Unless required by applicable law or agreed to in writing, software distributed +// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, either express or implied. See the License for the // specific language governing permissions and limitations under the License. #ifndef RAPIDJSON_ERROR_EN_H_ @@ -109,6 +109,9 @@ inline const RAPIDJSON_ERROR_CHARTYPE* GetValidateError_En(ValidateErrorCode val case kValidateErrorAnyOf: return RAPIDJSON_ERROR_STRING("Property did not match any of the sub-schemas specified by 'anyOf', refer to following errors."); case kValidateErrorNot: return RAPIDJSON_ERROR_STRING("Property matched the sub-schema specified by 'not'."); + case kValidateErrorReadOnly: return RAPIDJSON_ERROR_STRING("Property is read-only but has been provided when validation is for writing."); + case kValidateErrorWriteOnly: return RAPIDJSON_ERROR_STRING("Property is write-only but has been provided when validation is for reading."); + default: return RAPIDJSON_ERROR_STRING("Unknown error."); } } @@ -134,6 +137,10 @@ inline const RAPIDJSON_ERROR_CHARTYPE* GetValidateError_En(ValidateErrorCode val case kSchemaErrorRefNoRemoteProvider: return RAPIDJSON_ERROR_STRING("$ref is remote but there is no remote provider."); case kSchemaErrorRefNoRemoteSchema: return RAPIDJSON_ERROR_STRING("$ref '%value' is remote but the remote provider did not return a schema."); case kSchemaErrorRegexInvalid: return RAPIDJSON_ERROR_STRING("Invalid regular expression '%value' in 'pattern' or 'patternProperties'."); + case kSchemaErrorSpecUnknown: return RAPIDJSON_ERROR_STRING("JSON schema draft or OpenAPI version is not recognized."); + case kSchemaErrorSpecUnsupported: return RAPIDJSON_ERROR_STRING("JSON schema draft or OpenAPI version is not supported."); + case kSchemaErrorSpecIllegal: return RAPIDJSON_ERROR_STRING("Both JSON schema draft and OpenAPI version found in document."); + case kSchemaErrorReadOnlyAndWriteOnly: return RAPIDJSON_ERROR_STRING("Property must not be both 'readOnly' and 'writeOnly'."); default: return RAPIDJSON_ERROR_STRING("Unknown error."); } diff --git a/include/rapidjson/error/error.h b/include/rapidjson/error/error.h index d2a2fc486..8bdc39541 100644 --- a/include/rapidjson/error/error.h +++ b/include/rapidjson/error/error.h @@ -1,5 +1,5 @@ // Tencent is pleased to support the open source community by making RapidJSON available. -// +// // Copyright (C) 2015 THL A29 Limited, a Tencent company, and Milo Yip. // // Licensed under the MIT License (the "License"); you may not use this file except @@ -7,9 +7,9 @@ // // http://opensource.org/licenses/MIT // -// Unless required by applicable law or agreed to in writing, software distributed -// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -// CONDITIONS OF ANY KIND, either express or implied. See the License for the +// Unless required by applicable law or agreed to in writing, software distributed +// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, either express or implied. See the License for the // specific language governing permissions and limitations under the License. #ifndef RAPIDJSON_ERROR_ERROR_H_ @@ -192,7 +192,10 @@ enum ValidateErrorCode { kValidateErrorOneOfMatch, //!< Property matched more than one of the sub-schemas specified by 'oneOf'. kValidateErrorAllOf, //!< Property did not match all of the sub-schemas specified by 'allOf'. kValidateErrorAnyOf, //!< Property did not match any of the sub-schemas specified by 'anyOf'. - kValidateErrorNot //!< Property matched the sub-schema specified by 'not'. + kValidateErrorNot, //!< Property matched the sub-schema specified by 'not'. + + kValidateErrorReadOnly, //!< Property is read-only but has been provided when validation is for writing + kValidateErrorWriteOnly //!< Property is write-only but has been provided when validation is for reading }; //! Function pointer type of GetValidateError(). @@ -225,7 +228,11 @@ enum SchemaErrorCode { kSchemaErrorRefCyclical, //!< $ref is cyclical kSchemaErrorRefNoRemoteProvider, //!< $ref is remote but there is no remote provider kSchemaErrorRefNoRemoteSchema, //!< $ref is remote but the remote provider did not return a schema - kSchemaErrorRegexInvalid //!< Invalid regular expression in 'pattern' or 'patternProperties' + kSchemaErrorRegexInvalid, //!< Invalid regular expression in 'pattern' or 'patternProperties' + kSchemaErrorSpecUnknown, //!< JSON schema draft or OpenAPI version is not recognized + kSchemaErrorSpecUnsupported, //!< JSON schema draft or OpenAPI version is not supported + kSchemaErrorSpecIllegal, //!< Both JSON schema draft and OpenAPI version found in document + kSchemaErrorReadOnlyAndWriteOnly //!< Property must not be both 'readOnly' and 'writeOnly' }; //! Function pointer type of GetSchemaError(). diff --git a/include/rapidjson/schema.h b/include/rapidjson/schema.h index 836c0f1f3..0e45df6f8 100644 --- a/include/rapidjson/schema.h +++ b/include/rapidjson/schema.h @@ -1,5 +1,5 @@ // Tencent is pleased to support the open source community by making RapidJSON available-> -// +// // Copyright (C) 2015 THL A29 Limited, a Tencent company, and Milo Yip-> All rights reserved-> // // Licensed under the MIT License (the "License"); you may not use this file except @@ -7,9 +7,9 @@ // // http://opensource->org/licenses/MIT // -// Unless required by applicable law or agreed to in writing, software distributed -// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -// CONDITIONS OF ANY KIND, either express or implied-> See the License for the +// Unless required by applicable law or agreed to in writing, software distributed +// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, either express or implied-> See the License for the // specific language governing permissions and limitations under the License-> #ifndef RAPIDJSON_SCHEMA_H_ @@ -74,48 +74,94 @@ RAPIDJSON_NAMESPACE_BEGIN namespace internal { -inline void PrintInvalidKeyword(const char* keyword) { - printf("Fail keyword: %s\n", keyword); +inline void PrintInvalidKeywordData(const char* keyword) { + printf(" Fail keyword: '%s'\n", keyword); +} + +inline void PrintInvalidKeywordData(const wchar_t* keyword) { + wprintf(L" Fail keyword: '%ls'\n", keyword); +} + +inline void PrintInvalidDocumentData(const char* document) { + printf(" Fail document: '%s'\n", document); +} + +inline void PrintInvalidDocumentData(const wchar_t* document) { + wprintf(L" Fail document: '%ls'\n", document); +} + +inline void PrintValidatorPointersData(const char* s, const char* d, unsigned depth) { + printf(" Sch: %*s'%s'\n Doc: %*s'%s'\n", depth * 4, " ", s, depth * 4, " ", d); +} + +inline void PrintValidatorPointersData(const wchar_t* s, const wchar_t* d, unsigned depth) { + wprintf(L" Sch: %*ls'%ls'\n Doc: %*ls'%ls'\n", depth * 4, L" ", s, depth * 4, L" ", d); +} + +inline void PrintSchemaIdsData(const char* base, const char* local, const char* resolved) { + printf(" Resolving id: Base: '%s', Local: '%s', Resolved: '%s'\n", base, local, resolved); } -inline void PrintInvalidKeyword(const wchar_t* keyword) { - wprintf(L"Fail keyword: %ls\n", keyword); +inline void PrintSchemaIdsData(const wchar_t* base, const wchar_t* local, const wchar_t* resolved) { + wprintf(L" Resolving id: Base: '%ls', Local: '%ls', Resolved: '%ls'\n", base, local, resolved); } -inline void PrintInvalidDocument(const char* document) { - printf("Fail document: %s\n\n", document); +inline void PrintMethodData(const char* method) { + printf("%s\n", method); } -inline void PrintInvalidDocument(const wchar_t* document) { - wprintf(L"Fail document: %ls\n\n", document); +inline void PrintMethodData(const char* method, bool b) { + printf("%s, Data: '%s'\n", method, b ? "true" : "false"); } -inline void PrintValidatorPointers(unsigned depth, const char* s, const char* d) { - printf("S: %*s%s\nD: %*s%s\n\n", depth * 4, " ", s, depth * 4, " ", d); +inline void PrintMethodData(const char* method, int64_t i) { + printf("%s, Data: '%" PRId64 "'\n", method, i); } -inline void PrintValidatorPointers(unsigned depth, const wchar_t* s, const wchar_t* d) { - wprintf(L"S: %*ls%ls\nD: %*ls%ls\n\n", depth * 4, L" ", s, depth * 4, L" ", d); +inline void PrintMethodData(const char* method, uint64_t u) { + printf("%s, Data: '%" PRIu64 "'\n", method, u); +} + +inline void PrintMethodData(const char* method, double d) { + printf("%s, Data: '%lf'\n", method, d); +} + +inline void PrintMethodData(const char* method, const char* s) { + printf("%s, Data: '%s'\n", method, s); +} + +inline void PrintMethodData(const char* method, const wchar_t* s) { + wprintf(L"%hs, Data: '%ls'\n", method, s); +} + +inline void PrintMethodData(const char* method, const char* s1, const char* s2) { + printf("%s, Data: '%s', '%s'\n", method, s1, s2); +} + +inline void PrintMethodData(const char* method, const wchar_t* s1, const wchar_t* s2) { + wprintf(L"%hs, Data: '%ls', '%ls'\n", method, s1, s2); } } // namespace internal #endif // RAPIDJSON_SCHEMA_VERBOSE -/////////////////////////////////////////////////////////////////////////////// -// RAPIDJSON_INVALID_KEYWORD_RETURN - +#ifndef RAPIDJSON_SCHEMA_PRINT #if RAPIDJSON_SCHEMA_VERBOSE -#define RAPIDJSON_INVALID_KEYWORD_VERBOSE(keyword) internal::PrintInvalidKeyword(keyword) +#define RAPIDJSON_SCHEMA_PRINT(name, ...) internal::Print##name##Data(__VA_ARGS__) #else -#define RAPIDJSON_INVALID_KEYWORD_VERBOSE(keyword) +#define RAPIDJSON_SCHEMA_PRINT(name, ...) #endif +#endif + +/////////////////////////////////////////////////////////////////////////////// +// RAPIDJSON_INVALID_KEYWORD_RETURN #define RAPIDJSON_INVALID_KEYWORD_RETURN(code)\ RAPIDJSON_MULTILINEMACRO_BEGIN\ context.invalidCode = code;\ context.invalidKeyword = SchemaType::GetValidateErrorKeyword(code).GetString();\ - RAPIDJSON_INVALID_KEYWORD_VERBOSE(context.invalidKeyword);\ + RAPIDJSON_SCHEMA_PRINT(InvalidKeyword, context.invalidKeyword);\ return false;\ RAPIDJSON_MULTILINEMACRO_END @@ -138,9 +184,53 @@ RAPIDJSON_MULTILINEMACRO_END enum ValidateFlag { kValidateNoFlags = 0, //!< No flags are set. kValidateContinueOnErrorFlag = 1, //!< Don't stop after first validation error. + kValidateReadFlag = 2, //!< Validation is for a read semantic. + kValidateWriteFlag = 4, //!< Validation is for a write semantic. kValidateDefaultFlags = RAPIDJSON_VALIDATE_DEFAULT_FLAGS //!< Default validate flags. Can be customized by defining RAPIDJSON_VALIDATE_DEFAULT_FLAGS }; +/////////////////////////////////////////////////////////////////////////////// +// Specification +enum SchemaDraft { + kDraftUnknown = -1, + kDraftNone = 0, + kDraft03 = 3, + kDraftMin = 4, //!< Current minimum supported draft + kDraft04 = 4, + kDraft05 = 5, + kDraftMax = 5, //!< Current maximum supported draft + kDraft06 = 6, + kDraft07 = 7, + kDraft2019_09 = 8, + kDraft2020_12 = 9 +}; + +enum OpenApiVersion { + kVersionUnknown = -1, + kVersionNone = 0, + kVersionMin = 2, //!< Current minimum supported version + kVersion20 = 2, + kVersion30 = 3, + kVersionMax = 3, //!< Current maximum supported version + kVersion31 = 4, +}; + +struct Specification { + Specification(SchemaDraft d) : draft(d), oapi(kVersionNone) {} + Specification(OpenApiVersion o) : oapi(o) { + if (oapi == kVersion20) draft = kDraft04; + else if (oapi == kVersion30) draft = kDraft05; + else if (oapi == kVersion31) draft = kDraft2020_12; + else draft = kDraft04; + } + ~Specification() {} + bool IsSupported() const { + return ((draft >= kDraftMin && draft <= kDraftMax) && ((oapi == kVersionNone) || (oapi >= kVersionMin && oapi <= kVersionMax))); + } + SchemaDraft draft; + OpenApiVersion oapi; +}; + /////////////////////////////////////////////////////////////////////////////// // Forward declarations @@ -233,6 +323,8 @@ class IValidationErrorHandler { virtual void NotOneOf(ISchemaValidator** subvalidators, SizeType count) = 0; virtual void MultipleOneOf(SizeType index1, SizeType index2) = 0; virtual void Disallowed() = 0; + virtual void DisallowedWhenWriting() = 0; + virtual void DisallowedWhenReading() = 0; }; @@ -253,10 +345,10 @@ class Hasher { bool Uint(unsigned u) { Number n; n.u.u = u; n.d = static_cast(u); return WriteNumber(n); } bool Int64(int64_t i) { Number n; n.u.i = i; n.d = static_cast(i); return WriteNumber(n); } bool Uint64(uint64_t u) { Number n; n.u.u = u; n.d = static_cast(u); return WriteNumber(n); } - bool Double(double d) { - Number n; + bool Double(double d) { + Number n; if (d < 0) n.u.i = static_cast(d); - else n.u.u = static_cast(d); + else n.u.u = static_cast(d); n.d = d; return WriteNumber(n); } @@ -350,10 +442,11 @@ struct SchemaValidationContext { kPatternValidatorWithAdditionalProperty }; - SchemaValidationContext(SchemaValidatorFactoryType& f, ErrorHandlerType& eh, const SchemaType* s) : + SchemaValidationContext(SchemaValidatorFactoryType& f, ErrorHandlerType& eh, const SchemaType* s, unsigned fl = 0) : factory(f), error_handler(eh), schema(s), + flags(fl), valueSchema(), invalidKeyword(), invalidCode(), @@ -401,6 +494,7 @@ struct SchemaValidationContext { SchemaValidatorFactoryType& factory; ErrorHandlerType& error_handler; const SchemaType* schema; + unsigned flags; const SchemaType* valueSchema; const Ch* invalidKeyword; ValidateErrorCode invalidCode; @@ -443,6 +537,7 @@ class Schema { allocator_(allocator), uri_(schemaDocument->GetURI(), *allocator), id_(id, allocator), + spec_(schemaDocument->GetSpecification()), pointer_(p, allocator), typeless_(schemaDocument->GetTypeless()), enum_(), @@ -475,8 +570,15 @@ class Schema { maxLength_(~SizeType(0)), exclusiveMinimum_(false), exclusiveMaximum_(false), - defaultValueLength_(0) + defaultValueLength_(0), + readOnly_(false), + writeOnly_(false), + nullable_(false) { + GenericStringBuffer sb; + p.StringifyUriFragment(sb); + RAPIDJSON_SCHEMA_PRINT(Method, "Schema::Schema", sb.GetString(), id.GetString()); + typedef typename ValueType::ConstValueIterator ConstValueIterator; typedef typename ValueType::ConstMemberIterator ConstMemberIterator; @@ -495,10 +597,13 @@ class Schema { return; // If we have an id property, resolve it with the in-scope id + // Not supported for open api 2.0 or 3.0 + if (spec_.oapi != kVersion20 && spec_.oapi != kVersion30) if (const ValueType* v = GetMember(value, GetIdString())) { if (v->IsString()) { UriType local(*v, allocator); id_ = local.Resolve(id_, allocator); + RAPIDJSON_SCHEMA_PRINT(SchemaIds, id.GetString(), v->GetString(), id_.GetString()); } } @@ -525,8 +630,11 @@ class Schema { } } - if (schemaDocument) { + if (schemaDocument) AssignIfExist(allOf_, *schemaDocument, p, value, GetAllOfString(), document); + + // AnyOf, OneOf, Not not supported for open api 2.0 + if (schemaDocument && spec_.oapi != kVersion20) { AssignIfExist(anyOf_, *schemaDocument, p, value, GetAnyOfString(), document); AssignIfExist(oneOf_, *schemaDocument, p, value, GetOneOfString(), document); @@ -555,6 +663,8 @@ class Schema { if (itr->IsString()) AddUniqueElement(allProperties, *itr); + // Dependencies not supported for open api 2.0 and 3.0 + if (spec_.oapi != kVersion20 && spec_.oapi != kVersion30) if (dependencies && dependencies->IsObject()) for (ConstMemberIterator itr = dependencies->MemberBegin(); itr != dependencies->MemberEnd(); ++itr) { AddUniqueElement(allProperties, itr->name); @@ -584,6 +694,8 @@ class Schema { } } + // PatternProperties not supported for open api 2.0 and 3.0 + if (spec_.oapi != kVersion20 && spec_.oapi != kVersion30) if (const ValueType* v = GetMember(value, GetPatternPropertiesString())) { PointerType q = p.Append(GetPatternPropertiesString(), allocator_); patternProperties_ = static_cast(allocator_->Malloc(sizeof(PatternProperty) * v->MemberCount())); @@ -608,6 +720,8 @@ class Schema { } } + // Dependencies not supported for open api 2.0 and 3.0 + if (spec_.oapi != kVersion20 && spec_.oapi != kVersion30) if (dependencies && dependencies->IsObject()) { PointerType q = p.Append(GetDependenciesString(), allocator_); hasDependencies_ = true; @@ -659,6 +773,8 @@ class Schema { AssignIfExist(minItems_, value, GetMinItemsString()); AssignIfExist(maxItems_, value, GetMaxItemsString()); + // AdditionalItems not supported for openapi 2.0 and 3.0 + if (spec_.oapi != kVersion20 && spec_.oapi != kVersion30) if (const ValueType* v = GetMember(value, GetAdditionalItemsString())) { if (v->IsBool()) additionalItems_ = v->GetBool(); @@ -696,6 +812,23 @@ class Schema { if (v->IsString()) defaultValueLength_ = v->GetStringLength(); + // ReadOnly - open api only (until draft 7 supported) + // WriteOnly - open api 3 only (until draft 7 supported) + // Both can't be true + if (spec_.oapi != kVersionNone) + AssignIfExist(readOnly_, value, GetReadOnlyString()); + if (spec_.oapi >= kVersion30) + AssignIfExist(writeOnly_, value, GetWriteOnlyString()); + if (readOnly_ && writeOnly_) + schemaDocument->SchemaError(kSchemaErrorReadOnlyAndWriteOnly, p); + + // Nullable - open api 3 only + // If true add 'null' as allowable type + if (spec_.oapi >= kVersion30) { + AssignIfExist(nullable_, value, GetNullableString()); + if (nullable_) + AddType(GetNullString()); + } } ~Schema() { @@ -727,11 +860,16 @@ class Schema { return id_; } + const Specification& GetSpecification() const { + return spec_; + } + const PointerType& GetPointer() const { return pointer_; } bool BeginValue(Context& context) const { + RAPIDJSON_SCHEMA_PRINT(Method, "Schema::BeginValue"); if (context.inArray) { if (uniqueItems_) context.valueUniqueness = true; @@ -763,6 +901,7 @@ class Schema { } RAPIDJSON_FORCEINLINE bool EndValue(Context& context) const { + RAPIDJSON_SCHEMA_PRINT(Method, "Schema::EndValue"); // Only check pattern properties if we have validators if (context.patternPropertiesValidatorCount > 0) { bool otherValid = false; @@ -853,6 +992,7 @@ class Schema { } bool Null(Context& context) const { + RAPIDJSON_SCHEMA_PRINT(Method, "Schema::Null"); if (!(type_ & (1 << kNullSchemaType))) { DisallowedType(context, GetNullString()); RAPIDJSON_INVALID_KEYWORD_RETURN(kValidateErrorType); @@ -860,39 +1000,43 @@ class Schema { return CreateParallelValidator(context); } - bool Bool(Context& context, bool) const { - if (!(type_ & (1 << kBooleanSchemaType))) { - DisallowedType(context, GetBooleanString()); - RAPIDJSON_INVALID_KEYWORD_RETURN(kValidateErrorType); - } + bool Bool(Context& context, bool b) const { + RAPIDJSON_SCHEMA_PRINT(Method, "Schema::Bool", b); + if (!CheckBool(context, b)) + return false; return CreateParallelValidator(context); } bool Int(Context& context, int i) const { + RAPIDJSON_SCHEMA_PRINT(Method, "Schema::Int", (int64_t)i); if (!CheckInt(context, i)) return false; return CreateParallelValidator(context); } bool Uint(Context& context, unsigned u) const { + RAPIDJSON_SCHEMA_PRINT(Method, "Schema::Uint", (uint64_t)u); if (!CheckUint(context, u)) return false; return CreateParallelValidator(context); } bool Int64(Context& context, int64_t i) const { + RAPIDJSON_SCHEMA_PRINT(Method, "Schema::Int64", i); if (!CheckInt(context, i)) return false; return CreateParallelValidator(context); } bool Uint64(Context& context, uint64_t u) const { + RAPIDJSON_SCHEMA_PRINT(Method, "Schema::Uint64", u); if (!CheckUint(context, u)) return false; return CreateParallelValidator(context); } bool Double(Context& context, double d) const { + RAPIDJSON_SCHEMA_PRINT(Method, "Schema::Double", d); if (!(type_ & (1 << kNumberSchemaType))) { DisallowedType(context, GetNumberString()); RAPIDJSON_INVALID_KEYWORD_RETURN(kValidateErrorType); @@ -911,6 +1055,7 @@ class Schema { } bool String(Context& context, const Ch* str, SizeType length, bool) const { + RAPIDJSON_SCHEMA_PRINT(Method, "Schema::String", str); if (!(type_ & (1 << kStringSchemaType))) { DisallowedType(context, GetStringString()); RAPIDJSON_INVALID_KEYWORD_RETURN(kValidateErrorType); @@ -939,6 +1084,7 @@ class Schema { } bool StartObject(Context& context) const { + RAPIDJSON_SCHEMA_PRINT(Method, "Schema::StartObject"); if (!(type_ & (1 << kObjectSchemaType))) { DisallowedType(context, GetObjectString()); RAPIDJSON_INVALID_KEYWORD_RETURN(kValidateErrorType); @@ -960,6 +1106,8 @@ class Schema { } bool Key(Context& context, const Ch* str, SizeType len, bool) const { + RAPIDJSON_SCHEMA_PRINT(Method, "Schema::Key", str); + if (patternProperties_) { context.patternPropertiesSchemaCount = 0; for (SizeType i = 0; i < patternPropertyCount_; i++) @@ -1011,6 +1159,7 @@ class Schema { } bool EndObject(Context& context, SizeType memberCount) const { + RAPIDJSON_SCHEMA_PRINT(Method, "Schema::EndObject"); if (hasRequired_) { context.error_handler.StartMissingProperties(); for (SizeType index = 0; index < propertyCount_; index++) @@ -1058,6 +1207,7 @@ class Schema { } bool StartArray(Context& context) const { + RAPIDJSON_SCHEMA_PRINT(Method, "Schema::StartArray"); context.arrayElementIndex = 0; context.inArray = true; // Ensure we note that we are in an array @@ -1070,6 +1220,7 @@ class Schema { } bool EndArray(Context& context, SizeType elementCount) const { + RAPIDJSON_SCHEMA_PRINT(Method, "Schema::EndArray"); context.inArray = false; if (elementCount < minItems_) { @@ -1118,6 +1269,9 @@ class Schema { case kValidateErrorAnyOf: return GetAnyOfString(); case kValidateErrorNot: return GetNotString(); + case kValidateErrorReadOnly: return GetReadOnlyString(); + case kValidateErrorWriteOnly: return GetWriteOnlyString(); + default: return GetNullString(); } } @@ -1165,15 +1319,14 @@ class Schema { RAPIDJSON_STRING_(ExclusiveMaximum, 'e', 'x', 'c', 'l', 'u', 's', 'i', 'v', 'e', 'M', 'a', 'x', 'i', 'm', 'u', 'm') RAPIDJSON_STRING_(MultipleOf, 'm', 'u', 'l', 't', 'i', 'p', 'l', 'e', 'O', 'f') RAPIDJSON_STRING_(DefaultValue, 'd', 'e', 'f', 'a', 'u', 'l', 't') + RAPIDJSON_STRING_(Schema, '$', 's', 'c', 'h', 'e', 'm', 'a') RAPIDJSON_STRING_(Ref, '$', 'r', 'e', 'f') RAPIDJSON_STRING_(Id, 'i', 'd') - - RAPIDJSON_STRING_(SchemeEnd, ':') - RAPIDJSON_STRING_(AuthStart, '/', '/') - RAPIDJSON_STRING_(QueryStart, '?') - RAPIDJSON_STRING_(FragStart, '#') - RAPIDJSON_STRING_(Slash, '/') - RAPIDJSON_STRING_(Dot, '.') + RAPIDJSON_STRING_(Swagger, 's', 'w', 'a', 'g', 'g', 'e', 'r') + RAPIDJSON_STRING_(OpenApi, 'o', 'p', 'e', 'n', 'a', 'p', 'i') + RAPIDJSON_STRING_(ReadOnly, 'r', 'e', 'a', 'd', 'O', 'n', 'l', 'y') + RAPIDJSON_STRING_(WriteOnly, 'w', 'r', 'i', 't', 'e', 'O', 'n', 'l', 'y') + RAPIDJSON_STRING_(Nullable, 'n', 'u', 'l', 'l', 'a', 'b', 'l', 'e') #undef RAPIDJSON_STRING_ @@ -1307,6 +1460,7 @@ class Schema { // Creates parallel validators for allOf, anyOf, oneOf, not and schema dependencies, if required. // Also creates a hasher for enums and array uniqueness, if required. + // Also a useful place to add type-independent error checks. bool CreateParallelValidator(Context& context) const { if (enum_ || context.arrayUniqueness) context.hasher = context.factory.CreateHasher(); @@ -1337,6 +1491,16 @@ class Schema { } } + // Add any other type-independent checks here + if (readOnly_ && (context.flags & kValidateWriteFlag)) { + context.error_handler.DisallowedWhenWriting(); + RAPIDJSON_INVALID_KEYWORD_RETURN(kValidateErrorReadOnly); + } + if (writeOnly_ && (context.flags & kValidateReadFlag)) { + context.error_handler.DisallowedWhenReading(); + RAPIDJSON_INVALID_KEYWORD_RETURN(kValidateErrorWriteOnly); + } + return true; } @@ -1359,6 +1523,14 @@ class Schema { return false; } + bool CheckBool(Context& context, bool) const { + if (!(type_ & (1 << kBooleanSchemaType))) { + DisallowedType(context, GetBooleanString()); + RAPIDJSON_INVALID_KEYWORD_RETURN(kValidateErrorType); + } + return true; + } + bool CheckInt(Context& context, int64_t i) const { if (!(type_ & ((1 << kIntegerSchemaType) | (1 << kNumberSchemaType)))) { DisallowedType(context, GetIntegerString()); @@ -1524,6 +1696,7 @@ class Schema { AllocatorType* allocator_; SValue uri_; UriType id_; + Specification spec_; PointerType pointer_; const SchemaType* typeless_; uint64_t* enum_; @@ -1568,6 +1741,10 @@ class Schema { bool exclusiveMaximum_; SizeType defaultValueLength_; + + bool readOnly_; + bool writeOnly_; + bool nullable_; }; template @@ -1614,7 +1791,12 @@ class IGenericRemoteSchemaDocumentProvider { virtual ~IGenericRemoteSchemaDocumentProvider() {} virtual const SchemaDocumentType* GetRemoteDocument(const Ch* uri, SizeType length) = 0; - virtual const SchemaDocumentType* GetRemoteDocument(const GenericUri uri) { return GetRemoteDocument(uri.GetBaseString(), uri.GetBaseStringLength()); } + virtual const SchemaDocumentType* GetRemoteDocument(const GenericUri uri, Specification& spec) { + // Default implementation just calls through for compatibility + // Following line suppresses unused parameter warning + if (false) printf("GetRemoteDocument: %d %d\n", spec.draft, spec.oapi); + return GetRemoteDocument(uri.GetBaseString(), uri.GetBaseStringLength()); + } }; /////////////////////////////////////////////////////////////////////////////// @@ -1656,10 +1838,12 @@ class GenericSchemaDocument { \param remoteProvider An optional remote schema document provider for resolving remote reference. Can be null. \param allocator An optional allocator instance for allocating memory. Can be null. \param pointer An optional JSON pointer to the start of the schema document + \param spec Optional schema draft or OpenAPI version. Used if no specification in document. Defaults to draft-04. */ explicit GenericSchemaDocument(const ValueType& document, const Ch* uri = 0, SizeType uriLength = 0, IRemoteSchemaDocumentProviderType* remoteProvider = 0, Allocator* allocator = 0, - const PointerType& pointer = PointerType()) : // PR #1393 + const PointerType& pointer = PointerType(), // PR #1393 + const Specification& spec = Specification(kDraft04)) : remoteProvider_(remoteProvider), allocator_(allocator), ownAllocator_(), @@ -1667,9 +1851,11 @@ class GenericSchemaDocument { typeless_(), schemaMap_(allocator, kInitialSchemaMapSize), schemaRef_(allocator, kInitialSchemaRefSize), + spec_(spec), error_(kObjectType), currentError_() { + RAPIDJSON_SCHEMA_PRINT(Method, "GenericSchemaDocument::GenericSchemaDocument"); if (!allocator_) ownAllocator_ = allocator_ = RAPIDJSON_NEW(Allocator)(); @@ -1680,6 +1866,10 @@ class GenericSchemaDocument { typeless_ = static_cast(allocator_->Malloc(sizeof(SchemaType))); new (typeless_) SchemaType(this, PointerType(), ValueType(kObjectType).Move(), ValueType(kObjectType).Move(), allocator_, docId_); + // Establish the schema draft or open api version. + // We only ever look for '$schema' or 'swagger' or 'openapi' at the root of the document. + SetSchemaSpecification(document); + // Generate root schema, it will call CreateSchema() to create sub-schemas, // And call HandleRefSchema() if there are $ref. // PR #1393 use input pointer if supplied @@ -1713,6 +1903,7 @@ class GenericSchemaDocument { schemaRef_(std::move(rhs.schemaRef_)), uri_(std::move(rhs.uri_)), docId_(std::move(rhs.docId_)), + spec_(rhs.spec_), error_(std::move(rhs.error_)), currentError_(std::move(rhs.currentError_)) { @@ -1743,6 +1934,23 @@ class GenericSchemaDocument { const GValue& GetURI() const { return uri_; } + const Specification& GetSpecification() const { return spec_; } + bool IsSupportedSpecification() const { return spec_.IsSupported(); } + + //! Static method to get the specification of any schema document + // Returns kDraftNone if document is silent + static const Specification GetSpecification(const ValueType& document) { + SchemaDraft draft = GetSchemaDraft(document); + if (draft != kDraftNone) + return Specification(draft); + else { + OpenApiVersion oapi = GetOpenApiVersion(document); + if (oapi != kVersionNone) + return Specification(oapi); + } + return Specification(kDraftNone); + } + //! Get the root schema. const SchemaType& GetRoot() const { return *root_; } @@ -1761,6 +1969,10 @@ class GenericSchemaDocument { case kSchemaErrorRefNoRemoteProvider: return GetRefNoRemoteProviderString(); case kSchemaErrorRefNoRemoteSchema: return GetRefNoRemoteSchemaString(); case kSchemaErrorRegexInvalid: return GetRegexInvalidString(); + case kSchemaErrorSpecUnknown: return GetSpecUnknownString(); + case kSchemaErrorSpecUnsupported: return GetSpecUnsupportedString(); + case kSchemaErrorSpecIllegal: return GetSpecIllegalString(); + case kSchemaErrorReadOnlyAndWriteOnly: return GetReadOnlyAndWriteOnlyString(); default: return GetNullString(); } } @@ -1829,6 +2041,7 @@ class GenericSchemaDocument { } void AddCurrentError(const SchemaErrorCode code, const PointerType& location) { + RAPIDJSON_SCHEMA_PRINT(InvalidKeyword, GetSchemaErrorKeyword(code)); currentError_.AddMember(GetErrorCodeString(), code, *allocator_); AddErrorInstanceLocation(currentError_, location); AddError(GValue(GetSchemaErrorKeyword(code)).Move(), currentError_); @@ -1847,6 +2060,9 @@ class GenericSchemaDocument { RAPIDJSON_STRING_(Offset, 'o', 'f', 'f', 's', 'e', 't') RAPIDJSON_STRING_(Null, 'n', 'u', 'l', 'l') + RAPIDJSON_STRING_(SpecUnknown, 'S', 'p', 'e', 'c', 'U', 'n', 'k', 'n', 'o', 'w', 'n') + RAPIDJSON_STRING_(SpecUnsupported, 'S', 'p', 'e', 'c', 'U', 'n', 's', 'u', 'p', 'p', 'o', 'r', 't', 'e', 'd') + RAPIDJSON_STRING_(SpecIllegal, 'S', 'p', 'e', 'c', 'I', 'l', 'l', 'e', 'g', 'a', 'l') RAPIDJSON_STRING_(StartUnknown, 'S', 't', 'a', 'r', 't', 'U', 'n', 'k', 'n', 'o', 'w', 'n') RAPIDJSON_STRING_(RefPlainName, 'R', 'e', 'f', 'P', 'l', 'a', 'i', 'n', 'N', 'a', 'm', 'e') RAPIDJSON_STRING_(RefInvalid, 'R', 'e', 'f', 'I', 'n', 'v', 'a', 'l', 'i', 'd') @@ -1855,10 +2071,94 @@ class GenericSchemaDocument { RAPIDJSON_STRING_(RefCyclical, 'R', 'e', 'f', 'C', 'y', 'c', 'l', 'i', 'c', 'a', 'l') RAPIDJSON_STRING_(RefNoRemoteProvider, 'R', 'e', 'f', 'N', 'o', 'R', 'e', 'm', 'o', 't', 'e', 'P', 'r', 'o', 'v', 'i', 'd', 'e', 'r') RAPIDJSON_STRING_(RefNoRemoteSchema, 'R', 'e', 'f', 'N', 'o', 'R', 'e', 'm', 'o', 't', 'e', 'S', 'c', 'h', 'e', 'm', 'a') + RAPIDJSON_STRING_(ReadOnlyAndWriteOnly, 'R', 'e', 'a', 'd', 'O', 'n', 'l', 'y', 'A', 'n', 'd', 'W', 'r', 'i', 't', 'e', 'O', 'n', 'l', 'y') RAPIDJSON_STRING_(RegexInvalid, 'R', 'e', 'g', 'e', 'x', 'I', 'n', 'v', 'a', 'l', 'i', 'd') #undef RAPIDJSON_STRING_ + // Static method to get schema draft of any schema document + static SchemaDraft GetSchemaDraft(const ValueType& document) { + static const Ch kDraft03String[] = { 'h', 't', 't', 'p', ':', '/', '/', 'j', 's', 'o', 'n', '-', 's', 'c', 'h', 'e', 'm', 'a', '.', 'o', 'r', 'g', '/', 'd', 'r', 'a', 'f', 't', '-', '0', '3', '/', 's', 'c', 'h', 'e', 'm', 'a', '#', '\0' }; + static const Ch kDraft04String[] = { 'h', 't', 't', 'p', ':', '/', '/', 'j', 's', 'o', 'n', '-', 's', 'c', 'h', 'e', 'm', 'a', '.', 'o', 'r', 'g', '/', 'd', 'r', 'a', 'f', 't', '-', '0', '4', '/', 's', 'c', 'h', 'e', 'm', 'a', '#', '\0' }; + static const Ch kDraft05String[] = { 'h', 't', 't', 'p', ':', '/', '/', 'j', 's', 'o', 'n', '-', 's', 'c', 'h', 'e', 'm', 'a', '.', 'o', 'r', 'g', '/', 'd', 'r', 'a', 'f', 't', '-', '0', '5', '/', 's', 'c', 'h', 'e', 'm', 'a', '#', '\0' }; + static const Ch kDraft06String[] = { 'h', 't', 't', 'p', ':', '/', '/', 'j', 's', 'o', 'n', '-', 's', 'c', 'h', 'e', 'm', 'a', '.', 'o', 'r', 'g', '/', 'd', 'r', 'a', 'f', 't', '-', '0', '6', '/', 's', 'c', 'h', 'e', 'm', 'a', '#', '\0' }; + static const Ch kDraft07String[] = { 'h', 't', 't', 'p', ':', '/', '/', 'j', 's', 'o', 'n', '-', 's', 'c', 'h', 'e', 'm', 'a', '.', 'o', 'r', 'g', '/', 'd', 'r', 'a', 'f', 't', '-', '0', '7', '/', 's', 'c', 'h', 'e', 'm', 'a', '#', '\0' }; + static const Ch kDraft2019_09String[] = { 'h', 't', 't', 'p', 's', ':', '/', '/', 'j', 's', 'o', 'n', '-', 's', 'c', 'h', 'e', 'm', 'a', '.', 'o', 'r', 'g', '/', 'd', 'r', 'a', 'f', 't', '/', '2', '0', '1', '9', '-', '0', '9', '/', 's', 'c', 'h', 'e', 'm', 'a', '\0' }; + static const Ch kDraft2020_12String[] = { 'h', 't', 't', 'p', 's', ':', '/', '/', 'j', 's', 'o', 'n', '-', 's', 'c', 'h', 'e', 'm', 'a', '.', 'o', 'r', 'g', '/', 'd', 'r', 'a', 'f', 't', '/', '2', '0', '2', '0', '-', '1', '2', '/', 's', 'c', 'h', 'e', 'm', 'a', '\0' }; + + if (!document.IsObject()) { + return kDraftNone; + } + + // Get the schema draft from the $schema keyword at the supplied location + typename ValueType::ConstMemberIterator itr = document.FindMember(SchemaType::GetSchemaString()); + if (itr != document.MemberEnd()) { + if (!itr->value.IsString()) return kDraftUnknown; + const UriType draftUri(itr->value); + // Check base uri for match + if (draftUri.Match(UriType(kDraft04String), false)) return kDraft04; + if (draftUri.Match(UriType(kDraft05String), false)) return kDraft05; + if (draftUri.Match(UriType(kDraft06String), false)) return kDraft06; + if (draftUri.Match(UriType(kDraft07String), false)) return kDraft07; + if (draftUri.Match(UriType(kDraft03String), false)) return kDraft03; + if (draftUri.Match(UriType(kDraft2019_09String), false)) return kDraft2019_09; + if (draftUri.Match(UriType(kDraft2020_12String), false)) return kDraft2020_12; + return kDraftUnknown; + } + // $schema not found + return kDraftNone; + } + + + // Get open api version of any schema document + static OpenApiVersion GetOpenApiVersion(const ValueType& document) { + static const Ch kVersion20String[] = { '2', '.', '0', '\0' }; + static const Ch kVersion30String[] = { '3', '.', '0', '.', '\0' }; // ignore patch level + static const Ch kVersion31String[] = { '3', '.', '1', '.', '\0' }; // ignore patch level + static SizeType len = internal::StrLen(kVersion30String); + + if (!document.IsObject()) { + return kVersionNone; + } + + // Get the open api version from the swagger / openapi keyword at the supplied location + typename ValueType::ConstMemberIterator itr = document.FindMember(SchemaType::GetSwaggerString()); + if (itr == document.MemberEnd()) itr = document.FindMember(SchemaType::GetOpenApiString()); + if (itr != document.MemberEnd()) { + if (!itr->value.IsString()) return kVersionUnknown; + const ValueType kVersion20Value(kVersion20String); + if (kVersion20Value == itr->value) return kVersion20; // must match 2.0 exactly + const ValueType kVersion30Value(kVersion30String); + if (itr->value.GetStringLength() > len && kVersion30Value == ValueType(itr->value.GetString(), len)) return kVersion30; // must match 3.0.x + const ValueType kVersion31Value(kVersion31String); + if (itr->value.GetStringLength() > len && kVersion31Value == ValueType(itr->value.GetString(), len)) return kVersion31; // must match 3.1.x + return kVersionUnknown; + } + // swagger or openapi not found + return kVersionNone; + } + + // Get the draft of the schema or the open api version (which implies the draft). + // Report an error if schema draft or open api version not supported or not recognized, or both in document, and carry on. + void SetSchemaSpecification(const ValueType& document) { + // Look for '$schema', 'swagger' or 'openapi' keyword at document root + SchemaDraft docDraft = GetSchemaDraft(document); + OpenApiVersion docOapi = GetOpenApiVersion(document); + // Error if both in document + if (docDraft != kDraftNone && docOapi != kVersionNone) + SchemaError(kSchemaErrorSpecIllegal, PointerType()); + // Use document draft or open api version if present or use spec from constructor + if (docDraft != kDraftNone) + spec_ = Specification(docDraft); + else if (docOapi != kVersionNone) + spec_ = Specification(docOapi); + // Error if draft or version unknown + if (spec_.draft == kDraftUnknown || spec_.oapi == kVersionUnknown) + SchemaError(kSchemaErrorSpecUnknown, PointerType()); + else if (!spec_.IsSupported()) + SchemaError(kSchemaErrorSpecUnsupported, PointerType()); + } + // Changed by PR #1393 void CreateSchemaRecursive(const SchemaType** schema, const PointerType& pointer, const ValueType& v, const ValueType& document, const UriType& id) { if (v.GetType() == kObjectType) { @@ -1875,6 +2175,9 @@ class GenericSchemaDocument { // Changed by PR #1393 const UriType& CreateSchema(const SchemaType** schema, const PointerType& pointer, const ValueType& v, const ValueType& document, const UriType& id) { RAPIDJSON_ASSERT(pointer.IsValid()); + GenericStringBuffer sb; + pointer.StringifyUriFragment(sb); + RAPIDJSON_SCHEMA_PRINT(Method, "GenericSchemaDocument::CreateSchema", sb.GetString(), id.GetString()); if (v.IsObject()) { if (const SchemaType* sc = GetSchema(pointer)) { if (schema) @@ -1904,6 +2207,9 @@ class GenericSchemaDocument { if (itr == v.MemberEnd()) return false; + GenericStringBuffer sb; + source.StringifyUriFragment(sb); + RAPIDJSON_SCHEMA_PRINT(Method, "GenericSchemaDocument::HandleRefSchema", sb.GetString(), id.GetString()); // Resolve the source pointer to the $ref'ed schema (finally) new (schemaRef_.template Push()) SchemaRefPtr(&source); @@ -1915,6 +2221,7 @@ class GenericSchemaDocument { // First resolve $ref against the in-scope id UriType scopeId = UriType(id, allocator_); UriType ref = UriType(itr->value, allocator_).Resolve(scopeId, allocator_); + RAPIDJSON_SCHEMA_PRINT(SchemaIds, id.GetString(), itr->value.GetString(), ref.GetString()); // See if the resolved $ref minus the fragment matches a resolved id in this document // Search from the root. Returns the subschema in the document and its absolute JSON pointer. PointerType basePointer = PointerType(); @@ -1924,7 +2231,7 @@ class GenericSchemaDocument { if (!remoteProvider_) SchemaError(kSchemaErrorRefNoRemoteProvider, source); else { - if (const GenericSchemaDocument* remoteDocument = remoteProvider_->GetRemoteDocument(ref)) { + if (const GenericSchemaDocument* remoteDocument = remoteProvider_->GetRemoteDocument(ref, spec_)) { const Ch* s = ref.GetFragString(); len = ref.GetFragStringLength(); if (len <= 1 || s[1] == '/') { @@ -1979,10 +2286,13 @@ class GenericSchemaDocument { } } else { // Plain name fragment, relative to the resolved URI + // Not supported in open api 2.0 and 3.0 PointerType pointer(allocator_); + if (spec_.oapi == kVersion20 || spec_.oapi == kVersion30) + SchemaErrorValue(kSchemaErrorRefPlainName, source, s, len); // See if the fragment matches an id in this document. // Search from the base we just established. Returns the subschema in the document and its absolute JSON pointer. - if (const ValueType *pv = FindId(*base, ref, pointer, UriType(ref.GetBaseString(), ref.GetBaseStringLength(), allocator_), true, basePointer)) { + else if (const ValueType *pv = FindId(*base, ref, pointer, UriType(ref.GetBaseString(), ref.GetBaseStringLength(), allocator_), true, basePointer)) { if (IsCyclicRef(pointer)) SchemaErrorValue(kSchemaErrorRefCyclical, source, ref.GetString(), ref.GetStringLength()); else { @@ -2024,6 +2334,7 @@ class GenericSchemaDocument { } // See if it matches if (localuri.Match(finduri, full)) { + RAPIDJSON_SCHEMA_PRINT(Method, "GenericSchemaDocument::FindId (match)", full ? localuri.GetString() : localuri.GetBaseString()); resval = const_cast(&doc); resptr = here; return resval; @@ -2050,6 +2361,7 @@ class GenericSchemaDocument { // Added by PR #1393 void AddSchemaRefs(SchemaType* schema) { + RAPIDJSON_SCHEMA_PRINT(Method, "GenericSchemaDocument::AddSchemaRefs"); while (!schemaRef_.Empty()) { SchemaRefPtr *ref = schemaRef_.template Pop(1); SchemaEntry *entry = schemaMap_.template Push(); @@ -2093,6 +2405,7 @@ class GenericSchemaDocument { internal::Stack schemaRef_; // Stores Pointer(s) from $ref(s) until resolved GValue uri_; // Schema document URI UriType docId_; + Specification spec_; GValue error_; GValue currentError_; }; @@ -2158,11 +2471,10 @@ class GenericSchemaValidator : currentError_(), missingDependents_(), valid_(true), - flags_(kValidateDefaultFlags) -#if RAPIDJSON_SCHEMA_VERBOSE - , depth_(0) -#endif + flags_(kValidateDefaultFlags), + depth_(0) { + RAPIDJSON_SCHEMA_PRINT(Method, "GenericSchemaValidator::GenericSchemaValidator"); } //! Constructor with output handler. @@ -2190,11 +2502,10 @@ class GenericSchemaValidator : currentError_(), missingDependents_(), valid_(true), - flags_(kValidateDefaultFlags) -#if RAPIDJSON_SCHEMA_VERBOSE - , depth_(0) -#endif + flags_(kValidateDefaultFlags), + depth_(0) { + RAPIDJSON_SCHEMA_PRINT(Method, "GenericSchemaValidator::GenericSchemaValidator (output handler)"); } //! Destructor. @@ -2455,6 +2766,14 @@ class GenericSchemaValidator : currentError_.SetObject(); AddCurrentError(kValidateErrorNot); } + void DisallowedWhenWriting() { + currentError_.SetObject(); + AddCurrentError(kValidateErrorReadOnly); + } + void DisallowedWhenReading() { + currentError_.SetObject(); + AddCurrentError(kValidateErrorWriteOnly); + } #define RAPIDJSON_STRING_(name, ...) \ static const StringRefType& Get##name##String() {\ @@ -2477,21 +2796,12 @@ class GenericSchemaValidator : #undef RAPIDJSON_STRING_ -#if RAPIDJSON_SCHEMA_VERBOSE -#define RAPIDJSON_SCHEMA_HANDLE_BEGIN_VERBOSE_() \ -RAPIDJSON_MULTILINEMACRO_BEGIN\ - *documentStack_.template Push() = '\0';\ - documentStack_.template Pop(1);\ - internal::PrintInvalidDocument(documentStack_.template Bottom());\ -RAPIDJSON_MULTILINEMACRO_END -#else -#define RAPIDJSON_SCHEMA_HANDLE_BEGIN_VERBOSE_() -#endif - #define RAPIDJSON_SCHEMA_HANDLE_BEGIN_(method, arg1)\ if (!valid_) return false; \ if ((!BeginValue() && !GetContinueOnErrors()) || (!CurrentSchema().method arg1 && !GetContinueOnErrors())) {\ - RAPIDJSON_SCHEMA_HANDLE_BEGIN_VERBOSE_();\ + *documentStack_.template Push() = '\0';\ + documentStack_.template Pop(1);\ + RAPIDJSON_SCHEMA_PRINT(InvalidDocument, documentStack_.template Bottom());\ valid_ = false;\ return valid_;\ } @@ -2530,6 +2840,7 @@ RAPIDJSON_MULTILINEMACRO_END { RAPIDJSON_SCHEMA_HANDLE_VALUE_(String, (CurrentContext(), str, length, copy), (str, length, copy)); } bool StartObject() { + RAPIDJSON_SCHEMA_PRINT(Method, "GenericSchemaValidator::StartObject"); RAPIDJSON_SCHEMA_HANDLE_BEGIN_(StartObject, (CurrentContext())); RAPIDJSON_SCHEMA_HANDLE_PARALLEL_(StartObject, ()); valid_ = !outputHandler_ || outputHandler_->StartObject(); @@ -2537,6 +2848,7 @@ RAPIDJSON_MULTILINEMACRO_END } bool Key(const Ch* str, SizeType len, bool copy) { + RAPIDJSON_SCHEMA_PRINT(Method, "GenericSchemaValidator::Key", str); if (!valid_) return false; AppendToken(str, len); if (!CurrentSchema().Key(CurrentContext(), str, len, copy) && !GetContinueOnErrors()) { @@ -2549,6 +2861,7 @@ RAPIDJSON_MULTILINEMACRO_END } bool EndObject(SizeType memberCount) { + RAPIDJSON_SCHEMA_PRINT(Method, "GenericSchemaValidator::EndObject"); if (!valid_) return false; RAPIDJSON_SCHEMA_HANDLE_PARALLEL_(EndObject, (memberCount)); if (!CurrentSchema().EndObject(CurrentContext(), memberCount) && !GetContinueOnErrors()) { @@ -2559,6 +2872,7 @@ RAPIDJSON_MULTILINEMACRO_END } bool StartArray() { + RAPIDJSON_SCHEMA_PRINT(Method, "GenericSchemaValidator::StartArray"); RAPIDJSON_SCHEMA_HANDLE_BEGIN_(StartArray, (CurrentContext())); RAPIDJSON_SCHEMA_HANDLE_PARALLEL_(StartArray, ()); valid_ = !outputHandler_ || outputHandler_->StartArray(); @@ -2566,6 +2880,7 @@ RAPIDJSON_MULTILINEMACRO_END } bool EndArray(SizeType elementCount) { + RAPIDJSON_SCHEMA_PRINT(Method, "GenericSchemaValidator::EndArray"); if (!valid_) return false; RAPIDJSON_SCHEMA_HANDLE_PARALLEL_(EndArray, (elementCount)); if (!CurrentSchema().EndArray(CurrentContext(), elementCount) && !GetContinueOnErrors()) { @@ -2575,17 +2890,16 @@ RAPIDJSON_MULTILINEMACRO_END RAPIDJSON_SCHEMA_HANDLE_END_(EndArray, (elementCount)); } -#undef RAPIDJSON_SCHEMA_HANDLE_BEGIN_VERBOSE_ #undef RAPIDJSON_SCHEMA_HANDLE_BEGIN_ #undef RAPIDJSON_SCHEMA_HANDLE_PARALLEL_ #undef RAPIDJSON_SCHEMA_HANDLE_VALUE_ // Implementation of ISchemaStateFactory virtual ISchemaValidator* CreateSchemaValidator(const SchemaType& root, const bool inheritContinueOnErrors) { + *documentStack_.template Push() = '\0'; + documentStack_.template Pop(1); ISchemaValidator* sv = new (GetStateAllocator().Malloc(sizeof(GenericSchemaValidator))) GenericSchemaValidator(*schemaDocument_, root, documentStack_.template Bottom(), documentStack_.GetSize(), -#if RAPIDJSON_SCHEMA_VERBOSE depth_ + 1, -#endif &GetStateAllocator()); sv->SetValidateFlags(inheritContinueOnErrors ? GetValidateFlags() : GetValidateFlags() & ~(unsigned)kValidateContinueOnErrorFlag); return sv; @@ -2629,9 +2943,7 @@ RAPIDJSON_MULTILINEMACRO_END const SchemaDocumentType& schemaDocument, const SchemaType& root, const char* basePath, size_t basePathSize, -#if RAPIDJSON_SCHEMA_VERBOSE unsigned depth, -#endif StateAllocator* allocator = 0, size_t schemaStackCapacity = kDefaultSchemaStackCapacity, size_t documentStackCapacity = kDefaultDocumentStackCapacity) @@ -2647,11 +2959,10 @@ RAPIDJSON_MULTILINEMACRO_END currentError_(), missingDependents_(), valid_(true), - flags_(kValidateDefaultFlags) -#if RAPIDJSON_SCHEMA_VERBOSE - , depth_(depth) -#endif + flags_(kValidateDefaultFlags), + depth_(depth) { + RAPIDJSON_SCHEMA_PRINT(Method, "GenericSchemaValidator::GenericSchemaValidator (internal)", basePath && basePathSize ? basePath : ""); if (basePath && basePathSize) memcpy(documentStack_.template Push(basePathSize), basePath, basePathSize); } @@ -2667,6 +2978,7 @@ RAPIDJSON_MULTILINEMACRO_END } bool BeginValue() { + RAPIDJSON_SCHEMA_PRINT(Method, "GenericSchemaValidator::BeginValue"); if (schemaStack_.Empty()) PushSchema(root_); else { @@ -2699,17 +3011,15 @@ RAPIDJSON_MULTILINEMACRO_END } bool EndValue() { + RAPIDJSON_SCHEMA_PRINT(Method, "GenericSchemaValidator::EndValue"); if (!CurrentSchema().EndValue(CurrentContext()) && !GetContinueOnErrors()) return false; -#if RAPIDJSON_SCHEMA_VERBOSE GenericStringBuffer sb; - schemaDocument_->GetPointer(&CurrentSchema()).Stringify(sb); - + schemaDocument_->GetPointer(&CurrentSchema()).StringifyUriFragment(sb); *documentStack_.template Push() = '\0'; documentStack_.template Pop(1); - internal::PrintValidatorPointers(depth_, sb.GetString(), documentStack_.template Bottom()); -#endif + RAPIDJSON_SCHEMA_PRINT(ValidatorPointers, sb.GetString(), documentStack_.template Bottom(), depth_); void* hasher = CurrentContext().hasher; uint64_t h = hasher && CurrentContext().arrayUniqueness ? static_cast(hasher)->GetHashCode() : 0; @@ -2760,7 +3070,7 @@ RAPIDJSON_MULTILINEMACRO_END } } - RAPIDJSON_FORCEINLINE void PushSchema(const SchemaType& schema) { new (schemaStack_.template Push()) Context(*this, *this, &schema); } + RAPIDJSON_FORCEINLINE void PushSchema(const SchemaType& schema) { new (schemaStack_.template Push()) Context(*this, *this, &schema, flags_); } RAPIDJSON_FORCEINLINE void PopSchema() { Context* c = schemaStack_.template Pop(1); @@ -2862,9 +3172,7 @@ RAPIDJSON_MULTILINEMACRO_END ValueType missingDependents_; bool valid_; unsigned flags_; -#if RAPIDJSON_SCHEMA_VERBOSE unsigned depth_; -#endif }; typedef GenericSchemaValidator SchemaValidator; diff --git a/test/unittest/schematest.cpp b/test/unittest/schematest.cpp index a3f5f93b5..7387c0896 100644 --- a/test/unittest/schematest.cpp +++ b/test/unittest/schematest.cpp @@ -118,12 +118,7 @@ TEST(SchemaValidator, Hasher) { #define VALIDATE_(schema, json, expected, expected2) \ {\ EXPECT_TRUE(expected2 == schema.GetError().ObjectEmpty());\ - if (expected2 && !schema.GetError().ObjectEmpty()) {\ - StringBuffer ssb;\ - Writer ws(ssb);\ - schema.GetError().Accept(ws);\ - printf("Schema error: %s\n", ssb.GetString());\ - }\ + EXPECT_TRUE(schema.IsSupportedSpecification());\ SchemaValidator validator(schema);\ Document d;\ /*printf("\n%s\n", json);*/\ @@ -162,12 +157,7 @@ TEST(SchemaValidator, Hasher) { flags, SchemaValidatorType, PointerType) \ {\ EXPECT_TRUE(schema.GetError().ObjectEmpty());\ - if (!schema.GetError().ObjectEmpty()) {\ - StringBuffer ssb;\ - Writer ws(ssb);\ - schema.GetError().Accept(ws);\ - printf("Schema error: %s\n", ssb.GetString());\ - }\ + EXPECT_TRUE(schema.IsSupportedSpecification());\ SchemaValidatorType validator(schema);\ validator.SetValidateFlags(flags);\ Document d;\ @@ -2163,9 +2153,13 @@ class RemoteSchemaDocumentProvider : public IGenericRemoteSchemaDocumentProvider } virtual const SchemaDocumentType* GetRemoteDocument(const char* uri, SizeType length) { + //printf("GetRemoteDocument : %s\n", uri); for (size_t i = 0; i < kCount; i++) - if (typename SchemaDocumentType::GValue(uri, length) == sd_[i]->GetURI()) + if (typename SchemaDocumentType::GValue(uri, length) == sd_[i]->GetURI()) { + //printf("Matched document"); return sd_[i]; + } + //printf("No matched document"); return 0; } @@ -2999,6 +2993,334 @@ TEST(SchemaValidator, DuplicateKeyword) { // SchemaDocument tests +// Specification (schema draft, open api version) +TEST(SchemaValidator, Schema_SupportedNotObject) { + Document sd; + sd.Parse("true"); + ASSERT_FALSE(sd.HasParseError()); + SchemaDocument s(sd); + ASSERT_TRUE(s.IsSupportedSpecification()); + ASSERT_TRUE(s.GetSpecification().draft == kDraft04); + ASSERT_TRUE(s.GetSpecification().oapi == kVersionNone); + EXPECT_TRUE(s.GetError().ObjectEmpty()); +} + +TEST(SchemaValidator, Schema_SupportedNoSpec) { + Document sd; + sd.Parse("{\"type\": \"integer\"}"); + ASSERT_FALSE(sd.HasParseError()); + SchemaDocument s(sd); + ASSERT_TRUE(s.IsSupportedSpecification()); + ASSERT_TRUE(s.GetSpecification().draft == kDraft04); + ASSERT_TRUE(s.GetSpecification().oapi == kVersionNone); + EXPECT_TRUE(s.GetError().ObjectEmpty()); +} + +TEST(SchemaValidator, Schema_SupportedNoSpecStatic) { + typedef GenericSchemaDocument > SchemaDocumentType; + Document sd; + sd.Parse("{\"type\": \"integer\"}"); + ASSERT_FALSE(sd.HasParseError()); + Specification spec = SchemaDocumentType::GetSpecification(sd); + ASSERT_FALSE(spec.IsSupported()); + ASSERT_TRUE(spec.draft == kDraftNone); + ASSERT_TRUE(spec.oapi == kVersionNone); +} + +TEST(SchemaValidator, Schema_SupportedDraft5Static) { + typedef GenericSchemaDocument > SchemaDocumentType; + Document sd; + sd.Parse("{\"$schema\":\"http://json-schema.org/draft-05/schema#\", \"type\": \"integer\"}"); + ASSERT_FALSE(sd.HasParseError()); + Specification spec = SchemaDocumentType::GetSpecification(sd); + ASSERT_TRUE(spec.IsSupported()); + ASSERT_TRUE(spec.draft == kDraft05); + ASSERT_TRUE(spec.oapi == kVersionNone); +} + +TEST(SchemaValidator, Schema_SupportedDraft4) { + Document sd; + sd.Parse("{\"$schema\":\"http://json-schema.org/draft-04/schema#\", \"type\": \"integer\"}"); + ASSERT_FALSE(sd.HasParseError()); + SchemaDocument s(sd); + ASSERT_TRUE(s.IsSupportedSpecification()); + ASSERT_TRUE(s.GetSpecification().draft == kDraft04); + ASSERT_TRUE(s.GetSpecification().oapi == kVersionNone); + EXPECT_TRUE(s.GetError().ObjectEmpty()); +} + +TEST(SchemaValidator, Schema_SupportedDraft4NoFrag) { + Document sd; + sd.Parse("{\"$schema\":\"http://json-schema.org/draft-04/schema\", \"type\": \"integer\"}"); + ASSERT_FALSE(sd.HasParseError()); + SchemaDocument s(sd); + ASSERT_TRUE(s.IsSupportedSpecification()); + ASSERT_TRUE(s.GetSpecification().draft == kDraft04); + ASSERT_TRUE(s.GetSpecification().oapi == kVersionNone); + EXPECT_TRUE(s.GetError().ObjectEmpty()); +} + +TEST(SchemaValidator, Schema_SupportedDraft5) { + Document sd; + sd.Parse("{\"$schema\":\"http://json-schema.org/draft-05/schema#\", \"type\": \"integer\"}"); + ASSERT_FALSE(sd.HasParseError()); + SchemaDocument s(sd); + ASSERT_TRUE(s.IsSupportedSpecification()); + ASSERT_TRUE(s.GetSpecification().draft == kDraft05); + ASSERT_TRUE(s.GetSpecification().oapi == kVersionNone); + EXPECT_TRUE(s.GetError().ObjectEmpty()); +} + +TEST(SchemaValidator, Schema_SupportedDraft5NoFrag) { + Document sd; + sd.Parse("{\"$schema\":\"http://json-schema.org/draft-05/schema\", \"type\": \"integer\"}"); + ASSERT_FALSE(sd.HasParseError()); + SchemaDocument s(sd); + ASSERT_TRUE(s.IsSupportedSpecification()); + ASSERT_TRUE(s.GetSpecification().draft == kDraft05); + ASSERT_TRUE(s.GetSpecification().oapi == kVersionNone); + EXPECT_TRUE(s.GetError().ObjectEmpty()); +} + +TEST(SchemaValidator, Schema_IgnoreDraftEmbedded) { + Document sd; + sd.Parse("{\"root\": {\"$schema\":\"http://json-schema.org/draft-05/schema#\", \"type\": \"integer\"}}"); + ASSERT_FALSE(sd.HasParseError()); + SchemaDocument s(sd, 0, 0, 0, 0, SchemaDocument::PointerType("/root")); + ASSERT_TRUE(s.IsSupportedSpecification()); + ASSERT_TRUE(s.GetSpecification().draft == kDraft04); + ASSERT_TRUE(s.GetSpecification().oapi == kVersionNone); + EXPECT_TRUE(s.GetError().ObjectEmpty()); +} + +TEST(SchemaValidator, Schema_SupportedDraftOverride) { + Document sd; + sd.Parse("{\"type\": \"integer\"}"); + ASSERT_FALSE(sd.HasParseError()); + SchemaDocument s(sd, 0, 0, 0, 0, 0, Specification(kDraft04)); + ASSERT_TRUE(s.IsSupportedSpecification()); + ASSERT_TRUE(s.GetSpecification().draft == kDraft04); + ASSERT_TRUE(s.GetSpecification().oapi == kVersionNone); + EXPECT_TRUE(s.GetError().ObjectEmpty()); +} + +TEST(SchemaValidator, Schema_UnknownDraftOverride) { + Document sd; + sd.Parse("{\"type\": \"integer\"}"); + ASSERT_FALSE(sd.HasParseError()); + SchemaDocument s(sd, 0, 0, 0, 0, 0, Specification(kDraftUnknown)); + ASSERT_FALSE(s.IsSupportedSpecification()); + ASSERT_TRUE(s.GetSpecification().draft == kDraftUnknown); + ASSERT_TRUE(s.GetSpecification().oapi == kVersionNone); + SCHEMAERROR(s, "{\"SpecUnknown\":{\"errorCode\":10,\"instanceRef\":\"#\"}}"); +} + +TEST(SchemaValidator, Schema_UnsupportedDraftOverride) { + Document sd; + sd.Parse("{\"type\": \"integer\"}"); + ASSERT_FALSE(sd.HasParseError()); + SchemaDocument s(sd, 0, 0, 0, 0, 0, Specification(kDraft03)); + ASSERT_FALSE(s.IsSupportedSpecification()); + ASSERT_TRUE(s.GetSpecification().draft == kDraft03); + ASSERT_TRUE(s.GetSpecification().oapi == kVersionNone); + SCHEMAERROR(s, "{\"SpecUnsupported\":{\"errorCode\":11,\"instanceRef\":\"#\"}}"); +} + +TEST(SchemaValidator, Schema_UnknownDraft) { + Document sd; + sd.Parse("{\"$schema\":\"http://json-schema.org/draft-xxx/schema#\", \"type\": \"integer\"}"); + ASSERT_FALSE(sd.HasParseError()); + SchemaDocument s(sd); + ASSERT_FALSE(s.IsSupportedSpecification()); + ASSERT_TRUE(s.GetSpecification().draft == kDraftUnknown); + ASSERT_TRUE(s.GetSpecification().oapi == kVersionNone); + SCHEMAERROR(s, "{\"SpecUnknown\":{\"errorCode\":10,\"instanceRef\":\"#\"}}"); +} + +TEST(SchemaValidator, Schema_UnknownDraftNotString) { + Document sd; + sd.Parse("{\"$schema\": 4, \"type\": \"integer\"}"); + ASSERT_FALSE(sd.HasParseError()); + SchemaDocument s(sd); + ASSERT_FALSE(s.IsSupportedSpecification()); + ASSERT_TRUE(s.GetSpecification().draft == kDraftUnknown); + ASSERT_TRUE(s.GetSpecification().oapi == kVersionNone); + SCHEMAERROR(s, "{\"SpecUnknown\":{\"errorCode\":10,\"instanceRef\":\"#\"}}"); +} + +TEST(SchemaValidator, Schema_UnsupportedDraft3) { + Document sd; + sd.Parse("{\"$schema\":\"http://json-schema.org/draft-03/schema#\", \"type\": \"integer\"}"); + ASSERT_FALSE(sd.HasParseError()); + SchemaDocument s(sd); + ASSERT_FALSE(s.IsSupportedSpecification()); + ASSERT_TRUE(s.GetSpecification().draft == kDraft03); + ASSERT_TRUE(s.GetSpecification().oapi == kVersionNone); + SCHEMAERROR(s, "{\"SpecUnsupported\":{\"errorCode\":11,\"instanceRef\":\"#\"}}"); +} + +TEST(SchemaValidator, Schema_UnsupportedDraft6) { + Document sd; + sd.Parse("{\"$schema\":\"http://json-schema.org/draft-06/schema#\", \"type\": \"integer\"}"); + ASSERT_FALSE(sd.HasParseError()); + SchemaDocument s(sd); + ASSERT_FALSE(s.IsSupportedSpecification()); + ASSERT_TRUE(s.GetSpecification().draft == kDraft06); + SCHEMAERROR(s, "{\"SpecUnsupported\":{\"errorCode\":11,\"instanceRef\":\"#\"}}"); +} + +TEST(SchemaValidator, Schema_UnsupportedDraft7) { + Document sd; + sd.Parse("{\"$schema\":\"http://json-schema.org/draft-07/schema#\", \"type\": \"integer\"}"); + ASSERT_FALSE(sd.HasParseError()); + SchemaDocument s(sd); + ASSERT_FALSE(s.IsSupportedSpecification()); + ASSERT_TRUE(s.GetSpecification().draft == kDraft07); + ASSERT_TRUE(s.GetSpecification().oapi == kVersionNone); + SCHEMAERROR(s, "{\"SpecUnsupported\":{\"errorCode\":11,\"instanceRef\":\"#\"}}"); +} + +TEST(SchemaValidator, Schema_UnsupportedDraft2019_09) { + Document sd; + sd.Parse("{\"$schema\":\"https://json-schema.org/draft/2019-09/schema\", \"type\": \"integer\"}"); + ASSERT_FALSE(sd.HasParseError()); + SchemaDocument s(sd); + ASSERT_FALSE(s.IsSupportedSpecification()); + ASSERT_TRUE(s.GetSpecification().draft == kDraft2019_09); + ASSERT_TRUE(s.GetSpecification().oapi == kVersionNone); + SCHEMAERROR(s, "{\"SpecUnsupported\":{\"errorCode\":11,\"instanceRef\":\"#\"}}"); +} + +TEST(SchemaValidator, Schema_UnsupportedDraft2020_12) { + Document sd; + sd.Parse("{\"$schema\":\"https://json-schema.org/draft/2020-12/schema\", \"type\": \"integer\"}"); + ASSERT_FALSE(sd.HasParseError()); + SchemaDocument s(sd); + ASSERT_FALSE(s.IsSupportedSpecification()); + ASSERT_TRUE(s.GetSpecification().draft == kDraft2020_12); + ASSERT_TRUE(s.GetSpecification().oapi == kVersionNone); + SCHEMAERROR(s, "{\"SpecUnsupported\":{\"errorCode\":11,\"instanceRef\":\"#\"}}"); +} + +TEST(SchemaValidator, Schema_SupportedVersion20Static) { + typedef GenericSchemaDocument > SchemaDocumentType; + Document sd; + sd.Parse("{\"swagger\":\"2.0\"}"); + ASSERT_FALSE(sd.HasParseError()); + Specification spec = SchemaDocumentType::GetSpecification(sd); + ASSERT_TRUE(spec.IsSupported()); + ASSERT_TRUE(spec.draft == kDraft04); + ASSERT_TRUE(spec.oapi == kVersion20); +} + +TEST(SchemaValidator, Schema_SupportedVersion20) { + Document sd; + sd.Parse("{\"swagger\":\"2.0\"}"); + ASSERT_FALSE(sd.HasParseError()); + SchemaDocument s(sd); + ASSERT_TRUE(s.IsSupportedSpecification()); + ASSERT_TRUE(s.GetSpecification().oapi == kVersion20); + ASSERT_TRUE(s.GetSpecification().draft == kDraft04); + EXPECT_TRUE(s.GetError().ObjectEmpty()); +} + +TEST(SchemaValidator, Schema_SupportedVersion30x) { + Document sd; + sd.Parse("{\"openapi\":\"3.0.0\"}"); + ASSERT_FALSE(sd.HasParseError()); + SchemaDocument s(sd); + ASSERT_TRUE(s.IsSupportedSpecification()); + ASSERT_TRUE(s.GetSpecification().oapi == kVersion30); + ASSERT_TRUE(s.GetSpecification().draft == kDraft05); + EXPECT_TRUE(s.GetError().ObjectEmpty()); +} + +TEST(SchemaValidator, Schema_SupportedVersionOverride) { + Document sd; + sd.Parse("{\"type\": \"integer\"}"); + ASSERT_FALSE(sd.HasParseError()); + SchemaDocument s(sd, 0, 0, 0, 0, 0, Specification(kVersion20)); + ASSERT_TRUE(s.IsSupportedSpecification()); + ASSERT_TRUE(s.GetSpecification().oapi == kVersion20); + ASSERT_TRUE(s.GetSpecification().draft == kDraft04); + EXPECT_TRUE(s.GetError().ObjectEmpty()); +} + +TEST(SchemaValidator, Schema_UnknownVersionOverride) { + Document sd; + sd.Parse("{\"type\": \"integer\"}"); + ASSERT_FALSE(sd.HasParseError()); + SchemaDocument s(sd, 0, 0, 0, 0, 0, Specification(kVersionUnknown)); + ASSERT_FALSE(s.IsSupportedSpecification()); + ASSERT_TRUE(s.GetSpecification().oapi == kVersionUnknown); + ASSERT_TRUE(s.GetSpecification().draft == kDraft04); + SCHEMAERROR(s, "{\"SpecUnknown\":{\"errorCode\":10,\"instanceRef\":\"#\"}}"); +} + +TEST(SchemaValidator, Schema_UnsupportedVersionOverride) { + Document sd; + sd.Parse("{\"type\": \"integer\"}"); + ASSERT_FALSE(sd.HasParseError()); + SchemaDocument s(sd, 0, 0, 0, 0, 0, Specification(kVersion31)); + ASSERT_FALSE(s.IsSupportedSpecification()); + ASSERT_TRUE(s.GetSpecification().oapi == kVersion31); + ASSERT_TRUE(s.GetSpecification().draft == kDraft2020_12); + SCHEMAERROR(s, "{\"SpecUnsupported\":{\"errorCode\":11,\"instanceRef\":\"#\"}}"); +} + +TEST(SchemaValidator, Schema_UnknownVersion) { + Document sd; + sd.Parse("{\"openapi\":\"1.0\"}"); + ASSERT_FALSE(sd.HasParseError()); + SchemaDocument s(sd); + ASSERT_FALSE(s.IsSupportedSpecification()); + ASSERT_TRUE(s.GetSpecification().oapi == kVersionUnknown); + ASSERT_TRUE(s.GetSpecification().draft == kDraft04); + SCHEMAERROR(s, "{\"SpecUnknown\":{\"errorCode\":10,\"instanceRef\":\"#\"}}"); +} + +TEST(SchemaValidator, Schema_UnknownVersionShort) { + Document sd; + sd.Parse("{\"openapi\":\"3.0.\"}"); + ASSERT_FALSE(sd.HasParseError()); + SchemaDocument s(sd); + ASSERT_FALSE(s.IsSupportedSpecification()); + ASSERT_TRUE(s.GetSpecification().oapi == kVersionUnknown); + ASSERT_TRUE(s.GetSpecification().draft == kDraft04); + SCHEMAERROR(s, "{\"SpecUnknown\":{\"errorCode\":10,\"instanceRef\":\"#\"}}"); +} + +TEST(SchemaValidator, Schema_UnknownVersionNotString) { + Document sd; + sd.Parse("{\"swagger\": 2}"); + ASSERT_FALSE(sd.HasParseError()); + SchemaDocument s(sd); + ASSERT_FALSE(s.IsSupportedSpecification()); + ASSERT_TRUE(s.GetSpecification().oapi == kVersionUnknown); + ASSERT_TRUE(s.GetSpecification().draft == kDraft04); + SCHEMAERROR(s, "{\"SpecUnknown\":{\"errorCode\":10,\"instanceRef\":\"#\"}}"); +} + +TEST(SchemaValidator, Schema_UnsupportedVersion31) { + Document sd; + sd.Parse("{\"openapi\":\"3.1.0\"}"); + ASSERT_FALSE(sd.HasParseError()); + SchemaDocument s(sd); + ASSERT_FALSE(s.IsSupportedSpecification()); + ASSERT_TRUE(s.GetSpecification().oapi == kVersion31); + ASSERT_TRUE(s.GetSpecification().draft == kDraft2020_12); + SCHEMAERROR(s, "{\"SpecUnsupported\":{\"errorCode\":11,\"instanceRef\":\"#\"}}"); +} + +TEST(SchemaValidator, Schema_DraftAndVersion) { + Document sd; + sd.Parse("{\"swagger\": \"2.0\", \"$schema\": \"http://json-schema.org/draft-04/schema#\"}"); + ASSERT_FALSE(sd.HasParseError()); + SchemaDocument s(sd); + ASSERT_TRUE(s.IsSupportedSpecification()); + SCHEMAERROR(s, "{\"SpecIllegal\":{\"errorCode\":12,\"instanceRef\":\"#\"}}"); +} + TEST(SchemaValidator, Schema_StartUnknown) { Document sd; sd.Parse("{\"type\": \"integer\"}"); @@ -3007,6 +3329,25 @@ TEST(SchemaValidator, Schema_StartUnknown) { SCHEMAERROR(s, "{\"StartUnknown\":{\"errorCode\":1,\"instanceRef\":\"#\", \"value\":\"#/nowhere\"}}"); } +TEST(SchemaValidator, Schema_MultipleErrors) { + Document sd; + sd.Parse("{\"swagger\": \"foo\", \"$schema\": \"bar\"}"); + ASSERT_FALSE(sd.HasParseError()); + SchemaDocument s(sd); + SCHEMAERROR(s, "{ \"SpecUnknown\": {\"errorCode\":10,\"instanceRef\":\"#\"}," + " \"SpecIllegal\": {\"errorCode\":12,\"instanceRef\":\"#\"}" + "}"); +} + +// $ref is a non-JSON pointer fragment - not allowed when OpenAPI +TEST(SchemaValidator, Schema_RefPlainNameOpenApi) { + typedef GenericSchemaDocument > SchemaDocumentType; + Document sd; + sd.Parse("{\"swagger\": \"2.0\", \"type\": \"object\", \"properties\": {\"myInt1\": {\"$ref\": \"#myId\"}, \"myStr\": {\"type\": \"string\", \"id\": \"#myStrId\"}, \"myInt2\": {\"type\": \"integer\", \"id\": \"#myId\"}}}"); + SchemaDocumentType s(sd); + SCHEMAERROR(s, "{\"RefPlainName\":{\"errorCode\":2,\"instanceRef\":\"#/properties/myInt1\",\"value\":\"#myId\"}}"); +} + // $ref is a non-JSON pointer fragment - not allowed when remote document TEST(SchemaValidator, Schema_RefPlainNameRemote) { typedef GenericSchemaDocument > SchemaDocumentType; @@ -3019,9 +3360,10 @@ TEST(SchemaValidator, Schema_RefPlainNameRemote) { // $ref is an empty string TEST(SchemaValidator, Schema_RefEmptyString) { + typedef GenericSchemaDocument > SchemaDocumentType; Document sd; sd.Parse("{\"type\": \"object\", \"properties\": {\"myInt1\": {\"$ref\": \"\"}}}"); - SchemaDocument s(sd); + SchemaDocumentType s(sd); SCHEMAERROR(s, "{\"RefInvalid\":{\"errorCode\":3,\"instanceRef\":\"#/properties/myInt1\"}}"); } @@ -3046,9 +3388,10 @@ TEST(SchemaValidator, Schema_RefNoRemoteSchema) { // $ref pointer is invalid TEST(SchemaValidator, Schema_RefPointerInvalid) { + typedef GenericSchemaDocument > SchemaDocumentType; Document sd; sd.Parse("{\"type\": \"object\", \"properties\": {\"myInt\": {\"$ref\": \"#/&&&&&\"}}}"); - SchemaDocument s(sd); + SchemaDocumentType s(sd); SCHEMAERROR(s, "{\"RefPointerInvalid\":{\"errorCode\":4,\"instanceRef\":\"#/properties/myInt\",\"value\":\"#/&&&&&\",\"offset\":2}}"); } @@ -3064,17 +3407,19 @@ TEST(SchemaValidator, Schema_RefPointerInvalidRemote) { // $ref is unknown non-pointer TEST(SchemaValidator, Schema_RefUnknownPlainName) { + typedef GenericSchemaDocument > SchemaDocumentType; Document sd; sd.Parse("{\"type\": \"object\", \"properties\": {\"myInt\": {\"$ref\": \"#plainname\"}}}"); - SchemaDocument s(sd); + SchemaDocumentType s(sd); SCHEMAERROR(s, "{\"RefUnknown\":{\"errorCode\":5,\"instanceRef\":\"#/properties/myInt\",\"value\":\"#plainname\"}}"); } /// $ref is unknown pointer TEST(SchemaValidator, Schema_RefUnknownPointer) { + typedef GenericSchemaDocument > SchemaDocumentType; Document sd; sd.Parse("{\"type\": \"object\", \"properties\": {\"myInt\": {\"$ref\": \"#/a/b\"}}}"); - SchemaDocument s(sd); + SchemaDocumentType s(sd); SCHEMAERROR(s, "{\"RefUnknown\":{\"errorCode\":5,\"instanceRef\":\"#/properties/myInt\",\"value\":\"#/a/b\"}}"); } @@ -3090,6 +3435,7 @@ TEST(SchemaValidator, Schema_RefUnknownPointerRemote) { // $ref is cyclical TEST(SchemaValidator, Schema_RefCyclical) { + typedef GenericSchemaDocument > SchemaDocumentType; Document sd; sd.Parse("{\"type\": \"object\", \"properties\": {" " \"cyclic_source\": {" @@ -3099,10 +3445,130 @@ TEST(SchemaValidator, Schema_RefCyclical) { " \"$ref\": \"#/properties/cyclic_source\"" " }" "}}"); - SchemaDocument s(sd); + SchemaDocumentType s(sd); SCHEMAERROR(s, "{\"RefCyclical\":{\"errorCode\":6,\"instanceRef\":\"#/properties/cyclic_target\",\"value\":\"#/properties/cyclic_source\"}}"); } +TEST(SchemaValidator, Schema_ReadOnlyAndWriteOnly) { + Document sd; + sd.Parse("{\"type\": \"integer\", \"readOnly\": true, \"writeOnly\": true}"); + ASSERT_FALSE(sd.HasParseError()); + SchemaDocument s1(sd, 0, 0, 0, 0, 0, Specification(kDraft04)); + EXPECT_TRUE(s1.GetError().ObjectEmpty()); + SchemaDocument s2(sd, 0, 0, 0, 0, 0, Specification(kVersion30)); + SCHEMAERROR(s2, "{\"ReadOnlyAndWriteOnly\":{\"errorCode\":13,\"instanceRef\":\"#\"}}"); +} + +TEST(SchemaValidator, ReadOnlyWhenWriting) { + Document sd; + sd.Parse( + "{" + " \"type\":\"object\"," + " \"properties\": {" + " \"rprop\" : {" + " \"type\": \"string\"," + " \"readOnly\": true" + " }" + " }" + "}"); + SchemaDocument s(sd, 0, 0, 0, 0, 0, Specification(kVersion20)); + VALIDATE(s, "{ \"rprop\": \"hello\" }", true); + INVALIDATE_(s, "{ \"rprop\": \"hello\" }", "/properties/rprop", "readOnly", "/rprop", + "{ \"readOnly\": {" + " \"errorCode\": 26, \"instanceRef\": \"#/rprop\", \"schemaRef\": \"#/properties/rprop\"" + " }" + "}", + kValidateDefaultFlags | kValidateWriteFlag, SchemaValidator, Pointer); +} + +TEST(SchemaValidator, WriteOnlyWhenReading) { + Document sd; + sd.Parse( + "{" + " \"type\":\"object\"," + " \"properties\": {" + " \"wprop\" : {" + " \"type\": \"boolean\"," + " \"writeOnly\": true" + " }" + " }" + "}"); + SchemaDocument s(sd, 0, 0, 0, 0, 0, Specification(kVersion30)); + VALIDATE(s, "{ \"wprop\": true }", true); + INVALIDATE_(s, "{ \"wprop\": true }", "/properties/wprop", "writeOnly", "/wprop", + "{ \"writeOnly\": {" + " \"errorCode\": 27, \"instanceRef\": \"#/wprop\", \"schemaRef\": \"#/properties/wprop\"" + " }" + "}", + kValidateDefaultFlags | kValidateReadFlag, SchemaValidator, Pointer); +} + +TEST(SchemaValidator, NullableTrue) { + Document sd; + sd.Parse("{\"type\": \"string\", \"nullable\": true}"); + SchemaDocument s(sd, 0, 0, 0, 0, 0, kVersion20); + + VALIDATE(s, "\"hello\"", true); + INVALIDATE(s, "null", "", "type", "", + "{ \"type\": {" + " \"errorCode\": 20," + " \"instanceRef\": \"#\", \"schemaRef\": \"#\"," + " \"expected\": [\"string\"], \"actual\": \"null\"" + "}}"); + INVALIDATE(s, "false", "", "type", "", + "{ \"type\": {" + " \"errorCode\": 20," + " \"instanceRef\": \"#\", \"schemaRef\": \"#\"," + " \"expected\": [\"string\"], \"actual\": \"boolean\"" + "}}"); + + SchemaDocument s30(sd, 0, 0, 0, 0, 0, kVersion30); + + VALIDATE(s30, "\"hello\"", true); + VALIDATE(s30, "null", true); + INVALIDATE(s30, "false", "", "type", "", + "{ \"type\": {" + " \"errorCode\": 20," + " \"instanceRef\": \"#\", \"schemaRef\": \"#\"," + " \"expected\": [\"null\", \"string\"], \"actual\": \"boolean\"" + "}}"); +} + +TEST(SchemaValidator, NullableFalse) { + Document sd; + sd.Parse("{\"type\": \"string\", \"nullable\": false}"); + SchemaDocument s(sd, 0, 0, 0, 0, 0, kVersion20); + + VALIDATE(s, "\"hello\"", true); + INVALIDATE(s, "null", "", "type", "", + "{ \"type\": {" + " \"errorCode\": 20," + " \"instanceRef\": \"#\", \"schemaRef\": \"#\"," + " \"expected\": [\"string\"], \"actual\": \"null\"" + "}}"); + INVALIDATE(s, "false", "", "type", "", + "{ \"type\": {" + " \"errorCode\": 20," + " \"instanceRef\": \"#\", \"schemaRef\": \"#\"," + " \"expected\": [\"string\"], \"actual\": \"boolean\"" + "}}"); + + SchemaDocument s30(sd, 0, 0, 0, 0, 0, kVersion30); + + VALIDATE(s30, "\"hello\"", true); + INVALIDATE(s, "null", "", "type", "", + "{ \"type\": {" + " \"errorCode\": 20," + " \"instanceRef\": \"#\", \"schemaRef\": \"#\"," + " \"expected\": [\"string\"], \"actual\": \"null\"" + "}}"); + INVALIDATE(s30, "false", "", "type", "", + "{ \"type\": {" + " \"errorCode\": 20," + " \"instanceRef\": \"#\", \"schemaRef\": \"#\"," + " \"expected\": [\"string\"], \"actual\": \"boolean\"" + "}}"); +} #if defined(_MSC_VER) || defined(__clang__) RAPIDJSON_DIAG_POP