Веб-сайт самохостера Lotigara

summaryrefslogtreecommitdiff
path: root/source/core/StarJsonPath.hpp
diff options
context:
space:
mode:
Diffstat (limited to 'source/core/StarJsonPath.hpp')
-rw-r--r--source/core/StarJsonPath.hpp332
1 files changed, 332 insertions, 0 deletions
diff --git a/source/core/StarJsonPath.hpp b/source/core/StarJsonPath.hpp
new file mode 100644
index 0000000..0f0efa9
--- /dev/null
+++ b/source/core/StarJsonPath.hpp
@@ -0,0 +1,332 @@
+#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<TypeHint(String&, String const&, String::const_iterator&, String::const_iterator)> 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 <typename Jsonlike>
+ 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 <typename Jsonlike>
+ Maybe<Jsonlike> pathFind(Jsonlike base, PathParser parser, String const& path);
+
+ template <typename Jsonlike>
+ using JsonOp = function<Jsonlike(Jsonlike const&, Maybe<String> 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 <typename Jsonlike>
+ Jsonlike pathApply(Jsonlike const& base, PathParser parser, String const& path, JsonOp<Jsonlike> op);
+
+ // Sets a value on a Json document at the location referred to by path,
+ // returning the resulting new document.
+ template <typename Jsonlike>
+ 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 <typename Jsonlike>
+ 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 <typename Jsonlike>
+ Jsonlike pathAdd(Jsonlike const& base, PathParser parser, String const& path, Jsonlike const& value);
+
+ template <typename Jsonlike>
+ using EmptyPathOp = function<Jsonlike(Jsonlike const&)>;
+ template <typename Jsonlike>
+ using ObjectOp = function<Jsonlike(Jsonlike const&, String const&)>;
+ template <typename Jsonlike>
+ using ArrayOp = function<Jsonlike(Jsonlike const&, Maybe<size_t>)>;
+
+ template <typename Jsonlike>
+ JsonOp<Jsonlike> genericObjectArrayOp(String path, EmptyPathOp<Jsonlike> emptyPathOp, ObjectOp<Jsonlike> objectOp, ArrayOp<Jsonlike> 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 <typename Jsonlike>
+ Jsonlike get(Jsonlike const& base) {
+ return pathGet(base, m_parser, m_path);
+ }
+
+ template <typename Jsonlike>
+ Jsonlike apply(Jsonlike const& base, JsonOp<Jsonlike> op) {
+ return pathApply(base, m_parser, m_path, op);
+ }
+
+ template <typename Jsonlike>
+ Jsonlike apply(Jsonlike const& base,
+ EmptyPathOp<Jsonlike> emptyPathOp,
+ ObjectOp<Jsonlike> objectOp,
+ ArrayOp<Jsonlike> arrayOp) {
+ JsonOp<Jsonlike> combinedOp = genericObjectArrayOp(m_path, emptyPathOp, objectOp, arrayOp);
+ return pathApply(base, m_parser, m_path, combinedOp);
+ }
+
+ template <typename Jsonlike>
+ Jsonlike set(Jsonlike const& base, Jsonlike const& value) {
+ return pathSet(base, m_parser, m_path, value);
+ }
+
+ template <typename Jsonlike>
+ Jsonlike remove(Jsonlike const& base) {
+ return pathRemove(base, m_parser, m_path);
+ }
+
+ template <typename Jsonlike>
+ 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 <typename Jsonlike>
+ 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<size_t> i = maybeLexicalCast<size_t>(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 <typename Jsonlike>
+ Maybe<Jsonlike> 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<size_t> i = maybeLexicalCast<size_t>(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 <typename Jsonlike>
+ Jsonlike pathApply(String& buffer,
+ Jsonlike const& value,
+ PathParser parser,
+ String const& path,
+ String::const_iterator const current,
+ JsonOp<Jsonlike> 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<size_t> i = maybeLexicalCast<size_t>(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 <typename Jsonlike>
+ Jsonlike pathApply(Jsonlike const& base, PathParser parser, String const& path, JsonOp<Jsonlike> op) {
+ String buffer;
+ return pathApply(buffer, base, parser, path, path.begin(), op);
+ }
+
+ template <typename Jsonlike>
+ JsonOp<Jsonlike> genericObjectArrayOp(String path, EmptyPathOp<Jsonlike> emptyPathOp, ObjectOp<Jsonlike> objectOp, ArrayOp<Jsonlike> arrayOp) {
+ return [=](Jsonlike const& parent, Maybe<String> const& key) -> Jsonlike {
+ if (key.isNothing())
+ return emptyPathOp(parent);
+ if (parent.type() == Json::Type::Array) {
+ if (*key == "-")
+ return arrayOp(parent, {});
+ Maybe<size_t> i = maybeLexicalCast<size_t>(*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 <typename Jsonlike>
+ Jsonlike pathSet(Jsonlike const& base, PathParser parser, String const& path, Jsonlike const& value) {
+ EmptyPathOp<Jsonlike> emptyPathOp = [&value](Jsonlike const&) {
+ return value;
+ };
+ ObjectOp<Jsonlike> objectOp = [&value](Jsonlike const& object, String const& key) {
+ return object.set(key, value);
+ };
+ ArrayOp<Jsonlike> arrayOp = [&value](Jsonlike const& array, Maybe<size_t> i) {
+ if (i.isValid())
+ return array.set(*i, value);
+ return array.append(value);
+ };
+ return pathApply(base, parser, path, genericObjectArrayOp(path, emptyPathOp, objectOp, arrayOp));
+ }
+
+ template <typename Jsonlike>
+ Jsonlike pathRemove(Jsonlike const& base, PathParser parser, String const& path) {
+ EmptyPathOp<Jsonlike> emptyPathOp = [](Jsonlike const&) { return Json{}; };
+ ObjectOp<Jsonlike> 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<Jsonlike> arrayOp = [](Jsonlike const& array, Maybe<size_t> 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 <typename Jsonlike>
+ Jsonlike pathAdd(Jsonlike const& base, PathParser parser, String const& path, Jsonlike const& value) {
+ EmptyPathOp<Jsonlike> 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<Jsonlike> objectOp = [&value](Jsonlike const& object, String const& key) {
+ return object.set(key, value);
+ };
+ ArrayOp<Jsonlike> arrayOp = [&value](Jsonlike const& array, Maybe<size_t> i) {
+ if (i.isValid())
+ return array.insert(*i, value);
+ return array.append(value);
+ };
+ return pathApply(base, parser, path, genericObjectArrayOp(path, emptyPathOp, objectOp, arrayOp));
+ }
+}
+
+}
+
+#endif