diff --git a/include/valijson/internal/json_reference.hpp b/include/valijson/internal/json_reference.hpp new file mode 100644 index 0000000..c182188 --- /dev/null +++ b/include/valijson/internal/json_reference.hpp @@ -0,0 +1,111 @@ +#ifndef __VALIJSON_JSON_REFERENCE_HPP +#define __VALIJSON_JSON_REFERENCE_HPP + +#include +#include + +#include +#include + +#include + +namespace valijson { +namespace internal { +namespace json_reference { + +/** + * @brief Extract JSON Pointer portion of a JSON Reference + * + * @param jsonRef JSON Reference to extract from + * + * @return string containing JSON Pointer + * + * @throw std::runtime_error if the string does not contain a JSON Pointer + */ +inline std::string getJsonReferencePointer(const std::string &jsonRef) +{ + // Attempt to extract JSON Pointer if '#' character is present. Note + // that a valid pointer would contain at least a leading forward + // slash character. + const size_t ptrPos = jsonRef.find("#"); + if (ptrPos != std::string::npos) { + return jsonRef.substr(ptrPos + 1); + } + + throw std::runtime_error( + "JSON Reference value does not contain a valid JSON Pointer"); +} + +/** + * @brief Return reference to part of document referenced by JSON Pointer + * + * @param node node to use as root for JSON Pointer resolution + * @param jsonPointer string containing JSON Pointer + * + * @return reference to an instance AdapterType in the specified document + */ +template +inline AdapterType resolveJsonPointer(const AdapterType &node, + std::string jsonPointer) +{ + // TODO: This function will probably need to implement support for + // fetching documents referenced by JSON Pointers, similar to the + // populateSchema function. + + // Check for leading forward slash + if (jsonPointer.find("/") != 0) { + throw std::runtime_error( + "JSON Pointer must begin with reference to root node"); + } + + // Remove leading slash + jsonPointer = jsonPointer.substr(1); + + // Recursion bottoms out here + if (jsonPointer.empty()) { + return node; + } + + // Extract next directive + const std::string directive = jsonPointer.substr(1, jsonPointer.find("/")); + if (directive.empty()) { + throw std::runtime_error( + "JSON Pointer contains zero-length directive"); + } + + // Remove directive from remainder of JSON pointer + jsonPointer = jsonPointer.substr(directive.length()); + + if (node.isArray()) { + try { + // Fragment must be non-negative integer + const uint64_t index = boost::lexical_cast(jsonPointer); + typedef typename AdapterType::Array Array; + typename Array::const_iterator itr = node.asArray().begin(); + itr.advance(index); + // TODO: Check for array bounds + return resolveJsonPointer(*itr, jsonPointer); + + } catch (boost::bad_lexical_cast &) { + throw std::runtime_error("Invalid array index in JSON Reference: " + + directive); + } + + } else if (node.maybeObject()) { + typedef typename AdapterType::Object Object; + typename Object::const_iterator itr = node.asObject().find(directive); + if (itr == node.asObject().end()) { + throw std::runtime_error("Could not find element"); + } + return resolveJsonPointer(itr->second, jsonPointer); + + } + + throw std::runtime_error("Directive applied to simple value type"); +} + +} // namespace json_reference +} // namespace internal +} // namespace valijson + +#endif diff --git a/include/valijson/schema_parser.hpp b/include/valijson/schema_parser.hpp index 2a4c79d..d9ccea7 100644 --- a/include/valijson/schema_parser.hpp +++ b/include/valijson/schema_parser.hpp @@ -11,6 +11,7 @@ #include #include +#include #include namespace valijson { @@ -275,44 +276,16 @@ private: * * @return Optional string containing URI */ - static boost::optional getJsonReferenceUri( + inline boost::optional getJsonReferenceUri( const std::string &jsonRef, const Schema &schema) { - return schema.resolveUri(jsonRef); - } - - /** - * @brief Extract JSON Pointer portion of a JSON Reference - * - * @param jsonRef JSON Reference to extract from - * - * @return Optional string containing JSON Pointer - */ - static boost::optional getJsonReferencePointer( - const std::string &jsonRef) - { - return std::string(); - } - - /** - * @brief Return reference to part of document referenced by JSON Pointer - * - * @param node node to use as root for JSON Pointer resolution - * @param jsonPointer string containing JSON Pointer - * - * @return reference to an instance AdapterType in the specified document - */ - template - const AdapterType & resolveJsonPointer( - const AdapterType &node, - const std::string &jsonPointer) - { - // TODO: Complete functionality - // TODO: This function will probably need to implement support for - // fetching documents referenced by JSON Pointers, similar to the - // populateSchema function. - return node; + const size_t ptrPos = jsonRef.find("#"); + if (ptrPos != std::string::npos) { + return schema.resolveUri(jsonRef.substr(0, ptrPos)); + } else { + return schema.resolveUri(jsonRef); + } } /** @@ -346,12 +319,8 @@ private: getJsonReferenceUri(jsonRef, schema); // Extract JSON Pointer from JSON Reference - const boost::optional jsonPointer = - getJsonReferencePointer(jsonRef); - if (!jsonPointer) { - throw std::runtime_error( - "Failed to parse JSON pointer"); - } + const std::string jsonPointer = + internal::json_reference::getJsonReferencePointer(jsonRef); if (documentUri) { // Resolve reference against remote document @@ -372,14 +341,20 @@ private: "Failed to fetch referenced schema document."); } + const AdapterType &ref = + internal::json_reference::resolveJsonPointer(*docPtr, + jsonPointer); + // Resolve reference against retrieved document - const AdapterType &ref = resolveJsonPointer(*docPtr, *jsonPointer); populateSchema(ref, schema, fetchDoc, parentSchema, ownName); } else { + const AdapterType &ref = + internal::json_reference::resolveJsonPointer(node, + jsonPointer); + // Resolve reference against current document - const AdapterType &ref = resolveJsonPointer(node, *jsonPointer); populateSchema(ref, schema, fetchDoc, parentSchema, ownName); } @@ -990,7 +965,8 @@ private: const AdapterType *properties, const AdapterType *patternProperties, const AdapterType *additionalProperties, - boost::optional::Type > fetchDoc, + boost::optional::Type > + fetchDoc, Schema *parentSchema) { typedef typename AdapterType::ObjectMember Member; diff --git a/tests/test_fetch_document_callback.cpp b/tests/test_fetch_document_callback.cpp index c8bb519..e6b01ca 100644 --- a/tests/test_fetch_document_callback.cpp +++ b/tests/test_fetch_document_callback.cpp @@ -31,9 +31,7 @@ class TestFetchDocumentCallback : public ::testing::Test boost::shared_ptr fetchDocument(const std::string &uri) { - if (uri.compare("test") != 0) { - throw std::runtime_error("Could not resolve reference"); - } + EXPECT_STREQ("test", uri.c_str()); rapidjson::Value valueOfTypeAttribute; valueOfTypeAttribute.SetString("string", allocator); @@ -60,7 +58,7 @@ TEST_F(TestFetchDocumentCallback, Basics) rapidjson::Document schemaDocument; RapidJsonAdapter schemaDocumentAdapter(schemaDocument); schemaDocument.SetObject(); - schemaDocument.AddMember("$ref", "test", allocator); + schemaDocument.AddMember("$ref", "test#/", allocator); // Parse schema document Schema schema; diff --git a/tests/test_json_reference.cpp b/tests/test_json_reference.cpp new file mode 100644 index 0000000..f4ca68d --- /dev/null +++ b/tests/test_json_reference.cpp @@ -0,0 +1,46 @@ +#include + +#include + +#include + +using valijson::adapters::RapidJsonAdapter; +using valijson::internal::json_reference::resolveJsonPointer; + +namespace { + rapidjson::MemoryPoolAllocator allocator; +} + +class TestJsonReference : public testing::Test +{ + +}; + +TEST_F(TestJsonReference, PointerWithoutLeadingSlashShouldThrow) +{ + // Given an Adapter for a JSON object + rapidjson::Value value; + value.SetObject(); + const RapidJsonAdapter rootAdapter(value); + + // When a JSON Pointer that does not begin with a slash is resolved + // Then an exception should be thrown + EXPECT_THROW(resolveJsonPointer(rootAdapter, "#"), std::runtime_error); +} + +TEST_F(TestJsonReference, RootPointer) +{ + // Given an Adapter for a JSON object containing one attribute + rapidjson::Value value; + value.SetObject(); + value.AddMember("test", "test", allocator); + const RapidJsonAdapter rootAdapter(value); + + // When a JSON Pointer that points to the root node is resolved + const RapidJsonAdapter resultAdapter = resolveJsonPointer(rootAdapter, "/"); + + // Then the returned Adapter should also refer to the root node + EXPECT_TRUE(resultAdapter.isObject()); + const RapidJsonAdapter::Object object = resultAdapter.asObject(); + EXPECT_NE(object.end(), object.find("test")); +} diff --git a/xcode/valijson.xcodeproj/project.pbxproj b/xcode/valijson.xcodeproj/project.pbxproj index ddf7f54..66f7bbc 100644 --- a/xcode/valijson.xcodeproj/project.pbxproj +++ b/xcode/valijson.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 6A477F8517D6BCBB0013571C /* libboost_regex-mt.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 6A477F8417D6BCBB0013571C /* libboost_regex-mt.dylib */; }; 6A477F8617D6EA000013571C /* libboost_regex-mt.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 6A477F8417D6BCBB0013571C /* libboost_regex-mt.dylib */; }; + 6A506D201AF88E5D00C2C818 /* test_json_reference.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 6A506D1F1AF88E5D00C2C818 /* test_json_reference.cpp */; }; 6A725F4517F61D7000D6B2FF /* test_validation_errors.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 6A725F4317F61B5100D6B2FF /* test_validation_errors.cpp */; }; 6A725F4617F6404100D6B2FF /* test_adapter_comparison.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 6AB8FEB117E6DF9A0028E147 /* test_adapter_comparison.cpp */; }; 6A725F4717F6404100D6B2FF /* test_jsoncpp_adapter.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 6AB8FE9217E6BE770028E147 /* test_jsoncpp_adapter.cpp */; }; @@ -40,6 +41,18 @@ /* Begin PBXFileReference section */ 6A477F8417D6BCBB0013571C /* libboost_regex-mt.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = "libboost_regex-mt.dylib"; path = "/usr/local/lib/libboost_regex-mt.dylib"; sourceTree = ""; }; + 6A506D121AF884E100C2C818 /* draft-03.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "draft-03.json"; sourceTree = ""; }; + 6A506D131AF884E100C2C818 /* draft-04.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "draft-04.json"; sourceTree = ""; }; + 6A506D151AF884E100C2C818 /* draft-fge-json-schema-validation-00.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = "draft-fge-json-schema-validation-00.txt"; sourceTree = ""; }; + 6A506D161AF884E100C2C818 /* draft-luff-json-hyper-schema-00.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = "draft-luff-json-hyper-schema-00.txt"; sourceTree = ""; }; + 6A506D171AF884E100C2C818 /* draft-pbryan-zyp-json-ref-03.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = "draft-pbryan-zyp-json-ref-03.txt"; sourceTree = ""; }; + 6A506D181AF884E100C2C818 /* draft-zyp-json-schema-03.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = "draft-zyp-json-schema-03.txt"; sourceTree = ""; }; + 6A506D191AF884E100C2C818 /* draft-zyp-json-schema-04.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = "draft-zyp-json-schema-04.txt"; sourceTree = ""; }; + 6A506D1A1AF884E100C2C818 /* rfc3986-uri.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = "rfc3986-uri.txt"; sourceTree = ""; }; + 6A506D1B1AF884E100C2C818 /* rfc4627-json.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = "rfc4627-json.txt"; sourceTree = ""; }; + 6A506D1C1AF884E100C2C818 /* rfc6901-json-pointer.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = "rfc6901-json-pointer.txt"; sourceTree = ""; }; + 6A506D1E1AF88D8700C2C818 /* json_reference.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; name = json_reference.hpp; path = internal/json_reference.hpp; sourceTree = ""; }; + 6A506D1F1AF88E5D00C2C818 /* test_json_reference.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = test_json_reference.cpp; sourceTree = ""; }; 6A725F3617F61A4400D6B2FF /* array_doubles_10_20_30_40.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = array_doubles_10_20_30_40.json; sourceTree = ""; }; 6A725F3717F61A4400D6B2FF /* array_doubles_1_2_3.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = array_doubles_1_2_3.json; sourceTree = ""; }; 6A725F3817F61A4400D6B2FF /* array_doubles_1_2_3_4.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = array_doubles_1_2_3_4.json; sourceTree = ""; }; @@ -273,6 +286,49 @@ name = Frameworks; sourceTree = ""; }; + 6A506D101AF884D700C2C818 /* doc */ = { + isa = PBXGroup; + children = ( + 6A506D111AF884E100C2C818 /* schema */, + 6A506D141AF884E100C2C818 /* specifications */, + ); + name = doc; + sourceTree = ""; + }; + 6A506D111AF884E100C2C818 /* schema */ = { + isa = PBXGroup; + children = ( + 6A506D121AF884E100C2C818 /* draft-03.json */, + 6A506D131AF884E100C2C818 /* draft-04.json */, + ); + name = schema; + path = ../doc/schema; + sourceTree = ""; + }; + 6A506D141AF884E100C2C818 /* specifications */ = { + isa = PBXGroup; + children = ( + 6A506D151AF884E100C2C818 /* draft-fge-json-schema-validation-00.txt */, + 6A506D161AF884E100C2C818 /* draft-luff-json-hyper-schema-00.txt */, + 6A506D171AF884E100C2C818 /* draft-pbryan-zyp-json-ref-03.txt */, + 6A506D181AF884E100C2C818 /* draft-zyp-json-schema-03.txt */, + 6A506D191AF884E100C2C818 /* draft-zyp-json-schema-04.txt */, + 6A506D1A1AF884E100C2C818 /* rfc3986-uri.txt */, + 6A506D1B1AF884E100C2C818 /* rfc4627-json.txt */, + 6A506D1C1AF884E100C2C818 /* rfc6901-json-pointer.txt */, + ); + name = specifications; + path = ../doc/specifications; + sourceTree = ""; + }; + 6A506D1D1AF88D5E00C2C818 /* internal */ = { + isa = PBXGroup; + children = ( + 6A506D1E1AF88D8700C2C818 /* json_reference.hpp */, + ); + name = internal; + sourceTree = ""; + }; 6A725F3517F61A4400D6B2FF /* documents */ = { isa = PBXGroup; children = ( @@ -612,6 +668,7 @@ 6AB8FEB417E6E53D0028E147 /* data */, 6AB8FEB117E6DF9A0028E147 /* test_adapter_comparison.cpp */, 6AA8A5DA17F8BDCA002728A0 /* test_fetch_document_callback.cpp */, + 6A506D1F1AF88E5D00C2C818 /* test_json_reference.cpp */, 6AB8FE9217E6BE770028E147 /* test_jsoncpp_adapter.cpp */, 6AB8FEC417E92B100028E147 /* test_property_tree_adapter.cpp */, 6AC18D3917CC874100FE0EC9 /* test_rapidjson_adapter.cpp */, @@ -626,6 +683,7 @@ 6AC78AC417C5FBBC00674114 = { isa = PBXGroup; children = ( + 6A506D101AF884D700C2C818 /* doc */, 6AC78AEC17C5FC0700674114 /* examples */, 6AC78BDC17C5FC5F00674114 /* include */, 6AC18D3217CC869D00FE0EC9 /* tests */, @@ -670,6 +728,7 @@ children = ( 6AC78BDE17C5FC6A00674114 /* adapters */, 6AC78BE317C5FC6A00674114 /* constraints */, + 6A506D1D1AF88D5E00C2C818 /* internal */, 6A869A2F17CD8A81006864FA /* utils */, 6AC78BE917C5FC6A00674114 /* schema.hpp */, 6AC78BEA17C5FC6A00674114 /* schema_parser.hpp */, @@ -910,6 +969,7 @@ 6A725F4717F6404100D6B2FF /* test_jsoncpp_adapter.cpp in Sources */, 6A725F4817F6404100D6B2FF /* test_property_tree_adapter.cpp in Sources */, 6AD3490118FF56FB004BDEE7 /* gtest_main.cc in Sources */, + 6A506D201AF88E5D00C2C818 /* test_json_reference.cpp in Sources */, 6A725F4917F6404100D6B2FF /* test_rapidjson_adapter.cpp in Sources */, 6A725F4A17F6404100D6B2FF /* test_validator.cpp in Sources */, 6A725F4D17F8964B00D6B2FF /* test_uri_resolution.cpp in Sources */,