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 | 472 ++++++++++++++++++++++++------ test/unittest/schematest.cpp | 502 ++++++++++++++++++++++++++++++-- 4 files changed, 898 insertions(+), 110 deletions(-) diff --git a/include/rapidjson/error/en.h b/include/rapidjson/error/en.h index f27d62a..c87b04e 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 d2a2fc4..8bdc395 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 836c0f1..0e45df6 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 PrintInvalidKeyword(const wchar_t* keyword) { - wprintf(L"Fail keyword: %ls\n", keyword); +inline void PrintInvalidKeywordData(const wchar_t* keyword) { + wprintf(L" Fail keyword: '%ls'\n", keyword); } -inline void PrintInvalidDocument(const char* document) { - printf("Fail document: %s\n\n", document); +inline void PrintInvalidDocumentData(const char* document) { + printf(" Fail document: '%s'\n", document); } -inline void PrintInvalidDocument(const wchar_t* document) { - wprintf(L"Fail document: %ls\n\n", document); +inline void PrintInvalidDocumentData(const wchar_t* document) { + wprintf(L" Fail document: '%ls'\n", document); } -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 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 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 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 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 PrintMethodData(const char* method) { + printf("%s\n", method); +} + +inline void PrintMethodData(const char* method, bool b) { + printf("%s, Data: '%s'\n", method, b ? "true" : "false"); +} + +inline void PrintMethodData(const char* method, int64_t i) { + printf("%s, Data: '%" PRId64 "'\n", method, i); +} + +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 +#ifndef RAPIDJSON_SCHEMA_PRINT +#if RAPIDJSON_SCHEMA_VERBOSE +#define RAPIDJSON_SCHEMA_PRINT(name, ...) internal::Print##name##Data(__VA_ARGS__) +#else +#define RAPIDJSON_SCHEMA_PRINT(name, ...) +#endif +#endif + /////////////////////////////////////////////////////////////////////////////// // RAPIDJSON_INVALID_KEYWORD_RETURN -#if RAPIDJSON_SCHEMA_VERBOSE -#define RAPIDJSON_INVALID_KEYWORD_VERBOSE(keyword) internal::PrintInvalidKeyword(keyword) -#else -#define RAPIDJSON_INVALID_KEYWORD_VERBOSE(keyword) -#endif - #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 @@ public: 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 @@ public: 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 @@ public: allocator_(allocator), uri_(schemaDocument->GetURI(), *allocator), id_(id, allocator), + spec_(schemaDocument->GetSpecification()), pointer_(p, allocator), typeless_(schemaDocument->GetTypeless()), enum_(), @@ -475,8 +570,15 @@ public: 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 @@ public: 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 @@ public: } } - 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 @@ public: 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 @@ public: } } + // 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 @@ public: } } + // 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 @@ public: 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 @@ public: 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 @@ public: 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 @@ public: } 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 @@ public: } 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 @@ public: 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 @@ public: } 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 @@ public: } 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 @@ public: } 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 @@ public: } 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 @@ public: } 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 @@ public: } bool EndArray(Context& context, SizeType elementCount) const { + RAPIDJSON_SCHEMA_PRINT(Method, "Schema::EndArray"); context.inArray = false; if (elementCount < minItems_) { @@ -1118,6 +1269,9 @@ public: case kValidateErrorAnyOf: return GetAnyOfString(); case kValidateErrorNot: return GetNotString(); + case kValidateErrorReadOnly: return GetReadOnlyString(); + case kValidateErrorWriteOnly: return GetWriteOnlyString(); + default: return GetNullString(); } } @@ -1165,15 +1319,14 @@ public: 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 @@ private: // 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 @@ private: } } + // 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 @@ private: 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 @@ private: AllocatorType* allocator_; SValue uri_; UriType id_; + Specification spec_; PointerType pointer_; const SchemaType* typeless_; uint64_t* enum_; @@ -1568,6 +1741,10 @@ private: bool exclusiveMaximum_; SizeType defaultValueLength_; + + bool readOnly_; + bool writeOnly_; + bool nullable_; }; template @@ -1614,7 +1791,12 @@ public: 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 @@ public: \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 @@ public: 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 @@ public: 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 @@ public: 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 @@ public: 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 @@ public: 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 @@ public: } 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 @@ public: 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 @@ public: 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 @@ public: // 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 @@ public: 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 @@ public: // 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 @@ public: 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 @@ public: } } 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 @@ public: } // 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 @@ public: // 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 @@ public: 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 @@ public: 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 @@ public: 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 @@ public: 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 @@ public: #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 @@ private: 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 @@ private: 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 @@ private: } bool BeginValue() { + RAPIDJSON_SCHEMA_PRINT(Method, "GenericSchemaValidator::BeginValue"); if (schemaStack_.Empty()) PushSchema(root_); else { @@ -2699,17 +3011,15 @@ private: } 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 @@ private: } } - 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 @@ private: 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 a3f5f93..7387c08 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 @@ public: } 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