From 7698b3cd4868d4e649d2050a31b1c29e0bd1a562 Mon Sep 17 00:00:00 2001 From: Steve Hanson Date: Thu, 25 Feb 2021 21:45:29 +0000 Subject: [PATCH] code and tests --- include/rapidjson/document.h | 2 +- include/rapidjson/schema.h | 486 +++++++++++++++++++++++------ test/unittest/schematest.cpp | 590 ++++++++++++++++++++++++++++++++++- 3 files changed, 981 insertions(+), 97 deletions(-) diff --git a/include/rapidjson/document.h b/include/rapidjson/document.h index 028235e..0b123ae 100644 --- a/include/rapidjson/document.h +++ b/include/rapidjson/document.h @@ -1967,7 +1967,7 @@ public: case kArrayType: if (RAPIDJSON_UNLIKELY(!handler.StartArray())) return false; - for (const GenericValue* v = Begin(); v != End(); ++v) + for (ConstValueIterator v = Begin(); v != End(); ++v) if (RAPIDJSON_UNLIKELY(!v->Accept(handler))) return false; return handler.EndArray(data_.a.size); diff --git a/include/rapidjson/schema.h b/include/rapidjson/schema.h index 6576c25..9e99bf5 100644 --- a/include/rapidjson/schema.h +++ b/include/rapidjson/schema.h @@ -150,6 +150,9 @@ enum ValidateFlag { template class GenericSchemaDocument; +template +class Uri; + namespace internal { template @@ -432,11 +435,13 @@ public: typedef Schema SchemaType; typedef GenericValue SValue; typedef IValidationErrorHandler ErrorHandler; + typedef Uri UriType; friend class GenericSchemaDocument; - Schema(SchemaDocumentType* schemaDocument, const PointerType& p, const ValueType& value, const ValueType& document, AllocatorType* allocator) : + Schema(SchemaDocumentType* schemaDocument, const PointerType& p, const ValueType& value, const ValueType& document, AllocatorType* allocator, const UriType& id = UriType()) : allocator_(allocator), uri_(schemaDocument->GetURI(), *allocator), + id_(id), pointer_(p, allocator), typeless_(schemaDocument->GetTypeless()), enum_(), @@ -474,9 +479,30 @@ public: typedef typename ValueType::ConstValueIterator ConstValueIterator; typedef typename ValueType::ConstMemberIterator ConstMemberIterator; + // PR #1393 + // Early add this Schema and its $ref(s) in schemaDocument's map to avoid infinite + // recursion (with recursive schemas), since schemaDocument->getSchema() is always + // checked before creating a new one. Don't cache typeless_, though. + if (this != typeless_) { + typedef typename SchemaDocumentType::SchemaEntry SchemaEntry; + SchemaEntry *entry = schemaDocument->schemaMap_.template Push(); + new (entry) SchemaEntry(pointer_, this, true, allocator_); + schemaDocument->AddSchemaRefs(this); + } + if (!value.IsObject()) return; + // If we have an id property, resolve it with the in-scope id + if (const ValueType* v = GetMember(value, GetIdString())) { + if (v->IsString()) { + //std::cout << "Resolving local id '" << v->.GetString() << "' with in-scope id '" << id.GetString() << "'" << std::endl; + UriType local = UriType(*v); + local.Resolve(id_); + id_ = local; + } + } + if (const ValueType* v = GetMember(value, GetTypeString())) { type_ = 0; if (v->IsString()) @@ -507,7 +533,7 @@ public: } if (const ValueType* v = GetMember(value, GetNotString())) { - schemaDocument->CreateSchema(¬_, p.Append(GetNotString(), allocator_), *v, document); + schemaDocument->CreateSchema(¬_, p.Append(GetNotString(), allocator_), *v, document, id_); notValidatorIndex_ = validatorCount_; validatorCount_++; } @@ -524,7 +550,7 @@ public: if (properties && properties->IsObject()) for (ConstMemberIterator itr = properties->MemberBegin(); itr != properties->MemberEnd(); ++itr) AddUniqueElement(allProperties, itr->name); - + if (required && required->IsArray()) for (ConstValueIterator itr = required->Begin(); itr != required->End(); ++itr) if (itr->IsString()) @@ -555,7 +581,7 @@ public: for (ConstMemberIterator itr = properties->MemberBegin(); itr != properties->MemberEnd(); ++itr) { SizeType index; if (FindPropertyIndex(itr->name, &index)) - schemaDocument->CreateSchema(&properties_[index].schema, q.Append(itr->name, allocator_), itr->value, document); + schemaDocument->CreateSchema(&properties_[index].schema, q.Append(itr->name, allocator_), itr->value, document, id_); } } @@ -567,7 +593,7 @@ public: for (ConstMemberIterator itr = v->MemberBegin(); itr != v->MemberEnd(); ++itr) { new (&patternProperties_[patternPropertyCount_]) PatternProperty(); patternProperties_[patternPropertyCount_].pattern = CreatePattern(itr->name); - schemaDocument->CreateSchema(&patternProperties_[patternPropertyCount_].schema, q.Append(itr->name, allocator_), itr->value, document); + schemaDocument->CreateSchema(&patternProperties_[patternPropertyCount_].schema, q.Append(itr->name, allocator_), itr->value, document, id_); patternPropertyCount_++; } } @@ -599,7 +625,7 @@ public: } else if (itr->value.IsObject()) { hasSchemaDependencies_ = true; - schemaDocument->CreateSchema(&properties_[sourceIndex].dependenciesSchema, q.Append(itr->name, allocator_), itr->value, document); + schemaDocument->CreateSchema(&properties_[sourceIndex].dependenciesSchema, q.Append(itr->name, allocator_), itr->value, document, id_); properties_[sourceIndex].dependenciesValidatorIndex = validatorCount_; validatorCount_++; } @@ -611,7 +637,7 @@ public: if (v->IsBool()) additionalProperties_ = v->GetBool(); else if (v->IsObject()) - schemaDocument->CreateSchema(&additionalPropertiesSchema_, p.Append(GetAdditionalPropertiesString(), allocator_), *v, document); + schemaDocument->CreateSchema(&additionalPropertiesSchema_, p.Append(GetAdditionalPropertiesString(), allocator_), *v, document, id_); } AssignIfExist(minProperties_, value, GetMinPropertiesString()); @@ -621,12 +647,12 @@ public: if (const ValueType* v = GetMember(value, GetItemsString())) { PointerType q = p.Append(GetItemsString(), allocator_); if (v->IsObject()) // List validation - schemaDocument->CreateSchema(&itemsList_, q, *v, document); + schemaDocument->CreateSchema(&itemsList_, q, *v, document, id_); else if (v->IsArray()) { // Tuple validation itemsTuple_ = static_cast(allocator_->Malloc(sizeof(const Schema*) * v->Size())); SizeType index = 0; for (ConstValueIterator itr = v->Begin(); itr != v->End(); ++itr, index++) - schemaDocument->CreateSchema(&itemsTuple_[itemsTupleCount_++], q.Append(index, allocator_), *itr, document); + schemaDocument->CreateSchema(&itemsTuple_[itemsTupleCount_++], q.Append(index, allocator_), *itr, document, id_); } } @@ -637,7 +663,7 @@ public: if (v->IsBool()) additionalItems_ = v->GetBool(); else if (v->IsObject()) - schemaDocument->CreateSchema(&additionalItemsSchema_, p.Append(GetAdditionalItemsString(), allocator_), *v, document); + schemaDocument->CreateSchema(&additionalItemsSchema_, p.Append(GetAdditionalItemsString(), allocator_), *v, document, id_); } AssignIfExist(uniqueItems_, value, GetUniqueItemsString()); @@ -697,7 +723,11 @@ public: return uri_; } - const PointerType& GetPointer() const { + const UriType& GetId() const { + return id_; + } + + const PointerType& GetPointer() const { return pointer_; } @@ -826,7 +856,7 @@ public: } return CreateParallelValidator(context); } - + bool Bool(Context& context, bool) const { if (!(type_ & (1 << kBooleanSchemaType))) { DisallowedType(context, GetBooleanString()); @@ -870,13 +900,13 @@ public: if (!maximum_.IsNull() && !CheckDoubleMaximum(context, d)) return false; - + if (!multipleOf_.IsNull() && !CheckDoubleMultipleOf(context, d)) return false; - + return CreateParallelValidator(context); } - + bool String(Context& context, const Ch* str, SizeType length, bool) const { if (!(type_ & (1 << kStringSchemaType))) { DisallowedType(context, GetStringString()); @@ -925,7 +955,7 @@ public: return CreateParallelValidator(context); } - + bool Key(Context& context, const Ch* str, SizeType len, bool) const { if (patternProperties_) { context.patternPropertiesSchemaCount = 0; @@ -1018,7 +1048,7 @@ public: } } if (context.error_handler.EndDependencyErrors()) - RAPIDJSON_INVALID_KEYWORD_RETURN(kValidateErrorDependencies); + RAPIDJSON_INVALID_KEYWORD_RETURN(kValidateErrorDependencies); } return true; @@ -1038,12 +1068,12 @@ public: bool EndArray(Context& context, SizeType elementCount) const { context.inArray = false; - + if (elementCount < minItems_) { context.error_handler.TooFewItems(elementCount, minItems_); RAPIDJSON_INVALID_KEYWORD_RETURN(kValidateErrorMinItems); } - + if (elementCount > maxItems_) { context.error_handler.TooManyItems(elementCount, maxItems_); RAPIDJSON_INVALID_KEYWORD_RETURN(kValidateErrorMaxItems); @@ -1132,6 +1162,15 @@ 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_(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, '.') #undef RAPIDJSON_STRING_ @@ -1197,7 +1236,7 @@ private: out.schemas = static_cast(allocator_->Malloc(out.count * sizeof(const Schema*))); memset(out.schemas, 0, sizeof(Schema*)* out.count); for (SizeType i = 0; i < out.count; i++) - schemaDocument.CreateSchema(&out.schemas[i], q.Append(i, allocator_), (*v)[i], document); + schemaDocument.CreateSchema(&out.schemas[i], q.Append(i, allocator_), (*v)[i], document, id_); out.begin = validatorCount_; validatorCount_ += out.count; } @@ -1274,10 +1313,10 @@ private: if (anyOf_.schemas) CreateSchemaValidators(context, anyOf_, false); - + if (oneOf_.schemas) CreateSchemaValidators(context, oneOf_, false); - + if (not_) context.validators[notValidatorIndex_] = context.factory.CreateSchemaValidator(*not_, false); @@ -1301,7 +1340,7 @@ private: SizeType len = name.GetStringLength(); const Ch* str = name.GetString(); for (SizeType index = 0; index < propertyCount_; index++) - if (properties_[index].name.GetStringLength() == len && + if (properties_[index].name.GetStringLength() == len && (std::memcmp(properties_[index].name.GetString(), str, sizeof(Ch) * len) == 0)) { *outIndex = index; @@ -1462,7 +1501,7 @@ private: struct PatternProperty { PatternProperty() : schema(), pattern() {} - ~PatternProperty() { + ~PatternProperty() { if (pattern) { pattern->~RegexType(); AllocatorType::Free(pattern); @@ -1474,6 +1513,7 @@ private: AllocatorType* allocator_; SValue uri_; + UriType id_; PointerType pointer_; const SchemaType* typeless_; uint64_t* enum_; @@ -1516,7 +1556,7 @@ private: SValue multipleOf_; bool exclusiveMinimum_; bool exclusiveMaximum_; - + SizeType defaultValueLength_; }; @@ -1552,6 +1592,209 @@ struct TokenHelper { } // namespace internal +/////////////////////////////////////////////////////////////////////////////// +// Uri + +template +class Uri { +public: + typedef typename SchemaDocumentType::Ch Ch; + typedef typename SchemaDocumentType::AllocatorType AllocatorType; + typedef internal::Schema SchemaType; + typedef std::basic_string String; + + // Constructors + Uri() {} + + Uri(const String& uri) { + Parse(uri); + } + + Uri(const Ch* uri, SizeType len) { + Parse(String(uri, len)); + } + + // Use with specializations of GenericValue + template Uri(const T& uri) { + Parse(uri.template Get()); + } + + // Getters + const String& Get() { + // Create uri_ on-demand + if (uri_.empty()) uri_ = this->GetDoc() + frag_; + return uri_; } + + // Use with specializations of GenericValue + template void Get(T& uri, AllocatorType& allocator) { + uri.template Set(this->Get(), allocator); + } + + const String& GetDoc() { + // Create doc_ on-demand + if (doc_.empty()) doc_ = scheme_ + auth_ + path_ + query_; + return doc_; + } + const String& GetScheme() const { return scheme_; } + const String& GetAuth() const { return auth_; } + const String& GetPath() const { return path_; } + const String& GetQuery() const { return query_; } + const String& GetFrag() const { return frag_; } + + const Ch* GetString() { return this->Get().c_str(); } + SizeType GetStringLength() { return static_cast(this->Get().length()); } + + const Ch* GetDocString() { return this->GetDoc().c_str(); } + SizeType GetDocStringLength() { return static_cast(this->GetDoc().length()); } + + const Ch* GetFragString() const { return frag_.c_str(); } + SizeType GetFragStringLength() const { return static_cast(frag_.length()); } + + // Resolve this URI against a base URI in accordance with URI resolution rules at + // https://tools.ietf.org/html/rfc3986 + // Use for resolving an id or $ref with an in-scope id. + // This URI is updated in place where needed from the base URI. + Uri& Resolve(const Uri& base) + { + if (!scheme_.empty()) { + // Use all of this URI + RemoveDotSegments(path_); + } else { + if (!auth_.empty()) { + RemoveDotSegments(path_); + } else { + if (path_.empty()) { + path_ = base.GetPath(); + if (query_.empty()) { + query_ = base.GetQuery(); + } + } else { + static const String slash = SchemaType::GetSlashString().GetString(); + if (path_.find(slash) == 0) { + // Absolute path - replace all the path + RemoveDotSegments(path_); + } else { + // Relative path - append to path after last slash + String p; + if (!base.GetAuth().empty() && base.GetPath().empty()) p = slash; + std::size_t lastslashpos = base.GetPath().find_last_of(slash); + path_ = p + base.GetPath().substr(0, lastslashpos + 1) + path_; + RemoveDotSegments(path_); + } + } + auth_ = base.GetAuth(); + } + scheme_ = base.GetScheme(); + } + //std::cout << " Resolved uri: " << this->GetString() << std::endl; + return *this; + } + +private: + // Parse a URI into constituent scheme, authority, path, query, fragment + // Supports URIs that match regex ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))? as per + // https://tools.ietf.org/html/rfc3986 + void Parse(const String& uri) { + std::size_t start = 0, pos1 = 0, pos2 = 0; + const std::size_t len = uri.length(); + static const String schemeEnd = SchemaType::GetSchemeEndString().GetString(); + static const String authStart = SchemaType::GetAuthStartString().GetString(); + static const String pathStart = SchemaType::GetSlashString().GetString(); + static const String queryStart = SchemaType::GetQueryStartString().GetString(); + static const String fragStart = SchemaType::GetFragStartString().GetString(); + // Look for scheme ([^:/?#]+):)? + if (start < len) { + pos1 = uri.find(schemeEnd); + if (pos1 != std::string::npos) { + pos2 = uri.find_first_of(pathStart + queryStart + fragStart); + if (pos1 < pos2) { + pos1 += schemeEnd.length(); + scheme_ = uri.substr(start, pos1); + start = pos1; + } + } + } + // Look for auth (//([^/?#]*))? + if (start < len) { + pos1 = uri.find(authStart, start); + if (pos1 == start) { + pos2 = uri.find_first_of(pathStart + queryStart + fragStart, start + authStart.length()); + auth_ = uri.substr(start, pos2 - start); + start = pos2; + } + } + // Look for path ([^?#]*) + if (start < len) { + pos2 = uri.find_first_of(queryStart + fragStart, start); + if (start != pos2) { + path_ = uri.substr(start, pos2 - start); + if (path_.find(pathStart) == 0) { // absolute path - normalize + RemoveDotSegments(path_); + } + start = pos2; + } + } + // Look for query (\?([^#]*))? + if (start < len) { + pos2 = uri.find(fragStart, start); + if (start != pos2) { + query_ = uri.substr(start, pos2 - start); + start = pos2; + } + } + // Look for fragment (#(.*))? + if (start < len) { + frag_ = uri.substr(start); + } + //std::cout << " Parsed uri: " << "s: " << scheme_.c_str() << " a: " << auth_.c_str() << " p: " << path_.c_str() << " q: " << query_.c_str() << " f: " << frag_.c_str() << std::endl; + } + + // Remove . and .. segments from a path + // https://tools.ietf.org/html/rfc3986 + void RemoveDotSegments(String& path) { + String temp = path; + path.clear(); + static const String slash = SchemaType::GetSlashString().GetString(); + static const String dot = SchemaType::GetDotString().GetString(); + std::size_t pos = 0; + // Loop through each path segment + while (pos != std::string::npos) { + //std::cout << "Temp: '" << temp.c_str() << "' Path: '" << path.c_str() << "'" << std::endl; + pos = temp.find_first_of(slash); + // Get next segment + String seg = temp.substr(0, pos); + if (seg == dot) { + // Discard . segment + } else if (seg == dot + dot) { + // Backup a .. segment + // We expect to find a previously added slash at the end or nothing + std::size_t pos1 = path.find_last_of(slash); + // Make sure we don't go beyond the start + if (pos1 != std::string::npos && pos1 != 0) { + // Find the next to last slash and back up to it + pos1 = path.find_last_of(slash, pos1 - 1); + path = path.substr(0, pos1 + 1); + } + } else { + // Copy segment and add slash if not at end + path += seg; + if (pos != std::string::npos) path += slash; + } + // Move to next segment if not at end + if (pos != std::string::npos) temp = temp.substr(pos + 1); + } + //std::cout << "Final Temp: '" << temp.c_str() << "' Final Path: '" << path.c_str() << "'" << std::endl; + } + + String uri_; // Created on-demand + String doc_; // Created on-demand + String scheme_; // Includes the : + String auth_; // Includes the // + String path_; // Absolute if starts with / + String query_; // Includes the ? + String frag_; // Includes the # +}; + /////////////////////////////////////////////////////////////////////////////// // IGenericRemoteSchemaDocumentProvider @@ -1562,6 +1805,7 @@ public: virtual ~IGenericRemoteSchemaDocumentProvider() {} virtual const SchemaDocumentType* GetRemoteDocument(const Ch* uri, SizeType length) = 0; + virtual const SchemaDocumentType* GetRemoteDocument(Uri uri) { return GetRemoteDocument(uri.GetDocString(), uri.GetDocStringLength()); } }; /////////////////////////////////////////////////////////////////////////////// @@ -1586,7 +1830,8 @@ public: typedef typename EncodingType::Ch Ch; typedef internal::Schema SchemaType; typedef GenericPointer PointerType; - typedef GenericValue URIType; + typedef GenericValue SValue; + typedef Uri UriType; friend class internal::Schema; template friend class GenericSchemaValidator; @@ -1600,9 +1845,11 @@ public: \param uriLength Length of \c name, in code points. \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 */ explicit GenericSchemaDocument(const ValueType& document, const Ch* uri = 0, SizeType uriLength = 0, - IRemoteSchemaDocumentProviderType* remoteProvider = 0, Allocator* allocator = 0) : + IRemoteSchemaDocumentProviderType* remoteProvider = 0, Allocator* allocator = 0, + const PointerType& pointer = PointerType() : // PR #1393 remoteProvider_(remoteProvider), allocator_(allocator), ownAllocator_(), @@ -1616,30 +1863,20 @@ public: Ch noUri[1] = {0}; uri_.SetString(uri ? uri : noUri, uriLength, *allocator_); + UriType baseId(uri_); typeless_ = static_cast(allocator_->Malloc(sizeof(SchemaType))); - new (typeless_) SchemaType(this, PointerType(), ValueType(kObjectType).Move(), ValueType(kObjectType).Move(), allocator_); + new (typeless_) SchemaType(this, PointerType(), ValueType(kObjectType).Move(), ValueType(kObjectType).Move(), allocator_, baseId); // Generate root schema, it will call CreateSchema() to create sub-schemas, - // And call AddRefSchema() if there are $ref. - CreateSchemaRecursive(&root_, PointerType(), document, document); - - // Resolve $ref - while (!schemaRef_.Empty()) { - SchemaRefEntry* refEntry = schemaRef_.template Pop(1); - if (const SchemaType* s = GetSchema(refEntry->target)) { - if (refEntry->schema) - *refEntry->schema = s; - - // Create entry in map if not exist - if (!GetSchema(refEntry->source)) { - new (schemaMap_.template Push()) SchemaEntry(refEntry->source, const_cast(s), false, allocator_); - } - } - else if (refEntry->schema) - *refEntry->schema = typeless_; - - refEntry->~SchemaRefEntry(); + // And call HandleRefSchema() if there are $ref. + // PR #1393 use input pointer if supplied + root_ = typeless_; + if (pointer.GetTokenCount() == 0) { + CreateSchemaRecursive(&root_, pointer, document, document, baseId); + } + else if (const ValueType* v = pointer.Get(document)) { + CreateSchema(&root_, pointer, *v, document, baseId); } RAPIDJSON_ASSERT(root_ != 0); @@ -1679,7 +1916,7 @@ public: RAPIDJSON_DELETE(ownAllocator_); } - const URIType& GetURI() const { return uri_; } + const SValue& GetURI() const { return uri_; } //! Get the root schema. const SchemaType& GetRoot() const { return *root_; } @@ -1690,12 +1927,7 @@ private: //! Prohibit assignment GenericSchemaDocument& operator=(const GenericSchemaDocument&); - struct SchemaRefEntry { - SchemaRefEntry(const PointerType& s, const PointerType& t, const SchemaType** outSchema, Allocator *allocator) : source(s, allocator), target(t, allocator), schema(outSchema) {} - PointerType source; - PointerType target; - const SchemaType** schema; - }; + typedef const PointerType* SchemaRefPtr; // PR #1393 struct SchemaEntry { SchemaEntry(const PointerType& p, SchemaType* s, bool o, Allocator* allocator) : pointer(p, allocator), schema(s), owned(o) {} @@ -1710,79 +1942,153 @@ private: bool owned; }; - void CreateSchemaRecursive(const SchemaType** schema, const PointerType& pointer, const ValueType& v, const ValueType& document) { - if (schema) - *schema = typeless_; - + // 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) { - const SchemaType* s = GetSchema(pointer); - if (!s) - CreateSchema(schema, pointer, v, document); + CreateSchema(schema, pointer, v, document, id); for (typename ValueType::ConstMemberIterator itr = v.MemberBegin(); itr != v.MemberEnd(); ++itr) - CreateSchemaRecursive(0, pointer.Append(itr->name, allocator_), itr->value, document); + CreateSchemaRecursive(0, pointer.Append(itr->name, allocator_), itr->value, document, id); } else if (v.GetType() == kArrayType) for (SizeType i = 0; i < v.Size(); i++) - CreateSchemaRecursive(0, pointer.Append(i, allocator_), v[i], document); + CreateSchemaRecursive(0, pointer.Append(i, allocator_), v[i], document, id); } - void CreateSchema(const SchemaType** schema, const PointerType& pointer, const ValueType& v, const ValueType& document) { + // Changed by PR #1393 + void CreateSchema(const SchemaType** schema, const PointerType& pointer, const ValueType& v, const ValueType& document, const UriType& id) { RAPIDJSON_ASSERT(pointer.IsValid()); if (v.IsObject()) { - if (!HandleRefSchema(pointer, schema, v, document)) { - SchemaType* s = new (allocator_->Malloc(sizeof(SchemaType))) SchemaType(this, pointer, v, document, allocator_); - new (schemaMap_.template Push()) SchemaEntry(pointer, s, true, allocator_); + if (const SchemaType* sc = GetSchema(pointer)) { + if (schema) + *schema = sc; + //std::cout << "Using Schema with id " << sc->GetId().GetString() << std::endl; + AddSchemaRefs(const_cast(sc)); + } + else if (!HandleRefSchema(pointer, schema, v, document, id)) { + // The new schema adds itself and its $ref(s) to schemaMap_ + SchemaType* s = new (allocator_->Malloc(sizeof(SchemaType))) SchemaType(this, pointer, v, document, allocator_, id); if (schema) *schema = s; } } + else { + if (schema) + *schema = typeless_; + AddSchemaRefs(typeless_); + } } - bool HandleRefSchema(const PointerType& source, const SchemaType** schema, const ValueType& v, const ValueType& document) { - static const Ch kRefString[] = { '$', 'r', 'e', 'f', '\0' }; - static const ValueType kRefValue(kRefString, 4); - - typename ValueType::ConstMemberIterator itr = v.FindMember(kRefValue); + // Changed by PR #1393 + bool HandleRefSchema(const PointerType& source, const SchemaType** schema, const ValueType& v, const ValueType& document, const UriType& id) { + //std::cout << "HandleRefSchema called with id " << id.GetString() << std::endl; + typename ValueType::ConstMemberIterator itr = v.FindMember(SchemaType::GetRefString()); if (itr == v.MemberEnd()) return false; + // Resolve the source pointer to the $ref'ed schema (finally) + new (schemaRef_.template Push()) SchemaRefPtr(&source); + if (itr->value.IsString()) { SizeType len = itr->value.GetStringLength(); if (len > 0) { const Ch* s = itr->value.GetString(); - SizeType i = 0; - while (i < len && s[i] != '#') // Find the first # - i++; - - if (i > 0) { // Remote reference, resolve immediately + if (s[0] != '#') { // Remote reference - resolve $ref against the in-scope id if (remoteProvider_) { - if (const GenericSchemaDocument* remoteDocument = remoteProvider_->GetRemoteDocument(s, i)) { - PointerType pointer(&s[i], len - i, allocator_); + UriType ref = UriType(itr->value); + ref.Resolve(id); + //std::cout << "Resolved $ref '" << s << "' against in-scope id '" << id.GetString() << "' giving '" << ref.GetDocString() << "'" << std::endl; + if (const GenericSchemaDocument* remoteDocument = remoteProvider_->GetRemoteDocument(ref)) { + // Create a pointer from the # onwards + const PointerType pointer(ref.GetFragString(), ref.GetFragStringLength(), allocator_); if (pointer.IsValid()) { if (const SchemaType* sc = remoteDocument->GetSchema(pointer)) { if (schema) *schema = sc; - new (schemaMap_.template Push()) SchemaEntry(source, const_cast(sc), false, allocator_); + AddSchemaRefs(const_cast(sc)); return true; } } } } } - else if (s[i] == '#') { // Local reference, defer resolution - PointerType pointer(&s[i], len - i, allocator_); - if (pointer.IsValid()) { - if (const ValueType* nv = pointer.Get(document)) - if (HandleRefSchema(source, schema, *nv, document)) - return true; - - new (schemaRef_.template Push()) SchemaRefEntry(source, pointer, schema, allocator_); + else { // Local reference + if (len == 1 || s[1] == '/') { + // JSON pointer + const PointerType pointer(s, len, allocator_); + if (pointer.IsValid() && !IsCyclicRef(pointer)) { + if (const ValueType *nv = pointer.Get(document)) { + CreateSchema(schema, pointer, *nv, document, id); return true; + } } + } else { + // Internal reference to an id + const ValueType val(s, len); + PointerType pointer = PointerType(); + ValueType *nv = FindId(document, val, pointer); + if (nv && !IsCyclicRef(pointer)) { + CreateSchema(schema, pointer, *nv, document, id); + return true; + } + } } } } + + // Invalid/Unknown $ref + if (schema) + *schema = typeless_; + AddSchemaRefs(typeless_); + return true; + } + + //! Find the first subschema with 'id' string property matching the specified value. + // Return a pointer to the subschema and its JSON pointer. + ValueType* FindId(const ValueType& doc, const ValueType& findval, PointerType& resptr, const PointerType& here = PointerType()) const { + SizeType i = 0; + ValueType* resval = 0; + switch(doc.GetType()) { + case kObjectType: + for (typename ValueType::ConstMemberIterator m = doc.MemberBegin(); m != doc.MemberEnd(); ++m) { + if (m->name == SchemaType::GetIdString() && m->value.GetType() == kStringType && m->value == findval) { + // Found the 'id' with the value + resval = const_cast(&doc); + resptr = here; + } else if (m->value.GetType() == kObjectType || m->value.GetType() == kArrayType) { + resval = FindId(m->value, findval, resptr, here.Append(m->name.GetString(), m->name.GetStringLength(), allocator_)); + } + if (resval) break; + } + return resval; + case kArrayType: + for (typename ValueType::ConstValueIterator v = doc.Begin(); v != doc.End(); ++v) { + if (v->GetType() == kObjectType || v->GetType() == kArrayType) { + resval = FindId(*v, findval, resptr, here.Append(i, allocator_)); + } + if (resval) break; + i++; + } + return resval; + default: + return resval; + } + } + + // Added by PR #1393 + void AddSchemaRefs(SchemaType* schema) { + while (!schemaRef_.Empty()) { + SchemaRefPtr *ref = schemaRef_.template Pop(1); + SchemaEntry *entry = schemaMap_.template Push(); + new (entry) SchemaEntry(**ref, schema, false, allocator_); + } + } + + // Added by PR #1393 + bool IsCyclicRef(const PointerType& pointer) const { + for (const SchemaRefPtr* ref = schemaRef_.template Bottom(); ref != schemaRef_.template End(); ++ref) + if (pointer == **ref) + return true; return false; } @@ -1811,8 +2117,8 @@ private: const SchemaType* root_; //!< Root schema. SchemaType* typeless_; internal::Stack schemaMap_; // Stores created Pointer -> Schemas - internal::Stack schemaRef_; // Stores Pointer from $ref and schema which holds the $ref - URIType uri_; + internal::Stack schemaRef_; // Stores Pointer(s) from $ref(s) until resolved + SValue uri_; }; //! GenericSchemaDocument using Value type. diff --git a/test/unittest/schematest.cpp b/test/unittest/schematest.cpp index f381b4e..b3b9291 100644 --- a/test/unittest/schematest.cpp +++ b/test/unittest/schematest.cpp @@ -13,6 +13,7 @@ // specific language governing permissions and limitations under the License. #define RAPIDJSON_SCHEMA_VERBOSE 0 +#define RAPIDJSON_HAS_STDSTRING 1 #include "unittest.h" #include "rapidjson/schema.h" @@ -1811,6 +1812,189 @@ TEST(SchemaValidator, EscapedPointer) { "}}"); } +TEST(SchemaValidator, SchemaPointer) { + Document sd; + sd.Parse( + "{" + " \"swagger\": \"2.0\"," + " \"paths\": {" + " \"/some/path\": {" + " \"post\": {" + " \"parameters\": [" + " {" + " \"in\": \"body\"," + " \"name\": \"body\"," + " \"schema\": {" + " \"properties\": {" + " \"a\": {" + " \"$ref\": \"#/definitions/Prop_a\"" + " }," + " \"b\": {" + " \"type\": \"integer\"" + " }" + " }," + " \"type\": \"object\"" + " }" + " }" + " ]," + " \"responses\": {" + " \"200\": {" + " \"schema\": {" + " \"$ref\": \"#/definitions/Resp_200\"" + " }" + " }" + " }" + " }" + " }" + " }," + " \"definitions\": {" + " \"Prop_a\": {" + " \"properties\": {" + " \"c\": {" + " \"enum\": [" + " \"C1\"," + " \"C2\"," + " \"C3\"" + " ]," + " \"type\": \"string\"" + " }," + " \"d\": {" + " \"$ref\": \"#/definitions/Prop_d\"" + " }," + " \"s\": {" + " \"type\": \"string\"" + " }" + " }," + " \"required\": [\"c\"]," + " \"type\": \"object\"" + " }," + " \"Prop_d\": {" + " \"properties\": {" + " \"a\": {" + " \"$ref\": \"#/definitions/Prop_a\"" + " }," + " \"c\": {" + " \"$ref\": \"#/definitions/Prop_a/properties/c\"" + " }" + " }," + " \"type\": \"object\"" + " }," + " \"Resp_200\": {" + " \"properties\": {" + " \"e\": {" + " \"type\": \"string\"" + " }," + " \"f\": {" + " \"type\": \"boolean\"" + " }," + " \"cyclic_source\": {" + " \"$ref\": \"#/definitions/Resp_200/properties/cyclic_target\"" + " }," + " \"cyclic_target\": {" + " \"$ref\": \"#/definitions/Resp_200/properties/cyclic_source\"" + " }" + " }," + " \"type\": \"object\"" + " }" + " }" + "}"); + SchemaDocument s1(sd, NULL, 0, NULL, NULL, Pointer("#/paths/~1some~1path/post/parameters/0/schema")); + VALIDATE(s1, + "{" + " \"a\": {" + " \"c\": \"C1\"," + " \"d\": {" + " \"a\": {" + " \"c\": \"C2\"" + " }," + " \"c\": \"C3\"" + " }" + " }," + " \"b\": 123" + "}", + true); + INVALIDATE(s1, + "{" + " \"a\": {" + " \"c\": \"C1\"," + " \"d\": {" + " \"a\": {" + " \"c\": \"C2\"" + " }," + " \"c\": \"C3\"" + " }" + " }," + " \"b\": \"should be an int\"" + "}", + "#/paths/~1some~1path/post/parameters/0/schema/properties/b", "type", "#/b", + "{ \"type\": {" + " \"errorCode\": 20," + " \"instanceRef\":\"#/b\"," + " \"schemaRef\":\"#/paths/~1some~1path/post/parameters/0/schema/properties/b\"," + " \"expected\": [\"integer\"], \"actual\":\"string\"" + "}}"); + INVALIDATE(s1, + "{" + " \"a\": {" + " \"c\": \"C1\"," + " \"d\": {" + " \"a\": {" + " \"c\": \"should be within enum\"" + " }," + " \"c\": \"C3\"" + " }" + " }," + " \"b\": 123" + "}", + "#/definitions/Prop_a/properties/c", "enum", "#/a/d/a/c", + "{ \"enum\": {" + " \"errorCode\": 19," + " \"instanceRef\":\"#/a/d/a/c\"," + " \"schemaRef\":\"#/definitions/Prop_a/properties/c\"" + "}}"); + INVALIDATE(s1, + "{" + " \"a\": {" + " \"c\": \"C1\"," + " \"d\": {" + " \"a\": {" + " \"s\": \"required 'c' is missing\"" + " }" + " }" + " }," + " \"b\": 123" + "}", + "#/definitions/Prop_a", "required", "#/a/d/a", + "{ \"required\": {" + " \"errorCode\": 15," + " \"missing\":[\"c\"]," + " \"instanceRef\":\"#/a/d/a\"," + " \"schemaRef\":\"#/definitions/Prop_a\"" + "}}"); + SchemaDocument s2(sd, NULL, 0, NULL, NULL, Pointer("#/paths/~1some~1path/post/responses/200/schema")); + VALIDATE(s2, + "{ \"e\": \"some string\", \"f\": false }", + true); + INVALIDATE(s2, + "{ \"e\": true, \"f\": false }", + "#/definitions/Resp_200/properties/e", "type", "#/e", + "{ \"type\": {" + " \"errorCode\": 20," + " \"instanceRef\":\"#/e\"," + " \"schemaRef\":\"#/definitions/Resp_200/properties/e\"," + " \"expected\": [\"string\"], \"actual\":\"boolean\"" + "}}"); + INVALIDATE(s2, + "{ \"e\": \"some string\", \"f\": 123 }", + "#/definitions/Resp_200/properties/f", "type", "#/f", + "{ \"type\": {" + " \"errorCode\": 20," + " \"instanceRef\":\"#/f\"," + " \"schemaRef\":\"#/definitions/Resp_200/properties/f\"," + " \"expected\": [\"boolean\"], \"actual\":\"integer\"" + "}}"); +} + template static char* ReadFile(const char* filename, Allocator& allocator) { const char *paths[] = { @@ -1952,7 +2136,7 @@ public: virtual const SchemaDocumentType* GetRemoteDocument(const char* uri, SizeType length) { for (size_t i = 0; i < kCount; i++) - if (typename SchemaDocumentType::URIType(uri, length) == sd_[i]->GetURI()) + if (typename SchemaDocumentType::SType(uri, length) == sd_[i]->GetURI()) return sd_[i]; return 0; } @@ -2032,7 +2216,7 @@ TEST(SchemaValidator, TestSuite) { ADD_FAILURE(); } else { - //printf("json test suite file %s parsed ok\n", filename); + //printf("\njson test suite file %s parsed ok\n", filename); GenericDocument, MemoryPoolAllocator<>, MemoryPoolAllocator<> > d(&documentAllocator, 1024, &documentStackAllocator); d.Parse(json); if (d.HasParseError()) { @@ -2042,12 +2226,14 @@ TEST(SchemaValidator, TestSuite) { else { for (Value::ConstValueIterator schemaItr = d.Begin(); schemaItr != d.End(); ++schemaItr) { { + const char* description1 = (*schemaItr)["description"].GetString(); + //printf("\ncompiling schema for json test %s \n", description1); SchemaDocumentType schema((*schemaItr)["schema"], filenames[i], static_cast(strlen(filenames[i])), &provider, &schemaAllocator); GenericSchemaValidator >, MemoryPoolAllocator<> > validator(schema, &validatorAllocator); - const char* description1 = (*schemaItr)["description"].GetString(); const Value& tests = (*schemaItr)["tests"]; for (Value::ConstValueIterator testItr = tests.Begin(); testItr != tests.End(); ++testItr) { const char* description2 = (*testItr)["description"].GetString(); + //printf("running json test %s \n", description2); if (!onlyRunDescription || strcmp(description2, onlyRunDescription) == 0) { const Value& data = (*testItr)["data"]; bool expected = (*testItr)["valid"].GetBool(); @@ -2075,8 +2261,8 @@ TEST(SchemaValidator, TestSuite) { jsonAllocator.Clear(); } printf("%d / %d passed (%2d%%)\n", passCount, testCount, passCount * 100 / testCount); -// if (passCount != testCount) -// ADD_FAILURE(); + if (passCount != testCount) + ADD_FAILURE(); } TEST(SchemaValidatingReader, Simple) { @@ -2244,6 +2430,154 @@ TEST(SchemaValidator, Ref_remote) { kValidateDefaultFlags, SchemaValidatorType, PointerType); } +// Merge with id where $ref is full URI +TEST(SchemaValidator, Ref_remote_change_resolution_scope_uri) { + typedef GenericSchemaDocument > SchemaDocumentType; + RemoteSchemaDocumentProvider provider; + Document sd; + sd.Parse("{\"id\": \"http://ignore/blah#/ref\", \"type\": \"object\", \"properties\": {\"myInt\": {\"$ref\": \"http://localhost:1234/subSchemas.json#/integer\"}}}"); + SchemaDocumentType s(sd, 0, 0, &provider); + typedef GenericSchemaValidator >, MemoryPoolAllocator<> > SchemaValidatorType; + typedef GenericPointer > PointerType; + INVALIDATE_(s, "{\"myInt\": null}", "/integer", "type", "/myInt", + "{ \"type\": {" + " \"errorCode\": 20," + " \"instanceRef\": \"#/myInt\"," + " \"schemaRef\": \"http://localhost:1234/subSchemas.json#/integer\"," + " \"expected\": [\"integer\"], \"actual\": \"null\"" + "}}", + kValidateDefaultFlags, SchemaValidatorType, PointerType); +} + +// Merge with id where $ref is a relative path +TEST(SchemaValidator, Ref_remote_change_resolution_scope_relative_path) { + typedef GenericSchemaDocument > SchemaDocumentType; + RemoteSchemaDocumentProvider provider; + Document sd; + sd.Parse("{\"id\": \"http://localhost:1234/\", \"type\": \"object\", \"properties\": {\"myInt\": {\"$ref\": \"subSchemas.json#/integer\"}}}"); + SchemaDocumentType s(sd, 0, 0, &provider); + typedef GenericSchemaValidator >, MemoryPoolAllocator<> > SchemaValidatorType; + typedef GenericPointer > PointerType; + INVALIDATE_(s, "{\"myInt\": null}", "/integer", "type", "/myInt", + "{ \"type\": {" + " \"errorCode\": 20," + " \"instanceRef\": \"#/myInt\"," + " \"schemaRef\": \"http://localhost:1234/subSchemas.json#/integer\"," + " \"expected\": [\"integer\"], \"actual\": \"null\"" + "}}", + kValidateDefaultFlags, SchemaValidatorType, PointerType); +} + +// Merge with id where $ref is an absolute path +TEST(SchemaValidator, Ref_remote_change_resolution_scope_absolute_path) { + typedef GenericSchemaDocument > SchemaDocumentType; + RemoteSchemaDocumentProvider provider; + Document sd; + sd.Parse("{\"id\": \"http://localhost:1234/xxxx\", \"type\": \"object\", \"properties\": {\"myInt\": {\"$ref\": \"/subSchemas.json#/integer\"}}}"); + SchemaDocumentType s(sd, 0, 0, &provider); + typedef GenericSchemaValidator >, MemoryPoolAllocator<> > SchemaValidatorType; + typedef GenericPointer > PointerType; + INVALIDATE_(s, "{\"myInt\": null}", "/integer", "type", "/myInt", + "{ \"type\": {" + " \"errorCode\": 20," + " \"instanceRef\": \"#/myInt\"," + " \"schemaRef\": \"http://localhost:1234/subSchemas.json#/integer\"," + " \"expected\": [\"integer\"], \"actual\": \"null\"" + "}}", + kValidateDefaultFlags, SchemaValidatorType, PointerType); +} + +// Merge with id where $ref is an absolute path, and the document has a base URI +TEST(SchemaValidator, Ref_remote_change_resolution_scope_absolute_path_document) { + typedef GenericSchemaDocument > SchemaDocumentType; + RemoteSchemaDocumentProvider provider; + Document sd; + sd.Parse("{\"type\": \"object\", \"properties\": {\"myInt\": {\"$ref\": \"/subSchemas.json#/integer\"}}}"); + SchemaDocumentType s(sd, "http://localhost:1234/xxxx", 26, &provider); + typedef GenericSchemaValidator >, MemoryPoolAllocator<> > SchemaValidatorType; + typedef GenericPointer > PointerType; + INVALIDATE_(s, "{\"myInt\": null}", "/integer", "type", "/myInt", + "{ \"type\": {" + " \"errorCode\": 20," + " \"instanceRef\": \"#/myInt\"," + " \"schemaRef\": \"http://localhost:1234/subSchemas.json#/integer\"," + " \"expected\": [\"integer\"], \"actual\": \"null\"" + "}}", + kValidateDefaultFlags, SchemaValidatorType, PointerType); +} + +// $ref is a non-JSON pointer fragment and there a matching id +TEST(SchemaValidator, Ref_internal_id_1) { + typedef GenericSchemaDocument > SchemaDocumentType; + Document sd; + sd.Parse("{\"type\": \"object\", \"properties\": {\"myInt1\": {\"$ref\": \"#myId\"}, \"myStr\": {\"type\": \"string\", \"id\": \"#myStrId\"}, \"myInt2\": {\"type\": \"integer\", \"id\": \"#myId\"}}}"); + SchemaDocumentType s(sd); + typedef GenericSchemaValidator >, MemoryPoolAllocator<> > SchemaValidatorType; + typedef GenericPointer > PointerType; + INVALIDATE_(s, "{\"myInt1\": null}", "/properties/myInt2", "type", "/myInt1", + "{ \"type\": {" + " \"errorCode\": 20," + " \"instanceRef\": \"#/myInt1\"," + " \"schemaRef\": \"#/properties/myInt2\"," + " \"expected\": [\"integer\"], \"actual\": \"null\"" + "}}", + kValidateDefaultFlags, SchemaValidatorType, PointerType); +} + +// $ref is a non-JSON pointer fragment and there are two matching ids so we take the first +TEST(SchemaValidator, Ref_internal_id_2) { + typedef GenericSchemaDocument > SchemaDocumentType; + Document sd; + sd.Parse("{\"type\": \"object\", \"properties\": {\"myInt1\": {\"$ref\": \"#myId\"}, \"myInt2\": {\"type\": \"integer\", \"id\": \"#myId\"}, \"myStr\": {\"type\": \"string\", \"id\": \"#myId\"}}}"); + SchemaDocumentType s(sd); + typedef GenericSchemaValidator >, MemoryPoolAllocator<> > SchemaValidatorType; + typedef GenericPointer > PointerType; + INVALIDATE_(s, "{\"myInt1\": null}", "/properties/myInt2", "type", "/myInt1", + "{ \"type\": {" + " \"errorCode\": 20," + " \"instanceRef\": \"#/myInt1\"," + " \"schemaRef\": \"#/properties/myInt2\"," + " \"expected\": [\"integer\"], \"actual\": \"null\"" + "}}", + kValidateDefaultFlags, SchemaValidatorType, PointerType); +} + +// $ref is a non-JSON pointer fragment and there is a matching id within array +TEST(SchemaValidator, Ref_internal_id_in_array) { + typedef GenericSchemaDocument > SchemaDocumentType; + Document sd; + sd.Parse("{\"type\": \"object\", \"properties\": {\"myInt1\": {\"$ref\": \"#myId\"}, \"myInt2\": {\"anyOf\": [{\"type\": \"string\", \"id\": \"#myStrId\"}, {\"type\": \"integer\", \"id\": \"#myId\"}]}}}"); + SchemaDocumentType s(sd); + typedef GenericSchemaValidator >, MemoryPoolAllocator<> > SchemaValidatorType; + typedef GenericPointer > PointerType; + INVALIDATE_(s, "{\"myInt1\": null}", "/properties/myInt2/anyOf/1", "type", "/myInt1", + "{ \"type\": {" + " \"errorCode\": 20," + " \"instanceRef\": \"#/myInt1\"," + " \"schemaRef\": \"#/properties/myInt2/anyOf/1\"," + " \"expected\": [\"integer\"], \"actual\": \"null\"" + "}}", + kValidateDefaultFlags, SchemaValidatorType, PointerType); +} + +// $ref is a non-JSON pointer fragment and there is a matching id, and the schema is embedded in the document +TEST(SchemaValidator, Ref_internal_id_and_schema_pointer) { + typedef GenericSchemaDocument > SchemaDocumentType; + Document sd; + sd.Parse("{ \"schema\": {\"type\": \"object\", \"properties\": {\"myInt1\": {\"$ref\": \"#myId\"}, \"myInt2\": {\"anyOf\": [{\"type\": \"integer\", \"id\": \"#myId\"}]}}}}"); + typedef GenericPointer > PointerType; + SchemaDocumentType s(sd, 0, 0, 0, 0, PointerType("/schema")); + typedef GenericSchemaValidator >, MemoryPoolAllocator<> > SchemaValidatorType; + INVALIDATE_(s, "{\"myInt1\": null}", "/schema/properties/myInt2/anyOf/0", "type", "/myInt1", + "{ \"type\": {" + " \"errorCode\": 20," + " \"instanceRef\": \"#/myInt1\"," + " \"schemaRef\": \"#/schema/properties/myInt2/anyOf/0\"," + " \"expected\": [\"integer\"], \"actual\": \"null\"" + "}}", + kValidateDefaultFlags, SchemaValidatorType, PointerType); +} + TEST(SchemaValidator, Ref_remote_issue1210) { class SchemaDocumentProvider : public IRemoteSchemaDocumentProvider { SchemaDocument** collection; @@ -2260,7 +2594,7 @@ TEST(SchemaValidator, Ref_remote_issue1210) { SchemaDocumentProvider(SchemaDocument** collection) : collection(collection) { } virtual const SchemaDocument* GetRemoteDocument(const char* uri, SizeType length) { int i = 0; - while (collection[i] && SchemaDocument::URIType(uri, length) != collection[i]->GetURI()) ++i; + while (collection[i] && SchemaDocument::SValue(uri, length) != collection[i]->GetURI()) ++i; return collection[i]; } }; @@ -2582,6 +2916,250 @@ TEST(SchemaValidator, Schema_UnknownError) { ASSERT_TRUE(SchemaValidator::SchemaType::GetValidateErrorKeyword(kValidateErrors).GetString() == std::string("null")); } +TEST(SchemaValidator, Uri_Parse) { + typedef std::basic_string String; + typedef Uri >> Uri; + MemoryPoolAllocator allocator; + + String s = "http://auth/path?query#frag"; + Value v; + v.SetString(s, allocator); + Uri u = Uri(v); + EXPECT_TRUE(u.GetScheme() == "http:"); + EXPECT_TRUE(u.GetAuth() == "//auth"); + EXPECT_TRUE(u.GetPath() == "/path"); + EXPECT_TRUE(u.GetDoc() == "http://auth/path?query"); + EXPECT_TRUE(u.GetQuery() == "?query"); + EXPECT_TRUE(u.GetFrag() == "#frag"); + Value w; + u.Get(w, allocator); + EXPECT_TRUE(*w.GetString() == *v.GetString()); + + s = "urn:uuid:ee564b8a-7a87-4125-8c96-e9f123d6766f"; + v.SetString(s, allocator); + u = Uri(v); + EXPECT_TRUE(u.GetScheme() == "urn:"); + EXPECT_TRUE(u.GetAuth() == ""); + EXPECT_TRUE(u.GetPath() == "uuid:ee564b8a-7a87-4125-8c96-e9f123d6766f"); + EXPECT_TRUE(u.GetDoc() == "urn:uuid:ee564b8a-7a87-4125-8c96-e9f123d6766f"); + EXPECT_TRUE(u.GetQuery() == ""); + EXPECT_TRUE(u.GetFrag() == ""); + u.Get(w, allocator); + EXPECT_TRUE(*w.GetString() == *v.GetString()); + + s = ""; + v.SetString(s, allocator); + u = Uri(v); + EXPECT_TRUE(u.GetScheme() == ""); + EXPECT_TRUE(u.GetAuth() == ""); + EXPECT_TRUE(u.GetPath() == ""); + EXPECT_TRUE(u.GetDoc() == ""); + EXPECT_TRUE(u.GetQuery() == ""); + EXPECT_TRUE(u.GetFrag() == ""); + + s = "http://auth/"; + v.SetString(s, allocator); + u = Uri(v); + EXPECT_TRUE(u.GetScheme() == "http:"); + EXPECT_TRUE(u.GetAuth() == "//auth"); + EXPECT_TRUE(u.GetPath() == "/"); + EXPECT_TRUE(u.GetDoc() == "http://auth/"); + EXPECT_TRUE(u.GetQuery() == ""); + EXPECT_TRUE(u.GetFrag() == ""); + + s = "/path/sub"; + u = Uri(s); + EXPECT_TRUE(u.GetScheme() == ""); + EXPECT_TRUE(u.GetAuth() == ""); + EXPECT_TRUE(u.GetPath() == "/path/sub"); + EXPECT_TRUE(u.GetDoc() == "/path/sub"); + EXPECT_TRUE(u.GetQuery() == ""); + EXPECT_TRUE(u.GetFrag() == ""); + + // absolute path gets normalized + s = "/path/../sub/"; + u = Uri(s); + EXPECT_TRUE(u.GetScheme() == ""); + EXPECT_TRUE(u.GetAuth() == ""); + EXPECT_TRUE(u.GetPath() == "/sub/"); + EXPECT_TRUE(u.GetDoc() == "/sub/"); + EXPECT_TRUE(u.GetQuery() == ""); + EXPECT_TRUE(u.GetFrag() == ""); + + // relative path does not + s = "path/../sub"; + u = Uri(s); + EXPECT_TRUE(u.GetScheme() == ""); + EXPECT_TRUE(u.GetAuth() == ""); + EXPECT_TRUE(u.GetPath() == "path/../sub"); + EXPECT_TRUE(u.GetDoc() == "path/../sub"); + EXPECT_TRUE(u.GetQuery() == ""); + EXPECT_TRUE(u.GetFrag() == ""); + + s = "http://auth#frag/stuff"; + u = Uri(s); + EXPECT_TRUE(u.GetScheme() == "http:"); + EXPECT_TRUE(u.GetAuth() == "//auth"); + EXPECT_TRUE(u.GetPath() == ""); + EXPECT_TRUE(u.GetDoc() == "http://auth"); + EXPECT_TRUE(u.GetQuery() == ""); + EXPECT_TRUE(u.GetFrag() == "#frag/stuff"); + EXPECT_TRUE(u.Get() == s); + + s = "#frag/stuff"; + u = Uri(s); + EXPECT_TRUE(u.GetScheme() == ""); + EXPECT_TRUE(u.GetAuth() == ""); + EXPECT_TRUE(u.GetPath() == ""); + EXPECT_TRUE(u.GetDoc() == ""); + EXPECT_TRUE(u.GetQuery() == ""); + EXPECT_TRUE(u.GetFrag() == "#frag/stuff"); + EXPECT_TRUE(u.Get() == s); + + Value::Ch c[] = { '#', 'f', 'r', 'a', 'g', '/', 's', 't', 'u', 'f', 'f', '\0'}; + u = Uri(c, 11); + EXPECT_TRUE(String(u.GetString()) == "#frag/stuff"); + EXPECT_TRUE(u.GetStringLength() == 11); + EXPECT_TRUE(String(u.GetDocString()) == ""); + EXPECT_TRUE(u.GetDocStringLength() == 0); + EXPECT_TRUE(String(u.GetFragString()) == "#frag/stuff"); + EXPECT_TRUE(u.GetFragStringLength() == 11); +} + +TEST(SchemaValidator, Uri_Resolve) { + typedef std::basic_string String; + typedef Uri >> Uri; + + // ref is full uri + Uri base = Uri(String("http://auth/path/#frag")); + Uri ref = Uri(String("http://newauth/newpath#newfrag")); + EXPECT_TRUE(ref.Resolve(base).Get() == "http://newauth/newpath#newfrag"); + + base = Uri(String("/path/#frag")); + ref = Uri(String("http://newauth/newpath#newfrag")); + EXPECT_TRUE(ref.Resolve(base).Get() == "http://newauth/newpath#newfrag"); + + // ref is alternate uri + base = Uri(String("http://auth/path/#frag")); + ref = Uri(String("urn:uuid:ee564b8a-7a87-4125-8c96-e9f123d6766f")); + EXPECT_TRUE(ref.Resolve(base).Get() == "urn:uuid:ee564b8a-7a87-4125-8c96-e9f123d6766f"); + + // ref is absolute path + base = Uri(String("http://auth/path/#")); + ref = Uri(String("/newpath#newfrag")); + EXPECT_TRUE(ref.Resolve(base).Get() == "http://auth/newpath#newfrag"); + + // ref is relative path + base = Uri(String("http://auth/path/file.json#frag")); + ref = Uri(String("newfile.json#")); + EXPECT_TRUE(ref.Resolve(base).Get() == "http://auth/path/newfile.json#"); + + base = Uri(String("http://auth/path/file.json#frag/stuff")); + ref = Uri(String("newfile.json#newfrag/newstuff")); + EXPECT_TRUE(ref.Resolve(base).Get() == "http://auth/path/newfile.json#newfrag/newstuff"); + + base = Uri(String("file.json")); + ref = Uri(String("newfile.json")); + EXPECT_TRUE(ref.Resolve(base).Get() == "newfile.json"); + + base = Uri(String("file.json")); + ref = Uri(String("./newfile.json")); + EXPECT_TRUE(ref.Resolve(base).Get() == "newfile.json"); + + base = Uri(String("file.json")); + ref = Uri(String("parent/../newfile.json")); + EXPECT_TRUE(ref.Resolve(base).Get() == "newfile.json"); + + base = Uri(String("file.json")); + ref = Uri(String("parent/./newfile.json")); + EXPECT_TRUE(ref.Resolve(base).Get() == "parent/newfile.json"); + + base = Uri(String("file.json")); + ref = Uri(String("../../parent/.././newfile.json")); + EXPECT_TRUE(ref.Resolve(base).Get() == "newfile.json"); + + base = Uri(String("http://auth")); + ref = Uri(String("newfile.json")); + EXPECT_TRUE(ref.Resolve(base).Get() == "http://auth/newfile.json"); + + // ref is fragment + base = Uri(String("#frag/stuff")); + ref = Uri(String("#newfrag/newstuff")); + EXPECT_TRUE(ref.Resolve(base).Get() == "#newfrag/newstuff"); + + // test ref fragment always wins + base = Uri(String("/path#frag")); + ref = Uri(String("")); + EXPECT_TRUE(ref.Resolve(base).Get() == "/path"); + + // Examples from RFC3896 + base = Uri(String("http://a/b/c/d;p?q")); + ref = Uri(String("g:h")); + EXPECT_TRUE(ref.Resolve(base).Get() == "g:h"); + ref = Uri(String("g")); + EXPECT_TRUE(ref.Resolve(base).Get() == "http://a/b/c/g"); + ref = Uri(String("./g")); + EXPECT_TRUE(ref.Resolve(base).Get() == "http://a/b/c/g"); + ref = Uri(String("g/")); + EXPECT_TRUE(ref.Resolve(base).Get() == "http://a/b/c/g/"); + ref = Uri(String("/g")); + EXPECT_TRUE(ref.Resolve(base).Get() == "http://a/g"); + ref = Uri(String("//g")); + EXPECT_TRUE(ref.Resolve(base).Get() == "http://g"); + ref = Uri(String("?y")); + EXPECT_TRUE(ref.Resolve(base).Get() == "http://a/b/c/d;p?y"); + ref = Uri(String("g?y")); + EXPECT_TRUE(ref.Resolve(base).Get() == "http://a/b/c/g?y"); + ref = Uri(String("#s")); + EXPECT_TRUE(ref.Resolve(base).Get() == "http://a/b/c/d;p?q#s"); + ref = Uri(String("g#s")); + EXPECT_TRUE(ref.Resolve(base).Get() == "http://a/b/c/g#s"); + ref = Uri(String("g?y#s")); + EXPECT_TRUE(ref.Resolve(base).Get() == "http://a/b/c/g?y#s"); + ref = Uri(String(";x")); + EXPECT_TRUE(ref.Resolve(base).Get() == "http://a/b/c/;x"); + ref = Uri(String("g;x")); + EXPECT_TRUE(ref.Resolve(base).Get() == "http://a/b/c/g;x"); + ref = Uri(String("g;x?y#s")); + EXPECT_TRUE(ref.Resolve(base).Get() == "http://a/b/c/g;x?y#s"); + ref = Uri(String("")); + EXPECT_TRUE(ref.Resolve(base).Get() == "http://a/b/c/d;p?q"); + ref = Uri(String(".")); + EXPECT_TRUE(ref.Resolve(base).Get() == "http://a/b/c/"); + ref = Uri(String("./")); + EXPECT_TRUE(ref.Resolve(base).Get() == "http://a/b/c/"); + ref = Uri(String("..")); + EXPECT_TRUE(ref.Resolve(base).Get() == "http://a/b/"); + ref = Uri(String("../")); + EXPECT_TRUE(ref.Resolve(base).Get() == "http://a/b/"); + ref = Uri(String("../g")); + EXPECT_TRUE(ref.Resolve(base).Get() == "http://a/b/g"); + ref = Uri(String("../..")); + EXPECT_TRUE(ref.Resolve(base).Get() == "http://a/"); + ref = Uri(String("../../")); + EXPECT_TRUE(ref.Resolve(base).Get() == "http://a/"); + ref = Uri(String("../../g")); + EXPECT_TRUE(ref.Resolve(base).Get() == "http://a/g"); + ref = Uri(String("../../../g")); + EXPECT_TRUE(ref.Resolve(base).Get() == "http://a/g"); + ref = Uri(String("../../../../g")); + EXPECT_TRUE(ref.Resolve(base).Get() == "http://a/g"); + ref = Uri(String("/./g")); + EXPECT_TRUE(ref.Resolve(base).Get() == "http://a/g"); + ref = Uri(String("/../g")); + EXPECT_TRUE(ref.Resolve(base).Get() == "http://a/g"); + ref = Uri(String("g.")); + EXPECT_TRUE(ref.Resolve(base).Get() == "http://a/b/c/g."); + ref = Uri(String(".g")); + EXPECT_TRUE(ref.Resolve(base).Get() == "http://a/b/c/.g"); + ref = Uri(String("g..")); + EXPECT_TRUE(ref.Resolve(base).Get() == "http://a/b/c/g.."); + ref = Uri(String("..g")); + EXPECT_TRUE(ref.Resolve(base).Get() == "http://a/b/c/..g"); + ref = Uri(String("g#s/../x")); + EXPECT_TRUE(ref.Resolve(base).Get() == "http://a/b/c/g#s/../x"); +} + #if defined(_MSC_VER) || defined(__clang__) RAPIDJSON_DIAG_POP #endif