From 72ebc4e7cc1bdcea9814f2dbc5f852a872508442 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Barto=C5=A1?= Date: Mon, 1 Jun 2026 10:48:47 +0200 Subject: [PATCH 1/9] Expose native column decoder bridge APIs Expose SyntaxInterface decoder snapshots and text decoding for jaspSyntax, add status-returning QML option parsing, and make quiet bridge logging actually discard output. Ensure the native ColumnEncoder is initialized before option encoding/decoding and catch decoder bridge failures at the C ABI boundary. --- Common/columnencoder.cpp | 17 +++ Common/columnencoder.h | 4 +- Common/log.cpp | 14 +- SyntaxInterface/syntaxbridge.cpp | 163 +++++++++++++++++++++-- SyntaxInterface/syntaxbridge_interface.h | 4 + 5 files changed, 188 insertions(+), 14 deletions(-) diff --git a/Common/columnencoder.cpp b/Common/columnencoder.cpp index 4f90638d05..0564ae496c 100644 --- a/Common/columnencoder.cpp +++ b/Common/columnencoder.cpp @@ -670,6 +670,22 @@ ColumnEncoder::colVec ColumnEncoder::columnNamesEncoded() return _columnEncoder ? _columnEncoder->_encodedNames : colVec(); } +ColumnEncoder::colMap ColumnEncoder::decodingMapSnapshot() +{ + columnEncoder(); + return decodingMap(); +} + +std::string ColumnEncoder::decodeAllWithMapping(const std::string & text, const ColumnEncoder::colMap & decodingMap) +{ + ColumnEncoder::colVec encodedNames; + for(const auto & keyVal : decodingMap) + encodedNames.push_back(keyVal.first); + + sortVectorBigToSmall(encodedNames); + return replaceAll(text, decodingMap, encodedNames); +} + void ColumnEncoder::_convertPreloadingDataOption(Json::Value & options, const std::string& optionName, colsPlusTypes& colTypes) { std::string optionKey = options[optionName].isMember("optionKey") ? options[optionName]["optionKey"].asString() : ""; @@ -827,6 +843,7 @@ void ColumnEncoder::_addTypeToColumnNamesInOptionsRecursively(Json::Value & opti ColumnEncoder::colsPlusTypes ColumnEncoder::encodeColumnNamesinOptions(Json::Value & options, bool preloadingData) { + columnEncoder(); colsPlusTypes getTheseCols; _addTypeToColumnNamesInOptionsRecursively(options, preloadingData, getTheseCols); diff --git a/Common/columnencoder.h b/Common/columnencoder.h index bfd798dad2..04adf34667 100644 --- a/Common/columnencoder.h +++ b/Common/columnencoder.h @@ -63,6 +63,8 @@ private: ColumnEncoder() { invalidateAll(); } static colVec columnNames(); static colVec columnNamesEncoded(); + static colMap decodingMapSnapshot(); + static std::string decodeAllWithMapping(const std::string & text, const colMap & decodingMap); bool shouldEncode(const std::string & in); bool shouldDecode(const std::string & in); @@ -87,7 +89,7 @@ private: ColumnEncoder() { invalidateAll(); } static std::string encodeAll(const std::string & text) { return replaceAll(text, encodingMap(), originalNames()); } ///Replace all occurences of encoded columnNames in a string by their decoded versions, regardless of word boundaries or parentheses. - static std::string decodeAll(const std::string & text) { return replaceAll(text, decodingMap(), encodedNames()); } + static std::string decodeAll(const std::string & text) { columnEncoder(); return replaceAll(text, decodingMap(), encodedNames()); } ///Replace all occurences of columnNames in a string by their encoded versions in all json-names and string-values, regardless of word boundaries or parentheses. static void encodeJson(Json::Value & json, bool replaceNames = false, bool replaceStrict = false); diff --git a/Common/log.cpp b/Common/log.cpp index af0d354766..22d99820d8 100644 --- a/Common/log.cpp +++ b/Common/log.cpp @@ -15,7 +15,19 @@ std::ofstream Log::_logFile;// = bofstream(); -std::ostream* Log::_nullStream = &std::cout; +namespace +{ + class NullLogBuffer : public std::streambuf + { + protected: + int overflow(int c) override { return traits_type::not_eof(c); } + }; + + NullLogBuffer nullLogBuffer; + std::ostream nullLogStream(&nullLogBuffer); +} + +std::ostream* Log::_nullStream = &nullLogStream; std::string Log::logFileNameBase = ""; diff --git a/SyntaxInterface/syntaxbridge.cpp b/SyntaxInterface/syntaxbridge.cpp index 82aa02a711..d6c241a8f2 100644 --- a/SyntaxInterface/syntaxbridge.cpp +++ b/SyntaxInterface/syntaxbridge.cpp @@ -43,9 +43,11 @@ #include "modules/dynamicmodule.h" #include "archivereader.h" #include "databaseinterface.h" +#include "columnencoder.h" #include #include +#include #include #ifdef USE_QT_STATIC_LIBS @@ -152,6 +154,98 @@ static const char* statusError(Json::Value status, const std::string & error) return statusResult(status); } +static Json::Value columnDecoderSnapshotJson() +{ + Json::Value snapshot(Json::objectValue); + Json::Value columns(Json::arrayValue); + const ColumnEncoder::colMap decodingMap = ColumnEncoder::decodingMapSnapshot(); + + for(const auto & keyVal : decodingMap) + { + Json::Value column(Json::objectValue); + column["encoded"] = keyVal.first; + column["decoded"] = keyVal.second; + columns.append(column); + } + + snapshot["version"] = 1; + snapshot["columns"] = columns; + return snapshot; +} + +struct ColumnDecoderSnapshot +{ + ColumnEncoder::colMap decodingMap; + bool supplied = false; +}; + +static ColumnDecoderSnapshot columnDecoderMapFromSnapshot(const Json::Value & snapshot) +{ + ColumnDecoderSnapshot snapshotState; + snapshotState.supplied = true; + const Json::Value & columns = snapshot["columns"]; + if(!columns.isArray()) + return snapshotState; + + for(const Json::Value & column : columns) + if(column.isObject() && column["encoded"].isString() && column["decoded"].isString()) + snapshotState.decodingMap[column["encoded"].asString()] = column["decoded"].asString(); + + return snapshotState; +} + +static ColumnDecoderSnapshot columnDecoderMapFromSnapshotString(const char * snapshotJson) +{ + if(!snapshotJson || std::string(snapshotJson).empty()) + return ColumnDecoderSnapshot(); + + Json::Value snapshot; + Json::Reader reader; + if(!reader.parse(snapshotJson, snapshot)) + throw std::runtime_error("Could not parse column decoder snapshot JSON."); + + return columnDecoderMapFromSnapshot(snapshot); +} + +static Json::Value parseStringArrayJson(const char * valuesJson) +{ + if(!valuesJson) + throw std::runtime_error("Cannot decode column text from a null JSON payload."); + + Json::Value values; + Json::Reader reader; + if(!reader.parse(valuesJson, values)) + throw std::runtime_error("Could not parse column text JSON payload."); + if(!values.isArray()) + throw std::runtime_error("Column text JSON payload must be an array."); + + return values; +} + +static Json::Value decodeColumnTextJson(const Json::Value & values, const ColumnDecoderSnapshot & snapshot) +{ + Json::Value decodedValues(Json::arrayValue); + + for(const Json::Value & value : values) + { + if(value.isNull()) + { + decodedValues.append(Json::Value()); + } + else if(value.isString()) + { + const std::string text = value.asString(); + decodedValues.append(snapshot.supplied ? ColumnEncoder::decodeAllWithMapping(text, snapshot.decodingMap) : ColumnEncoder::decodeAll(text)); + } + else + { + throw std::runtime_error("Column text JSON payload must contain only strings or null values."); + } + } + + return decodedValues; +} + static Json::Value analysisOptionsStatus(const char * filePath, int analysisNr) { Json::Value status = statusBase("syntaxBridgeAnalysisOptionsFromJaspFile"); @@ -459,11 +553,22 @@ const char* STDCALL syntaxBridgeLoadDataSetFromJaspFileStatus(const char * fileP const char* STDCALL syntaxBridgeLoadQmlAndParseOptions(const char* moduleName, const char* analysisName, const char* qmlFile, const char* options, const char* version, bool preloadData) { - if (!init()) - { - Log::log() << "Error during initialization" << std::endl; + Json::Value status; + Json::Reader reader; + if (!reader.parse(syntaxBridgeLoadQmlAndParseOptionsStatus(moduleName, analysisName, qmlFile, options, version, preloadData), status)) return ""; - } + if (!status["ok"].asBool()) + return ""; + + static std::string result; + result = status["options"].toStyledString(); + return result.c_str(); +} + +const char* STDCALL syntaxBridgeLoadQmlAndParseOptionsStatus(const char* moduleName, const char* analysisName, const char* qmlFile, const char* options, const char* version, bool preloadData) +{ + if (!init()) + return statusError(statusBase("syntaxBridgeLoadQmlAndParseOptions"), "Error during initialization."); std::string qmlFileStr = qmlFile, versionStr = version, @@ -475,8 +580,7 @@ const char* STDCALL syntaxBridgeLoadQmlAndParseOptions(const char* moduleName, c if (!form) { - Log::log() << "Cannot create QML Form " << qmlFileStr << std::endl; - return ""; + return statusError(statusBase("syntaxBridgeLoadQmlAndParseOptions"), "Cannot create QML Form " + qmlFileStr); } Json::Value parsedOptions; @@ -484,8 +588,7 @@ const char* STDCALL syntaxBridgeLoadQmlAndParseOptions(const char* moduleName, c if (!form->parseOptions(options, parsedOptions, errorMsg)) { - Log::log() << "Error when parsing options: " << errorMsg << std::endl; - return ""; + return statusError(statusBase("syntaxBridgeLoadQmlAndParseOptions"), "Error when parsing options: " + errorMsg); } gl_extraEncodings->setCurrentNamesFromOptionsMeta(parsedOptions); @@ -494,10 +597,10 @@ const char* STDCALL syntaxBridgeLoadQmlAndParseOptions(const char* moduleName, c rbridge_setWantedCols(analysisColsTypes); - static std::string result; - result = parsedOptions.toStyledString(); - - return result.c_str(); + Json::Value status = statusBase("syntaxBridgeLoadQmlAndParseOptions"); + status["ok"] = true; + status["options"] = parsedOptions; + return statusResult(status); } const char* STDCALL syntaxBridgeAnalysisOptionsFromJaspFile(const char * filePath, int analysisNr) @@ -655,6 +758,42 @@ const char* STDCALL syntaxBridgeGetVariableNames() return result.c_str(); } +void STDCALL syntaxBridgeSetVerbose(bool verbose) +{ + gl_verbose = verbose; + Log::setDefaultDestination(verbose ? logType::cout : logType::null); + Log::setWhere(verbose ? logType::cout : logType::null); +} + +const char* STDCALL syntaxBridgeColumnDecoderSnapshot() +{ + static std::string result; + + result = columnDecoderSnapshotJson().toStyledString(); + return result.c_str(); +} + +const char* STDCALL syntaxBridgeDecodeColumnText(const char* valuesJson, const char* decoderSnapshotJson) +{ + static std::string result; + + try + { + Json::Value values = parseStringArrayJson(valuesJson); + ColumnDecoderSnapshot snapshot = columnDecoderMapFromSnapshotString(decoderSnapshotJson); + result = decodeColumnTextJson(values, snapshot).toStyledString(); + return result.c_str(); + } + catch(const std::exception & exception) + { + return statusError(statusBase("syntaxBridgeDecodeColumnText"), exception.what()); + } + catch(...) + { + return statusError(statusBase("syntaxBridgeDecodeColumnText"), "Unknown error while decoding column text."); + } +} + } // extern "C" diff --git a/SyntaxInterface/syntaxbridge_interface.h b/SyntaxInterface/syntaxbridge_interface.h index e228746288..56814dc1f6 100644 --- a/SyntaxInterface/syntaxbridge_interface.h +++ b/SyntaxInterface/syntaxbridge_interface.h @@ -63,12 +63,16 @@ SYNTAX_INTERFACE void STDCALL syntaxBridgeLoadDataSet(const SyntaxBridgeDataS SYNTAX_INTERFACE void STDCALL syntaxBridgeLoadDataSetFromJaspFile(const char * filePath, bool dbInMemory); SYNTAX_INTERFACE const char* STDCALL syntaxBridgeLoadDataSetFromJaspFileStatus(const char * filePath, bool dbInMemory); SYNTAX_INTERFACE const char* STDCALL syntaxBridgeLoadQmlAndParseOptions(const char * moduleName, const char* analysisName, const char* qmlFile, const char* options, const char* version, bool preloadData); +SYNTAX_INTERFACE const char* STDCALL syntaxBridgeLoadQmlAndParseOptionsStatus(const char * moduleName, const char* analysisName, const char* qmlFile, const char* options, const char* version, bool preloadData); SYNTAX_INTERFACE const char* STDCALL syntaxBridgeAnalysisOptionsFromJaspFile(const char * filePath, int analysisNr); SYNTAX_INTERFACE const char* STDCALL syntaxBridgeAnalysisOptionsFromJaspFileStatus(const char * filePath, int analysisNr); SYNTAX_INTERFACE const char* STDCALL syntaxBridgeGenerateModuleWrappers(const char* name); SYNTAX_INTERFACE const char* STDCALL syntaxBridgeGenerateAnalysisWrapper(const char* modulePath, const char* analysisName); SYNTAX_INTERFACE const char* STDCALL syntaxBridgeParseDescription(const char* modulePath); SYNTAX_INTERFACE const char* STDCALL syntaxBridgeGetVariableNames(); +SYNTAX_INTERFACE void STDCALL syntaxBridgeSetVerbose(bool verbose); +SYNTAX_INTERFACE const char* STDCALL syntaxBridgeColumnDecoderSnapshot(); +SYNTAX_INTERFACE const char* STDCALL syntaxBridgeDecodeColumnText(const char* valuesJson, const char* decoderSnapshotJson); } // extern "C" From 7a0fa75b8b35d3cb37ee110bf082b66bc000d79a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Barto=C5=A1?= Date: Wed, 3 Jun 2026 15:13:28 +0200 Subject: [PATCH 2/9] Expose native column encoder context --- Common/columnencoder.cpp | 16 --- Common/columnencoder.h | 3 +- Engine/jaspBase | 2 +- SyntaxInterface/syntaxbridge.cpp | 151 +++++++++++++++++------ SyntaxInterface/syntaxbridge_interface.h | 4 +- 5 files changed, 117 insertions(+), 59 deletions(-) diff --git a/Common/columnencoder.cpp b/Common/columnencoder.cpp index 0564ae496c..1015338f24 100644 --- a/Common/columnencoder.cpp +++ b/Common/columnencoder.cpp @@ -670,22 +670,6 @@ ColumnEncoder::colVec ColumnEncoder::columnNamesEncoded() return _columnEncoder ? _columnEncoder->_encodedNames : colVec(); } -ColumnEncoder::colMap ColumnEncoder::decodingMapSnapshot() -{ - columnEncoder(); - return decodingMap(); -} - -std::string ColumnEncoder::decodeAllWithMapping(const std::string & text, const ColumnEncoder::colMap & decodingMap) -{ - ColumnEncoder::colVec encodedNames; - for(const auto & keyVal : decodingMap) - encodedNames.push_back(keyVal.first); - - sortVectorBigToSmall(encodedNames); - return replaceAll(text, decodingMap, encodedNames); -} - void ColumnEncoder::_convertPreloadingDataOption(Json::Value & options, const std::string& optionName, colsPlusTypes& colTypes) { std::string optionKey = options[optionName].isMember("optionKey") ? options[optionName]["optionKey"].asString() : ""; diff --git a/Common/columnencoder.h b/Common/columnencoder.h index 04adf34667..a993985a4d 100644 --- a/Common/columnencoder.h +++ b/Common/columnencoder.h @@ -63,12 +63,11 @@ private: ColumnEncoder() { invalidateAll(); } static colVec columnNames(); static colVec columnNamesEncoded(); - static colMap decodingMapSnapshot(); - static std::string decodeAllWithMapping(const std::string & text, const colMap & decodingMap); bool shouldEncode(const std::string & in); bool shouldDecode(const std::string & in); void setCurrentNames(const colTypeMap & names); + const colTypeMap& currentNames() const { return _dataSetTypes; } void updateColumnTypesOnly(const colTypeMap & names); void setCurrentNames(const std::vector & names, bool generateTypesEncoding=true); ///< Do not use! Deprecated void setCurrentColumnTypePerName(const colTypeMap & theMap); ///< Do not use! Deprecated diff --git a/Engine/jaspBase b/Engine/jaspBase index 93f004588f..967e59397e 160000 --- a/Engine/jaspBase +++ b/Engine/jaspBase @@ -1 +1 @@ -Subproject commit 93f004588f6d56b46d23210e23bff003ae7b1ff4 +Subproject commit 967e59397ea19e1c80a2bdbf14531bb87d0d99f7 diff --git a/SyntaxInterface/syntaxbridge.cpp b/SyntaxInterface/syntaxbridge.cpp index d6c241a8f2..cad41cf3d7 100644 --- a/SyntaxInterface/syntaxbridge.cpp +++ b/SyntaxInterface/syntaxbridge.cpp @@ -154,59 +154,133 @@ static const char* statusError(Json::Value status, const std::string & error) return statusResult(status); } -static Json::Value columnDecoderSnapshotJson() +static ColumnEncoder * ensureExtraColumnEncoder() { - Json::Value snapshot(Json::objectValue); - Json::Value columns(Json::arrayValue); - const ColumnEncoder::colMap decodingMap = ColumnEncoder::decodingMapSnapshot(); + if(!gl_extraEncodings) + gl_extraEncodings = new ColumnEncoder("JaspExtraOptions_"); + + return gl_extraEncodings; +} - for(const auto & keyVal : decodingMap) +static Json::Value columnTypesToJson(const ColumnEncoder::colTypeMap & columnTypes) +{ + Json::Value columns(Json::arrayValue); + for(const auto & nameType : columnTypes) { Json::Value column(Json::objectValue); - column["encoded"] = keyVal.first; - column["decoded"] = keyVal.second; + column["name"] = nameType.first; + column["type"] = columnTypeToString(nameType.second); columns.append(column); } - snapshot["version"] = 1; - snapshot["columns"] = columns; - return snapshot; + return columns; } -struct ColumnDecoderSnapshot +static ColumnEncoder::colTypeMap columnTypesFromJson(const Json::Value & columns, const char * fieldName) { - ColumnEncoder::colMap decodingMap; - bool supplied = false; -}; + ColumnEncoder::colTypeMap columnTypes; -static ColumnDecoderSnapshot columnDecoderMapFromSnapshot(const Json::Value & snapshot) -{ - ColumnDecoderSnapshot snapshotState; - snapshotState.supplied = true; - const Json::Value & columns = snapshot["columns"]; + if(columns.isNull()) + return columnTypes; if(!columns.isArray()) - return snapshotState; + throw std::runtime_error(std::string("Column encoder context field '") + fieldName + "' must be an array."); for(const Json::Value & column : columns) - if(column.isObject() && column["encoded"].isString() && column["decoded"].isString()) - snapshotState.decodingMap[column["encoded"].asString()] = column["decoded"].asString(); + { + if(!column.isObject() || !column["name"].isString() || !column["type"].isString()) + throw std::runtime_error(std::string("Column encoder context field '") + fieldName + "' must contain objects with string 'name' and 'type' fields."); + + columnTypes[column["name"].asString()] = columnTypeFromString(column["type"].asString()); + } + + return columnTypes; +} + +static ColumnEncoder::colTypeMap currentDatasetColumnTypes() +{ + DataSet * dataSet = gl_dataBridge ? gl_dataBridge->provideAndUpdateDataSet() : nullptr; + return dataSet ? dataSet->getColumnTypesMap() : ColumnEncoder::colTypeMap(); +} + +static Json::Value columnEncoderContextJson() +{ + Json::Value context(Json::objectValue); + context["version"] = 1; + context["columns"] = columnTypesToJson(currentDatasetColumnTypes()); + context["extra"] = columnTypesToJson(gl_extraEncodings ? gl_extraEncodings->currentNames() : ColumnEncoder::colTypeMap()); + + return context; +} - return snapshotState; +struct ColumnEncoderContext +{ + ColumnEncoder::colTypeMap columns; + ColumnEncoder::colTypeMap extra; + bool supplied = false; +}; + +static ColumnEncoderContext columnEncoderContextFromJson(const Json::Value & context) +{ + if(!context.isObject()) + throw std::runtime_error("Column encoder context must be a JSON object."); + + if(!context.isMember("version") || !context["version"].isInt()) + throw std::runtime_error("Column encoder context must contain integer version 1."); + if(context["version"].asInt() != 1) + throw std::runtime_error("Unsupported column encoder context version."); + + ColumnEncoderContext encoderContext; + encoderContext.supplied = true; + encoderContext.columns = columnTypesFromJson(context["columns"], "columns"); + encoderContext.extra = columnTypesFromJson(context["extra"], "extra"); + + return encoderContext; } -static ColumnDecoderSnapshot columnDecoderMapFromSnapshotString(const char * snapshotJson) +static ColumnEncoderContext columnEncoderContextFromString(const char * contextJson) { - if(!snapshotJson || std::string(snapshotJson).empty()) - return ColumnDecoderSnapshot(); + if(!contextJson || std::string(contextJson).empty()) + return ColumnEncoderContext(); - Json::Value snapshot; + Json::Value context; Json::Reader reader; - if(!reader.parse(snapshotJson, snapshot)) - throw std::runtime_error("Could not parse column decoder snapshot JSON."); + if(!reader.parse(contextJson, context)) + throw std::runtime_error("Could not parse column encoder context JSON."); - return columnDecoderMapFromSnapshot(snapshot); + return columnEncoderContextFromJson(context); } +class ScopedColumnEncoderContext +{ +public: + ScopedColumnEncoderContext(const ColumnEncoderContext & context) + : _supplied(context.supplied) + { + if(!_supplied) + return; + + _previousColumns = ColumnEncoder::columnEncoder()->currentNames(); + _previousExtra = gl_extraEncodings ? gl_extraEncodings->currentNames() : ColumnEncoder::colTypeMap(); + + ColumnEncoder::columnEncoder()->setCurrentNames(context.columns); + ensureExtraColumnEncoder()->setCurrentNames(context.extra); + } + + ~ScopedColumnEncoderContext() + { + if(!_supplied) + return; + + ColumnEncoder::columnEncoder()->setCurrentNames(_previousColumns); + ensureExtraColumnEncoder()->setCurrentNames(_previousExtra); + } + +private: + bool _supplied = false; + ColumnEncoder::colTypeMap _previousColumns; + ColumnEncoder::colTypeMap _previousExtra; +}; + static Json::Value parseStringArrayJson(const char * valuesJson) { if(!valuesJson) @@ -222,9 +296,10 @@ static Json::Value parseStringArrayJson(const char * valuesJson) return values; } -static Json::Value decodeColumnTextJson(const Json::Value & values, const ColumnDecoderSnapshot & snapshot) +static Json::Value decodeColumnTextJson(const Json::Value & values, const ColumnEncoderContext & context) { Json::Value decodedValues(Json::arrayValue); + ScopedColumnEncoderContext scopedContext(context); for(const Json::Value & value : values) { @@ -235,7 +310,7 @@ static Json::Value decodeColumnTextJson(const Json::Value & values, const Column else if(value.isString()) { const std::string text = value.asString(); - decodedValues.append(snapshot.supplied ? ColumnEncoder::decodeAllWithMapping(text, snapshot.decodingMap) : ColumnEncoder::decodeAll(text)); + decodedValues.append(ColumnEncoder::decodeAll(text)); } else { @@ -765,23 +840,23 @@ void STDCALL syntaxBridgeSetVerbose(bool verbose) Log::setWhere(verbose ? logType::cout : logType::null); } -const char* STDCALL syntaxBridgeColumnDecoderSnapshot() +const char* STDCALL syntaxBridgeColumnEncoderContext() { static std::string result; - result = columnDecoderSnapshotJson().toStyledString(); + result = columnEncoderContextJson().toStyledString(); return result.c_str(); } -const char* STDCALL syntaxBridgeDecodeColumnText(const char* valuesJson, const char* decoderSnapshotJson) +const char* STDCALL syntaxBridgeDecodeColumnText(const char* valuesJson, const char* encoderContextJson) { static std::string result; try { Json::Value values = parseStringArrayJson(valuesJson); - ColumnDecoderSnapshot snapshot = columnDecoderMapFromSnapshotString(decoderSnapshotJson); - result = decodeColumnTextJson(values, snapshot).toStyledString(); + ColumnEncoderContext context = columnEncoderContextFromString(encoderContextJson); + result = decodeColumnTextJson(values, context).toStyledString(); return result.c_str(); } catch(const std::exception & exception) @@ -860,7 +935,7 @@ bool init(bool dbInMemory) QmlUtils::registerQmlModuleTypes(); createDataBridge(dbInMemory); - gl_extraEncodings = new ColumnEncoder("JaspExtraOptions_"); + ensureExtraColumnEncoder(); rbridge_init(gl_dataBridge, sendMessage, [](){ return false; }, gl_extraEncodings, gl_param_resultFont.c_str(), false); gl_rBridgeInitialized = true; diff --git a/SyntaxInterface/syntaxbridge_interface.h b/SyntaxInterface/syntaxbridge_interface.h index 56814dc1f6..8c531a21fd 100644 --- a/SyntaxInterface/syntaxbridge_interface.h +++ b/SyntaxInterface/syntaxbridge_interface.h @@ -71,8 +71,8 @@ SYNTAX_INTERFACE const char* STDCALL syntaxBridgeGenerateAnalysisWrapper(const SYNTAX_INTERFACE const char* STDCALL syntaxBridgeParseDescription(const char* modulePath); SYNTAX_INTERFACE const char* STDCALL syntaxBridgeGetVariableNames(); SYNTAX_INTERFACE void STDCALL syntaxBridgeSetVerbose(bool verbose); -SYNTAX_INTERFACE const char* STDCALL syntaxBridgeColumnDecoderSnapshot(); -SYNTAX_INTERFACE const char* STDCALL syntaxBridgeDecodeColumnText(const char* valuesJson, const char* decoderSnapshotJson); +SYNTAX_INTERFACE const char* STDCALL syntaxBridgeColumnEncoderContext(); +SYNTAX_INTERFACE const char* STDCALL syntaxBridgeDecodeColumnText(const char* valuesJson, const char* encoderContextJson); } // extern "C" From ed7c12e05b7772883991eaaa9ba9d1c66a2acb60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Barto=C5=A1?= Date: Wed, 3 Jun 2026 16:00:06 +0200 Subject: [PATCH 3/9] Keep encoder context with ColumnEncoder --- Common/columnencoder.h | 1 + Common/columnencodercontext.cpp | 161 +++++++++++++++++++++++++++++++ Common/columnencodercontext.h | 61 ++++++++++++ Common/log.cpp | 2 + Engine/engine.cpp | 2 +- Engine/jaspBase | 2 +- SyntaxInterface/syntaxbridge.cpp | 161 ++----------------------------- 7 files changed, 234 insertions(+), 156 deletions(-) create mode 100644 Common/columnencodercontext.cpp create mode 100644 Common/columnencodercontext.h diff --git a/Common/columnencoder.h b/Common/columnencoder.h index a993985a4d..f3216f5558 100644 --- a/Common/columnencoder.h +++ b/Common/columnencoder.h @@ -63,6 +63,7 @@ private: ColumnEncoder() { invalidateAll(); } static colVec columnNames(); static colVec columnNamesEncoded(); + static const char* extraOptionsPrefix() { return "JaspExtraOptions_"; } bool shouldEncode(const std::string & in); bool shouldDecode(const std::string & in); diff --git a/Common/columnencodercontext.cpp b/Common/columnencodercontext.cpp new file mode 100644 index 0000000000..a944d618f0 --- /dev/null +++ b/Common/columnencodercontext.cpp @@ -0,0 +1,161 @@ +// +// Copyright (C) 2013-2025 University of Amsterdam +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +#include "columnencodercontext.h" + +#include + +static Json::Value columnTypesToJson(const ColumnEncoder::colTypeMap & columnTypes) +{ + Json::Value columns(Json::arrayValue); + for(const auto & nameType : columnTypes) + { + Json::Value column(Json::objectValue); + column["name"] = nameType.first; + column["type"] = columnTypeToString(nameType.second); + columns.append(column); + } + + return columns; +} + +static ColumnEncoder::colTypeMap columnTypesFromJson(const Json::Value & columns, const char * fieldName) +{ + ColumnEncoder::colTypeMap columnTypes; + + if(columns.isNull()) + return columnTypes; + if(!columns.isArray()) + throw std::runtime_error(std::string("Column encoder context field '") + fieldName + "' must be an array."); + + for(const Json::Value & column : columns) + { + if(!column.isObject() || !column["name"].isString() || !column["type"].isString()) + throw std::runtime_error(std::string("Column encoder context field '") + fieldName + "' must contain objects with string 'name' and 'type' fields."); + + columnTypes[column["name"].asString()] = columnTypeFromString(column["type"].asString()); + } + + return columnTypes; +} + +static Json::Value parseStringArrayJson(const char * valuesJson) +{ + if(!valuesJson) + throw std::runtime_error("Cannot decode column text from a null JSON payload."); + + Json::Value values; + Json::Reader reader; + if(!reader.parse(valuesJson, values)) + throw std::runtime_error("Could not parse column text JSON payload."); + if(!values.isArray()) + throw std::runtime_error("Column text JSON payload must be an array."); + + return values; +} + +ColumnEncoderContext::ColumnEncoderContext(const ColumnEncoder::colTypeMap & columns, const ColumnEncoder::colTypeMap & extra) + : _columns(columns), _extra(extra), _supplied(true) +{ +} + +ColumnEncoderContext ColumnEncoderContext::fromJson(const Json::Value & context) +{ + if(!context.isObject()) + throw std::runtime_error("Column encoder context must be a JSON object."); + + if(!context.isMember("version") || !context["version"].isInt()) + throw std::runtime_error("Column encoder context must contain integer version 1."); + if(context["version"].asInt() != Version) + throw std::runtime_error("Unsupported column encoder context version."); + + return ColumnEncoderContext( + columnTypesFromJson(context["columns"], "columns"), + columnTypesFromJson(context["extra"], "extra") + ); +} + +ColumnEncoderContext ColumnEncoderContext::fromJsonString(const char * contextJson) +{ + if(!contextJson || std::string(contextJson).empty()) + return ColumnEncoderContext(); + + Json::Value context; + Json::Reader reader; + if(!reader.parse(contextJson, context)) + throw std::runtime_error("Could not parse column encoder context JSON."); + + return fromJson(context); +} + +Json::Value ColumnEncoderContext::toJson() const +{ + Json::Value context(Json::objectValue); + context["version"] = Version; + context["columns"] = columnTypesToJson(_columns); + context["extra"] = columnTypesToJson(_extra); + + return context; +} + +ScopedColumnEncoderContext::ScopedColumnEncoderContext(const ColumnEncoderContext & context, ColumnEncoder & extraEncoder) + : _supplied(context.supplied()), _extraEncoder(extraEncoder) +{ + if(!_supplied) + return; + + _previousColumns = ColumnEncoder::columnEncoder()->currentNames(); + _previousExtra = _extraEncoder.currentNames(); + + ColumnEncoder::columnEncoder()->setCurrentNames(context.columns()); + _extraEncoder.setCurrentNames(context.extra()); +} + +ScopedColumnEncoderContext::~ScopedColumnEncoderContext() +{ + if(!_supplied) + return; + + ColumnEncoder::columnEncoder()->setCurrentNames(_previousColumns); + _extraEncoder.setCurrentNames(_previousExtra); +} + +Json::Value decodeColumnTextJson(const char * valuesJson, const char * encoderContextJson, ColumnEncoder & extraEncoder) +{ + Json::Value values = parseStringArrayJson(valuesJson); + ColumnEncoderContext context = ColumnEncoderContext::fromJsonString(encoderContextJson); + Json::Value decodedValues(Json::arrayValue); + ScopedColumnEncoderContext scopedContext(context, extraEncoder); + + for(const Json::Value & value : values) + { + if(value.isNull()) + { + decodedValues.append(Json::Value()); + } + else if(value.isString()) + { + decodedValues.append(ColumnEncoder::decodeAll(value.asString())); + } + else + { + throw std::runtime_error("Column text JSON payload must contain only strings or null values."); + } + } + + return decodedValues; +} diff --git a/Common/columnencodercontext.h b/Common/columnencodercontext.h new file mode 100644 index 0000000000..57f84065c5 --- /dev/null +++ b/Common/columnencodercontext.h @@ -0,0 +1,61 @@ +// +// Copyright (C) 2013-2025 University of Amsterdam +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// + +#ifndef COLUMNENCODERCONTEXT_H +#define COLUMNENCODERCONTEXT_H + +#include "columnencoder.h" + +class ColumnEncoderContext +{ +public: + static constexpr int Version = 1; + + ColumnEncoderContext() = default; + ColumnEncoderContext(const ColumnEncoder::colTypeMap & columns, const ColumnEncoder::colTypeMap & extra); + + static ColumnEncoderContext fromJson(const Json::Value & context); + static ColumnEncoderContext fromJsonString(const char * contextJson); + + Json::Value toJson() const; + + const ColumnEncoder::colTypeMap& columns() const { return _columns; } + const ColumnEncoder::colTypeMap& extra() const { return _extra; } + bool supplied() const { return _supplied; } + +private: + ColumnEncoder::colTypeMap _columns; + ColumnEncoder::colTypeMap _extra; + bool _supplied = false; +}; + +class ScopedColumnEncoderContext +{ +public: + ScopedColumnEncoderContext(const ColumnEncoderContext & context, ColumnEncoder & extraEncoder); + ~ScopedColumnEncoderContext(); + +private: + bool _supplied = false; + ColumnEncoder & _extraEncoder; + ColumnEncoder::colTypeMap _previousColumns; + ColumnEncoder::colTypeMap _previousExtra; +}; + +Json::Value decodeColumnTextJson(const char * valuesJson, const char * encoderContextJson, ColumnEncoder & extraEncoder); + +#endif // COLUMNENCODERCONTEXT_H diff --git a/Common/log.cpp b/Common/log.cpp index 22d99820d8..b42efa1dd6 100644 --- a/Common/log.cpp +++ b/Common/log.cpp @@ -17,6 +17,8 @@ std::ofstream Log::_logFile;// = bofstream(); namespace { + // Used before Log::init() receives an application-owned null stream. The + // default logType::null destination must discard output, not write to cout. class NullLogBuffer : public std::streambuf { protected: diff --git a/Engine/engine.cpp b/Engine/engine.cpp index ca54c7d57f..97e8c9a7b2 100644 --- a/Engine/engine.cpp +++ b/Engine/engine.cpp @@ -76,7 +76,7 @@ Engine::Engine(int slaveNo, unsigned long parentPID) assert(_EngineInstance == NULL); _EngineInstance = this; - _extraEncodings = new ColumnEncoder("JaspExtraOptions_"); + _extraEncodings = new ColumnEncoder(ColumnEncoder::extraOptionsPrefix()); } void Engine::initialize() diff --git a/Engine/jaspBase b/Engine/jaspBase index 967e59397e..93f004588f 160000 --- a/Engine/jaspBase +++ b/Engine/jaspBase @@ -1 +1 @@ -Subproject commit 967e59397ea19e1c80a2bdbf14531bb87d0d99f7 +Subproject commit 93f004588f6d56b46d23210e23bff003ae7b1ff4 diff --git a/SyntaxInterface/syntaxbridge.cpp b/SyntaxInterface/syntaxbridge.cpp index cad41cf3d7..6744627b2c 100644 --- a/SyntaxInterface/syntaxbridge.cpp +++ b/SyntaxInterface/syntaxbridge.cpp @@ -44,10 +44,10 @@ #include "archivereader.h" #include "databaseinterface.h" #include "columnencoder.h" +#include "columnencodercontext.h" #include #include -#include #include #ifdef USE_QT_STATIC_LIBS @@ -157,45 +157,11 @@ static const char* statusError(Json::Value status, const std::string & error) static ColumnEncoder * ensureExtraColumnEncoder() { if(!gl_extraEncodings) - gl_extraEncodings = new ColumnEncoder("JaspExtraOptions_"); + gl_extraEncodings = new ColumnEncoder(ColumnEncoder::extraOptionsPrefix()); return gl_extraEncodings; } -static Json::Value columnTypesToJson(const ColumnEncoder::colTypeMap & columnTypes) -{ - Json::Value columns(Json::arrayValue); - for(const auto & nameType : columnTypes) - { - Json::Value column(Json::objectValue); - column["name"] = nameType.first; - column["type"] = columnTypeToString(nameType.second); - columns.append(column); - } - - return columns; -} - -static ColumnEncoder::colTypeMap columnTypesFromJson(const Json::Value & columns, const char * fieldName) -{ - ColumnEncoder::colTypeMap columnTypes; - - if(columns.isNull()) - return columnTypes; - if(!columns.isArray()) - throw std::runtime_error(std::string("Column encoder context field '") + fieldName + "' must be an array."); - - for(const Json::Value & column : columns) - { - if(!column.isObject() || !column["name"].isString() || !column["type"].isString()) - throw std::runtime_error(std::string("Column encoder context field '") + fieldName + "' must contain objects with string 'name' and 'type' fields."); - - columnTypes[column["name"].asString()] = columnTypeFromString(column["type"].asString()); - } - - return columnTypes; -} - static ColumnEncoder::colTypeMap currentDatasetColumnTypes() { DataSet * dataSet = gl_dataBridge ? gl_dataBridge->provideAndUpdateDataSet() : nullptr; @@ -204,121 +170,10 @@ static ColumnEncoder::colTypeMap currentDatasetColumnTypes() static Json::Value columnEncoderContextJson() { - Json::Value context(Json::objectValue); - context["version"] = 1; - context["columns"] = columnTypesToJson(currentDatasetColumnTypes()); - context["extra"] = columnTypesToJson(gl_extraEncodings ? gl_extraEncodings->currentNames() : ColumnEncoder::colTypeMap()); - - return context; -} - -struct ColumnEncoderContext -{ - ColumnEncoder::colTypeMap columns; - ColumnEncoder::colTypeMap extra; - bool supplied = false; -}; - -static ColumnEncoderContext columnEncoderContextFromJson(const Json::Value & context) -{ - if(!context.isObject()) - throw std::runtime_error("Column encoder context must be a JSON object."); - - if(!context.isMember("version") || !context["version"].isInt()) - throw std::runtime_error("Column encoder context must contain integer version 1."); - if(context["version"].asInt() != 1) - throw std::runtime_error("Unsupported column encoder context version."); - - ColumnEncoderContext encoderContext; - encoderContext.supplied = true; - encoderContext.columns = columnTypesFromJson(context["columns"], "columns"); - encoderContext.extra = columnTypesFromJson(context["extra"], "extra"); - - return encoderContext; -} - -static ColumnEncoderContext columnEncoderContextFromString(const char * contextJson) -{ - if(!contextJson || std::string(contextJson).empty()) - return ColumnEncoderContext(); - - Json::Value context; - Json::Reader reader; - if(!reader.parse(contextJson, context)) - throw std::runtime_error("Could not parse column encoder context JSON."); - - return columnEncoderContextFromJson(context); -} - -class ScopedColumnEncoderContext -{ -public: - ScopedColumnEncoderContext(const ColumnEncoderContext & context) - : _supplied(context.supplied) - { - if(!_supplied) - return; - - _previousColumns = ColumnEncoder::columnEncoder()->currentNames(); - _previousExtra = gl_extraEncodings ? gl_extraEncodings->currentNames() : ColumnEncoder::colTypeMap(); - - ColumnEncoder::columnEncoder()->setCurrentNames(context.columns); - ensureExtraColumnEncoder()->setCurrentNames(context.extra); - } - - ~ScopedColumnEncoderContext() - { - if(!_supplied) - return; - - ColumnEncoder::columnEncoder()->setCurrentNames(_previousColumns); - ensureExtraColumnEncoder()->setCurrentNames(_previousExtra); - } - -private: - bool _supplied = false; - ColumnEncoder::colTypeMap _previousColumns; - ColumnEncoder::colTypeMap _previousExtra; -}; - -static Json::Value parseStringArrayJson(const char * valuesJson) -{ - if(!valuesJson) - throw std::runtime_error("Cannot decode column text from a null JSON payload."); - - Json::Value values; - Json::Reader reader; - if(!reader.parse(valuesJson, values)) - throw std::runtime_error("Could not parse column text JSON payload."); - if(!values.isArray()) - throw std::runtime_error("Column text JSON payload must be an array."); - - return values; -} - -static Json::Value decodeColumnTextJson(const Json::Value & values, const ColumnEncoderContext & context) -{ - Json::Value decodedValues(Json::arrayValue); - ScopedColumnEncoderContext scopedContext(context); - - for(const Json::Value & value : values) - { - if(value.isNull()) - { - decodedValues.append(Json::Value()); - } - else if(value.isString()) - { - const std::string text = value.asString(); - decodedValues.append(ColumnEncoder::decodeAll(text)); - } - else - { - throw std::runtime_error("Column text JSON payload must contain only strings or null values."); - } - } - - return decodedValues; + return ColumnEncoderContext( + currentDatasetColumnTypes(), + gl_extraEncodings ? gl_extraEncodings->currentNames() : ColumnEncoder::colTypeMap() + ).toJson(); } static Json::Value analysisOptionsStatus(const char * filePath, int analysisNr) @@ -854,9 +709,7 @@ const char* STDCALL syntaxBridgeDecodeColumnText(const char* valuesJson, const c try { - Json::Value values = parseStringArrayJson(valuesJson); - ColumnEncoderContext context = columnEncoderContextFromString(encoderContextJson); - result = decodeColumnTextJson(values, context).toStyledString(); + result = decodeColumnTextJson(valuesJson, encoderContextJson, *ensureExtraColumnEncoder()).toStyledString(); return result.c_str(); } catch(const std::exception & exception) From c21e37f98f5b67efb64af3e11e3ce66c751a6db3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Barto=C5=A1?= Date: Wed, 3 Jun 2026 18:38:53 +0200 Subject: [PATCH 4/9] Use ColumnEncoder decodeJson for scoped replay --- Common/columnencodercontext.cpp | 35 +++++++--------------------- Common/columnencodercontext.h | 2 +- Common/log.cpp | 16 +------------ SyntaxInterface/syntaxbridge.cpp | 40 +++++++++++++++++++++++++++++--- 4 files changed, 48 insertions(+), 45 deletions(-) diff --git a/Common/columnencodercontext.cpp b/Common/columnencodercontext.cpp index a944d618f0..d31cbad17d 100644 --- a/Common/columnencodercontext.cpp +++ b/Common/columnencodercontext.cpp @@ -53,19 +53,17 @@ static ColumnEncoder::colTypeMap columnTypesFromJson(const Json::Value & columns return columnTypes; } -static Json::Value parseStringArrayJson(const char * valuesJson) +static Json::Value parsePayloadJson(const char * payloadJson) { - if(!valuesJson) + if(!payloadJson) throw std::runtime_error("Cannot decode column text from a null JSON payload."); - Json::Value values; + Json::Value payload; Json::Reader reader; - if(!reader.parse(valuesJson, values)) + if(!reader.parse(payloadJson, payload)) throw std::runtime_error("Could not parse column text JSON payload."); - if(!values.isArray()) - throw std::runtime_error("Column text JSON payload must be an array."); - return values; + return payload; } ColumnEncoderContext::ColumnEncoderContext(const ColumnEncoder::colTypeMap & columns, const ColumnEncoder::colTypeMap & extra) @@ -134,28 +132,13 @@ ScopedColumnEncoderContext::~ScopedColumnEncoderContext() _extraEncoder.setCurrentNames(_previousExtra); } -Json::Value decodeColumnTextJson(const char * valuesJson, const char * encoderContextJson, ColumnEncoder & extraEncoder) +Json::Value decodeColumnJson(const char * payloadJson, const char * encoderContextJson, ColumnEncoder & extraEncoder, bool replaceNames) { - Json::Value values = parseStringArrayJson(valuesJson); + Json::Value payload = parsePayloadJson(payloadJson); ColumnEncoderContext context = ColumnEncoderContext::fromJsonString(encoderContextJson); - Json::Value decodedValues(Json::arrayValue); ScopedColumnEncoderContext scopedContext(context, extraEncoder); - for(const Json::Value & value : values) - { - if(value.isNull()) - { - decodedValues.append(Json::Value()); - } - else if(value.isString()) - { - decodedValues.append(ColumnEncoder::decodeAll(value.asString())); - } - else - { - throw std::runtime_error("Column text JSON payload must contain only strings or null values."); - } - } + ColumnEncoder::decodeJson(payload, replaceNames); - return decodedValues; + return payload; } diff --git a/Common/columnencodercontext.h b/Common/columnencodercontext.h index 57f84065c5..181c019edd 100644 --- a/Common/columnencodercontext.h +++ b/Common/columnencodercontext.h @@ -56,6 +56,6 @@ class ScopedColumnEncoderContext ColumnEncoder::colTypeMap _previousExtra; }; -Json::Value decodeColumnTextJson(const char * valuesJson, const char * encoderContextJson, ColumnEncoder & extraEncoder); +Json::Value decodeColumnJson(const char * payloadJson, const char * encoderContextJson, ColumnEncoder & extraEncoder, bool replaceNames = true); #endif // COLUMNENCODERCONTEXT_H diff --git a/Common/log.cpp b/Common/log.cpp index b42efa1dd6..af0d354766 100644 --- a/Common/log.cpp +++ b/Common/log.cpp @@ -15,21 +15,7 @@ std::ofstream Log::_logFile;// = bofstream(); -namespace -{ - // Used before Log::init() receives an application-owned null stream. The - // default logType::null destination must discard output, not write to cout. - class NullLogBuffer : public std::streambuf - { - protected: - int overflow(int c) override { return traits_type::not_eof(c); } - }; - - NullLogBuffer nullLogBuffer; - std::ostream nullLogStream(&nullLogBuffer); -} - -std::ostream* Log::_nullStream = &nullLogStream; +std::ostream* Log::_nullStream = &std::cout; std::string Log::logFileNameBase = ""; diff --git a/SyntaxInterface/syntaxbridge.cpp b/SyntaxInterface/syntaxbridge.cpp index 6744627b2c..ff78912add 100644 --- a/SyntaxInterface/syntaxbridge.cpp +++ b/SyntaxInterface/syntaxbridge.cpp @@ -46,6 +46,8 @@ #include "columnencoder.h" #include "columnencodercontext.h" +#include +#include #include #include @@ -87,6 +89,31 @@ static std::string gl_param_resultFont = "freesans,sans-serif"; #endif +namespace +{ + class SyntaxBridgeNullBuffer : public std::streambuf + { + protected: + int overflow(int c) override { return traits_type::not_eof(c); } + }; + + SyntaxBridgeNullBuffer gl_nullLogBuffer; + std::ostream gl_nullLogStream(&gl_nullLogBuffer); + bool gl_loggingInitialized = false; +} + +static void configureBridgeLogging(bool verbose) +{ + if(!gl_loggingInitialized) + { + Log::init(&gl_nullLogStream); + gl_loggingInitialized = true; + } + + Log::setDefaultDestination(verbose ? logType::cout : logType::null); + Log::setWhere(verbose ? logType::cout : logType::null); +} + static bool readJaspJsonEntry(Json::Value & root, const char * filePath, const char * entry, std::string * error = nullptr) { try @@ -150,6 +177,7 @@ static const char* statusError(Json::Value status, const std::string & error) { status["ok"] = false; status["error"] = error; + configureBridgeLogging(gl_verbose); Log::log() << error << std::endl; return statusResult(status); } @@ -535,6 +563,8 @@ const char* STDCALL syntaxBridgeLoadQmlAndParseOptionsStatus(const char* moduleN const char* STDCALL syntaxBridgeAnalysisOptionsFromJaspFile(const char * filePath, int analysisNr) { + configureBridgeLogging(gl_verbose); + static std::string result; result = ""; @@ -552,6 +582,8 @@ const char* STDCALL syntaxBridgeAnalysisOptionsFromJaspFile(const char * filePat const char* STDCALL syntaxBridgeAnalysisOptionsFromJaspFileStatus(const char * filePath, int analysisNr) { + configureBridgeLogging(gl_verbose); + Json::Value status = analysisOptionsStatus(filePath, analysisNr); if (!status["ok"].asBool() && status.isMember("error")) Log::log() << status["error"].asString() << std::endl; @@ -691,8 +723,7 @@ const char* STDCALL syntaxBridgeGetVariableNames() void STDCALL syntaxBridgeSetVerbose(bool verbose) { gl_verbose = verbose; - Log::setDefaultDestination(verbose ? logType::cout : logType::null); - Log::setWhere(verbose ? logType::cout : logType::null); + configureBridgeLogging(verbose); } const char* STDCALL syntaxBridgeColumnEncoderContext() @@ -709,7 +740,8 @@ const char* STDCALL syntaxBridgeDecodeColumnText(const char* valuesJson, const c try { - result = decodeColumnTextJson(valuesJson, encoderContextJson, *ensureExtraColumnEncoder()).toStyledString(); + configureBridgeLogging(gl_verbose); + result = decodeColumnJson(valuesJson, encoderContextJson, *ensureExtraColumnEncoder()).toStyledString(); return result.c_str(); } catch(const std::exception & exception) @@ -748,6 +780,8 @@ void sendMessage(const char * msg) bool init(bool dbInMemory) { + configureBridgeLogging(gl_verbose); + if (gl_initialized) return true; gl_initialized = true; gl_initializedDbInMemory = dbInMemory; From 3c0575ce696183cfca994edab227ca270060b4c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Barto=C5=A1?= Date: Wed, 3 Jun 2026 22:30:15 +0200 Subject: [PATCH 5/9] Move extra option encodings into DataBridge --- Common/columnencoder.h | 1 - CommonData/databridge.cpp | 1 + CommonData/databridge.h | 10 +++++++++ CommonData/rbridge.cpp | 10 ++++----- CommonData/rbridge.h | 2 +- Engine/engine.cpp | 6 ++--- Engine/engine.h | 1 - SyntaxInterface/syntaxbridge.cpp | 38 ++++++++++++++++---------------- 8 files changed, 37 insertions(+), 32 deletions(-) diff --git a/Common/columnencoder.h b/Common/columnencoder.h index f3216f5558..a993985a4d 100644 --- a/Common/columnencoder.h +++ b/Common/columnencoder.h @@ -63,7 +63,6 @@ private: ColumnEncoder() { invalidateAll(); } static colVec columnNames(); static colVec columnNamesEncoded(); - static const char* extraOptionsPrefix() { return "JaspExtraOptions_"; } bool shouldEncode(const std::string & in); bool shouldDecode(const std::string & in); diff --git a/CommonData/databridge.cpp b/CommonData/databridge.cpp index dfb51a11bb..9bc78931ed 100644 --- a/CommonData/databridge.cpp +++ b/CommonData/databridge.cpp @@ -23,6 +23,7 @@ #include "timers.h" DataBridge::DataBridge(unsigned long sessionID, bool useMemory) + : _extraEncodings(new ColumnEncoder(ExtraOptionsPrefix)) { JASPTIMER_START(TempFiles Attach); TempFiles::attach(sessionID); diff --git a/CommonData/databridge.h b/CommonData/databridge.h index 0f0c2b6634..e1d09f1507 100644 --- a/CommonData/databridge.h +++ b/CommonData/databridge.h @@ -21,6 +21,10 @@ #include "dataset.h" +#include + +class ColumnEncoder; + class DataBridge { public: @@ -44,6 +48,8 @@ class DataBridge void provideSpecificFileName( const std::string & specificName, std::string & root, std::string & relativePath); int dataSetRowCount() { return static_cast(provideAndUpdateDataSet()->rowCount()); } void updateOptionsAccordingToMeta(Json::Value & options); + ColumnEncoder * extraEncodings() { return _extraEncodings.get(); } + const ColumnEncoder * extraEncodings() const { return _extraEncodings.get(); } protected: bool isColumnNameOk(const std::string & columnName); @@ -54,6 +60,10 @@ class DataBridge DatabaseInterface * _db = nullptr; int _analysisId = -1; std::function _datasetProvidedCallback; + +private: + static constexpr const char * ExtraOptionsPrefix = "JaspExtraOptions_"; + std::unique_ptr _extraEncodings; }; #endif // DATABRIDGE_H diff --git a/CommonData/rbridge.cpp b/CommonData/rbridge.cpp index d3df6ed30e..e2fa2e4e92 100644 --- a/CommonData/rbridge.cpp +++ b/CommonData/rbridge.cpp @@ -65,12 +65,12 @@ void rbridge_setDataBridge(DataBridge * dataBridge) { data_bridge = dataBridge; rbridge_dataSet = nullptr; + extraEncodings = dataBridge ? dataBridge->extraEncodings() : nullptr; } void rbridge_clearDataBridge() { - data_bridge = nullptr; - rbridge_dataSet = nullptr; + rbridge_setDataBridge(nullptr); } const std::string jaspBaseDistributionSamplersR = @@ -102,14 +102,12 @@ const std::string jaspBaseTransformPowerR = #include "jaspBase_transformPower.h" ; -void rbridge_init(DataBridge * dataBridge, sendFuncDef sendToDesktopFunction, pollMessagesFuncDef pollMessagesFunction, ColumnEncoder * extraEncoder, const char * resultFont, bool insideJasp) +void rbridge_init(DataBridge * dataBridge, sendFuncDef sendToDesktopFunction, pollMessagesFuncDef pollMessagesFunction, const char * resultFont, bool insideJasp) { JASPTIMER_SCOPE(rbridge_init); + Log::log() << "Setting DataBridge and extraEncodings." << std::endl; rbridge_setDataBridge(dataBridge); - - Log::log() << "Setting extraEncodings." << std::endl; - extraEncodings = extraEncoder; Log::log() << "Collecting RBridgeCallBacks." << std::endl; RBridgeCallBacks callbacks = { diff --git a/CommonData/rbridge.h b/CommonData/rbridge.h index 997e23fbd6..a29f9b8ce3 100644 --- a/CommonData/rbridge.h +++ b/CommonData/rbridge.h @@ -82,7 +82,7 @@ extern "C" { void rbridge_setDataBridge(DataBridge * dataBridge); void rbridge_clearDataBridge(); - void rbridge_init(DataBridge * dataBridge, sendFuncDef sendToDesktopFunction, pollMessagesFuncDef pollMessagesFunction, ColumnEncoder * encoder, const char * resultFont, bool insideJasp = true); + void rbridge_init(DataBridge * dataBridge, sendFuncDef sendToDesktopFunction, pollMessagesFuncDef pollMessagesFunction, const char * resultFont, bool insideJasp = true); void rbridge_memoryCleaning(); diff --git a/Engine/engine.cpp b/Engine/engine.cpp index 97e8c9a7b2..4ab1312ec3 100644 --- a/Engine/engine.cpp +++ b/Engine/engine.cpp @@ -75,8 +75,6 @@ Engine::Engine(int slaveNo, unsigned long parentPID) JASPTIMER_SCOPE(Engine Constructor); assert(_EngineInstance == NULL); _EngineInstance = this; - - _extraEncodings = new ColumnEncoder(ColumnEncoder::extraOptionsPrefix()); } void Engine::initialize() @@ -88,7 +86,7 @@ void Engine::initialize() std::string memoryName = "JASP-IPC-" + std::to_string(_parentPID); _channel = new IPCChannel(memoryName, _engineNum, true); - rbridge_init(this, SendFunctionForJaspresults, PollMessagesFunctionForJaspResults, _extraEncodings, _resultFont.c_str()); + rbridge_init(this, SendFunctionForJaspresults, PollMessagesFunctionForJaspResults, _resultFont.c_str()); Log::log() << "rbridge_init completed" << std::endl; @@ -679,7 +677,7 @@ void Engine::receiveAnalysisMessage(const Json::Value & jsonRequest) Log::log(false) << _analysisTitle << " with ID " << _analysisId << std::endl; - _extraEncodings->setCurrentNamesFromOptionsMeta(optionsEnc); + extraEncodings()->setCurrentNamesFromOptionsMeta(optionsEnc); _analysisOptions = optionsEnc; //store unencoded } diff --git a/Engine/engine.h b/Engine/engine.h index f29bfc099b..6963263776 100644 --- a/Engine/engine.h +++ b/Engine/engine.h @@ -97,7 +97,6 @@ class Engine : public DataBridge const int _engineNum; const unsigned long _parentPID; IPCChannel * _channel = nullptr; - ColumnEncoder * _extraEncodings = nullptr; engineState _engineState = engineState::initializing, _lastRequest = engineState::initializing; Status _analysisStatus = Status::empty; diff --git a/SyntaxInterface/syntaxbridge.cpp b/SyntaxInterface/syntaxbridge.cpp index ff78912add..c0dfdfc6d0 100644 --- a/SyntaxInterface/syntaxbridge.cpp +++ b/SyntaxInterface/syntaxbridge.cpp @@ -67,7 +67,6 @@ static bool gl_jaspBaseInitialized = false; static QGuiApplication * gl_application = nullptr; static QQmlEngine * gl_qmlEngine = nullptr; static DataBridge * gl_dataBridge = nullptr; -static ColumnEncoder * gl_extraEncodings = nullptr; static QMap > gl_qmlFormMap; static int gl_applicationArgc = 0; static std::vector gl_applicationArgvStorage; @@ -182,12 +181,18 @@ static const char* statusError(Json::Value status, const std::string & error) return statusResult(status); } -static ColumnEncoder * ensureExtraColumnEncoder() +static ColumnEncoder * extraColumnEncoder() { - if(!gl_extraEncodings) - gl_extraEncodings = new ColumnEncoder(ColumnEncoder::extraOptionsPrefix()); + return gl_dataBridge ? gl_dataBridge->extraEncodings() : nullptr; +} + +static ColumnEncoder & requireExtraColumnEncoder() +{ + ColumnEncoder * encoder = extraColumnEncoder(); + if(!encoder) + throw std::runtime_error("Cannot access extra option encodings without an initialized DataBridge."); - return gl_extraEncodings; + return *encoder; } static ColumnEncoder::colTypeMap currentDatasetColumnTypes() @@ -198,9 +203,11 @@ static ColumnEncoder::colTypeMap currentDatasetColumnTypes() static Json::Value columnEncoderContextJson() { + ColumnEncoder * extraEncoder = extraColumnEncoder(); + return ColumnEncoderContext( currentDatasetColumnTypes(), - gl_extraEncodings ? gl_extraEncodings->currentNames() : ColumnEncoder::colTypeMap() + extraEncoder ? extraEncoder->currentNames() : ColumnEncoder::colTypeMap() ).toJson(); } @@ -254,8 +261,8 @@ static void clearRequestedDataState() ColumnEncoder::colTypeMap noColumns; ColumnEncoder::columnEncoder()->setCurrentNames(noColumns); - if (gl_extraEncodings) - gl_extraEncodings->setCurrentNames(noColumns); + if (ColumnEncoder * encoder = extraColumnEncoder()) + encoder->setCurrentNames(noColumns); } static void clearDataBridgeState() @@ -367,12 +374,6 @@ void STDCALL syntaxBridgeShutdown() syntaxBridgeClearQmlState(); clearDataBridgeState(); - if (gl_extraEncodings) - { - delete gl_extraEncodings; - gl_extraEncodings = nullptr; - } - if (gl_initialized) { DataSetProvider * provider = DataSetProvider::getProvider(gl_initializedDbInMemory, false, gl_application); @@ -549,7 +550,7 @@ const char* STDCALL syntaxBridgeLoadQmlAndParseOptionsStatus(const char* moduleN return statusError(statusBase("syntaxBridgeLoadQmlAndParseOptions"), "Error when parsing options: " + errorMsg); } - gl_extraEncodings->setCurrentNamesFromOptionsMeta(parsedOptions); + gl_dataBridge->extraEncodings()->setCurrentNamesFromOptionsMeta(parsedOptions); gl_dataBridge->updateOptionsAccordingToMeta(parsedOptions); ColumnEncoder::colsPlusTypes analysisColsTypes = ColumnEncoder::encodeColumnNamesinOptions(parsedOptions, preloadData); @@ -741,7 +742,7 @@ const char* STDCALL syntaxBridgeDecodeColumnText(const char* valuesJson, const c try { configureBridgeLogging(gl_verbose); - result = decodeColumnJson(valuesJson, encoderContextJson, *ensureExtraColumnEncoder()).toStyledString(); + result = decodeColumnJson(valuesJson, encoderContextJson, requireExtraColumnEncoder()).toStyledString(); return result.c_str(); } catch(const std::exception & exception) @@ -822,9 +823,8 @@ bool init(bool dbInMemory) QmlUtils::registerQmlModuleTypes(); createDataBridge(dbInMemory); - ensureExtraColumnEncoder(); - rbridge_init(gl_dataBridge, sendMessage, [](){ return false; }, gl_extraEncodings, gl_param_resultFont.c_str(), false); + rbridge_init(gl_dataBridge, sendMessage, [](){ return false; }, gl_param_resultFont.c_str(), false); gl_rBridgeInitialized = true; return true; @@ -835,7 +835,7 @@ void ensureRBridgeInitialized() if (gl_rBridgeInitialized) return; - rbridge_init(gl_dataBridge, sendMessage, [](){ return false; }, gl_extraEncodings, gl_param_resultFont.c_str(), false); + rbridge_init(gl_dataBridge, sendMessage, [](){ return false; }, gl_param_resultFont.c_str(), false); gl_rBridgeInitialized = true; } From 647fde916a5dabdcbc8c4e49d2e4010faa7f14cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Barto=C5=A1?= Date: Thu, 4 Jun 2026 09:16:43 +0200 Subject: [PATCH 6/9] Use existing Boost null stream in SyntaxInterface --- SyntaxInterface/CMakeLists.txt | 4 ++++ SyntaxInterface/syntaxbridge.cpp | 15 ++++----------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/SyntaxInterface/CMakeLists.txt b/SyntaxInterface/CMakeLists.txt index 756a95fd74..ff46fc6c43 100644 --- a/SyntaxInterface/CMakeLists.txt +++ b/SyntaxInterface/CMakeLists.txt @@ -48,6 +48,10 @@ target_compile_definitions( JASP_R_INTERFACE_LIBRARY ) +if(MSVC) + target_compile_options(SyntaxInterface PRIVATE /EHsc) +endif() + if(USE_QT_STATIC_LIBS) target_compile_definitions( SyntaxInterface diff --git a/SyntaxInterface/syntaxbridge.cpp b/SyntaxInterface/syntaxbridge.cpp index c0dfdfc6d0..5dcadd99b2 100644 --- a/SyntaxInterface/syntaxbridge.cpp +++ b/SyntaxInterface/syntaxbridge.cpp @@ -46,8 +46,8 @@ #include "columnencoder.h" #include "columnencodercontext.h" -#include -#include +#include "boost/iostreams/stream.hpp" +#include #include #include @@ -90,15 +90,8 @@ static std::string gl_param_resultFont = namespace { - class SyntaxBridgeNullBuffer : public std::streambuf - { - protected: - int overflow(int c) override { return traits_type::not_eof(c); } - }; - - SyntaxBridgeNullBuffer gl_nullLogBuffer; - std::ostream gl_nullLogStream(&gl_nullLogBuffer); - bool gl_loggingInitialized = false; + boost::iostreams::stream gl_nullLogStream((boost::iostreams::null_sink())); + bool gl_loggingInitialized = false; } static void configureBridgeLogging(bool verbose) From 1766f05e3ab8cc59e680870eba3eddb89bdf908c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Barto=C5=A1?= Date: Thu, 4 Jun 2026 14:40:41 +0200 Subject: [PATCH 7/9] Centralize SyntaxInterface logging setup --- SyntaxInterface/syntaxbridge.cpp | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/SyntaxInterface/syntaxbridge.cpp b/SyntaxInterface/syntaxbridge.cpp index 5dcadd99b2..ed78a564ae 100644 --- a/SyntaxInterface/syntaxbridge.cpp +++ b/SyntaxInterface/syntaxbridge.cpp @@ -169,8 +169,6 @@ static const char* statusError(Json::Value status, const std::string & error) { status["ok"] = false; status["error"] = error; - configureBridgeLogging(gl_verbose); - Log::log() << error << std::endl; return statusResult(status); } @@ -557,18 +555,12 @@ const char* STDCALL syntaxBridgeLoadQmlAndParseOptionsStatus(const char* moduleN const char* STDCALL syntaxBridgeAnalysisOptionsFromJaspFile(const char * filePath, int analysisNr) { - configureBridgeLogging(gl_verbose); - static std::string result; result = ""; Json::Value status = analysisOptionsStatus(filePath, analysisNr); if (!status["ok"].asBool()) - { - if (status.isMember("error")) - Log::log() << status["error"].asString() << std::endl; return result.c_str(); - } result = status["options"].toStyledString(); return result.c_str(); @@ -576,11 +568,7 @@ const char* STDCALL syntaxBridgeAnalysisOptionsFromJaspFile(const char * filePat const char* STDCALL syntaxBridgeAnalysisOptionsFromJaspFileStatus(const char * filePath, int analysisNr) { - configureBridgeLogging(gl_verbose); - Json::Value status = analysisOptionsStatus(filePath, analysisNr); - if (!status["ok"].asBool() && status.isMember("error")) - Log::log() << status["error"].asString() << std::endl; return statusResult(status); } @@ -717,7 +705,8 @@ const char* STDCALL syntaxBridgeGetVariableNames() void STDCALL syntaxBridgeSetVerbose(bool verbose) { gl_verbose = verbose; - configureBridgeLogging(verbose); + if (gl_loggingInitialized) + configureBridgeLogging(verbose); } const char* STDCALL syntaxBridgeColumnEncoderContext() @@ -734,7 +723,6 @@ const char* STDCALL syntaxBridgeDecodeColumnText(const char* valuesJson, const c try { - configureBridgeLogging(gl_verbose); result = decodeColumnJson(valuesJson, encoderContextJson, requireExtraColumnEncoder()).toStyledString(); return result.c_str(); } From 32ca2fb6fa54da363ae165009548dc6a23643578 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Barto=C5=A1?= Date: Thu, 4 Jun 2026 15:06:29 +0200 Subject: [PATCH 8/9] Include extra option encodings in bridge predicates --- CommonData/rbridge.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CommonData/rbridge.cpp b/CommonData/rbridge.cpp index e2fa2e4e92..8d36db04d5 100644 --- a/CommonData/rbridge.cpp +++ b/CommonData/rbridge.cpp @@ -209,12 +209,12 @@ extern "C" int STDCALL rbridge_decodeColumnType(const char * in) extern "C" bool STDCALL rbridge_shouldEncodeColumnName(const char * in) { - return ColumnEncoder::columnEncoder()->shouldEncode(in); + return (extraEncodings && extraEncodings->shouldEncode(in)) || ColumnEncoder::columnEncoder()->shouldEncode(in); } extern "C" bool STDCALL rbridge_shouldDecodeColumnName(const char * in) { - return ColumnEncoder::columnEncoder()->shouldDecode(in); + return (extraEncodings && extraEncodings->shouldDecode(in)) || ColumnEncoder::columnEncoder()->shouldDecode(in); } extern "C" const char * STDCALL rbridge_encodeAllColumnNames(const char * in) From 37de18a9faeed98c50d8e013316f6f936a919239 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Barto=C5=A1?= Date: Fri, 5 Jun 2026 15:57:22 +0200 Subject: [PATCH 9/9] Add native column decoder context tests --- Tests/CMakeLists.txt | 24 ++++ Tests/testcolumnencodercontext.cpp | 222 +++++++++++++++++++++++++++++ Tests/testcolumnencodercontext.h | 35 +++++ 3 files changed, 281 insertions(+) create mode 100644 Tests/testcolumnencodercontext.cpp create mode 100644 Tests/testcolumnencodercontext.h diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt index 65e88ab6bd..729816b800 100644 --- a/Tests/CMakeLists.txt +++ b/Tests/CMakeLists.txt @@ -46,12 +46,22 @@ qt_add_executable( $<$:${CMAKE_SOURCE_DIR}/Desktop/JASP.exe.manifest> ) +qt_add_executable( + JASPTestColumnEncoderContext + ${SYSTEM_TYPE} + testcolumnencodercontext.h + testcolumnencodercontext.cpp + $<$:${_R_Framework}> + $<$:${CMAKE_SOURCE_DIR}/Desktop/JASP.exe.manifest> +) + add_dependencies(JASPTest JASPDesktopLib) add_dependencies(JASPTestDebugData JASPDesktopLib) add_dependencies(JASPTestEngine JASPDesktopLib) add_dependencies(JASPQuickTest JASPDesktopLib) add_dependencies(JASPTestCsvPrev JASPDesktopLib) +add_dependencies(JASPTestColumnEncoderContext Common) target_include_directories( JASPTest @@ -86,6 +96,12 @@ target_include_directories( JASPDesktopLib ) +target_include_directories( + JASPTestColumnEncoderContext + PUBLIC + ${PROJECT_SOURCE_DIR}/Common +) + target_link_libraries( JASPTest PUBLIC @@ -135,6 +151,13 @@ target_link_libraries( Qt::Test ) +target_link_libraries( + JASPTestColumnEncoderContext + PUBLIC + Common + Qt::Test +) + target_compile_definitions( JASPTest PUBLIC TESTLIBRARY_DIR=${CMAKE_SOURCE_DIR}/Tests/TestLibrary @@ -171,6 +194,7 @@ add_test(NAME testCsvPreview COMMAND JASPTestCsvPrev) add_test(NAME testDebugData COMMAND JASPTestDebugData) add_test(NAME testEngine COMMAND JASPTestEngine) add_test(NAME testQuick COMMAND JASPQuickTest) +add_test(NAME testColumnEncoderContext COMMAND JASPTestColumnEncoderContext) diff --git a/Tests/testcolumnencodercontext.cpp b/Tests/testcolumnencodercontext.cpp new file mode 100644 index 0000000000..a2443637ea --- /dev/null +++ b/Tests/testcolumnencodercontext.cpp @@ -0,0 +1,222 @@ +// +// Copyright (C) 2026 University of Amsterdam +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public +// License along with this program. If not, see +// . +// + +#include "testcolumnencodercontext.h" + +#include "columnencodercontext.h" + +#include + +#include +#include +#include + +namespace +{ +constexpr const char * ExtraOptionsPrefix = "JaspExtraOptions_"; + +ColumnEncoder::colTypeMap emptyNames() +{ + return ColumnEncoder::colTypeMap(); +} + +ColumnEncoder::colTypeMap names(std::initializer_list> values) +{ + ColumnEncoder::colTypeMap result; + for(const auto & value : values) + result[value.first] = value.second; + return result; +} + +ColumnEncoderContext context(ColumnEncoder::colTypeMap columns, ColumnEncoder::colTypeMap extra) +{ + return ColumnEncoderContext(columns, extra); +} + +std::string contextJson(const ColumnEncoderContext & encoderContext) +{ + return encoderContext.toJson().toStyledString(); +} + +void activateContext(const ColumnEncoderContext & encoderContext, ColumnEncoder & extraEncoder) +{ + ColumnEncoder::columnEncoder()->setCurrentNames(encoderContext.columns()); + extraEncoder.setCurrentNames(encoderContext.extra()); +} + +std::string encodeColumn(const ColumnEncoderContext & encoderContext, ColumnEncoder & extraEncoder, const std::string & column) +{ + ScopedColumnEncoderContext scopedContext(encoderContext, extraEncoder); + return ColumnEncoder::columnEncoder()->encode(column); +} + +std::string encodeExtra(const ColumnEncoderContext & encoderContext, ColumnEncoder & extraEncoder, const std::string & value) +{ + ScopedColumnEncoderContext scopedContext(encoderContext, extraEncoder); + return extraEncoder.encode(value); +} + +std::string payloadJson(const ColumnEncoderContext & encoderContext, ColumnEncoder & extraEncoder, + const std::string & firstColumn, + const std::string & secondColumn, + const std::string & extraValue) +{ + const std::string encodedFirstColumn = encodeColumn(encoderContext, extraEncoder, firstColumn); + const std::string encodedSecondColumn = encodeColumn(encoderContext, extraEncoder, secondColumn); + const std::string encodedExtraValue = encodeExtra(encoderContext, extraEncoder, extraValue); + + Json::Value payload(Json::objectValue); + payload[encodedFirstColumn] = encodedExtraValue; + payload[encodedExtraValue] = encodedFirstColumn; + payload["nested"][0] = encodedSecondColumn; + payload["nested"][1]["label"] = std::string("label:") + encodedExtraValue; + payload["nested"][1]["formula"] = encodedFirstColumn + " + " + encodedSecondColumn; + return payload.toStyledString(); +} + +Json::Value decodePayload(const std::string & payload, const std::string & encoderContextJson, ColumnEncoder & extraEncoder) +{ + return decodeColumnJson(payload.c_str(), encoderContextJson.c_str(), extraEncoder); +} + +void verifyDecodedPayload(const Json::Value & decoded, + const std::string & firstColumn, + const std::string & secondColumn, + const std::string & extraValue) +{ + QVERIFY2(decoded.isObject(), "Decoded payload should be a JSON object."); + QVERIFY2(decoded.isMember(firstColumn), "Encoded object member name was not decoded with the expected dataset column context."); + QVERIFY2(decoded.isMember(extraValue), "Encoded extra-option member name was not decoded with the expected extra-option context."); + QCOMPARE(QString::fromStdString(decoded[firstColumn].asString()), QString::fromStdString(extraValue)); + QCOMPARE(QString::fromStdString(decoded[extraValue].asString()), QString::fromStdString(firstColumn)); + QCOMPARE(QString::fromStdString(decoded["nested"][0].asString()), QString::fromStdString(secondColumn)); + QCOMPARE(QString::fromStdString(decoded["nested"][1]["label"].asString()), QString::fromStdString("label:" + extraValue)); + QCOMPARE(QString::fromStdString(decoded["nested"][1]["formula"].asString()), QString::fromStdString(firstColumn + " + " + secondColumn)); +} + +std::string captureError(const std::function & function) +{ + try + { + function(); + } + catch(const std::exception & exception) + { + return exception.what(); + } + + return ""; +} +} + +void TestColumnEncoderContext::init() +{ + ColumnEncoder::columnEncoder()->setCurrentNames(emptyNames()); +} + +void TestColumnEncoderContext::cleanup() +{ + ColumnEncoder::columnEncoder()->setCurrentNames(emptyNames()); +} + +void TestColumnEncoderContext::scopedContextsDecodeInterleavedAndRestoreLiveState() +{ + ColumnEncoder extraEncoder(ExtraOptionsPrefix); + + const ColumnEncoderContext liveContext = context( + names({{"live group", columnType::nominal}, {"live score", columnType::scale}}), + names({{"live factor A", columnType::unknown}, {"live factor B", columnType::unknown}}) + ); + + activateContext(liveContext, extraEncoder); + + const std::string livePayload = payloadJson(liveContext, extraEncoder, "live group", "live score", "live factor A"); + + const ColumnEncoderContext firstArchiveContext = context( + names({{"archive A group", columnType::nominal}, {"archive A score", columnType::scale}}), + names({{"archive A level", columnType::unknown}}) + ); + const ColumnEncoderContext secondArchiveContext = context( + names({{"archive B group", columnType::ordinal}, {"archive B score", columnType::scale}}), + names({{"archive B level", columnType::unknown}}) + ); + const ColumnEncoderContext thirdArchiveContext = context( + names({{"archive C group", columnType::nominal}, {"archive C score", columnType::scale}}), + names({{"archive C level", columnType::unknown}}) + ); + + const std::string firstArchivePayload = payloadJson(firstArchiveContext, extraEncoder, "archive A group", "archive A score", "archive A level"); + const std::string secondArchivePayload = payloadJson(secondArchiveContext, extraEncoder, "archive B group", "archive B score", "archive B level"); + const std::string thirdArchivePayload = payloadJson(thirdArchiveContext, extraEncoder, "archive C group", "archive C score", "archive C level"); + + verifyDecodedPayload(decodePayload(livePayload, "", extraEncoder), "live group", "live score", "live factor A"); + + verifyDecodedPayload(decodePayload(firstArchivePayload, contextJson(firstArchiveContext), extraEncoder), "archive A group", "archive A score", "archive A level"); + verifyDecodedPayload(decodePayload(livePayload, "", extraEncoder), "live group", "live score", "live factor A"); + + verifyDecodedPayload(decodePayload(secondArchivePayload, contextJson(secondArchiveContext), extraEncoder), "archive B group", "archive B score", "archive B level"); + verifyDecodedPayload(decodePayload(livePayload, "", extraEncoder), "live group", "live score", "live factor A"); + + verifyDecodedPayload(decodePayload(thirdArchivePayload, contextJson(thirdArchiveContext), extraEncoder), "archive C group", "archive C score", "archive C level"); + verifyDecodedPayload(decodePayload(livePayload, "", extraEncoder), "live group", "live score", "live factor A"); + + verifyDecodedPayload(decodePayload(firstArchivePayload, contextJson(firstArchiveContext), extraEncoder), "archive A group", "archive A score", "archive A level"); + verifyDecodedPayload(decodePayload(thirdArchivePayload, contextJson(thirdArchiveContext), extraEncoder), "archive C group", "archive C score", "archive C level"); + verifyDecodedPayload(decodePayload(secondArchivePayload, contextJson(secondArchiveContext), extraEncoder), "archive B group", "archive B score", "archive B level"); + + verifyDecodedPayload(decodePayload(livePayload, "", extraEncoder), "live group", "live score", "live factor A"); + QVERIFY2(ColumnEncoder::columnEncoder()->currentNames() == liveContext.columns(), "Dataset column context was not restored after scoped decodes."); + QVERIFY2(extraEncoder.currentNames() == liveContext.extra(), "Extra-option context was not restored after scoped decodes."); +} + +void TestColumnEncoderContext::malformedContextDoesNotMutateLiveState() +{ + ColumnEncoder extraEncoder(ExtraOptionsPrefix); + + const ColumnEncoderContext liveContext = context( + names({{"live group", columnType::nominal}, {"live score", columnType::scale}}), + names({{"live factor", columnType::unknown}}) + ); + + activateContext(liveContext, extraEncoder); + + const std::string livePayload = payloadJson(liveContext, extraEncoder, "live group", "live score", "live factor"); + + const std::string parseError = captureError([&]() + { + decodePayload(livePayload, "{not-json", extraEncoder); + }); + const std::string parseErrorMessage = "Unexpected parse error: " + parseError; + QVERIFY2(parseError.find("Could not parse column encoder context JSON.") != std::string::npos, + parseErrorMessage.c_str()); + verifyDecodedPayload(decodePayload(livePayload, "", extraEncoder), "live group", "live score", "live factor"); + + const std::string schemaError = captureError([&]() + { + decodePayload(livePayload, "{\"version\":1,\"columns\":{},\"extra\":[]}", extraEncoder); + }); + const std::string schemaErrorMessage = "Unexpected schema error: " + schemaError; + QVERIFY2(schemaError.find("Column encoder context field 'columns' must be an array.") != std::string::npos, + schemaErrorMessage.c_str()); + verifyDecodedPayload(decodePayload(livePayload, "", extraEncoder), "live group", "live score", "live factor"); + + QVERIFY2(ColumnEncoder::columnEncoder()->currentNames() == liveContext.columns(), "Dataset column context changed after malformed context errors."); + QVERIFY2(extraEncoder.currentNames() == liveContext.extra(), "Extra-option context changed after malformed context errors."); +} + +QTEST_MAIN(TestColumnEncoderContext) diff --git a/Tests/testcolumnencodercontext.h b/Tests/testcolumnencodercontext.h new file mode 100644 index 0000000000..a3a268ce30 --- /dev/null +++ b/Tests/testcolumnencodercontext.h @@ -0,0 +1,35 @@ +// +// Copyright (C) 2026 University of Amsterdam +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public +// License along with this program. If not, see +// . +// + +#ifndef TESTCOLUMNENCODERCONTEXT_H +#define TESTCOLUMNENCODERCONTEXT_H + +#include + +class TestColumnEncoderContext : public QObject +{ + Q_OBJECT + +private slots: + void init(); + void cleanup(); + void scopedContextsDecodeInterleavedAndRestoreLiveState(); + void malformedContextDoesNotMutateLiveState(); +}; + +#endif // TESTCOLUMNENCODERCONTEXT_H