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

summaryrefslogtreecommitdiff
path: root/source/json_tool
diff options
context:
space:
mode:
authorKae <80987908+Novaenia@users.noreply.github.com>2023-06-20 14:33:09 +1000
committerKae <80987908+Novaenia@users.noreply.github.com>2023-06-20 14:33:09 +1000
commit6352e8e3196f78388b6c771073f9e03eaa612673 (patch)
treee23772f79a7fbc41bc9108951e9e136857484bf4 /source/json_tool
parent6741a057e5639280d85d0f88ba26f000baa58f61 (diff)
everything everywhere
all at once
Diffstat (limited to 'source/json_tool')
-rw-r--r--source/json_tool/CMakeLists.txt19
-rw-r--r--source/json_tool/editor_gui.cpp206
-rw-r--r--source/json_tool/editor_gui.hpp56
-rw-r--r--source/json_tool/json_tool.cpp471
-rw-r--r--source/json_tool/json_tool.hpp150
5 files changed, 902 insertions, 0 deletions
diff --git a/source/json_tool/CMakeLists.txt b/source/json_tool/CMakeLists.txt
new file mode 100644
index 0000000..bd122f5
--- /dev/null
+++ b/source/json_tool/CMakeLists.txt
@@ -0,0 +1,19 @@
+INCLUDE_DIRECTORIES (
+ ${STAR_EXTERN_INCLUDES}
+ ${STAR_CORE_INCLUDES}
+ )
+
+FIND_PACKAGE (Qt5Core)
+FIND_PACKAGE (Qt5Widgets)
+
+SET (CMAKE_INCLUDE_CURRENT_DIR ON)
+SET (CMAKE_AUTOMOC ON)
+
+ADD_EXECUTABLE (json_tool WIN32
+ $<TARGET_OBJECTS:star_extern> $<TARGET_OBJECTS:star_core>
+ json_tool.cpp editor_gui.cpp)
+QT5_USE_MODULES (json_tool Widgets Gui Core)
+TARGET_LINK_LIBRARIES (json_tool ${STAR_EXT_LIBS})
+
+SET (CMAKE_AUTOMOC OFF)
+SET (CMAKE_INCLUDE_CURRENT_DIR OFF)
diff --git a/source/json_tool/editor_gui.cpp b/source/json_tool/editor_gui.cpp
new file mode 100644
index 0000000..d533392
--- /dev/null
+++ b/source/json_tool/editor_gui.cpp
@@ -0,0 +1,206 @@
+#include <QApplication>
+#include <QGridLayout>
+#include <QPushButton>
+#include "StarFile.hpp"
+#include "json_tool.hpp"
+#include "editor_gui.hpp"
+
+using namespace Star;
+
+int const ImagePreviewWidth = 300;
+int const ImagePreviewHeight = 600;
+
+JsonEditor::JsonEditor(JsonPath::PathPtr const& path, Options const& options, List<String> const& files)
+ : QMainWindow(), m_path(path), m_editFormat(options.editFormat), m_options(options), m_files(files), m_fileIndex(0) {
+ auto* w = new QWidget(this);
+ w->setObjectName("background");
+ setCentralWidget(w);
+ setWindowTitle("Json Editor");
+ resize(1280, 720);
+
+ auto* layout = new QGridLayout(centralWidget());
+
+ QFont font("Monospace");
+ font.setStyleHint(QFont::StyleHint::Monospace);
+
+ m_jsonDocument = new QTextDocument("Hello world");
+ m_jsonDocument->setDefaultFont(font);
+
+ m_statusLabel = new QLabel(centralWidget());
+ layout->addWidget(m_statusLabel, 0, 0, 1, 5);
+
+ m_jsonPreview = new QTextEdit(this);
+ m_jsonPreview->setReadOnly(true);
+ m_jsonPreview->setDocument(m_jsonDocument);
+ layout->addWidget(m_jsonPreview, 1, 0, 1, 5);
+
+ m_backButton = new QPushButton(centralWidget());
+ m_backButton->setText("« Back");
+ layout->addWidget(m_backButton, 2, 0);
+ connect(m_backButton, SIGNAL(pressed()), this, SLOT(back()));
+
+ m_pathLabel = new QLabel(centralWidget());
+ layout->addWidget(m_pathLabel, 2, 1);
+
+ m_imageLabel = new QLabel(centralWidget());
+ m_imageLabel->setMaximumSize(ImagePreviewWidth, ImagePreviewHeight);
+ m_imageLabel->setMinimumSize(ImagePreviewWidth, ImagePreviewHeight);
+ m_imageLabel->setAlignment(Qt::AlignCenter);
+ if (!m_options.editorImages.empty())
+ layout->addWidget(m_imageLabel, 1, 5);
+
+ m_valueEditor = new QLineEdit(centralWidget());
+ m_valueEditor->setFont(font);
+ layout->addWidget(m_valueEditor, 2, 2);
+ connect(m_valueEditor, SIGNAL(returnPressed()), this, SLOT(next()));
+ connect(m_valueEditor, SIGNAL(textChanged(QString const&)), this, SLOT(updatePreview(QString const&)));
+
+ m_nextButton = new QPushButton(centralWidget());
+ m_nextButton->setText("Next »");
+ m_nextButton->setDefault(true);
+ layout->addWidget(m_nextButton, 2, 3);
+ connect(m_nextButton, SIGNAL(pressed()), this, SLOT(next()));
+
+ m_errorDialog = new QErrorMessage(this);
+ m_errorDialog->setModal(true);
+
+ displayCurrentFile();
+}
+
+void JsonEditor::next() {
+ if (!m_valueEditor->isEnabled() || saveChanges()) {
+ ++m_fileIndex;
+ if (m_fileIndex >= m_files.size()) {
+ close();
+ return;
+ }
+
+ displayCurrentFile();
+ }
+}
+
+void JsonEditor::back() {
+ if (m_fileIndex == 0)
+ return;
+
+ --m_fileIndex;
+ displayCurrentFile();
+}
+
+void JsonEditor::updatePreview(QString const& valueStr) {
+ try {
+ FormattedJson newValue = m_editFormat->toJson(valueStr.toStdString());
+ FormattedJson preview = addOrSet(false, m_path, m_currentJson, m_options.insertLocation, newValue);
+ m_jsonDocument->setPlainText(preview.repr().utf8Ptr());
+
+ } catch (JsonException const&) {
+ } catch (JsonParsingException const&) {
+ // Don't update the preview if it's not valid Json.
+ }
+}
+
+bool JsonEditor::saveChanges() {
+ try {
+ FormattedJson newValue = m_editFormat->toJson(m_valueEditor->text().toStdString());
+ m_currentJson = addOrSet(false, m_path, m_currentJson, m_options.insertLocation, newValue);
+ String repr = reprWithLineEnding(m_currentJson);
+ File::writeFile(repr, m_files.get(m_fileIndex));
+ return true;
+
+ } catch (StarException const& e) {
+ m_errorDialog->showMessage(e.what());
+ return false;
+ }
+}
+
+void JsonEditor::displayCurrentFile() {
+ String file = m_files.get(m_fileIndex);
+
+ size_t progress = (m_fileIndex + 1) * 100 / m_files.size();
+ String status = strf("Editing file %s/%s (%s%%): %s", m_fileIndex + 1, m_files.size(), progress, file);
+ m_statusLabel->setText(status.utf8Ptr());
+
+ m_backButton->setEnabled(m_fileIndex != 0);
+ m_nextButton->setText(m_fileIndex == m_files.size() - 1 ? "Done" : "Next »");
+
+ m_pathLabel->setText(m_path->path().utf8Ptr());
+
+ m_imageLabel->setText("No preview");
+ m_jsonDocument->setPlainText("");
+ m_valueEditor->setText("");
+ m_valueEditor->setEnabled(false);
+
+ try {
+ m_currentJson = FormattedJson::parse(File::readFileString(file));
+
+ m_jsonDocument->setPlainText(m_currentJson.repr().utf8Ptr());
+
+ updateValueEditor();
+
+ updateImagePreview();
+
+ } catch (StarException const& e) {
+ // Something else went wrong (maybe while parsing the document) and allowing
+ // the user to edit this file might cause us to lose data.
+ m_errorDialog->showMessage(e.what());
+ }
+
+ m_jsonPreview->moveCursor(QTextCursor::Start);
+
+ m_valueEditor->selectAll();
+ m_valueEditor->setFocus(Qt::FocusReason::OtherFocusReason);
+}
+
+void JsonEditor::updateValueEditor() {
+ FormattedJson value;
+ try {
+ value = m_path->get(m_currentJson);
+ } catch (JsonPath::TraversalException const&) {
+ // Path does not already exist in the Json document. We're adding it.
+ value = m_editFormat->getDefault();
+ }
+
+ String valueText;
+ try {
+ valueText = m_editFormat->fromJson(value);
+ } catch (JsonException const& e) {
+ // The value already present in the was no thte type we expected, e.g.
+ // it was an int, when we wanted a string array for CSV.
+ // Clear the value already present.
+ m_errorDialog->showMessage(e.what());
+ valueText = m_editFormat->fromJson(m_editFormat->getDefault());
+ }
+ m_valueEditor->setText(valueText.utf8Ptr());
+ m_valueEditor->setEnabled(true);
+}
+
+void JsonEditor::updateImagePreview() {
+ String file = m_files.get(m_fileIndex);
+ for (JsonPath::PathPtr const& imagePath : m_options.editorImages) {
+ try {
+ String image = imagePath->get(m_currentJson).toJson().toString();
+ image = File::relativeTo(File::dirName(file), image.extract(":"));
+
+ QPixmap pixmap = QPixmap(image.utf8Ptr()).scaledToWidth(ImagePreviewWidth);
+ if (pixmap.height() > ImagePreviewHeight)
+ pixmap = pixmap.scaledToHeight(ImagePreviewHeight);
+ m_imageLabel->setPixmap(pixmap);
+ break;
+ } catch (JsonPath::TraversalException const&) {
+ }
+ }
+}
+
+int Star::edit(int argc, char* argv[], JsonPath::PathPtr const& path, Options const& options, List<Input> const& inputs) {
+ QApplication app(argc, argv);
+ StringList files;
+ for (Input const& input : inputs) {
+ if (input.is<FindInput>())
+ files.appendAll(findFiles(input.get<FindInput>()));
+ else
+ files.append(input.get<FileInput>().filename);
+ }
+ JsonEditor e(path, options, files);
+ e.show();
+ return app.exec();
+}
diff --git a/source/json_tool/editor_gui.hpp b/source/json_tool/editor_gui.hpp
new file mode 100644
index 0000000..d24c029
--- /dev/null
+++ b/source/json_tool/editor_gui.hpp
@@ -0,0 +1,56 @@
+#ifndef EDITOR_GUI_HPP
+#define EDITOR_GUI_HPP
+
+#include <QErrorMessage>
+#include <QLabel>
+#include <QLineEdit>
+#include <QMainWindow>
+#include <QScrollBar>
+#include <QTextEdit>
+
+#include "json_tool.hpp"
+
+namespace Star {
+
+class JsonEditor : public QMainWindow {
+ Q_OBJECT
+
+public:
+ explicit JsonEditor(JsonPath::PathPtr const& path, Options const& options, List<String> const& files);
+
+private slots:
+ void next();
+ void back();
+ void updatePreview(QString const& valueStr);
+
+private:
+ // Returns false if the change can't be made or the edit is invalid Json
+ bool saveChanges();
+
+ void displayCurrentFile();
+ void updateValueEditor();
+ void updateImagePreview();
+
+ QLabel* m_statusLabel;
+ QLabel* m_pathLabel;
+ QLabel* m_imageLabel;
+ QTextEdit* m_jsonPreview;
+ QTextDocument* m_jsonDocument;
+ QLineEdit* m_valueEditor;
+ QErrorMessage* m_errorDialog;
+ QPushButton* m_backButton;
+ QPushButton* m_nextButton;
+
+ JsonPath::PathPtr m_path;
+ JsonInputFormatPtr m_editFormat;
+ Options m_options;
+ List<String> m_files;
+ size_t m_fileIndex;
+ FormattedJson m_currentJson;
+};
+
+int edit(int argc, char* argv[], JsonPath::PathPtr const& path, Options const& options, List<Input> const& inputs);
+
+}
+
+#endif
diff --git a/source/json_tool/json_tool.cpp b/source/json_tool/json_tool.cpp
new file mode 100644
index 0000000..13c2200
--- /dev/null
+++ b/source/json_tool/json_tool.cpp
@@ -0,0 +1,471 @@
+#include "StarFile.hpp"
+#include "json_tool.hpp"
+#include "editor_gui.hpp"
+
+// Tool for scripting and mass-editing of JSON+Comments files without affecting
+// formatting.
+
+using namespace Star;
+
+FormattedJson GenericInputFormat::toJson(String const& input) const {
+ return FormattedJson::parse(input);
+}
+
+String GenericInputFormat::fromJson(FormattedJson const& json) const {
+ return json.repr();
+}
+
+FormattedJson GenericInputFormat::getDefault() const {
+ return FormattedJson::ofType(Json::Type::Null);
+}
+
+FormattedJson CommaSeparatedStrings::toJson(String const& input) const {
+ if (input.trim() == "")
+ return FormattedJson::ofType(Json::Type::Array);
+ StringList strings = input.split(',');
+ JsonArray array = strings.transformed(bind(&String::trim, _1, "")).transformed(construct<Json>());
+ return Json(array);
+}
+
+String CommaSeparatedStrings::fromJson(FormattedJson const& json) const {
+ StringList strings = json.toJson().toArray().transformed(bind(&Json::toString, _1));
+ return strings.join(", ");
+}
+
+FormattedJson CommaSeparatedStrings::getDefault() const {
+ return FormattedJson::ofType(Json::Type::Array);
+}
+
+FormattedJson StringInputFormat::toJson(String const& input) const {
+ return FormattedJson(Json(input));
+}
+
+String StringInputFormat::fromJson(FormattedJson const& json) const {
+ return json.toJson().toString();
+}
+
+FormattedJson StringInputFormat::getDefault() const {
+ return FormattedJson(Json(""));
+}
+
+String Star::reprWithLineEnding(FormattedJson const& json) {
+ // Append a newline, preserving the newline style of the file, e.g windows or
+ // unix.
+ String repr = json.repr();
+ if (repr.contains("\r"))
+ return strf("%s\r\n", repr);
+ return strf("%s\n", repr);
+}
+
+void OutputOnSeparateLines::out(FormattedJson const& json) {
+ coutf("%s", reprWithLineEnding(json));
+}
+
+void OutputOnSeparateLines::flush() {}
+
+void ArrayOutput::out(FormattedJson const& json) {
+ if (!m_unique || !m_results.contains(json))
+ m_results.append(json);
+}
+
+void ArrayOutput::flush() {
+ FormattedJson array = FormattedJson::ofType(Json::Type::Array);
+ for (FormattedJson const& result : m_results) {
+ array = array.append(result);
+ }
+ coutf("%s", reprWithLineEnding(array));
+}
+
+FormattedJson Star::addOrSet(bool add,
+ JsonPath::PathPtr path,
+ FormattedJson const& input,
+ InsertLocation insertLocation,
+ FormattedJson const& value) {
+ JsonPath::EmptyPathOp<FormattedJson> emptyPathOp = [&value, add](FormattedJson const& document) {
+ if (!add || document.type() == Json::Type::Null)
+ return value;
+ throw JsonException("Cannot add a value to the entire document, it is not empty.");
+ };
+ JsonPath::ObjectOp<FormattedJson> objectOp = [&value, &insertLocation](
+ FormattedJson const& object, String const& key) {
+ if (insertLocation.is<AtBeginning>())
+ return object.prepend(key, value);
+ if (insertLocation.is<AtEnd>())
+ return object.append(key, value);
+ if (insertLocation.is<BeforeKey>())
+ return object.insertBefore(key, value, insertLocation.get<BeforeKey>().key);
+ if (insertLocation.is<AfterKey>())
+ return object.insertAfter(key, value, insertLocation.get<AfterKey>().key);
+ return object.set(key, value);
+ };
+ JsonPath::ArrayOp<FormattedJson> arrayOp = [&value, add](FormattedJson const& array, Maybe<size_t> i) {
+ if (i.isValid()) {
+ if (add)
+ return array.insert(*i, value);
+ return array.set(*i, value);
+ }
+ return array.append(value);
+ };
+ return path->apply(input, emptyPathOp, objectOp, arrayOp);
+}
+
+void forEachFileRecursive(String const& directory, function<void(String)> func) {
+ for (pair<String, bool> entry : File::dirList(directory)) {
+ String filename = File::relativeTo(directory, entry.first);
+ if (entry.second)
+ forEachFileRecursive(filename, func);
+ else
+ func(filename);
+ }
+}
+
+StringList Star::findFiles(FindInput const& findArgs) {
+ StringList matches;
+ forEachFileRecursive(findArgs.directory,
+ [&findArgs, &matches](String const& filename) {
+ if (filename.endsWith(findArgs.filenameSuffix))
+ matches.append(filename);
+ });
+ return matches;
+}
+
+void forEachChild(FormattedJson const& parent, function<void(FormattedJson const&)> func) {
+ if (parent.isType(Json::Type::Object)) {
+ for (String const& key : parent.toJson().toObject().keys()) {
+ func(parent.get(key));
+ }
+ } else if (parent.isType(Json::Type::Array)) {
+ for (size_t i = 0; i < parent.size(); ++i) {
+ func(parent.get(i));
+ }
+ } else {
+ throw JsonPath::TraversalException::format(
+ "Cannot get the children of Json type %s, must be either Array or Object", parent.typeName());
+ }
+}
+
+bool process(function<void(FormattedJson const&)> output,
+ Command const& command,
+ Options const& options,
+ FormattedJson const& input) {
+ if (command.is<GetCommand>()) {
+ GetCommand const& getCmd = command.get<GetCommand>();
+ try {
+ FormattedJson value = getCmd.path->get(input);
+ if (getCmd.children) {
+ forEachChild(value, output);
+ } else {
+ output(value);
+ }
+ } catch (JsonPath::TraversalException const& e) {
+ if (!getCmd.opt)
+ throw e;
+ }
+
+ } else if (command.is<SetCommand>()) {
+ SetCommand const& setCmd = command.get<SetCommand>();
+ output(addOrSet(false, setCmd.path, input, options.insertLocation, setCmd.value));
+
+ } else if (command.is<AddCommand>()) {
+ AddCommand const& addCmd = command.get<AddCommand>();
+ output(addOrSet(true, addCmd.path, input, options.insertLocation, addCmd.value));
+
+ } else if (command.is<RemoveCommand>()) {
+ output(command.get<RemoveCommand>().path->remove(input));
+
+ } else {
+ starAssert(command.empty());
+ output(input);
+ }
+ return true;
+}
+
+bool process(
+ function<void(FormattedJson const&)> output, Command const& command, Options const& options, String const& input) {
+ FormattedJson inJson = FormattedJson::parse(input);
+ return process(output, command, options, inJson);
+}
+
+bool process(
+ function<void(FormattedJson const&)> output, Command const& command, Options const& options, Input const& input) {
+ if (input.is<JsonLiteralInput>()) {
+ return process(output, command, options, input.get<JsonLiteralInput>().json);
+ }
+
+ bool success = true;
+ StringList files;
+
+ if (input.is<FileInput>()) {
+ files = StringList{input.get<FileInput>().filename};
+ } else {
+ files = findFiles(input.get<FindInput>());
+ }
+
+ for (String const& file : files) {
+ if (options.inPlace) {
+ output = [&file](FormattedJson const& json) { File::writeFile(reprWithLineEnding(json), file); };
+ }
+ success &= process(output, command, options, File::readFileString(file));
+ }
+ return success;
+}
+
+JsonPath::PathPtr parsePath(String const& path) {
+ if (path.beginsWith("/"))
+ return make_shared<JsonPath::Pointer>(path);
+ return make_shared<JsonPath::QueryPath>(path);
+}
+
+pair<JsonPath::PathPtr, bool> parseGetPath(String path) {
+ // --get and --opt have a special syntax for getting the child values of
+ // the value at the given path. These end with *, e.g.:
+ // /foo/bar/*
+ // foo.bar.*
+ // foo.bar[*]
+
+ bool children = false;
+ if (path.endsWith("/*") || path.endsWith(".*")) {
+ path = path.substr(0, path.size() - 2);
+ children = true;
+ } else if (path.endsWith("[*]")) {
+ path = path.substr(0, path.size() - 3);
+ children = true;
+ }
+ return make_pair(parsePath(path), children);
+}
+
+Maybe<ParsedArgs> parseArgs(int argc, char** argv) {
+ // Skip the program name
+ Deque<String> args = Deque<String>(argv + 1, argv + argc);
+
+ ParsedArgs parsed;
+
+ // Parse option arguments
+ while (!args.empty()) {
+ String const& arg = args.takeFirst();
+ if (arg == "--get" || arg == "--opt") {
+ // Retrieve values at a given path in the Json document
+ if (!parsed.command.empty() || args.empty())
+ return {};
+ JsonPath::PathPtr path;
+ bool children = false;
+ tie(path, children) = parseGetPath(args.takeFirst());
+ parsed.command = GetCommand{path, arg == "--opt", children};
+
+ } else if (arg == "--set") {
+ // Set the value at the given path in the Json document
+ if (!parsed.command.empty() || args.size() < 2)
+ return {};
+ JsonPath::PathPtr path = parsePath(args.takeFirst());
+ FormattedJson value = FormattedJson::parse(args.takeFirst());
+ parsed.command = SetCommand{path, value};
+
+ } else if (arg == "--add") {
+ // Add (insert) a path to a Json document
+ if (!parsed.command.empty() || args.size() < 2)
+ return {};
+ JsonPath::PathPtr path = parsePath(args.takeFirst());
+ FormattedJson value = FormattedJson::parse(args.takeFirst());
+ parsed.command = AddCommand{path, value};
+
+ } else if (arg == "--remove") {
+ // Remove a path from a Json document
+ if (!parsed.command.empty() || args.empty())
+ return {};
+ parsed.command = RemoveCommand{parsePath(args.takeFirst())};
+
+ } else if (arg == "--edit") {
+ // Interactive bullk Json editor
+ if (!parsed.command.empty() || args.empty())
+ return {};
+ parsed.command = EditCommand{parsePath(args.takeFirst())};
+
+ } else if (arg == "--editor-image") {
+ if (args.empty())
+ return {};
+ parsed.options.editorImages.append(parsePath(args.takeFirst()));
+
+ } else if (arg == "--input") {
+ // Configure the input syntax for --edit
+ if (args.empty())
+ return {};
+ String format = args.takeFirst();
+ if (format == "json" || format == "generic")
+ parsed.options.editFormat = make_shared<GenericInputFormat>();
+ else if (format == "css" || format == "csv")
+ parsed.options.editFormat = make_shared<CommaSeparatedStrings>();
+ else if (format == "string")
+ parsed.options.editFormat = make_shared<StringInputFormat>();
+ else
+ return {};
+
+ } else if (arg == "--array") {
+ // Output multiple results as a single array
+ parsed.options.output = make_shared<ArrayOutput>(false);
+
+ } else if (arg == "--array-unique") {
+ // Output multiple results as a single array, with duplicate results
+ // removed
+ parsed.options.output = make_shared<ArrayOutput>(true);
+
+ } else if (arg == "--help") {
+ return {};
+
+ } else if (arg == "-j") {
+ // Use command line argument as input
+ parsed.inputs.append(JsonLiteralInput{args.takeFirst()});
+
+ } else if (arg == "--find") {
+ // Search for files recursively in the given directory with a given
+ // suffix.
+ if (args.size() < 2)
+ return {};
+ String directory = args.takeFirst();
+ String suffix = args.takeFirst();
+ parsed.inputs.append(FindInput{directory, suffix});
+
+ } else if (arg == "-i") {
+ // Update files in place rather than print to stdout
+ parsed.options.inPlace = true;
+
+ } else if (arg == "--at") {
+ // Insert new object keys at the beginning or end of the document
+ if (!parsed.options.insertLocation.empty() || args.size() < 1)
+ return {};
+ String pos = args.takeFirst();
+ if (pos == "beginning" || pos == "start")
+ parsed.options.insertLocation = AtBeginning{};
+ else if (pos == "end")
+ parsed.options.insertLocation = AtEnd{};
+ else
+ return {};
+
+ } else if (arg == "--before") {
+ // Insert new object keys before the given key
+ if (!parsed.options.insertLocation.empty() || args.size() < 1)
+ return {};
+ parsed.options.insertLocation = BeforeKey{args.takeFirst()};
+
+ } else if (arg == "--after") {
+ // Insert new object keys after the given key
+ if (!parsed.options.insertLocation.empty() || args.size() < 1)
+ return {};
+ parsed.options.insertLocation = AfterKey{args.takeFirst()};
+
+ } else {
+ if (!File::exists(arg)) {
+ cerrf("File %s doesn't exist\n", arg);
+ return {};
+ }
+ parsed.inputs.append(FileInput{arg});
+ }
+ }
+
+ if (!parsed.options.output)
+ parsed.options.output = make_shared<OutputOnSeparateLines>();
+
+ bool anyFileInputs = false, anyNonFileInputs = false;
+ for (Input const& input : parsed.inputs) {
+ if (input.is<FindInput>() || input.is<FileInput>()) {
+ anyFileInputs = true;
+ } else {
+ anyNonFileInputs = true;
+ }
+ }
+ if (parsed.command.is<EditCommand>() && !anyFileInputs) {
+ cerrf("Files to edit must be supplied when using --edit.\n");
+ return {};
+ }
+
+ if (parsed.options.inPlace && !anyFileInputs) {
+ cerrf("In-place writing (-i) can only be used with files specified on the command line.\n");
+ return {};
+ }
+ if (parsed.options.inPlace && parsed.command.is<EditCommand>()) {
+ cerrf("Interactive edit (--edit) is always in-place. Explicitly specifying -i is not needed.\n");
+ return {};
+ }
+ if (parsed.command.is<EditCommand>() && anyNonFileInputs) {
+ cerrf("Interactie edit (--edit) can only be used with file input sources.\n");
+ return {};
+ }
+
+ if (parsed.options.editFormat && !parsed.command.is<EditCommand>()) {
+ cerrf("--input can only be used with --edit.\n");
+ return {};
+ } else if (!parsed.options.editFormat && parsed.command.is<EditCommand>()) {
+ parsed.options.editFormat = make_shared<GenericInputFormat>();
+ }
+
+ return parsed;
+}
+
+String readStdin() {
+ String result;
+ char buffer[1024];
+ while (!feof(stdin)) {
+ size_t readBytes = fread(buffer, 1, 1024, stdin);
+ result.append(buffer, readBytes);
+ }
+ return result;
+}
+
+int main(int argc, char** argv) {
+ try {
+ Maybe<ParsedArgs> parsedArgs = parseArgs(argc, argv);
+
+ if (!parsedArgs) {
+ cerrf("Usage: %s [--get <json-path>] (-j <json> | <json-file>*)\n", argv[0]);
+ cerrf(
+ "Usage: %s --set <json-path> <json> [-i] [(--at (beginning|end) | --before <key> | --after <key>)] (-j "
+ "<json> | <json-file>*)\n",
+ argv[0]);
+ cerrf(
+ "Usage: %s --add <json-path> <json> [-i] [(--at (beginning|end) | --before <key> | --after <key>)] (-j "
+ "<json> | <json-file>*)\n",
+ argv[0]);
+ cerrf(
+ "Usage: %s --edit <json-path> [(--at (beginning|end) | --before <key> | --after <key>)] [--input "
+ "(csv|json|string)] <json-file>+\n",
+ argv[0]);
+ cerrf("\n");
+ cerrf("Example: %s --get /dialog/0/message guard.npctype\n", argv[0]);
+ cerrf("Example: %s --get 'foo[0]' -j '{\"foo\":[0,1,2,3]}'\n", argv[0]);
+ cerrf("Example: %s --edit /tags --input csv --find ../assets/ .object\n", argv[0]);
+ return 1;
+ }
+
+ OutputPtr output = parsedArgs->options.output;
+ bool success = true;
+
+ if (parsedArgs->command.is<EditCommand>()) {
+ return edit(argc, argv, parsedArgs->command.get<EditCommand>().path, parsedArgs->options, parsedArgs->inputs);
+
+ } else if (parsedArgs->inputs.size() == 0) {
+ // No files were provided. Reading from stdin
+ success &= process(output->toFunction(), parsedArgs->command, parsedArgs->options, readStdin());
+ } else {
+ for (Input const& input : parsedArgs->inputs) {
+ success &= process(output->toFunction(), parsedArgs->command, parsedArgs->options, input);
+ }
+ }
+
+ output->flush();
+
+ if (!success)
+ return 1;
+ return 0;
+
+ } catch (JsonParsingException const& e) {
+ cerrf("%s\n", e.what());
+ return 1;
+
+ } catch (JsonException const& e) {
+ cerrf("%s\n", e.what());
+ return 1;
+
+ } catch (std::exception const& e) {
+ cerrf("Exception caught: %s\n", outputException(e, true));
+ return 1;
+ }
+}
diff --git a/source/json_tool/json_tool.hpp b/source/json_tool/json_tool.hpp
new file mode 100644
index 0000000..6f4c337
--- /dev/null
+++ b/source/json_tool/json_tool.hpp
@@ -0,0 +1,150 @@
+#ifndef JSON_TOOL_HPP
+#define JSON_TOOL_HPP
+
+#include "StarFormattedJson.hpp"
+#include "StarJsonPath.hpp"
+
+namespace Star {
+
+struct GetCommand {
+ JsonPath::PathPtr path;
+ bool opt;
+ bool children;
+};
+
+struct SetCommand {
+ JsonPath::PathPtr path;
+ FormattedJson value;
+};
+
+struct AddCommand {
+ JsonPath::PathPtr path;
+ FormattedJson value;
+};
+
+struct RemoveCommand {
+ JsonPath::PathPtr path;
+};
+
+struct EditCommand {
+ JsonPath::PathPtr path;
+};
+
+typedef MVariant<GetCommand, SetCommand, AddCommand, RemoveCommand, EditCommand> Command;
+
+struct AtBeginning {};
+
+struct AtEnd {};
+
+struct BeforeKey {
+ String key;
+};
+
+struct AfterKey {
+ String key;
+};
+
+typedef MVariant<AtBeginning, AtEnd, BeforeKey, AfterKey> InsertLocation;
+
+STAR_CLASS(JsonInputFormat);
+
+class JsonInputFormat {
+public:
+ virtual ~JsonInputFormat() {}
+ virtual FormattedJson toJson(String const& input) const = 0;
+ virtual String fromJson(FormattedJson const& json) const = 0;
+ virtual FormattedJson getDefault() const = 0;
+};
+
+class GenericInputFormat : public JsonInputFormat {
+public:
+ virtual FormattedJson toJson(String const& input) const override;
+ virtual String fromJson(FormattedJson const& json) const override;
+ virtual FormattedJson getDefault() const override;
+};
+
+class CommaSeparatedStrings : public JsonInputFormat {
+public:
+ virtual FormattedJson toJson(String const& input) const override;
+ virtual String fromJson(FormattedJson const& json) const override;
+ virtual FormattedJson getDefault() const override;
+};
+
+class StringInputFormat : public JsonInputFormat {
+public:
+ virtual FormattedJson toJson(String const& input) const override;
+ virtual String fromJson(FormattedJson const& json) const override;
+ virtual FormattedJson getDefault() const override;
+};
+
+STAR_CLASS(Output);
+
+class Output {
+public:
+ virtual ~Output() {}
+ virtual void out(FormattedJson const& json) = 0;
+ virtual void flush() = 0;
+
+ function<void(FormattedJson const& json)> toFunction() {
+ return [this](FormattedJson const& json) { this->out(json); };
+ }
+};
+
+class OutputOnSeparateLines : public Output {
+public:
+ virtual void out(FormattedJson const& json) override;
+ virtual void flush() override;
+};
+
+class ArrayOutput : public Output {
+public:
+ ArrayOutput(bool unique) : m_unique(unique), m_results() {}
+
+ virtual void out(FormattedJson const& json) override;
+ virtual void flush() override;
+
+private:
+ bool m_unique;
+ List<FormattedJson> m_results;
+};
+
+struct Options {
+ Options() : inPlace(false), insertLocation(), editFormat(nullptr), editorImages(), output() {}
+
+ bool inPlace;
+ InsertLocation insertLocation;
+ JsonInputFormatPtr editFormat;
+ List<JsonPath::PathPtr> editorImages;
+ OutputPtr output;
+};
+
+struct JsonLiteralInput {
+ String json;
+};
+
+struct FileInput {
+ String filename;
+};
+
+struct FindInput {
+ String directory;
+ String filenameSuffix;
+};
+
+typedef MVariant<JsonLiteralInput, FileInput, FindInput> Input;
+
+struct ParsedArgs {
+ ParsedArgs() : inputs(), command(), options() {}
+
+ List<Input> inputs;
+ Command command;
+ Options options;
+};
+
+FormattedJson addOrSet(bool add, JsonPath::PathPtr path, FormattedJson const& input, InsertLocation insertLocation, FormattedJson const& value);
+String reprWithLineEnding(FormattedJson const& json);
+StringList findFiles(FindInput const& findArgs);
+
+}
+
+#endif