diff --git a/README.md b/README.md index 2333e8a..797e438 100644 --- a/README.md +++ b/README.md @@ -175,6 +175,24 @@ The main exceptions are Support for JSON References is in development. It is mostly working, however some of the test cases added to [JSON Schema Test Suite](https://github.com/json-schema/JSON-Schema-Test-Suite) for v6/v7 are still failing. +## JSON Inspector + +An example application based on Qt is also included under [inspector](./inspector). It can be used to experiment with JSON Schemas and target documents. JSON Inspector is a self-contained CMake project, so it must be built separately: + +```bash + cd inspector + mkdir build + cd build + cmake .. + make +``` + +Schemas and target documents can be loaded from file or entered manually. Content is parsed dynamically, so you get rapid feedback. + +Here is a screenshot of JSON Inspector in action: + +![JSON Inspector in action](./doc/inspector/screenshot.png) + ## Documentation ## Doxygen documentation can be built by running 'doxygen' from the project root directory. Generated documentation will be placed in 'doc/html'. Other relevant documentation such as schemas and specifications have been included in the 'doc' directory. diff --git a/doc/inspector/screenshot.png b/doc/inspector/screenshot.png new file mode 100644 index 0000000..4294040 Binary files /dev/null and b/doc/inspector/screenshot.png differ diff --git a/inspector/.gitignore b/inspector/.gitignore new file mode 100644 index 0000000..af21fe5 --- /dev/null +++ b/inspector/.gitignore @@ -0,0 +1,3 @@ +build/ +cmake-build-*/ +CMakeFiles/ diff --git a/inspector/CMakeLists.txt b/inspector/CMakeLists.txt new file mode 100644 index 0000000..8320921 --- /dev/null +++ b/inspector/CMakeLists.txt @@ -0,0 +1,39 @@ +# Reference: http://qt-project.org/doc/qt-5.0/qtdoc/cmake-manual.html + +cmake_minimum_required(VERSION 3.0) + +# Add folder where are supportive functions +set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +# Include Qt basic functions +include(QtCommon) + +# Basic information about project + +project(inspector VERSION 1.0) + +# Set PROJECT_VERSION_PATCH and PROJECT_VERSION_TWEAK to 0 if not present, needed by add_project_meta +fix_project_version() + +# Set additional project information +set(COPYRIGHT "Copyright (c) 2020 Tristan Penman. All rights reserved.") +set(IDENTIFIER "com.tristanpenman.valijson.inspector") + +set(SOURCE_FILES + src/highlighter.cpp + src/main.cpp + src/window.cpp +) + +include_directories(SYSTEM ../include) + +add_project_meta(META_FILES_TO_INCLUDE) + +find_package(Qt5 COMPONENTS Widgets REQUIRED) + +add_executable(${PROJECT_NAME} ${OS_BUNDLE} # Expands to WIN32 or MACOS_BUNDLE depending on OS + ${SOURCE_FILES} ${META_FILES_TO_INCLUDE} ${RESOURCE_FILES} +) + +target_link_libraries(${PROJECT_NAME} Qt5::Widgets) diff --git a/inspector/cmake/MacOSXBundleInfo.plist.in b/inspector/cmake/MacOSXBundleInfo.plist.in new file mode 100644 index 0000000..ce11313 --- /dev/null +++ b/inspector/cmake/MacOSXBundleInfo.plist.in @@ -0,0 +1,38 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + ${MACOSX_BUNDLE_EXECUTABLE_NAME} + CFBundleGetInfoString + ${MACOSX_BUNDLE_INFO_STRING} + CFBundleIconFile + ${MACOSX_BUNDLE_ICON_FILE} + CFBundleIdentifier + ${MACOSX_BUNDLE_GUI_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleLongVersionString + ${MACOSX_BUNDLE_LONG_VERSION_STRING} + CFBundleName + JSON Inspector + CFBundlePackageType + APPL + CFBundleShortVersionString + ${MACOSX_BUNDLE_SHORT_VERSION_STRING} + CFBundleSignature + ???? + CFBundleVersion + ${MACOSX_BUNDLE_BUNDLE_VERSION} + CSResourcesFileMapped + + LSRequiresCarbon + + NSHumanReadableCopyright + ${MACOSX_BUNDLE_COPYRIGHT} + NSPrincipalClass + NSApplication + + diff --git a/inspector/cmake/QtCommon.cmake b/inspector/cmake/QtCommon.cmake new file mode 100644 index 0000000..ef633ba --- /dev/null +++ b/inspector/cmake/QtCommon.cmake @@ -0,0 +1,82 @@ +macro(fix_project_version) +if (NOT PROJECT_VERSION_PATCH) + set(PROJECT_VERSION_PATCH 0) +endif() + +if (NOT PROJECT_VERSION_TWEAK) + set(PROJECT_VERSION_TWEAK 0) +endif() +endmacro() + +macro(add_project_meta FILES_TO_INCLUDE) +if (NOT RESOURCE_FOLDER) + set(RESOURCE_FOLDER res) +endif() + +if (NOT ICON_NAME) + set(ICON_NAME AppIcon) +endif() + +if (APPLE) + set(ICON_FILE ${RESOURCE_FOLDER}/${ICON_NAME}.icns) +elseif (WIN32) + set(ICON_FILE ${RESOURCE_FOLDER}/${ICON_NAME}.ico) +endif() + +if (WIN32) + configure_file("${PROJECT_SOURCE_DIR}/cmake/windows_metafile.rc.in" + "windows_metafile.rc" + ) + set(RES_FILES "windows_metafile.rc") + set(CMAKE_RC_COMPILER_INIT windres) + ENABLE_LANGUAGE(RC) + SET(CMAKE_RC_COMPILE_OBJECT " -O coff -i -o ") +endif() + +if (APPLE) + set_source_files_properties(${ICON_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) + + # Identify MacOS bundle + set(MACOSX_BUNDLE_BUNDLE_NAME ${PROJECT_NAME}) + set(MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}) + set(MACOSX_BUNDLE_LONG_VERSION_STRING ${PROJECT_VERSION}) + set(MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}") + set(MACOSX_BUNDLE_COPYRIGHT ${COPYRIGHT}) + set(MACOSX_BUNDLE_GUI_IDENTIFIER ${IDENTIFIER}) + set(MACOSX_BUNDLE_ICON_FILE ${ICON_NAME}) +endif() + +if (APPLE) + set(${FILES_TO_INCLUDE} ${ICON_FILE}) +elseif (WIN32) + set(${FILES_TO_INCLUDE} ${RES_FILES}) +endif() +endmacro() + +macro(init_os_bundle) +if (APPLE) + set(OS_BUNDLE MACOSX_BUNDLE) +elseif (WIN32) + set(OS_BUNDLE WIN32) +endif() +endmacro() + +macro(fix_win_compiler) +if (MSVC) + set_target_properties(${PROJECT_NAME} PROPERTIES + WIN32_EXECUTABLE YES + LINK_FLAGS "/ENTRY:mainCRTStartup" + ) +endif() +endmacro() + +macro(init_qt) +# Let's do the CMake job for us +set(CMAKE_AUTOMOC ON) # For meta object compiler +set(CMAKE_AUTORCC ON) # Resource files +set(CMAKE_AUTOUIC ON) # UI files +endmacro() + +init_os_bundle() +init_qt() +fix_win_compiler() diff --git a/inspector/cmake/windows_metafile.rc.in b/inspector/cmake/windows_metafile.rc.in new file mode 100644 index 0000000..0200869 --- /dev/null +++ b/inspector/cmake/windows_metafile.rc.in @@ -0,0 +1,28 @@ +#include "winver.h" + +IDI_ICON1 ICON DISCARDABLE "@ICON_FILE@" + +VS_VERSION_INFO VERSIONINFO + FILEVERSION @PROJECT_VERSION_MAJOR@,@PROJECT_VERSION_MINOR@,@PROJECT_VERSION_PATCH@,@PROJECT_VERSION_TWEAK@ + PRODUCTVERSION @PROJECT_VERSION_MAJOR@,@PROJECT_VERSION_MINOR@,@PROJECT_VERSION_PATCH@,@PROJECT_VERSION_TWEAK@ + FILEFLAGS 0x0L + FILEFLAGSMASK 0x3fL + FILEOS 0x00040004L + FILETYPE 0x1L + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "000004b0" + BEGIN + VALUE "CompanyName", "@COMPANY@" + VALUE "FileDescription", "@PROJECT_NAME@" + VALUE "FileVersion", "@PROJECT_VERSION@" + VALUE "LegalCopyright", "@COPYRIGHT@" + VALUE "InternalName", "@PROJECT_NAME@" + VALUE "OriginalFilename", "@PROJECT_NAME@.exe" + VALUE "ProductName", "@PROJECT_NAME@" + VALUE "ProductVersion", "@PROJECT_VERSION@" + END + END +END diff --git a/inspector/inspector.qrc b/inspector/inspector.qrc new file mode 100644 index 0000000..7072e5a --- /dev/null +++ b/inspector/inspector.qrc @@ -0,0 +1,4 @@ + + + + diff --git a/inspector/res/AppIcon.icns b/inspector/res/AppIcon.icns new file mode 100644 index 0000000..b9854d3 Binary files /dev/null and b/inspector/res/AppIcon.icns differ diff --git a/inspector/res/AppIcon.ico b/inspector/res/AppIcon.ico new file mode 100644 index 0000000..20777aa Binary files /dev/null and b/inspector/res/AppIcon.ico differ diff --git a/inspector/src/highlighter.cpp b/inspector/src/highlighter.cpp new file mode 100644 index 0000000..556d187 --- /dev/null +++ b/inspector/src/highlighter.cpp @@ -0,0 +1,12 @@ +#include "highlighter.h" + +Highlighter::Highlighter(QTextDocument *parent) + : QSyntaxHighlighter(parent) +{ + // TODO +} + +void Highlighter::highlightBlock(const QString &text) +{ + // TODO +} diff --git a/inspector/src/highlighter.h b/inspector/src/highlighter.h new file mode 100644 index 0000000..5eafd58 --- /dev/null +++ b/inspector/src/highlighter.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +class Highlighter : public QSyntaxHighlighter +{ + Q_OBJECT + +public: + Highlighter(QTextDocument * parent = 0); + +protected: + void highlightBlock(const QString & text) override; +}; diff --git a/inspector/src/main.cpp b/inspector/src/main.cpp new file mode 100644 index 0000000..b294385 --- /dev/null +++ b/inspector/src/main.cpp @@ -0,0 +1,12 @@ +#include +#include "window.h" + +int main(int argc, char *argv[]) +{ + QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + + QApplication app(argc, argv); + Window window; + window.show(); + return app.exec(); +} diff --git a/inspector/src/window.cpp b/inspector/src/window.cpp new file mode 100644 index 0000000..c59db24 --- /dev/null +++ b/inspector/src/window.cpp @@ -0,0 +1,206 @@ +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "highlighter.h" +#include "window.h" + +Window::Window(QWidget * parent) + : QMainWindow(parent) + , m_schema(nullptr) +{ + setWindowTitle("JSON Inspector"); + + m_documentEditor = createEditor(false); + m_schemaEditor = createEditor(false); + m_errors = createEditor(true); + + auto documentTabWidget = createTabWidget(m_documentEditor, "Document"); + auto schemaTabWidget = createTabWidget(m_schemaEditor, "Schema"); + auto horizontalSplitter = createSplitter(schemaTabWidget, documentTabWidget, true); + + auto errorsTabWidget = createTabWidget(m_errors, "Errors"); + auto verticalSplitter = createSplitter(horizontalSplitter, errorsTabWidget, false); + verticalSplitter->setStretchFactor(0, 2); + verticalSplitter->setStretchFactor(1, 1); + + auto toolBar = createToolBar(); + auto statusBar = createStatusBar(); + + addToolBar(toolBar); + setCentralWidget(verticalSplitter); + setStatusBar(statusBar); + + connect(m_documentEditor, SIGNAL(textChanged()), this, SLOT(refreshDocumentJson())); + connect(m_schemaEditor, SIGNAL(textChanged()), this, SLOT(refreshSchemaJson())); +} + +QTextEdit * Window::createEditor(bool readOnly) +{ + QFont font; + font.setFamily("Courier"); + font.setFixedPitch(true); + font.setPointSize(12); + + auto editor = new QTextEdit(); + editor->setFont(font); + editor->setReadOnly(readOnly); + + auto highlighter = new Highlighter(editor->document()); + + return editor; +} + +QSplitter * Window::createSplitter(QWidget * left, QWidget * right, bool horizontal) +{ + auto splitter = new QSplitter(horizontal ? Qt::Horizontal : Qt::Vertical); + splitter->setChildrenCollapsible(false); + splitter->insertWidget(0, left); + splitter->insertWidget(1, right); + + return splitter; +} + +QStatusBar * Window::createStatusBar() +{ + return new QStatusBar(); +} + +QTabWidget * Window::createTabWidget(QWidget * child, const QString & name) +{ + auto tabWidget = new QTabWidget(); + tabWidget->addTab(child, name); + tabWidget->setDocumentMode(true); + + return tabWidget; +} + +QToolBar * Window::createToolBar() +{ + auto toolbar = new QToolBar(); + toolbar->setMovable(false); + + auto openMenu = new QMenu("Open"); + auto openSchemaAction = openMenu->addAction("Open Schema..."); + auto openDocumentAction = openMenu->addAction("Open Document..."); + + auto openButton = new QToolButton(); + openButton->setMenu(openMenu); + openButton->setPopupMode(QToolButton::MenuButtonPopup); + openButton->setText("Open"); + openButton->setToolButtonStyle(Qt::ToolButtonTextOnly); + toolbar->addWidget(openButton); + + connect(openButton, &QToolButton::clicked, openButton, &QToolButton::showMenu); + connect(openDocumentAction, SIGNAL(triggered()), this, SLOT(showOpenDocumentDialog())); + connect(openSchemaAction, SIGNAL(triggered()), this, SLOT(showOpenSchemaDialog())); + + return toolbar; +} + +void Window::refreshDocumentJson() +{ + QJsonParseError error; + m_document = QJsonDocument::fromJson(m_documentEditor->toPlainText().toUtf8(), &error); + if (m_document.isNull()) { + m_errors->setText(error.errorString()); + return; + } + + if (m_schema) { + validate(); + } else { + m_errors->setText(""); + } +} + +void Window::refreshSchemaJson() +{ + QJsonParseError error; + auto schemaDoc = QJsonDocument::fromJson(m_schemaEditor->toPlainText().toUtf8(), &error); + if (schemaDoc.isNull()) { + m_errors->setText(error.errorString()); + return; + } + + try { + valijson::adapters::QtJsonAdapter adapter(schemaDoc.object()); + valijson::SchemaParser parser; + delete m_schema; + m_schema = new valijson::Schema(); + parser.populateSchema(adapter, *m_schema); + m_errors->setText(""); + + } catch (std::runtime_error error) { + delete m_schema; + m_schema = nullptr; + m_errors->setText(error.what()); + } + + validate(); +} + +void Window::showOpenDocumentDialog() +{ + const QString fileName = QFileDialog::getOpenFileName(this, "Open Document", QString(), QString("*.json")); + if (!fileName.isEmpty()) { + QFile file(fileName); + file.open(QFile::ReadOnly | QFile::Text); + m_documentEditor->setText(file.readAll()); + } +} + +void Window::showOpenSchemaDialog() +{ + const QString fileName = QFileDialog::getOpenFileName(this, "Open Schema", QString(), QString("*.json")); + if (!fileName.isEmpty()) { + QFile file(fileName); + file.open(QFile::ReadOnly | QFile::Text); + m_schemaEditor->setText(file.readAll()); + } +} + +void Window::validate() +{ + valijson::ValidationResults results; + valijson::Validator validator; + valijson::adapters::QtJsonAdapter adapter(m_document.object()); + + if (validator.validate(*m_schema, adapter, &results)) { + m_errors->setText("Document is valid."); + return; + } + + valijson::ValidationResults::Error error; + unsigned int errorNum = 1; + std::stringstream ss; + while (results.popError(error)) { + std::string context; + for (auto itr = error.context.begin(); itr != error.context.end(); itr++) { + context += *itr; + } + + ss << "Error #" << errorNum << std::endl + << " context: " << context << std::endl + << " desc: " << error.description << std::endl; + ++errorNum; + } + + m_errors->setText(QString::fromStdString(ss.str())); +} \ No newline at end of file diff --git a/inspector/src/window.h b/inspector/src/window.h new file mode 100644 index 0000000..5315126 --- /dev/null +++ b/inspector/src/window.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include + +class QJsonDocument; +class QSplitter; +class QStatusBar; +class QTabWidget; +class QTextEdit; +class QToolBar; + +namespace valijson { + class Schema; +} + +class Window : public QMainWindow +{ + Q_OBJECT + +public: + Window(QWidget * parent = 0); + +public slots: + void refreshDocumentJson(); + void refreshSchemaJson(); + + void showOpenDocumentDialog(); + void showOpenSchemaDialog(); + +private: + QTextEdit * createEditor(bool readOnly); + QSplitter * createSplitter(QWidget * left, QWidget * right, bool horizontal); + QStatusBar * createStatusBar(); + QTabWidget * createTabWidget(QWidget * child, const QString & name); + QToolBar * createToolBar(); + + void validate(); + + QTextEdit * m_documentEditor; + QTextEdit * m_schemaEditor; + + QTextEdit * m_errors; + + QJsonDocument m_document; + + valijson::Schema * m_schema; +};