#ifndef STAR_JSON_PATH_HPP #define STAR_JSON_PATH_HPP #include "StarLexicalCast.hpp" #include "StarJson.hpp" namespace Star { namespace JsonPath { enum class TypeHint { Array, Object }; typedef function PathParser; STAR_EXCEPTION(ParsingException, JsonException); STAR_EXCEPTION(TraversalException, JsonException); // Parses RFC 6901 JSON Pointers, e.g. /foo/bar/4/baz TypeHint parsePointer(String& outputBuffer, String const& path, String::const_iterator& iterator, String::const_iterator end); // Parses JavaScript-like paths, e.g. foo.bar[4].baz TypeHint parseQueryPath(String& outputBuffer, String const& path, String::const_iterator& iterator, String::const_iterator end); // Retrieves the portion of the Json document referred to by the given path. template Jsonlike pathGet(Jsonlike base, PathParser parser, String const& path); // Find a given portion of the JSON document, if it exists. Instead of // throwing a TraversalException if a portion of the path is invalid, simply // returns nothing. template Maybe pathFind(Jsonlike base, PathParser parser, String const& path); template using JsonOp = function const&)>; // Applies a function to the portion of the Json document referred to by the // given path, returning the resulting new document. If the end of the path // doesn't exist, the JsonOp is called with None, and its result will be // inserted into the document. If the path already existed and the JsonOp // returns None, it is erased. This is not as well-optimized as pathGet, but // also not on the critical path for anything. template Jsonlike pathApply(Jsonlike const& base, PathParser parser, String const& path, JsonOp op); // Sets a value on a Json document at the location referred to by path, // returning the resulting new document. template Jsonlike pathSet(Jsonlike const& base, PathParser parser, String const& path, Jsonlike const& value); // Erases the location referred to by the path from the document template Jsonlike pathRemove(Jsonlike const& base, PathParser parser, String const& path); // Performs RFC6902 (JSON Patching) add operation. Inserts into arrays, or // appends if the last path segment is "-". On objects, does the same as // pathSet. template Jsonlike pathAdd(Jsonlike const& base, PathParser parser, String const& path, Jsonlike const& value); template using EmptyPathOp = function; template using ObjectOp = function; template using ArrayOp = function)>; template JsonOp genericObjectArrayOp(String path, EmptyPathOp emptyPathOp, ObjectOp objectOp, ArrayOp arrayOp); STAR_CLASS(Path); STAR_CLASS(Pointer); STAR_CLASS(QueryPath); class Path { public: Path(PathParser parser, String const& path) : m_parser(parser), m_path(path) {} template Jsonlike get(Jsonlike const& base) { return pathGet(base, m_parser, m_path); } template Jsonlike apply(Jsonlike const& base, JsonOp op) { return pathApply(base, m_parser, m_path, op); } template Jsonlike apply(Jsonlike const& base, EmptyPathOp emptyPathOp, ObjectOp objectOp, ArrayOp arrayOp) { JsonOp combinedOp = genericObjectArrayOp(m_path, emptyPathOp, objectOp, arrayOp); return pathApply(base, m_parser, m_path, combinedOp); } template Jsonlike set(Jsonlike const& base, Jsonlike const& value) { return pathSet(base, m_parser, m_path, value); } template Jsonlike remove(Jsonlike const& base) { return pathRemove(base, m_parser, m_path); } template Jsonlike add(Jsonlike const& base, Jsonlike const& value) { return pathAdd(base, m_parser, m_path, value); } String const& path() const { return m_path; } private: PathParser m_parser; String m_path; }; class Pointer : public Path { public: Pointer(String const& path) : Path(parsePointer, path) {} }; class QueryPath : public Path { public: QueryPath(String const& path) : Path(parseQueryPath, path) {} }; template Jsonlike pathGet(Jsonlike value, PathParser parser, String const& path) { String buffer; buffer.reserve(path.size()); auto pos = path.begin(); while (pos != path.end()) { parser(buffer, path, pos, path.end()); if (value.type() == Json::Type::Array) { if (buffer == "-") throw TraversalException::format("Tried to get key '%s' in non-object type in pathGet(\"%s\")", buffer, path); Maybe i = maybeLexicalCast(buffer); if (!i) throw TraversalException::format("Cannot parse '%s' as index in pathGet(\"%s\")", buffer, path); if (*i < value.size()) value = value.get(*i); else throw TraversalException::format("Index %s out of range in pathGet(\"%s\")", buffer, path); } else if (value.type() == Json::Type::Object) { if (value.contains(buffer)) value = value.get(buffer); else throw TraversalException::format("No such key '%s' in pathGet(\"%s\")", buffer, path); } else { throw TraversalException::format("Tried to get key '%s' in non-object type in pathGet(\"%s\")", buffer, path); } } return value; } template Maybe pathFind(Jsonlike value, PathParser parser, String const& path) { String buffer; buffer.reserve(path.size()); auto pos = path.begin(); while (pos != path.end()) { parser(buffer, path, pos, path.end()); if (value.type() == Json::Type::Array) { if (buffer == "-") return {}; Maybe i = maybeLexicalCast(buffer); if (i && *i < value.size()) value = value.get(*i); else return {}; } else if (value.type() == Json::Type::Object) { if (value.contains(buffer)) value = value.get(buffer); else return {}; } else { return {}; } } return value; } template Jsonlike pathApply(String& buffer, Jsonlike const& value, PathParser parser, String const& path, String::const_iterator const current, JsonOp op) { if (current == path.end()) return op(value, {}); String::const_iterator iterator = current; parser(buffer, path, iterator, path.end()); if (value.type() == Json::Type::Array) { if (iterator == path.end()) { return op(value, buffer); } else { Maybe i = maybeLexicalCast(buffer); if (!i) throw TraversalException::format("Cannot parse '%s' as index in pathApply(\"%s\")", buffer, path); if (*i >= value.size()) throw TraversalException::format("Index %s out of range in pathApply(\"%s\")", buffer, path); return value.set(*i, pathApply(buffer, value.get(*i), parser, path, iterator, op)); } } else if (value.type() == Json::Type::Object) { if (iterator == path.end()) { return op(value, buffer); } else { if (!value.contains(buffer)) throw TraversalException::format("No such key '%s' in pathApply(\"%s\")", buffer, path); Jsonlike newChild = pathApply(buffer, value.get(buffer), parser, path, iterator, op); iterator = current; // pathApply just mutated buffer. Recover the current path component: parser(buffer, path, iterator, path.end()); return value.set(buffer, newChild); } } else { throw TraversalException::format("Tried to get key '%s' in non-object type in pathApply(\"%s\")", buffer, path); } } template Jsonlike pathApply(Jsonlike const& base, PathParser parser, String const& path, JsonOp op) { String buffer; return pathApply(buffer, base, parser, path, path.begin(), op); } template JsonOp genericObjectArrayOp(String path, EmptyPathOp emptyPathOp, ObjectOp objectOp, ArrayOp arrayOp) { return [=](Jsonlike const& parent, Maybe const& key) -> Jsonlike { if (key.isNothing()) return emptyPathOp(parent); if (parent.type() == Json::Type::Array) { if (*key == "-") return arrayOp(parent, {}); Maybe i = maybeLexicalCast(*key); if (!i) throw TraversalException::format("Cannot parse '%s' as index in Json path \"%s\"", *key, path); if (i && *i > parent.size()) throw TraversalException::format("Index %s out of range in Json path \"%s\"", *key, path); if (i && *i == parent.size()) i = {}; return arrayOp(parent, i); } else if (parent.type() == Json::Type::Object) { return objectOp(parent, *key); } else { throw TraversalException::format("Tried to set key '%s' in non-object type in pathSet(\"%s\")", *key, path); } }; } template Jsonlike pathSet(Jsonlike const& base, PathParser parser, String const& path, Jsonlike const& value) { EmptyPathOp emptyPathOp = [&value](Jsonlike const&) { return value; }; ObjectOp objectOp = [&value](Jsonlike const& object, String const& key) { return object.set(key, value); }; ArrayOp arrayOp = [&value](Jsonlike const& array, Maybe i) { if (i.isValid()) return array.set(*i, value); return array.append(value); }; return pathApply(base, parser, path, genericObjectArrayOp(path, emptyPathOp, objectOp, arrayOp)); } template Jsonlike pathRemove(Jsonlike const& base, PathParser parser, String const& path) { EmptyPathOp emptyPathOp = [](Jsonlike const&) { return Json{}; }; ObjectOp objectOp = [](Jsonlike const& object, String const& key) { if (!object.contains(key)) throw TraversalException::format("Could not find \"%s\" to remove", key); return object.eraseKey(key); }; ArrayOp arrayOp = [](Jsonlike const& array, Maybe i) { if (i.isValid()) return array.eraseIndex(*i); throw TraversalException("Could not remove element after end of array"); }; return pathApply(base, parser, path, genericObjectArrayOp(path, emptyPathOp, objectOp, arrayOp)); } template Jsonlike pathAdd(Jsonlike const& base, PathParser parser, String const& path, Jsonlike const& value) { EmptyPathOp emptyPathOp = [&value](Jsonlike const& document) { if (document.type() == Json::Type::Null) return value; throw JsonException("Cannot add a value to the entire document, it is not empty."); }; ObjectOp objectOp = [&value](Jsonlike const& object, String const& key) { return object.set(key, value); }; ArrayOp arrayOp = [&value](Jsonlike const& array, Maybe i) { if (i.isValid()) return array.insert(*i, value); return array.append(value); }; return pathApply(base, parser, path, genericObjectArrayOp(path, emptyPathOp, objectOp, arrayOp)); } } } #endif