diff options
author | Kae <80987908+Novaenia@users.noreply.github.com> | 2023-06-20 14:33:09 +1000 |
---|---|---|
committer | Kae <80987908+Novaenia@users.noreply.github.com> | 2023-06-20 14:33:09 +1000 |
commit | 6352e8e3196f78388b6c771073f9e03eaa612673 (patch) | |
tree | e23772f79a7fbc41bc9108951e9e136857484bf4 /source/json_tool | |
parent | 6741a057e5639280d85d0f88ba26f000baa58f61 (diff) |
everything everywhere
all at once
Diffstat (limited to 'source/json_tool')
-rw-r--r-- | source/json_tool/CMakeLists.txt | 19 | ||||
-rw-r--r-- | source/json_tool/editor_gui.cpp | 206 | ||||
-rw-r--r-- | source/json_tool/editor_gui.hpp | 56 | ||||
-rw-r--r-- | source/json_tool/json_tool.cpp | 471 | ||||
-rw-r--r-- | source/json_tool/json_tool.hpp | 150 |
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 |