From 2001c6631dca5b316972a6fdab0896084e4ea24a Mon Sep 17 00:00:00 2001 From: Christian Muehlbauer Date: Wed, 11 Jun 2025 15:54:10 +0200 Subject: [PATCH 1/7] Update IL dependencies and fix babel vulnerability Signed-off-by: Christian Muehlbauer --- cdsp/information-layer/package-lock.json | 1248 +++++++--------------- cdsp/information-layer/package.json | 13 +- 2 files changed, 368 insertions(+), 893 deletions(-) diff --git a/cdsp/information-layer/package-lock.json b/cdsp/information-layer/package-lock.json index e8f4e4ab..8f048481 100644 --- a/cdsp/information-layer/package-lock.json +++ b/cdsp/information-layer/package-lock.json @@ -11,21 +11,19 @@ "ajv": "^8.17.1", "ajv-errors": "^3.0.0", "ajv-formats": "^3.0.1", - "dotenv": "^16.4.5", + "dotenv": "^16.5.0", "js-yaml": "^4.1.0", - "realm": "^12.13.1", - "thrift": "^0.14.1", - "uuid": "^10.0.0", - "ws": "^8.18.0" + "thrift": "^0.22.0", + "uuid": "^11.1.0", + "ws": "^8.18.2" }, "devDependencies": { - "@types/ajv": "^0.0.5", "@types/jest": "^29.5.14", "@types/js-yaml": "^4.0.9", - "@types/node": "^22.7.0", + "@types/node": "^22.15.29", "@types/thrift": "^0.10.17", "@types/uuid": "^10.0.0", - "@types/ws": "^8.5.12", + "@types/ws": "^8.18.1", "copyfiles": "^2.4.1", "eslint": "^9.9.1", "eslint-config-prettier": "^9.1.0", @@ -53,14 +51,15 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.25.7.tgz", - "integrity": "sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/highlight": "^7.25.7", - "picocolors": "^1.0.0" + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" @@ -218,9 +217,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz", - "integrity": "sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, "license": "MIT", "engines": { @@ -228,9 +227,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", - "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "dev": true, "license": "MIT", "engines": { @@ -248,121 +247,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.7.tgz", - "integrity": "sha512-Sv6pASx7Esm38KQpF/U/OXLwPPrdGHNKoeblRxgZRLXnAtnkEe4ptJPDtAZM7fBLadbc1Q07kQpSiGQ0Jg6tRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.25.7", - "@babel/types": "^7.25.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.7.tgz", - "integrity": "sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==", + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.4.tgz", + "integrity": "sha512-Y+bO6U+I7ZKaM5G5rDUZiYfUvQPUibYmAFe7EnKdnKBbVXDZxvp+MWOH5gYciY0EPk4EScsuFMQBbEfpdRKSCQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/parser": { - "version": "7.25.8", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.8.tgz", - "integrity": "sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ==", + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", + "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.25.8" + "@babel/types": "^7.27.3" }, "bin": { "parser": "bin/babel-parser.js" @@ -611,15 +516,15 @@ } }, "node_modules/@babel/template": { - "version": "7.25.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.7.tgz", - "integrity": "sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.7", - "@babel/parser": "^7.25.7", - "@babel/types": "^7.25.7" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -655,15 +560,14 @@ } }, "node_modules/@babel/types": { - "version": "7.25.8", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.8.tgz", - "integrity": "sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.3.tgz", + "integrity": "sha512-Y1GkI4ktrtvmawoSq+4FCVHNryea6uR+qUQy0AGxLSsjCX0nVmkYQMBLHDkXZuo5hGx7eYdnIaslsdBFm7zbUw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.7", - "@babel/helper-validator-identifier": "^7.25.7", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -677,17 +581,20 @@ "license": "MIT" }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", "dev": true, "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, + "funding": { + "url": "https://opencollective.com/eslint" + }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } @@ -706,9 +613,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", - "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, "license": "MIT", "engines": { @@ -716,13 +623,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", - "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.4", + "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -730,20 +637,33 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", + "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/core": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.6.0.tgz", - "integrity": "sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/eslintrc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", - "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", "dev": true, "license": "MIT", "dependencies": { @@ -788,33 +708,23 @@ "dev": true, "license": "MIT" }, - "node_modules/@eslint/eslintrc/node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@eslint/js": { - "version": "9.12.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.12.0.tgz", - "integrity": "sha512-eohesHH8WFRUprDNyEREgqP6beG6htMeUYeCpkEgBCieCMme5r9zFWjzAJp//9S+Kub4rqE+jXe9Cp1a7IYIIA==", + "version": "9.28.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.28.0.tgz", + "integrity": "sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg==", "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" } }, "node_modules/@eslint/object-schema": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", - "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -822,12 +732,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.4.tgz", - "integrity": "sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", + "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", "dev": true, "license": "Apache-2.0", "dependencies": { + "@eslint/core": "^0.14.0", "levn": "^0.4.1" }, "engines": { @@ -835,9 +746,9 @@ } }, "node_modules/@humanfs/core": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.0.tgz", - "integrity": "sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==", + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -845,19 +756,33 @@ } }, "node_modules/@humanfs/node": { - "version": "0.16.5", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.5.tgz", - "integrity": "sha512-KSPA4umqSG4LHYRodq31VDwKAvaTF4xmVlzM8Aeh4PlU1JQ3IG0wiA8C25d3RQ9nJyM3mBHyI53K06VVL/oFFg==", + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanfs/core": "^0.19.0", + "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" }, "engines": { "node": ">=18.18.0" } }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -873,9 +798,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1490,24 +1415,16 @@ } }, "node_modules/@pkgr/core": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", - "integrity": "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.7.tgz", + "integrity": "sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==", "dev": true, "license": "MIT", "engines": { "node": "^12.20.0 || ^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/unts" - } - }, - "node_modules/@realm/fetch": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@realm/fetch/-/fetch-0.1.1.tgz", - "integrity": "sha512-hkTprw79RXGv54Je0DrjpQPLaz4QID2dO3FmthAQQWAkqwyrqMzrCGzJzLlmTKWZFsgLrN8KQyNewod27P+nJg==", - "engines": { - "node": ">=18" + "url": "https://opencollective.com/pkgr" } }, "node_modules/@sinclair/typebox": { @@ -1537,13 +1454,6 @@ "@sinonjs/commons": "^3.0.0" } }, - "node_modules/@types/ajv": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/@types/ajv/-/ajv-0.0.5.tgz", - "integrity": "sha512-0UD98SZLwPk6cTMZoLs8W4XSAWgqT9nyoZiBw+uXEPf1u5h+Dn2ztMG4Is9pDA+9D7GKbCTfIkQK/hNgoWVQcQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1659,13 +1569,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.7.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", - "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "version": "22.15.29", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.29.tgz", + "integrity": "sha512-LNdjOkUDlU1RZb8e1kOIUpN1qQUlzGkEtbVNo53vbrwDg5om6oduhm4SiUaPW5ASTXhAiP0jInWG8Qx9fVlOeQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.21.0" } }, "node_modules/@types/node-int64": { @@ -1712,9 +1622,9 @@ "license": "MIT" }, "node_modules/@types/ws": { - "version": "8.5.12", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.12.tgz", - "integrity": "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", "dev": true, "license": "MIT", "dependencies": { @@ -1739,21 +1649,21 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.1.tgz", - "integrity": "sha512-xfvdgA8AP/vxHgtgU310+WBnLB4uJQ9XdyP17RebG26rLtDrQJV3ZYrcopX91GrHmMoH8bdSwMRh2a//TiJ1jQ==", + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.33.1.tgz", + "integrity": "sha512-TDCXj+YxLgtvxvFlAvpoRv9MAncDLBV2oT9Bd7YBGC/b/sEURoOYuIwLI99rjWOfY3QtDzO+mk0n4AmdFExW8A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.8.1", - "@typescript-eslint/type-utils": "8.8.1", - "@typescript-eslint/utils": "8.8.1", - "@typescript-eslint/visitor-keys": "8.8.1", + "@typescript-eslint/scope-manager": "8.33.1", + "@typescript-eslint/type-utils": "8.33.1", + "@typescript-eslint/utils": "8.33.1", + "@typescript-eslint/visitor-keys": "8.33.1", "graphemer": "^1.4.0", - "ignore": "^5.3.1", + "ignore": "^7.0.0", "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1763,26 +1673,32 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "@typescript-eslint/parser": "^8.33.1", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.8.1.tgz", - "integrity": "sha512-hQUVn2Lij2NAxVFEdvIGxT9gP1tq2yM83m+by3whWFsWC+1y8pxxxHUFE1UqDu2VsGi2i6RLcv4QvouM84U+ow==", + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.33.1.tgz", + "integrity": "sha512-qwxv6dq682yVvgKKp2qWwLgRbscDAYktPptK4JPojCwwi3R9cwrvIxS4lvBpzmcqzR4bdn54Z0IG1uHFskW4dA==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.8.1", - "@typescript-eslint/types": "8.8.1", - "@typescript-eslint/typescript-estree": "8.8.1", - "@typescript-eslint/visitor-keys": "8.8.1", + "@typescript-eslint/scope-manager": "8.33.1", + "@typescript-eslint/types": "8.33.1", + "@typescript-eslint/typescript-estree": "8.33.1", + "@typescript-eslint/visitor-keys": "8.33.1", "debug": "^4.3.4" }, "engines": { @@ -1793,43 +1709,78 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.33.1.tgz", + "integrity": "sha512-DZR0efeNklDIHHGRpMpR5gJITQpu6tLr9lDJnKdONTC7vvzOlLAG/wcfxcdxEWrbiZApcoBCzXqU/Z458Za5Iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.33.1", + "@typescript-eslint/types": "^8.33.1", + "debug": "^4.3.4" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.8.1.tgz", - "integrity": "sha512-X4JdU+66Mazev/J0gfXlcC/dV6JI37h+93W9BRYXrSn0hrE64IoWgVkO9MSJgEzoWkxONgaQpICWg8vAN74wlA==", + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.33.1.tgz", + "integrity": "sha512-dM4UBtgmzHR9bS0Rv09JST0RcHYearoEoo3pG5B6GoTR9XcyeqX87FEhPo+5kTvVfKCvfHaHrcgeJQc6mrDKrA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.8.1", - "@typescript-eslint/visitor-keys": "8.8.1" + "@typescript-eslint/types": "8.33.1", + "@typescript-eslint/visitor-keys": "8.33.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.33.1.tgz", + "integrity": "sha512-STAQsGYbHCF0/e+ShUQ4EatXQ7ceh3fBCXkNU7/MZVKulrlq1usH7t2FhxvCpuCi5O5oi1vmVaAjrGeL71OK1g==", + "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.8.1.tgz", - "integrity": "sha512-qSVnpcbLP8CALORf0za+vjLYj1Wp8HSoiI8zYU5tHxRVj30702Z1Yw4cLwfNKhTPWp5+P+k1pjmD5Zd1nhxiZA==", + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.33.1.tgz", + "integrity": "sha512-1cG37d9xOkhlykom55WVwG2QRNC7YXlxMaMzqw2uPeJixBFfKWZgaP/hjAObqMN/u3fr5BrTwTnc31/L9jQ2ww==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.8.1", - "@typescript-eslint/utils": "8.8.1", + "@typescript-eslint/typescript-estree": "8.33.1", + "@typescript-eslint/utils": "8.33.1", "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1838,16 +1789,15 @@ "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.8.1.tgz", - "integrity": "sha512-WCcTP4SDXzMd23N27u66zTKMuEevH4uzU8C9jf0RO4E04yVHgQgW+r+TeVTNnO1KIfrL8ebgVVYYMMO3+jC55Q==", + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.33.1.tgz", + "integrity": "sha512-xid1WfizGhy/TKMTwhtVOgalHwPtV8T32MS9MaH50Cwvz6x6YqRIPdD2WvW0XaqOzTV9p5xdLY0h/ZusU5Lokg==", "dev": true, "license": "MIT", "engines": { @@ -1859,20 +1809,22 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.8.1.tgz", - "integrity": "sha512-A5d1R9p+X+1js4JogdNilDuuq+EHZdsH9MjTVxXOdVFfTJXunKJR/v+fNNyO4TnoOn5HqobzfRlc70NC6HTcdg==", + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.33.1.tgz", + "integrity": "sha512-+s9LYcT8LWjdYWu7IWs7FvUxpQ/DGkdjZeE/GGulHvv8rvYwQvVaUZ6DE+j5x/prADUgSbbCWZ2nPI3usuVeOA==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.8.1", - "@typescript-eslint/visitor-keys": "8.8.1", + "@typescript-eslint/project-service": "8.33.1", + "@typescript-eslint/tsconfig-utils": "8.33.1", + "@typescript-eslint/types": "8.33.1", + "@typescript-eslint/visitor-keys": "8.33.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1881,10 +1833,8 @@ "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { @@ -1914,16 +1864,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.8.1.tgz", - "integrity": "sha512-/QkNJDbV0bdL7H7d0/y0qBbV2HTtf0TIyjSDTvvmQEzeVx8jEImEbLuOA4EsvE8gIgqMitns0ifb5uQhMj8d9w==", + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.33.1.tgz", + "integrity": "sha512-52HaBiEQUaRYqAXpfzWSR2U3gxk92Kw006+xZpElaPMg3C4PgM+A5LqwoQI1f9E5aZ/qlxAZxzm42WX+vn92SQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.8.1", - "@typescript-eslint/types": "8.8.1", - "@typescript-eslint/typescript-estree": "8.8.1" + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.33.1", + "@typescript-eslint/types": "8.33.1", + "@typescript-eslint/typescript-estree": "8.33.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1933,18 +1883,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.8.1.tgz", - "integrity": "sha512-0/TdC3aeRAsW7MDvYRwEc1Uwm0TIBfzjPFgg60UU2Haj5qsCs9cc3zNgY71edqE3LbWfF/WoZQd3lJoDXFQpag==", + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.33.1.tgz", + "integrity": "sha512-3i8NrFcZeeDHJ+7ZUuDkGT+UHq+XoFGsymNK2jZCOHcfEzRQ0BdpRtdpSx/Iyf3MHLWIcLS0COuOPibKQboIiQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.8.1", - "eslint-visitor-keys": "^3.4.3" + "@typescript-eslint/types": "8.33.1", + "eslint-visitor-keys": "^4.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1954,23 +1905,10 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/acorn": { - "version": "8.12.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", - "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "dev": true, "license": "MIT", "bin": { @@ -2240,60 +2178,6 @@ "dev": true, "license": "MIT" }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/bl/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/bl/node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2380,42 +2264,6 @@ "node-int64": "^0.4.0" } }, - "node_modules/bson": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/bson/-/bson-4.7.2.tgz", - "integrity": "sha512-Ry9wCtIZ5kGqkJoi6aD8KjxFZEx78guTQDnpXWiNthsxzrxAK/i8E6pCHAIZTbaEFWcOCvbecMukfK7XUvyLpQ==", - "license": "Apache-2.0", - "dependencies": { - "buffer": "^5.6.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -2491,12 +2339,6 @@ "node": ">=10" } }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" - }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -2652,6 +2494,7 @@ "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2665,21 +2508,6 @@ } } }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/dedent": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", @@ -2695,15 +2523,6 @@ } } }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -2718,16 +2537,7 @@ "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, "node_modules/detect-newline": { @@ -2751,9 +2561,9 @@ } }, "node_modules/dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", "license": "BSD-2-Clause", "engines": { "node": ">=12" @@ -2812,15 +2622,6 @@ "dev": true, "license": "MIT" }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -2855,32 +2656,33 @@ } }, "node_modules/eslint": { - "version": "9.12.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.12.0.tgz", - "integrity": "sha512-UVIOlTEWxwIopRL1wgSQYdnVDcEvs2wyaO6DGo5mXqe3r16IoCNWkR29iHhyaP4cICWjbgbmFUGAhh0GJRuGZw==", + "version": "9.28.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.28.0.tgz", + "integrity": "sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.11.0", - "@eslint/config-array": "^0.18.0", - "@eslint/core": "^0.6.0", - "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.12.0", - "@eslint/plugin-kit": "^0.2.0", - "@humanfs/node": "^0.16.5", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.20.0", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.14.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.28.0", + "@eslint/plugin-kit": "^0.3.1", + "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.3.1", + "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.1.0", - "eslint-visitor-keys": "^4.1.0", - "espree": "^10.2.0", + "eslint-scope": "^8.3.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -2894,8 +2696,7 @@ "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "text-table": "^0.2.0" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" @@ -2929,14 +2730,14 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", - "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.4.1.tgz", + "integrity": "sha512-9dF+KuU/Ilkq27A8idRP7N2DH8iUR6qXcjF3FR2wETY21PZdBrIjwCau8oboyGj9b7etWmTGEeM8e7oOed6ZWg==", "dev": true, "license": "MIT", "dependencies": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.9.1" + "synckit": "^0.11.7" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -2947,7 +2748,7 @@ "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", - "eslint-config-prettier": "*", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", "prettier": ">=3.0.0" }, "peerDependenciesMeta": { @@ -2960,9 +2761,9 @@ } }, "node_modules/eslint-scope": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.1.0.tgz", - "integrity": "sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2977,9 +2778,9 @@ } }, "node_modules/eslint-visitor-keys": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", - "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -3014,15 +2815,15 @@ "license": "MIT" }, "node_modules/espree": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz", - "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.12.0", + "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.1.0" + "eslint-visitor-keys": "^4.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3124,15 +2925,6 @@ "node": ">= 0.8.0" } }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, "node_modules/expect": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", @@ -3164,9 +2956,9 @@ "license": "Apache-2.0" }, "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, "license": "MIT", "dependencies": { @@ -3174,7 +2966,7 @@ "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "micromatch": "^4.0.8" }, "engines": { "node": ">=8.6.0" @@ -3214,9 +3006,9 @@ "license": "MIT" }, "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "dev": true, "license": "ISC", "dependencies": { @@ -3360,12 +3152,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "license": "MIT" - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3441,12 +3227,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "license": "MIT" - }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -3549,26 +3329,6 @@ "node": ">=10.17.0" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3580,9 +3340,9 @@ } }, "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3642,12 +3402,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, "license": "ISC" }, "node_modules/is-arrayish": { @@ -4066,19 +3821,6 @@ } } }, - "node_modules/jest-config/node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/jest-diff": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", @@ -4745,18 +4487,6 @@ "node": ">=6" } }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -4770,15 +4500,6 @@ "node": "*" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -4802,22 +4523,11 @@ "node": ">=10" } }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "license": "MIT" - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/napi-build-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", - "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "dev": true, "license": "MIT" }, "node_modules/natural-compare": { @@ -4827,30 +4537,12 @@ "dev": true, "license": "MIT" }, - "node_modules/node-abi": { - "version": "3.68.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.68.0.tgz", - "integrity": "sha512-7vbj10trelExNjFSBm5kTvZXXa7pZyKWx9RCKIyqe6I9Ev3IzGpQoqBP3a+cOdxY+pWj6VkP28n/2wWysBHD/A==", - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "license": "MIT" }, - "node_modules/node-machine-id": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/node-machine-id/-/node-machine-id-1.1.12.tgz", - "integrity": "sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==", - "license": "MIT" - }, "node_modules/node-releases": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", @@ -4896,6 +4588,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -5016,12 +4709,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "license": "MIT" - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5087,9 +4774,9 @@ } }, "node_modules/picocolors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", - "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true, "license": "ISC" }, @@ -5185,32 +4872,6 @@ "node": ">=8" } }, - "node_modules/prebuild-install": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", - "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^1.0.1", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5222,9 +4883,9 @@ } }, "node_modules/prettier": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", - "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "dev": true, "license": "MIT", "bin": { @@ -5299,16 +4960,6 @@ "node": ">= 6" } }, - "node_modules/pump": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", - "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5368,21 +5019,6 @@ ], "license": "MIT" }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -5403,33 +5039,6 @@ "string_decoder": "~0.10.x" } }, - "node_modules/realm": { - "version": "12.13.1", - "resolved": "https://registry.npmjs.org/realm/-/realm-12.13.1.tgz", - "integrity": "sha512-fAs70ZCBf1P7htVhOTrDMFHD6SzoGXVsALy6DpOPR6t0LXoK635cKxBMECX3bYdCgI7+riSfdoWXLA/7g5yTSQ==", - "deprecated": "This version uses Atlas Device Sync, please install `realm@community` and read https://github.com/realm/realm-js/blob/main/DEPRECATION.md for more information.", - "hasInstallScript": true, - "license": "apache-2.0", - "dependencies": { - "@realm/fetch": "^0.1.1", - "bson": "^4.7.2", - "debug": "^4.3.4", - "node-machine-id": "^1.1.12", - "path-browserify": "^1.0.1", - "prebuild-install": "^7.1.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "react-native": ">=0.71.0" - }, - "peerDependenciesMeta": { - "react-native": { - "optional": true - } - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -5511,9 +5120,9 @@ } }, "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, "license": "MIT", "engines": { @@ -5615,30 +5224,11 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -5677,51 +5267,6 @@ "dev": true, "license": "ISC" }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -5890,12 +5435,16 @@ } }, "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/supports-color": { @@ -5925,71 +5474,19 @@ } }, "node_modules/synckit": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.2.tgz", - "integrity": "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==", + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.8.tgz", + "integrity": "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==", "dev": true, "license": "MIT", "dependencies": { - "@pkgr/core": "^0.1.0", - "tslib": "^2.6.2" + "@pkgr/core": "^0.2.4" }, "engines": { "node": "^14.18.0 || >=16.0.0" }, "funding": { - "url": "https://opencollective.com/unts" - } - }, - "node_modules/tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tar-stream/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/tar-stream/node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" + "url": "https://opencollective.com/synckit" } }, "node_modules/test-exclude": { @@ -6007,24 +5504,17 @@ "node": ">=8" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true, - "license": "MIT" - }, "node_modules/thrift": { - "version": "0.14.2", - "resolved": "https://registry.npmjs.org/thrift/-/thrift-0.14.2.tgz", - "integrity": "sha512-bW8EaE6iw3hSt4HB2HpBdHW86Xpb9IUJfqufx4NwEu7OGuIpS0ISj+Yy1Z1Wvhfno6SPNhKRJ1qFXea84HcrOQ==", + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/thrift/-/thrift-0.22.0.tgz", + "integrity": "sha512-3+pgo/Q5ijfJH5RKzd87oFPqBcVaVJ/KP2jLG/lLI9vuy6N+0iL+E94P7sN3YbwV+lYnRYZjg4oEn4ARofd21Q==", "license": "Apache-2.0", "dependencies": { "browser-or-node": "^1.2.1", "isomorphic-ws": "^4.0.1", "node-int64": "^0.4.0", "q": "^1.5.0", - "ws": "^5.2.2" + "ws": "^5.2.3" }, "engines": { "node": ">= 10.18.0" @@ -6097,16 +5587,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6121,22 +5601,22 @@ } }, "node_modules/ts-api-utils": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", - "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18.12" }, "peerDependencies": { - "typescript": ">=4.2.0" + "typescript": ">=4.8.4" } }, "node_modules/ts-jest": { - "version": "29.2.5", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", - "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", + "version": "29.3.4", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.3.4.tgz", + "integrity": "sha512-Iqbrm8IXOmV+ggWHOTEbjwyCf2xZlUMv5npExksXohL+tk8va4Fjhb+X2+Rt9NBmgO7bJ8WpnMLOwih/DnMlFA==", "dev": true, "license": "MIT", "dependencies": { @@ -6147,7 +5627,8 @@ "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", - "semver": "^7.6.3", + "semver": "^7.7.2", + "type-fest": "^4.41.0", "yargs-parser": "^21.1.1" }, "bin": { @@ -6182,6 +5663,19 @@ } } }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ts-jest/node_modules/yargs-parser": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", @@ -6192,25 +5686,6 @@ "node": ">=12" } }, - "node_modules/tslib": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", - "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", - "dev": true, - "license": "0BSD" - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6248,9 +5723,9 @@ } }, "node_modules/typescript": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", - "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -6262,15 +5737,15 @@ } }, "node_modules/typescript-eslint": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.8.1.tgz", - "integrity": "sha512-R0dsXFt6t4SAFjUSKFjMh4pXDtq04SsFKCVGDP3ZOzNP7itF0jBcZYU4fMsZr4y7O7V7Nc751dDeESbe4PbQMQ==", + "version": "8.33.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.33.1.tgz", + "integrity": "sha512-AgRnV4sKkWOiZ0Kjbnf5ytTJXMUZQ0qhSVdQtDNYLPLnjsATEYhaO94GlRQwi4t4gO8FfjM6NnikHeKjUm8D7A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.8.1", - "@typescript-eslint/parser": "8.8.1", - "@typescript-eslint/utils": "8.8.1" + "@typescript-eslint/eslint-plugin": "8.33.1", + "@typescript-eslint/parser": "8.33.1", + "@typescript-eslint/utils": "8.33.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6279,16 +5754,15 @@ "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" } }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" }, @@ -6347,19 +5821,20 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, "license": "MIT" }, "node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/v8-to-istanbul": { @@ -6454,6 +5929,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, "license": "ISC" }, "node_modules/write-file-atomic": { @@ -6471,9 +5947,9 @@ } }, "node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/cdsp/information-layer/package.json b/cdsp/information-layer/package.json index e1b22d7c..258fbc1f 100644 --- a/cdsp/information-layer/package.json +++ b/cdsp/information-layer/package.json @@ -18,20 +18,19 @@ "ajv": "^8.17.1", "ajv-errors": "^3.0.0", "ajv-formats": "^3.0.1", - "dotenv": "^16.4.5", + "dotenv": "^16.5.0", "js-yaml": "^4.1.0", - "thrift": "^0.14.1", - "uuid": "^10.0.0", - "ws": "^8.18.0" + "thrift": "^0.22.0", + "uuid": "^11.1.0", + "ws": "^8.18.2" }, "devDependencies": { - "@types/ajv": "^0.0.5", "@types/jest": "^29.5.14", "@types/js-yaml": "^4.0.9", - "@types/node": "^22.7.0", + "@types/node": "^22.15.29", "@types/thrift": "^0.10.17", "@types/uuid": "^10.0.0", - "@types/ws": "^8.5.12", + "@types/ws": "^8.18.1", "copyfiles": "^2.4.1", "eslint": "^9.9.1", "eslint-config-prettier": "^9.1.0", From f303c8500482935110c229ab678b66a26c53d708 Mon Sep 17 00:00:00 2001 From: Christian Muehlbauer Date: Mon, 16 Feb 2026 14:25:29 +0100 Subject: [PATCH 2/7] Initializes RDFox using a non-secure HTTP channel (no TLS) and sets the persistence mode to file, satisfying the new required defaults. Signed-off-by: Christian Muehlbauer --- docker/docker-compose-cdsp.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-compose-cdsp.yml b/docker/docker-compose-cdsp.yml index f29c3462..484f48d8 100644 --- a/docker/docker-compose-cdsp.yml +++ b/docker/docker-compose-cdsp.yml @@ -63,7 +63,7 @@ services: - RDFOX_ROLE=root - RDFOX_PASSWORD=admin command: | - -persistence file + -persistence file -channel unsecure init restart: "no" # RDFox Daemon (persistent service) From bdead5e945b8c2b527f4af1b866ba6872fdbcf38 Mon Sep 17 00:00:00 2001 From: Haonan Qiu Date: Mon, 16 Feb 2026 15:44:17 +0100 Subject: [PATCH 3/7] Update to JSON-RPC 2.0 message format * Update the messaging format between servers/clients to JSON-RPC 2.0 into the COVESA Knowledge Layer. * Remove all tests that are not conpatible any more with the new JSON RPC 2.0 format. Signed-off-by: Haonan Qiu --- cdsp/knowledge-layer/CMakeLists.txt | 6 +- cdsp/knowledge-layer/Dockerfile | 1 + cdsp/knowledge-layer/README.md | 95 ++-- .../common-test-resources/README.md | 25 +- .../model/inputs/vehicle_data.json | 4 + .../model/inputs/vehicle_data_required.txt | 3 + .../use_case_fixture/model/model_config.json | 24 + .../model/ontologies/test_ontology.ttl | 25 + .../model/queries/output/test_query.rq | 18 + .../default/data_property.rq | 12 + .../default/object_property.rq | 14 + .../model/rules/test_rule.dlog | 39 ++ .../model/shacl/vehicle_shacl.ttl | 374 +++++++++++++++ .../utils/random_utils.cpp | 23 +- .../utils/random_utils.h | 11 +- cdsp/knowledge-layer/common/CMakeLists.txt | 9 + cdsp/knowledge-layer/common/README.md | 31 ++ cdsp/knowledge-layer/common/globals.cpp | 84 ++++ cdsp/knowledge-layer/common/globals.h | 12 + .../connector/data-objects/CMakeLists.txt | 5 +- .../data-objects/bo/data_message.cpp | 2 +- .../connector/data-objects/bo/error.cpp | 70 +++ .../connector/data-objects/bo/error.h | 24 + .../connector/data-objects/bo/get_message.cpp | 2 +- .../data-objects/bo/message_header.cpp | 24 +- .../data-objects/bo/message_header.h | 9 +- .../connector/data-objects/bo/metadata.cpp | 48 +- .../connector/data-objects/bo/metadata.h | 26 +- .../data-objects/bo/model_config.cpp | 60 +-- .../connector/data-objects/bo/model_config.h | 32 +- .../connector/data-objects/bo/node.cpp | 18 +- .../connector/data-objects/bo/node.h | 10 +- .../connector/data-objects/bo/set_message.cpp | 2 +- .../data-objects/bo/status_message.cpp | 69 +-- .../data-objects/bo/status_message.h | 18 +- .../data-objects/bo/subscribe_message.cpp | 2 +- .../bo/tests/utils/mock_model_config.h | 13 +- .../data-objects/bo/unsubscribe_message.cpp | 53 +++ .../data-objects/bo/unsubscribe_message.h | 20 + .../data-objects/dto/data_message_dto.h | 30 +- .../data-objects/dto/get_message_dto.h | 32 +- .../connector/data-objects/dto/metadata_dto.h | 83 +++- .../data-objects/dto/model_config_dto.h | 22 +- .../data-objects/dto/set_message_dto.h | 76 ++-- .../data-objects/dto/status_message_dto.h | 46 +- .../data-objects/dto/subscribe_message_dto.h | 37 +- .../dto/unsubscribe_message_dto.h | 43 ++ .../json-writer/src/json_writer.cpp | 417 ++++++++++------- .../json-writer/src/json_writer.h | 35 +- .../tests/json_writer_integration_test.cpp | 93 ++-- .../tests/json_writer_unit_test.cpp | 79 ++-- .../rdf-writer/src/triple_assembler.cpp | 3 +- .../tests/triple_assembler_unit_test.cpp | 29 +- .../tests/triple_writer_integration_test.cpp | 2 +- .../services/src/reasoning_query_service.cpp | 18 +- .../services/src/reasoning_query_service.h | 4 +- .../services/tests/CMakeLists.txt | 39 ++ ...asoning_query_service_integration_test.cpp | 134 ++++++ .../reasoning_query_service_unit_test.cpp | 54 +++ .../connector/utils/data_types.h | 77 +++- .../connector/websocket-client/CMakeLists.txt | 6 +- .../websocket-client/common/globals.cpp | 42 -- .../websocket-client/common/globals.h | 5 - .../websocket-client/runtime/CMakeLists.txt | 18 + .../websocket-client/runtime/README.md | 30 ++ .../runtime/request_registry.cpp | 82 ++++ .../runtime/request_registry.h | 48 ++ .../websocket-client/services/bo_service.cpp | 105 +++-- .../websocket-client/services/bo_service.h | 17 +- .../websocket-client/services/bo_to_dto.cpp | 102 +++-- .../websocket-client/services/bo_to_dto.h | 12 +- .../converters/data_message_converter.cpp | 256 ++++++++--- .../converters/data_message_converter.h | 27 +- .../converters/model_config_converter.cpp | 65 ++- .../converters/model_config_converter.h | 9 +- .../converters/status_message_converter.cpp | 59 ++- .../converters/status_message_converter.h | 3 +- .../websocket-client/services/dto_service.cpp | 230 +++++++--- .../websocket-client/services/dto_service.h | 4 +- .../websocket-client/services/dto_to_bo.h | 16 +- .../services/message_service.cpp | 299 ++++++++---- .../services/message_service.h | 37 +- .../services/system_configuration_service.cpp | 17 +- .../services/system_configuration_service.h | 14 +- .../services/tests/CMakeLists.txt | 98 ---- .../tests/bo_service_integration_test.cpp | 156 +++++-- .../tests/bo_to_dto_integration_test.cpp | 211 --------- .../data_message_dto_service_unit_test.cpp | 242 ---------- .../dto_to_data_message_integration_test.cpp | 429 ------------------ .../dto_to_model_config_integration_test.cpp | 187 ++++---- ...dto_to_status_message_integration_test.cpp | 83 ---- .../model_config_dto_service_unit_test.cpp | 6 - .../status_message_dto_service_unit_test.cpp | 140 ------ .../connector/websocket-client/src/main.cpp | 18 +- .../websocket-client/src/tests/CMakeLists.txt | 27 -- .../tests/utils/mock_websocket_connection.h | 86 ---- ...websocket_client_base_integration_test.cpp | 163 ------- .../websocket_client_base_integration_test.h | 39 -- ...et_client_coordinates_integration_test.cpp | 328 ------------- .../websocket_client_integration_test.cpp | 213 --------- .../websocket-client/src/websocket_client.cpp | 74 +-- .../websocket-client/src/websocket_client.h | 13 +- .../rdfox/tests/rdfox_adapter_unit_test.cpp | 126 ++--- 103 files changed, 3441 insertions(+), 3371 deletions(-) create mode 100644 cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/inputs/vehicle_data.json create mode 100644 cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/inputs/vehicle_data_required.txt create mode 100644 cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/model_config.json create mode 100644 cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/ontologies/test_ontology.ttl create mode 100644 cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/queries/output/test_query.rq create mode 100644 cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/queries/triple_assembler_helper/default/data_property.rq create mode 100644 cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/queries/triple_assembler_helper/default/object_property.rq create mode 100644 cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/rules/test_rule.dlog create mode 100644 cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/shacl/vehicle_shacl.ttl create mode 100644 cdsp/knowledge-layer/common/CMakeLists.txt create mode 100644 cdsp/knowledge-layer/common/README.md create mode 100644 cdsp/knowledge-layer/common/globals.cpp create mode 100644 cdsp/knowledge-layer/common/globals.h create mode 100644 cdsp/knowledge-layer/connector/data-objects/bo/error.cpp create mode 100644 cdsp/knowledge-layer/connector/data-objects/bo/error.h create mode 100644 cdsp/knowledge-layer/connector/data-objects/bo/unsubscribe_message.cpp create mode 100644 cdsp/knowledge-layer/connector/data-objects/bo/unsubscribe_message.h create mode 100644 cdsp/knowledge-layer/connector/data-objects/dto/unsubscribe_message_dto.h create mode 100644 cdsp/knowledge-layer/connector/json-rdf-convertor/services/tests/CMakeLists.txt create mode 100644 cdsp/knowledge-layer/connector/json-rdf-convertor/services/tests/reasoning_query_service_integration_test.cpp create mode 100644 cdsp/knowledge-layer/connector/json-rdf-convertor/services/tests/reasoning_query_service_unit_test.cpp delete mode 100644 cdsp/knowledge-layer/connector/websocket-client/common/globals.cpp delete mode 100644 cdsp/knowledge-layer/connector/websocket-client/common/globals.h create mode 100644 cdsp/knowledge-layer/connector/websocket-client/runtime/CMakeLists.txt create mode 100644 cdsp/knowledge-layer/connector/websocket-client/runtime/README.md create mode 100644 cdsp/knowledge-layer/connector/websocket-client/runtime/request_registry.cpp create mode 100644 cdsp/knowledge-layer/connector/websocket-client/runtime/request_registry.h delete mode 100644 cdsp/knowledge-layer/connector/websocket-client/services/tests/bo_to_dto_integration_test.cpp delete mode 100644 cdsp/knowledge-layer/connector/websocket-client/services/tests/data_message_dto_service_unit_test.cpp delete mode 100644 cdsp/knowledge-layer/connector/websocket-client/services/tests/dto_to_data_message_integration_test.cpp delete mode 100644 cdsp/knowledge-layer/connector/websocket-client/services/tests/dto_to_status_message_integration_test.cpp delete mode 100644 cdsp/knowledge-layer/connector/websocket-client/services/tests/status_message_dto_service_unit_test.cpp delete mode 100644 cdsp/knowledge-layer/connector/websocket-client/src/tests/CMakeLists.txt delete mode 100644 cdsp/knowledge-layer/connector/websocket-client/src/tests/utils/mock_websocket_connection.h delete mode 100644 cdsp/knowledge-layer/connector/websocket-client/src/tests/websocket_client_base_integration_test.cpp delete mode 100644 cdsp/knowledge-layer/connector/websocket-client/src/tests/websocket_client_base_integration_test.h delete mode 100644 cdsp/knowledge-layer/connector/websocket-client/src/tests/websocket_client_coordinates_integration_test.cpp delete mode 100644 cdsp/knowledge-layer/connector/websocket-client/src/tests/websocket_client_integration_test.cpp diff --git a/cdsp/knowledge-layer/CMakeLists.txt b/cdsp/knowledge-layer/CMakeLists.txt index c564f38c..51b0c537 100644 --- a/cdsp/knowledge-layer/CMakeLists.txt +++ b/cdsp/knowledge-layer/CMakeLists.txt @@ -103,22 +103,24 @@ add_compile_definitions(TEST_OUTPUT_DIR="${TEST_OUTPUT_DIR}") enable_testing() # Add subdirectories for project modules and tests +add_subdirectory(common) +add_subdirectory(common-test-resources) add_subdirectory(connector/json-rdf-convertor/json-writer) add_subdirectory(connector/json-rdf-convertor/json-writer/tests) add_subdirectory(connector/json-rdf-convertor/rdf-writer) add_subdirectory(connector/json-rdf-convertor/rdf-writer/tests) add_subdirectory(connector/json-rdf-convertor/rdf-writer/tests/serd_library_tests) add_subdirectory(connector/json-rdf-convertor/services) +add_subdirectory(connector/json-rdf-convertor/services/tests) add_subdirectory(connector/data-objects) add_subdirectory(connector/utils) add_subdirectory(connector/websocket-client) -add_subdirectory(connector/websocket-client/src/tests) +add_subdirectory(connector/websocket-client/runtime) add_subdirectory(connector/websocket-client/services/tests) add_subdirectory(symbolic-reasoner) add_subdirectory(symbolic-reasoner/rdfox/tests) add_subdirectory(symbolic-reasoner/rdfox/tests/rdfox-service-test) add_subdirectory(symbolic-reasoner/services/tests) -add_subdirectory(common-test-resources) # Define the main executable target add_executable(reasoner_client connector/websocket-client/src/main.cpp) diff --git a/cdsp/knowledge-layer/Dockerfile b/cdsp/knowledge-layer/Dockerfile index 42166e78..ce821b24 100644 --- a/cdsp/knowledge-layer/Dockerfile +++ b/cdsp/knowledge-layer/Dockerfile @@ -20,6 +20,7 @@ COPY connector connector COPY symbolic-reasoner symbolic-reasoner COPY third_party third_party COPY common-test-resources common-test-resources +COPY common common COPY CMakeLists.txt . WORKDIR /src/build diff --git a/cdsp/knowledge-layer/README.md b/cdsp/knowledge-layer/README.md index 47600932..9f5b409a 100644 --- a/cdsp/knowledge-layer/README.md +++ b/cdsp/knowledge-layer/README.md @@ -19,26 +19,27 @@ Before setting up the project, make sure you have the following installed: - **g++/clang++** with C++17 support - **Homebrew** (for macOS users) - A WebSocket server to connect to, see how to start the **information-layer** Websocket server [here](../information-layer/README.md) - + ### Installing Dependencies 1. **Install g++**: On windows you may use mingw, e.g. you can install mingw64 with msys64 from here: https://www.msys2.org/ - + Open the MSYS2 MinGW64 Shell. Make sure you're using the MSYS2 MinGW64 Shell (not the regular MSYS2 shell), as this is required for using the 64-bit GCC toolchain. You can find this in the Start menu as MSYS2 MinGW 64-bit. Run the following command: - ```bash - pacman -Syu # Update MSYS2 packages - pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-make # Install 64-bit GCC and Make - ``` - After the installation, verify that gcc and g++ are installed and accessible in the MSYS2 MinGW64 shell by running: - - ```bash - gcc --version - g++ --version - ``` - You should see the version information for the installed GCC toolchain. - + ```bash + pacman -Syu # Update MSYS2 packages + pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-make # Install 64-bit GCC and Make + ``` + + After the installation, verify that gcc and g++ are installed and accessible in the MSYS2 MinGW64 shell by running: + + ```bash + gcc --version + g++ --version + ``` + + You should see the version information for the installed GCC toolchain. 2. **Install Boost**: If you're on macOS, you can install Boost using Homebrew: @@ -48,27 +49,26 @@ Before setting up the project, make sure you have the following installed: ``` For Linux, you can install Boost using your package manager, or build from source: - + ```bash sudo apt-get install libboost-all-dev ``` On Windows you can download a zipfile or take an installer, e.g. from here: https://sourceforge.net/projects/boost/files/boost-binaries/. To use all functionality you have to build the boost library with the gcc compiler. - Open the MSYS2 MinGW64 Shell from the Start menu (not the standard MSYS2 shell), navigate to your Boost directory and build: + Open the MSYS2 MinGW64 Shell from the Start menu (not the standard MSYS2 shell), navigate to your Boost directory and build: ```bash cd /c/path/to/boost_1_86_0 ./bootstrap.sh gcc ./b2 --with-system --with-filesystem --with-thread - ``` - - Important Note: When you want to build the projekt with cmake later, cmake often expects specific library names. So if it does not find the libraries, you have to rename them, e.g.: + ``` + Important Note: When you want to build the projekt with cmake later, cmake often expects specific library names. So if it does not find the libraries, you have to rename them, e.g.: **libboost_system-mgw14-mt-x64-1_86.a** to **libboost_system.a** for the release version and **libboost_system-mgw14-mt-d-x64-1_86.a** tp **libboost_system-d.a** for the debug version. - + And the same for filesystem, thread, atomic and chrono. 3. **Install CMake**: @@ -79,32 +79,32 @@ Before setting up the project, make sure you have the following installed: sudo apt-get install cmake # For Linux ``` - On Windows you can get an installer here: https://cmake.org/download/. - + On Windows you can get an installer here: https://cmake.org/download/. + Make sure you have Windows SDK 10 installed. To check start the Visual Studio Installer (https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/) and look for Visual Studio Build Tools. Here look for the component 'Desktop development with C++' and install it if required. ## Project Setup **Build the WebSocket Client**: - The WebSocket client is located in the `cdsp/knowledge_layer/` directory. To build it from this directory: +The WebSocket client is located in the `cdsp/knowledge_layer/` directory. To build it from this directory: - ```bash - # Create a build directory - mkdir -p build - cd build - - # Run CMake to generate build files - # For Mac: - cmake .. - # For Windows you have to specify the location of the boost libraries: - cmake -DBOOST_ROOT= -DBOOST_LIBRARYDIR=/stage/lib .. - - # Build the project - make - ``` +```bash +# Create a build directory +mkdir -p build +cd build + +# Run CMake to generate build files +# For Mac: +cmake .. +# For Windows you have to specify the location of the boost libraries: +cmake -DBOOST_ROOT= -DBOOST_LIBRARYDIR=/stage/lib .. + +# Build the project +make +``` - If the build is successful, the WebSocket client executable will be generated in the `build/bin/` directory. +If the build is successful, the WebSocket client executable will be generated in the `build/bin/` directory. ## Running the Reasoner WebSocket Client @@ -116,11 +116,13 @@ The project requires certain environment variables to work with the WebSocket se - **HOST_WEBSOCKET_SERVER:** Specifies the hostname of the WebSocket server. The default is `127.0.0.1`. - **PORT_WEBSOCKET_SERVER:** Specifies the port for connecting to the WebSocket server. The default is `8080`. -- **[SCHEMA]_OBJECT_ID:** The Object ID environment variable is required to subscribe and retrieve information for a specific object, such as a VIN (Vehicle Identification Number) when working with Vehicle Signal Specification (VSS) data. [SCHEMA] represents the schema type being used, such as VEHICLE. For example, if you are working with Vehicle data under the VSS schema, the environment variable should be set as: `VEHICLE_OBJECT_ID`. Use the Object ID configured in the [`information-layer`](../information-layer/README.md). +- **TARGET_WEBSOCKET_SERVER:** The target endpoint on the WebSocket server to which the client will connect. The default is empty string. +- **[SCHEMA]\_OBJECT_ID:** The Object ID environment variable is required to subscribe and retrieve information for a specific object, such as a VIN (Vehicle Identification Number) when working with Vehicle Signal Specification (VSS) data. [SCHEMA] represents the schema type being used, such as VEHICLE. For example, if you are working with Vehicle data under the VSS schema, the environment variable should be set as: `VEHICLE_OBJECT_ID`. Use the Object ID configured in the [`information-layer`](../information-layer/README.md). - **HOST_REASONER_SERVER:** Hostname of the RDFox server. The default is `127.0.0.1`. - **PORT_REASONER_SERVER:** Port for RDFox server. The default is `12110`. - **AUTH_REASONER_SERVER_BASE64:** Base64-encoded credentials for RDFox authentication. The default is `cm9vdDphZG1pbg==` (For `root:admin` encoded in base64). - **REASONER_DATASTORE_NAME:** Data store used in RDFox server to store the generated data. The default is `ds-test`. +- **REASONER_ORIGIN_SYSTEM_NAME:** Origin system name for the reasoner server used to identify the source of the data. The default is `SemanticReasoner`. You can customize the WebSocket server configuration by adding the following environment variables in the `/docker/.env` file. Below is an example of what the file could look like: @@ -131,6 +133,7 @@ You can customize the WebSocket server configuration by adding the following env HOST_WEBSOCKET_SERVER="your_custom_host" PORT_WEBSOCKET_SERVER="your_custom_port" +TARGET_WEBSOCKET_SERVER="your_custom_target_endpoint" VEHICLE_OBJECT_ID="vin_to_subscribe" ################################## @@ -144,6 +147,7 @@ RDFOX_DATASTORE="your_custom_rdfox_server_data_store" ``` ### Start the Reasoner Websocket Client + After successfully building the client, you can run it with the following command (VIN is required, if any docker for the knowledge layer is running): ```bash @@ -167,22 +171,31 @@ This integration allows the Reasoner client to interpret data, create RDF triple This module includes a suite of tests to ensure functionality. You can run all tests using **CTest** or execute specific test binaries directly from the `./build/bin/tests/` directory. ## Running All Tests with CTest + From the build directory, run: + ```sh ctest ``` -or + +or + ```sh ctest --output-on-failure ``` + This executes all registered tests and displays failures in detail. ## Running a Specific Test + To execute a specific test binary, navigate to the `./build/bin/tests/` directory and run: + ```sh ./build/bin/tests/ ``` + For example: + ```sh ./build/bin/tests/websocket_client_integration_test -``` \ No newline at end of file +``` diff --git a/cdsp/knowledge-layer/common-test-resources/README.md b/cdsp/knowledge-layer/common-test-resources/README.md index 3c07daf5..06a275b1 100644 --- a/cdsp/knowledge-layer/common-test-resources/README.md +++ b/cdsp/knowledge-layer/common-test-resources/README.md @@ -3,30 +3,37 @@ The **Common Test Resources** module provides a set of reusable components that support testing across the project. This module does not contain direct test cases but instead offers **fixtures, utilities, and helper functions** that facilitate test setup, execution, and validation. ## Purpose + Testing often requires **setup, data generation, and environment configuration** that can be repetitive. To streamline the process, this module centralizes common testing functionalities, ensuring consistency and reducing redundant code. -## Mudule Structure +## Module Structure + The module consists of two primary subdirectories: ### 1. **Common Fixtures** (`common_fixtures/`) + Fixtures are used to set up and tear down test environments. They ensure that each test starts with a known, consistent state: -- Handles the initialization of valid test data related to server interactions. + +- `server_data_fixture.*` builds representative telemetry/server payloads for + connector and reasoner integration tests. +- `use_case_fixture/` contains a lightweight model hierarchy that mirrors the + on-disk structure of real use cases. ### 2. **Test Utilities** (`utils/`) + Utility functions that provide **data generation, randomization, and formatting** for test cases: -- Generates test data points. -- Handles test observation IDs. -- Generates random values for testing. -- Provides utilities for handling UTC dates. -- Generates Vehicle identification numbers (VINs) for tests. +- `data_points_utils.*` builds canonical data-point collections for subscriptions. +- `observation_id_utils.*` produces deterministic observation IDs for assertions. +- `random_utils.*` centralizes random number/string generation. +- `utc_date_utils.*` formats timestamps and offsets for time-based checks. +- `vin_utils.*` validates and generates VIN strings for vehicle-centric tests. ## How It Works + 1. **Fixtures** are used in test cases to **set up** test environments with valid data before execution and **clean up** afterward. 2. **Utilities** help create randomized or structured test data dynamically. 3. By using these resources, test cases remain **clean, readable, and maintainable** while avoiding code duplication. - > [!NOTE] > If new test utilities or fixtures are needed, consider adding them here instead of duplicating logic in multiple test files. - diff --git a/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/inputs/vehicle_data.json b/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/inputs/vehicle_data.json new file mode 100644 index 00000000..56eb1754 --- /dev/null +++ b/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/inputs/vehicle_data.json @@ -0,0 +1,4 @@ +{ + "subscribe": ["Chassis.SteeringWheel.Angle", "CurrentLocation", "Speed"], + "callback": [] +} diff --git a/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/inputs/vehicle_data_required.txt b/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/inputs/vehicle_data_required.txt new file mode 100644 index 00000000..37ceedb2 --- /dev/null +++ b/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/inputs/vehicle_data_required.txt @@ -0,0 +1,3 @@ +Vehicle.Chassis.SteeringWheel.Angle +Vehicle.CurrentLocation +Vehicle.Speed \ No newline at end of file diff --git a/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/model_config.json b/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/model_config.json new file mode 100644 index 00000000..91b5d28a --- /dev/null +++ b/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/model_config.json @@ -0,0 +1,24 @@ +{ + "inputs": { + "vehicle_data": "inputs/vehicle_data_required.txt" + }, + "ontologies": ["ontologies/test_ontology.ttl"], + "output": "output/", + "queries": { + "triple_assembler_helper": { + "default": [ + "queries/triple_assembler_helper/default/data_property.rq", + "queries/triple_assembler_helper/default/object_property.rq" + ] + }, + "output": "queries/output/" + }, + "rules": ["rules/test_rule.dlog"], + "shacl": ["shacl/vehicle_shacl.ttl"], + "reasoner_settings": { + "inference_engine": "RDFox", + "is_ai_reasoner_inference_results": true, + "output_format": "turtle", + "supported_schema_collections": ["vehicle"] + } +} diff --git a/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/ontologies/test_ontology.ttl b/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/ontologies/test_ontology.ttl new file mode 100644 index 00000000..e52f3ec0 --- /dev/null +++ b/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/ontologies/test_ontology.ttl @@ -0,0 +1,25 @@ +@prefix bmw: . +@prefix rdfs: . +@prefix sosa: . +@prefix xsd: . + +# Test the rule +# syntax issue +# semantic issue + +bmw:SlidingWindow1Min a bmw:1minSlideWindow ; + bmw:hasWindowSize "PT1M"^^xsd:duration ; + rdfs:label "60". + + +bmw:SlidingWindow3Sec a bmw:3SecSlideWindow ; + bmw:hasWindowSize "PT3S"^^xsd:duration; + rdfs:label "3". + +bmw:SlidingWindow6Sec a bmw:6SecSlideWindow ; + bmw:hasWindowSize "PT6S"^^xsd:duration; + rdfs:label "6". + +bmw:SlidingWindow5Min a bmw:5MinSlideWindow ; + bmw:hasWindowSize "PT5M"^^xsd:duration ; + rdfs:label "300". \ No newline at end of file diff --git a/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/queries/output/test_query.rq b/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/queries/output/test_query.rq new file mode 100644 index 00000000..7baf29a3 --- /dev/null +++ b/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/queries/output/test_query.rq @@ -0,0 +1,18 @@ +PREFIX bmw: +PREFIX sosa: +PREFIX xsd: + +SELECT ?Vehicle_Speed ?Vehicle_Chassis_SteeringWheel_Angle +WHERE { + ?speedObs a sosa:Observation ; + sosa:observedProperty bmw:speed ; + sosa:hasSimpleResult ?Vehicle_Speed ; + sosa:hasFeatureOfInterest ?vehicle . + ?angleObs a sosa:Observation ; + sosa:observedProperty bmw:angle ; + sosa:hasSimpleResult ?Vehicle_Chassis_SteeringWheel_Angle ; + sosa:hasFeatureOfInterest ?steeringWheel . + ?vehicle bmw:hasPart ?chassis . + ?chassis bmw:hasPart ?steeringWheel . + FILTER(?Vehicle_Speed > 80 && ABS(?Vehicle_Chassis_SteeringWheel_Angle) > 45) +} \ No newline at end of file diff --git a/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/queries/triple_assembler_helper/default/data_property.rq b/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/queries/triple_assembler_helper/default/data_property.rq new file mode 100644 index 00000000..0a4d2a56 --- /dev/null +++ b/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/queries/triple_assembler_helper/default/data_property.rq @@ -0,0 +1,12 @@ +prefix sh: + + SELECT ?class1 ?data_property ?datatype + WHERE { + ?S sh:name "%A%"; + sh:targetClass ?class1; + sh:property [ sh:name ?data_point_name; + sh:path ?data_property; + sh:datatype ?datatype + ]. + FILTER (contains("%B%", ?data_point_name)) #The first value is from the data single point + } \ No newline at end of file diff --git a/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/queries/triple_assembler_helper/default/object_property.rq b/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/queries/triple_assembler_helper/default/object_property.rq new file mode 100644 index 00000000..ba80e0f4 --- /dev/null +++ b/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/queries/triple_assembler_helper/default/object_property.rq @@ -0,0 +1,14 @@ +prefix sh: + +SELECT ?class1 ?object_property ?class2 +WHERE { + ?S sh:name "%A%"; + sh:targetClass ?class1; + sh:property [ sh:name "%B%"; + sh:path ?object_property; + sh:class ?class2; + sh:nodeKind sh:IRI]. + + } + + diff --git a/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/rules/test_rule.dlog b/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/rules/test_rule.dlog new file mode 100644 index 00000000..049fb917 --- /dev/null +++ b/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/rules/test_rule.dlog @@ -0,0 +1,39 @@ +@prefix bmw: . +@prefix sosa: . +@prefix xsd: . + +# --- Detect sharp turns at high speed --- +# Angle > 45°, Speed > 80 km/h + +bmw:SharpTurnEvent[?event], +bmw:hasVehicle[?event, ?vehicle], +bmw:hasSpeed[?event, ?speed_value], +bmw:hasAngle[?event, ?angle_value], +bmw:hasTimestamp[?event, ?ts_trimmed] :- + + # Observation of vehicle speed + sosa:Observation[?obs_speed], + sosa:observedProperty[?obs_speed, bmw:speed], + sosa:hasSimpleResult[?obs_speed, ?speed_value], + sosa:phenomenonTime[?obs_speed, ?ts_speed], + + # Observation of steering angle + sosa:Observation[?obs_angle], + sosa:observedProperty[?obs_angle, bmw:angle], + sosa:hasSimpleResult[?obs_angle, ?angle_value], + sosa:phenomenonTime[?obs_angle, ?ts_angle], + + # Optional: same vehicle if data linked by bmw:Vehicle + bmw:Vehicle[?vehicle], + sosa:madeBySensor[?obs_speed, ?vehicle], + sosa:madeBySensor[?obs_angle, ?vehicle], + + # Timestamp normalization + BIND(SUBSTR(STR(?ts_speed), 1, 19) AS ?ts_trimmed), + + # Filter: high speed and sharp turn + FILTER(xsd:float(?speed_value) > 80), + FILTER(ABS(xsd:float(?angle_value)) > 45), + + # Create (skolemize) a unique event instance + SKOLEM("SharpTurnEvent", ?vehicle, ?ts_trimmed, ?speed_value, ?angle_value, ?event). \ No newline at end of file diff --git a/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/shacl/vehicle_shacl.ttl b/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/shacl/vehicle_shacl.ttl new file mode 100644 index 00000000..f6553cf1 --- /dev/null +++ b/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/shacl/vehicle_shacl.ttl @@ -0,0 +1,374 @@ +@prefix rdf: . +@prefix rdfs: . +@prefix sh: . +@prefix xsd: . +@prefix bmw: . +@prefix val: . + + +val:VehicleShape a sh:NodeShape ; + sh:name "Vehicle" ; + sh:targetClass bmw:Vehicle ; + sh:property [ + sh:name "VIN" ; + sh:path bmw:vin ; + sh:datatype xsd:string ; + sh:minCount 1 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "CurrentLocation" ; + sh:path bmw:hasSignal ; + sh:class bmw:CurrentLocation ; + sh:node val:CurrentLocationShape ; + sh:nodeKind sh:IRI ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "Speed" ; + sh:path bmw:speed ; + sh:datatype xsd:float ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "PWFStatus" ; + sh:path bmw:PWFStatus ; + sh:datatype xsd:string ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "Cabin" ; + sh:path bmw:hasPart ; + sh:class bmw:Cabin ; + sh:node val:CabinShape ; + sh:nodeKind sh:IRI ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "Powertrain" ; + sh:path bmw:hasPart ; + sh:class bmw:Powertrain ; + sh:node val:PowertrainShape ; + sh:nodeKind sh:IRI ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "Chassis" ; + sh:path bmw:hasPart ; + sh:class bmw:Chassis ; + sh:node val:ChassisShape ; + sh:nodeKind sh:IRI ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] . + +val:ChassisShape a sh:NodeShape; + sh:name "Chassis"; + sh:targetClass bmw:Chassis; + sh:property [ + sh:name "SteeringWheel" ; + sh:path bmw:hasPart ; + sh:class bmw:SteeringWheel ; + sh:node val:SteeringWheelShape ; + sh:nodeKind sh:IRI ; + sh:minCount 1 ; + ] . + +val:SteeringWheelShape a sh:NodeShape; + sh:name "SteeringWheel"; + sh:targetClass bmw:SteeringWheel; + sh:property [ + sh:name "Angle" ; + sh:path bmw:angle ; + sh:datatype xsd:float ; + sh:minCount 1 ; + ] . + + +val:CurrentLocationShape a sh:NodeShape ; + sh:name "CurrentLocation" ; + sh:targetClass bmw:CurrentLocation ; + sh:property [ + sh:name "Latitude" ; + sh:path bmw:latitude ; + sh:datatype xsd:double ; + sh:minInclusive -90 ; + sh:maxInclusive 90 ; + val:unit "degrees" ; + sh:description "Current latitude of vehicle in WGS 84 geodetic coordinates." ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "Longitude" ; + sh:path bmw:longitude ; + sh:datatype xsd:double ; + sh:minInclusive -180 ; + sh:maxInclusive 180 ; + val:unit "degrees" ; + sh:description "Current longitude of vehicle in WGS 84 geodetic coordinates." ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "Timestamp" ; + sh:path bmw:timestamp ; + sh:datatype xsd:long ; + val:unit "iso8601" ; + sh:description "Timestamp from GNSS system for current location, formatted according to ISO 8601 with UTC time zone." ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "Heading" ; + sh:path bmw:heading ; + sh:datatype xsd:double ; + sh:minInclusive 0 ; + sh:maxInclusive 360 ; + val:unit "degrees" ; + sh:description "Current heading relative to geographic north. 0 = North, 90 = East, 180 = South, 270 = West." ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] . + +val:PowertrainShape a sh:NodeShape; + sh:name "Powertrain"; + sh:targetClass bmw:Powertrain; + sh:property [ + sh:name "TractionBattery" ; + sh:path bmw:hasPart ; + sh:class bmw:TractionBattery ; + sh:node val:TractionBatteryShape ; + sh:nodeKind sh:IRI ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "Transmission" ; + sh:path bmw:hasPart ; + sh:class bmw:Transmission ; + sh:node val:TransmissionShape ; + sh:nodeKind sh:IRI ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] . + +val:TractionBatteryShape a sh:NodeShape; + sh:name "TractionBattery"; + sh:targetClass bmw:TractionBattery ; + sh:property [ + sh:name "NominalVoltage" ; + sh:path bmw:nominalVoltage ; + sh:datatype xsd:int ; + sh:description "Nominal voltage typically refers to voltage of fully charged battery when delivering rated capacity." ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "StateOfCharge" ; + sh:path bmw:hasSignal ; + sh:class bmw:StateOfCharge; + sh:node val:StateOfChargeNodeShape; + sh:nodeKind sh:IRI ; + sh:description "Information on the state of charge of the vehicle's high voltage battery." ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] . + +val:StateOfChargeNodeShape a sh:NodeShape ; + sh:name "StateOfCharge" ; + sh:targetClass bmw:StateOfCharge ; + sh:property [ + sh:name "CurrentEnergy" ; + sh:path bmw:currentEnergy ; + sh:datatype xsd:float ; + sh:description "Physical state of charge of high voltage battery expressed in kWh." ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] . + + +val:TransmissionShape a sh:NodeShape ; + sh:name "Transmission" ; + sh:targetClass bmw:Transmission ; + sh:property [ + sh:name "CurrentGear" ; + sh:path bmw:currentGear ; + sh:datatype xsd:int ; + sh:description "Is item open or closed? True = Fully or partially open. False = Fully closed." ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] . + + +val:CabinShape a sh:NodeShape ; + sh:name "Cabin" ; + sh:targetClass bmw:Cabin ; + sh:property [ + sh:name "Light" ; + sh:path bmw:hasPart ; + sh:class bmw:CabinLight ; + sh:node val:CabinLightShape ; + sh:nodeKind sh:IRI ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "Door" ; + sh:path bmw:hasPart ; + sh:class bmw:CabinDoor ; + sh:node val:CabinDoorShape ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] . + +val:CabinDoorShape a sh:NodeShape ; + sh:name "Door" ; + sh:targetClass bmw:CabinDoor ; + sh:property [ + sh:path bmw:position ; + sh:name "Row" ; + sh:in ("Row1" "Row2") ; + sh:datatype xsd:string ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:path bmw:position ; + sh:name "Driver" ; + sh:in ("Driver") ; + sh:datatype xsd:string ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "Passenger" ; + sh:path bmw:position ; + sh:in ("Passenger") ; + sh:datatype xsd:string ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "Window" ; + sh:path bmw:hasPart ; + sh:class bmw:Window ; + sh:node val:WindowShape ; + sh:nodeKind sh:IRI ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] . + +val:WindowShape a sh:NodeShape ; + sh:name "Window" ; + sh:targetClass bmw:Window ; + sh:property [ + sh:name "IsOpen" ; + sh:path bmw:isOpen ; + sh:datatype xsd:boolean ; + sh:description "Is item open or closed? True = Fully or partially open. False = Fully closed." ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "Position" ; + sh:path bmw:windowPosition ; + sh:datatype xsd:int ; + sh:minInclusive 0 ; + sh:maxInclusive 100 ; + val:unit "percent" ; + sh:description "Item position. 0 = Start position 100 = End position." ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "Switch" ; + sh:path bmw:switch ; + sh:datatype xsd:string ; + sh:in ("INACTIVE" "CLOSE" "OPEN" "ONE_SHOT_CLOSE" "ONE_SHOT_OPEN") ; + sh:description "Switch controlling sliding action such as window, sunroof, or blind." ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] . + +val:CabinLightShape a sh:NodeShape ; + sh:name "Light" ; + sh:targetClass bmw:CabinLight ; + sh:property [ + sh:name "AmbientLight" ; + sh:path bmw:hasPart ; + sh:class bmw:AmbientLight ; + sh:node val:AmbientLightShape ; + sh:nodeKind sh:IRI ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "SpotLight" ; + sh:path bmw:hasPart ; + sh:class bmw:SpotLight ; + sh:node val:SpotLightShape ; + sh:nodeKind sh:IRI ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] . + +val:AmbientLightShape a sh:NodeShape ; + sh:name "AmbientLight" ; + sh:targetClass bmw:AmbientLight ; + sh:property [ + sh:path bmw:position ; + sh:name "Row" ; + sh:in ("Row1" "Row2") ; + sh:datatype xsd:string ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:path bmw:position ; + sh:name "Driver" ; + sh:in ("Driver") ; + sh:datatype xsd:string ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:path bmw:position ; + sh:name "Passenger" ; + sh:in ("Passenger") ; + sh:datatype xsd:string ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "Colour" ; + sh:path bmw:lightColor ; + sh:datatype xsd:string ; + sh:description "Hexadecimal color code represented as a 3-byte RGB (i.e. Red, Green, and Blue) value preceded by a hash symbol '#'."; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "Intensity" ; + sh:path bmw:lightIntensity ; + sh:datatype xsd:integer ; + sh:minInclusive 1 ; + sh:maxInclusive 100 ; + val:unit "percent" ; + sh:description "How much of the maximum possible brightness of the light is used. 1 = Maximum attenuation, 100 = No attenuation (i.e. full brightness)." ; + sh:minCount 0 ; + sh:maxCount 1 ; + ] ; + sh:property [ + sh:name "IsLightOn" ; + sh:path bmw:lightIsLightOn ; + sh:datatype xsd:boolean ; + sh:description "Indicates whether the light is turned on. True = On, False = Off." ; + sh:minCount 0 ; + sh:maxCount 1 ; +] . \ No newline at end of file diff --git a/cdsp/knowledge-layer/common-test-resources/utils/random_utils.cpp b/cdsp/knowledge-layer/common-test-resources/utils/random_utils.cpp index 34f075b8..14b2cbec 100644 --- a/cdsp/knowledge-layer/common-test-resources/utils/random_utils.cpp +++ b/cdsp/knowledge-layer/common-test-resources/utils/random_utils.cpp @@ -93,31 +93,38 @@ bool RandomUtils::generateRandomBool() { } /** - * @brief Generates a random UTC date as an ISO 8601 string with fractional seconds. + * @brief Generates a random UTC date as an ISO 8601 string with fractional + * seconds. * * @param start_year The earliest possible year for the random date. * @param end_year The latest possible year for the random date. * @return A random UTC date in ISO 8601 format with fractional seconds. */ -std::chrono::system_clock::time_point RandomUtils::generateRandomTimestamp(int start_year, - int end_year, +std::chrono::system_clock::time_point RandomUtils::generateRandomTimestamp(TimestampRange range, bool includeNanos) { // Convert years to time_t - std::tm start_tm = {0, 0, 0, 1, 0, start_year - 1900}; // Jan 1, start_year - std::tm end_tm = {0, 0, 0, 31, 11, end_year - 1900}; // Dec 31, end_year + constexpr int BASE_YEAR = 1900; + constexpr int LAST_DAY_OF_DECEMBER = 31; + constexpr int DECEMBER = 11; + + std::tm start_tm = {0, 0, 0, 1, 0, range.start_year - BASE_YEAR}; // Jan 1, start_year + + std::tm end_tm = { + 0, 0, 0, LAST_DAY_OF_DECEMBER, DECEMBER, range.end_year - BASE_YEAR}; // Dec 31, end_year std::time_t start_time = std::mktime(&start_tm); std::time_t end_time = std::mktime(&end_tm); // Generate a random time_t value between start_time and end_time - std::random_device rd; - std::mt19937 gen(rd()); + std::random_device random_device; + std::mt19937 gen(random_device()); std::uniform_int_distribution dist(start_time, end_time); std::time_t random_time = dist(gen); // Convert to time_point if (includeNanos) { - std::uniform_int_distribution nanos_dist(0, 999999999); + constexpr int MAX_NANOSECONDS = 999999999; + std::uniform_int_distribution nanos_dist(0, MAX_NANOSECONDS); return std::chrono::time_point_cast( std::chrono::system_clock::from_time_t(random_time) + std::chrono::nanoseconds(nanos_dist(gen))); diff --git a/cdsp/knowledge-layer/common-test-resources/utils/random_utils.h b/cdsp/knowledge-layer/common-test-resources/utils/random_utils.h index 659b0fa0..a3cee739 100644 --- a/cdsp/knowledge-layer/common-test-resources/utils/random_utils.h +++ b/cdsp/knowledge-layer/common-test-resources/utils/random_utils.h @@ -6,6 +6,14 @@ #include #include +constexpr int DEFAULT_START_YEAR = 2000; +constexpr int DEFAULT_END_YEAR = 2030; + +struct TimestampRange { + int start_year = DEFAULT_START_YEAR; + int end_year = DEFAULT_END_YEAR; +}; + /** * @brief Provides utility functions to generate random values. */ @@ -18,8 +26,7 @@ class RandomUtils { static double generateRandomDouble(double min, double max); static std::variant generateRandomValue(); static bool generateRandomBool(); - static std::chrono::system_clock::time_point generateRandomTimestamp(int start_year = 2000, - int end_year = 2030, + static std::chrono::system_clock::time_point generateRandomTimestamp(TimestampRange range, bool includeNanos = false); }; diff --git a/cdsp/knowledge-layer/common/CMakeLists.txt b/cdsp/knowledge-layer/common/CMakeLists.txt new file mode 100644 index 00000000..f2bbfbd8 --- /dev/null +++ b/cdsp/knowledge-layer/common/CMakeLists.txt @@ -0,0 +1,9 @@ +# Header-only common target with propagated includes +add_library(common + globals.cpp +) + +target_include_directories(common + INTERFACE + ${CMAKE_CURRENT_SOURCE_DIR} +) diff --git a/cdsp/knowledge-layer/common/README.md b/cdsp/knowledge-layer/common/README.md new file mode 100644 index 00000000..c5591834 --- /dev/null +++ b/cdsp/knowledge-layer/common/README.md @@ -0,0 +1,31 @@ +# Common Utilities + +The `common/` directory provides cross-cutting utilities used throughout the +Knowledge Layer. These helpers cover console logging and global runtime state +so that other modules can rely on a consistent API. + +## Components + +- `globals.*` – Shared runtime state: project root path, use-case path, and JSON-RPC + version string. These functions expose getters/setters so services can configure global + paths during startup. + +Both utilities are linked into most submodules via CMake to provide consistent +logging and configuration behavior. + +## Usage Example + +```cpp +#include "globals.h" +#include + +int main() { + setPathToUseCases("/path/to/use-case"); + std::cout << "Use-case path: " << getProjectRoot() << getPathToUseCases() << std::endl; +} +``` + +## Tests + +Modules that depend on these utilities include their usage in integration +tests; there are no dedicated unit tests inside `common/` itself. diff --git a/cdsp/knowledge-layer/common/globals.cpp b/cdsp/knowledge-layer/common/globals.cpp new file mode 100644 index 00000000..17aa3e4f --- /dev/null +++ b/cdsp/knowledge-layer/common/globals.cpp @@ -0,0 +1,84 @@ + +#include "globals.h" + +namespace { +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +std::string PROJECT_ROOT_RUNTIME; +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +bool IS_PROJECT_ROOT_SET = false; +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +std::string JSON_RPC_VERSION = "2.0"; +// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) +std::string USE_CASES_PATH = "/symbolic-reasoner/examples/use-case/model/"; +} // namespace + +/** + * @brief Retrieves the project root directory. + * + * This function returns a constant reference to a string representing + * the project root directory. If the project root is set, it returns + * the runtime project root; otherwise, it returns the default root. + * + * @return const std::string& A constant reference to the project root string. + */ +const std::string &getProjectRoot() { + static const std::string default_root = std::string(PROJECT_ROOT); + return IS_PROJECT_ROOT_SET ? PROJECT_ROOT_RUNTIME : default_root; +} + +/** + * @brief Sets the project root directory. + * + * This function sets the runtime project root directory to the specified + * path if it has not already been set. If the project root is already + * set, the function returns false without making any changes. + * + * @param root A constant reference to a string representing the new + * project root directory. + * @return true if the project root was successfully set; + * false if it was already set. + */ +bool setProjectRoot(const std::string &root) { + if (IS_PROJECT_ROOT_SET) { + return false; + } + PROJECT_ROOT_RUNTIME = root; + IS_PROJECT_ROOT_SET = true; + return true; +} + +/** + * @brief Retrieves the JSON-RPC version. + * + * This function returns a constant reference to a string representing + * the JSON-RPC version. + * + * @return const std::string& A constant reference to the JSON-RPC version + * string. + */ +const std::string &getJsonRpcVersion() { + static const std::string json_rpc_version = JSON_RPC_VERSION; + return json_rpc_version; +} + +/** + * @brief Sets the path to use cases. + * + * This function sets the path to use cases to the specified string. + * @param path A constant reference to a string representing the new path + * to use cases. + */ +void setPathToUseCases(const std::string &path) { USE_CASES_PATH = path; } + +/** + * @brief Retrieves the path to use cases. + * + * This function returns a constant reference to a string representing + * the path to use cases. + * + * @return const std::string& A constant reference to the path to use cases. + */ +const std::string &getPathToUseCases() { + static const std::string use_cases_path = USE_CASES_PATH; + return use_cases_path; +} \ No newline at end of file diff --git a/cdsp/knowledge-layer/common/globals.h b/cdsp/knowledge-layer/common/globals.h new file mode 100644 index 00000000..d65e2679 --- /dev/null +++ b/cdsp/knowledge-layer/common/globals.h @@ -0,0 +1,12 @@ +#pragma once +#ifndef GLOBALS_H +#define GLOBALS_H +#include + +const std::string &getProjectRoot(); +bool setProjectRoot(const std::string &root); +const std::string &getJsonRpcVersion(); +void setPathToUseCases(const std::string &path); +const std::string &getPathToUseCases(); + +#endif // GLOBALS_H \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/data-objects/CMakeLists.txt b/cdsp/knowledge-layer/connector/data-objects/CMakeLists.txt index f17689a7..778cc70c 100644 --- a/cdsp/knowledge-layer/connector/data-objects/CMakeLists.txt +++ b/cdsp/knowledge-layer/connector/data-objects/CMakeLists.txt @@ -1,6 +1,7 @@ -# Define the websocket_client library +# Define the data_objects library add_library(data_objects bo/data_message.cpp + bo/error.cpp bo/get_message.cpp bo/message_header.cpp bo/node.cpp @@ -11,6 +12,7 @@ add_library(data_objects bo/reasoner_settings.cpp bo/triple_assembler_helper.cpp bo/set_message.cpp + bo/unsubscribe_message.cpp ) # Include directories @@ -25,6 +27,7 @@ target_link_libraries(data_objects PRIVATE nlohmann_json::nlohmann_json utils + common ) # Add dependencies diff --git a/cdsp/knowledge-layer/connector/data-objects/bo/data_message.cpp b/cdsp/knowledge-layer/connector/data-objects/bo/data_message.cpp index 4aa0425e..33488332 100644 --- a/cdsp/knowledge-layer/connector/data-objects/bo/data_message.cpp +++ b/cdsp/knowledge-layer/connector/data-objects/bo/data_message.cpp @@ -50,7 +50,7 @@ std::vector DataMessage::getNodes() const { return nodes_; } */ std::ostream& operator<<(std::ostream& os, const DataMessage& message) { os << "DataMessage {\n"; - os << " ID: " << message.getHeader().getId() << "\n"; + os << " Instance: " << message.getHeader().getInstance() << "\n"; os << " Schema Type: " << schemaTypeToString(message.getHeader().getSchemaType()) << "\n"; os << " Nodes: [\n"; diff --git a/cdsp/knowledge-layer/connector/data-objects/bo/error.cpp b/cdsp/knowledge-layer/connector/data-objects/bo/error.cpp new file mode 100644 index 00000000..9511b29e --- /dev/null +++ b/cdsp/knowledge-layer/connector/data-objects/bo/error.cpp @@ -0,0 +1,70 @@ +#include "error.h" + +/** + * @brief Constructs an Error object with the given parameters. + * + * @param code An integer representing the error code. + * @param message A string containing the error message. Must not be empty. + * @param data An optional JSON object representing additional data for the + * error. + * + * @throws std::invalid_argument if the message is empty. + */ +Error::Error(int code, std::string message, std::optional data) + : code_(code), message_(std::move(message)), data_(std::move(data)) { + if (message_.empty()) { + throw std::invalid_argument("Error message cannot be empty"); + } +} + +/** + * @brief Retrieves the error code. + * + * This function returns the error code associated with the Error object. + * It is a constant member function, meaning it does not modify the state + * of the object. + * + * @return int The error code. + */ +int Error::getCode() const { return code_; } + +/** + * @brief Retrieves the error message. + * + * @return A constant reference to the error message string. + */ +const std::string &Error::getMessage() const { return message_; } + +/** + * @brief Retrieves the data for the error. + * + * This function returns an optional JSON object that contains the data + * for the error if it is available. + * + * @return A constant reference to an optional JSON object containing the error + * data. + */ +const std::optional &Error::getData() const { return data_; } + +/** + * @brief Overloads the << operator to print the Error object. + * + * This function allows the Error object to be printed using standard output + * streams. It formats the output to include the error code, message, and data + * if available. + * + * @param os The output stream to write to. + * @param error The Error object to print. + * @return std::ostream& The output stream with the Error object printed to it. + */ +std::ostream &operator<<(std::ostream &out_stream, const Error &error) { + out_stream << "{\n"; + out_stream << " code: " << error.getCode() << ",\n"; + out_stream << " message: " << error.getMessage() << ",\n"; + if (error.getData()) { + out_stream << " data: " << *error.getData() << ",\n"; + } + out_stream << "}"; + + return out_stream; +} \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/data-objects/bo/error.h b/cdsp/knowledge-layer/connector/data-objects/bo/error.h new file mode 100644 index 00000000..cd9971be --- /dev/null +++ b/cdsp/knowledge-layer/connector/data-objects/bo/error.h @@ -0,0 +1,24 @@ +#ifndef ERROR_H +#define ERROR_H + +#include +#include +#include +#include + +class Error { + public: + Error(int code, std::string message, std::optional data); + + [[nodiscard]] int getCode() const; + [[nodiscard]] const std::string &getMessage() const; + [[nodiscard]] const std::optional &getData() const; + friend std::ostream &operator<<(std::ostream &out_stream, const Error &error); + + private: + int code_; + std::string message_; + std::optional data_; +}; + +#endif // ERROR_H \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/data-objects/bo/get_message.cpp b/cdsp/knowledge-layer/connector/data-objects/bo/get_message.cpp index 4a8cba26..5662dcc8 100644 --- a/cdsp/knowledge-layer/connector/data-objects/bo/get_message.cpp +++ b/cdsp/knowledge-layer/connector/data-objects/bo/get_message.cpp @@ -46,7 +46,7 @@ std::vector GetMessage::getNodes() const { return nodes_; } std::ostream& operator<<(std::ostream& os, const GetMessage& message) { os << "GetMessage {" << "\n"; os << " Header: \n"; - os << " Id: " << message.getHeader().getId() << "\n"; + os << " Instance: " << message.getHeader().getInstance() << "\n"; os << " Schema Type: " << schemaTypeToString(message.getHeader().getSchemaType()) << "\n"; if (message.getNodes().empty()) { os << " Nodes: None" << "\n"; diff --git a/cdsp/knowledge-layer/connector/data-objects/bo/message_header.cpp b/cdsp/knowledge-layer/connector/data-objects/bo/message_header.cpp index 3dff8eb1..16fd339d 100644 --- a/cdsp/knowledge-layer/connector/data-objects/bo/message_header.cpp +++ b/cdsp/knowledge-layer/connector/data-objects/bo/message_header.cpp @@ -1,29 +1,27 @@ #include "message_header.h" -#include "helper.h" - /** - * @brief Constructs a MessageHeader object with the given id, and schema type. + * @brief Constructs a MessageHeader object with the given instance, and schema + * type. * - * @param id The unique identifier for the message header. Must not be empty. + * @param instance The instance for the message header. Must not be empty. * @param schema_type The schema type associated with the message. * - * @throws std::invalid_argument if the id is empty. + * @throws std::invalid_argument if the instance is empty. */ -MessageHeader::MessageHeader(const std::string& id, const SchemaType& schema_type) - : id_(id), schema_type_(schema_type) { - if (id.empty()) { - throw std::invalid_argument("MessageHeader id cannot be empty"); +MessageHeader::MessageHeader(const std::string& instance, const SchemaType& schema_type) + : instance_(instance), schema_type_(schema_type) { + if (instance.empty()) { + throw std::invalid_argument("MessageHeader instance cannot be empty"); } } /** - * @brief Retrieves the ID of the message header. + * @brief Retrieves the instance of the message header. * - * @return A string representing the ID of the message header. + * @return A string representing the instance of the message header. */ -std::string MessageHeader::getId() const { return id_; } - +std::string MessageHeader::getInstance() const { return instance_; } /** * @brief Retrieves the schema type associated with the message header. * diff --git a/cdsp/knowledge-layer/connector/data-objects/bo/message_header.h b/cdsp/knowledge-layer/connector/data-objects/bo/message_header.h index 617b4d68..7dfc911f 100644 --- a/cdsp/knowledge-layer/connector/data-objects/bo/message_header.h +++ b/cdsp/knowledge-layer/connector/data-objects/bo/message_header.h @@ -1,20 +1,19 @@ #ifndef DATA_HEADER_H #define DATA_HEADER_H -#include #include #include "data_types.h" class MessageHeader { public: - MessageHeader(const std::string& id, const SchemaType& schema_defintion); + MessageHeader(const std::string &instance, const SchemaType &schema_type); - std::string getId() const; - SchemaType getSchemaType() const; + [[nodiscard]] std::string getInstance() const; + [[nodiscard]] SchemaType getSchemaType() const; private: - std::string id_; + std::string instance_; SchemaType schema_type_; }; diff --git a/cdsp/knowledge-layer/connector/data-objects/bo/metadata.cpp b/cdsp/knowledge-layer/connector/data-objects/bo/metadata.cpp index 2e2a9a6c..7962c679 100644 --- a/cdsp/knowledge-layer/connector/data-objects/bo/metadata.cpp +++ b/cdsp/knowledge-layer/connector/data-objects/bo/metadata.cpp @@ -1,20 +1,20 @@ #include "metadata.h" -#include "helper.h" - /** - * @brief Constructs a Metadata object with optional timestamps for when the data was received and - * generated. + * @brief Constructs a Metadata object with optional timestamps for when the + * data was received and generated. * - * @param timestamp_received An optional timestamp indicating when the data was received. - * If not provided, the current system time is used. - * @param timestamp_generated An optional timestamp indicating when the data was generated. + * @param timestamp_received An optional timestamp indicating when the data was + * received. If not provided, the current system time is used. + * @param timestamp_generated An optional timestamp indicating when the data was + * generated. + * @param confidence An optional confidence value associated with the metadata. */ -Metadata::Metadata(const std::optional& timestamp_received, - const std::optional& timestamp_generated) - : generated_(timestamp_generated) { - if (timestamp_received.has_value()) { - received_ = timestamp_received.value(); +Metadata::Metadata(const Timestamps ×tamps, const std::optional &origin, + const std::optional> &confidence) + : generated_(timestamps.generated), origin_(origin), confidence_(confidence) { + if (timestamps.received.has_value()) { + received_ = timestamps.received.value(); } else { received_ = std::chrono::system_clock::now(); } @@ -23,8 +23,8 @@ Metadata::Metadata(const std::optional& t /** * @brief Retrieves the generated time point of the metadata. * - * @return An optional containing the time point when the metadata was generated. - * If the time point is not set, the optional will be empty. + * @return An optional containing the time point when the metadata was + * generated. If the time point is not set, the optional will be empty. */ std::optional Metadata::getGenerated() const { return generated_; @@ -35,4 +35,22 @@ std::optional Metadata::getGenerated() co * * @return The time point when the metadata was received. */ -std::chrono::system_clock::time_point Metadata::getReceived() const { return received_; } \ No newline at end of file +std::chrono::system_clock::time_point Metadata::getReceived() const { return received_; } + +/** + * @brief Retrieves the system origin information of the data. + * + * @return An optional containing the origin information. If the origin is not + * set, the optional will be empty. + */ +std::optional Metadata::getOriginType() const { return origin_; } + +/** + * @brief Retrieves the confidence value associated with the metadata. + * + * @return An optional containing a pair of confidence type and value. + * If the confidence is not set, the optional will be empty. + */ +std::optional> Metadata::getConfidence() const { + return confidence_; +} \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/data-objects/bo/metadata.h b/cdsp/knowledge-layer/connector/data-objects/bo/metadata.h index 7407d0c7..faf2fbd6 100644 --- a/cdsp/knowledge-layer/connector/data-objects/bo/metadata.h +++ b/cdsp/knowledge-layer/connector/data-objects/bo/metadata.h @@ -4,20 +4,34 @@ #include #include +#include "data_types.h" + class Metadata { public: + struct Timestamps { + std::optional received; + std::optional generated; + }; + + struct OriginType { + std::optional name; + std::optional uri; + }; explicit Metadata( - const std::optional& timestamp_received = - std::nullopt, - const std::optional& timestamp_generated = - std::nullopt); + const Timestamps ×tamps = {std::nullopt, std::nullopt}, + const std::optional &origin = std::nullopt, + const std::optional> &confidence = std::nullopt); - std::optional getGenerated() const; - std::chrono::system_clock::time_point getReceived() const; + [[nodiscard]] std::optional getGenerated() const; + [[nodiscard]] std::chrono::system_clock::time_point getReceived() const; + [[nodiscard]] std::optional getOriginType() const; + [[nodiscard]] std::optional> getConfidence() const; private: std::optional generated_; std::chrono::system_clock::time_point received_; + std::optional origin_; + std::optional> confidence_; }; #endif // METADATA_H \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/data-objects/bo/model_config.cpp b/cdsp/knowledge-layer/connector/data-objects/bo/model_config.cpp index c6d96927..837495ff 100644 --- a/cdsp/knowledge-layer/connector/data-objects/bo/model_config.cpp +++ b/cdsp/knowledge-layer/connector/data-objects/bo/model_config.cpp @@ -1,25 +1,27 @@ #include "model_config.h" +#include "data_types.h" #include "helper.h" /** * @brief Constructs a ModelConfig object with the specified parameters. * - * @param inputs A map of SchemaType to a vector of strings representing the inputs. - * This map cannot be empty. + * @param supported_data_points A map of SchemaType to a SchemaInput object representing the + * supported data points. This cannot be empty and must contain all supported schema + * collections. * @param ontologies A vector of pairs, each containing a ReasonerSyntaxType and a string, - * representing the ontologies. + * representing the ontologies. * @param output_path A string specifying the path for the output. This cannot be empty. * @param reasoner_rules A vector of pairs, each containing a RuleLanguageType and a string, - * representing the reasoner rules. This cannot be empty. + * representing the reasoner rules. This cannot be empty. * @param validation_shapes A vector of pairs, each containing a ReasonerSyntaxType and a string, - * representing the validation shapes. This cannot be empty. + * representing the validation shapes. This cannot be empty. * @param triple_assembler_helper A TripleAssemblerHelper object containing the * queries to create triples. This cannot be empty. - * @param reasoning_output_queries A vector of pairs, each containing a ReasonerSyntaxType and a - * string, representing the query rules output. This cannot be empty. + * @param reasoning_output_queries A vector of ReasoningOutputQuery objects + * representing the reasoning output queries. This cannot be empty. * @param reasoner_settings A ReasonerSettings object containing the settings for the reasoner. - * It must have supported schema collections. + * It must have supported schema collections. * * @throws std::invalid_argument if the inputs map is empty. * @throws std::invalid_argument if the output path is empty. @@ -35,15 +37,15 @@ * supported schema collections or a default query. */ ModelConfig::ModelConfig( - const std::map>& inputs, + const std::map& supported_data_points, const std::vector>& ontologies, const std::string& output_path, const std::vector>& reasoner_rules, const std::vector>& validation_shapes, const TripleAssemblerHelper& triple_assembler_helper, - const std::vector>& reasoning_output_queries, + const std::vector& reasoning_output_queries, const ReasonerSettings& reasoner_settings) - : inputs_(inputs), + : inputs_(supported_data_points), ontologies_(ontologies), output_path_(output_path), reasoner_rules_(reasoner_rules), @@ -113,12 +115,14 @@ std::map ModelConfig::getObjectId() const { return obje /** * @brief Retrieves the input configurations for the model. * - * This function returns a map where each key is a SchemaType and the corresponding value - * is a vector of strings representing the inputs associated with that schema type. + * This function returns a map where each key is a SchemaType and the + * corresponding value is a SchemaInput object representing the inputs + * associated with that schema type. * - * @return A map of SchemaType to a vector of input strings. + * @return A map of SchemaType to a SchemaInput object representing the + * inputs. */ -std::map> ModelConfig::getInputs() const { return inputs_; } +std::map ModelConfig::getInputs() const { return inputs_; } /** * @brief Retrieves the list of ontologies. @@ -178,17 +182,15 @@ TripleAssemblerHelper ModelConfig::getQueriesTripleAssemblerHelper() const { } /** - * @brief Retrieves the queries rules output configuration. + * @brief Retrieves the reasoning output queries stored in the model + * configuration. * - * This function returns a vector of pairs, where each pair consists of a - * QueryLanguageType and a corresponding string. These pairs represent the - * queries used to get the rules output associated with the model configuration. + * This function returns a vector of ReasoningOutputQuery objects that + * represent the reasoning output queries stored in the model configuration. * - * @return A vector of pairs containing QueryLanguageType and string, - * representing the queries of the rules output. + * @return A vector of ReasoningOutputQuery objects. */ -std::vector> ModelConfig::getReasoningOutputQueries() - const { +std::vector ModelConfig::getReasoningOutputQueries() const { return reasoning_output_queries_; } @@ -220,8 +222,9 @@ std::ostream& operator<<(std::ostream& os, const ModelConfig& config) { os << " Inputs: {\n"; for (const auto& [schema, data_points] : config.getInputs()) { os << " " << schemaTypeToString(schema) << ": [\n"; - for (const auto& data_point : data_points) { - os << " " << data_point << ",\n"; + os << " Subscribe: [\n"; + for (const auto& sub : data_points.subscribe) { + os << " " << sub << ",\n"; } os << " ]\n"; } @@ -273,10 +276,11 @@ std::ostream& operator<<(std::ostream& os, const ModelConfig& config) { os << " }\n"; } os << " Queries Rules Output: [\n"; - for (const auto& [syntax, content] : config.getReasoningOutputQueries()) { + for (const auto& query : config.getReasoningOutputQueries()) { os << " {\n"; - os << " Query Type: " << queryLanguageTypeToContentType(syntax) << ",\n"; - os << " Content: " << content << "\n"; + os << " Query Language: " << queryLanguageTypeToContentType(query.query_language) + << ",\n"; + os << " Query: " << query.query << "\n"; os << " }\n"; } os << " ]\n"; diff --git a/cdsp/knowledge-layer/connector/data-objects/bo/model_config.h b/cdsp/knowledge-layer/connector/data-objects/bo/model_config.h index 0f406697..66ce405e 100644 --- a/cdsp/knowledge-layer/connector/data-objects/bo/model_config.h +++ b/cdsp/knowledge-layer/connector/data-objects/bo/model_config.h @@ -9,39 +9,43 @@ #include "reasoner_settings.h" #include "triple_assembler_helper.h" +struct SchemaInputList { + std::vector subscribe; + + bool operator==(const SchemaInputList& other) const { return subscribe == other.subscribe; } +}; + class ModelConfig { public: - ModelConfig( - const std::map>& supported_data_points, - const std::vector>& ontologies, - const std::string& output_path, - const std::vector>& reasoner_rules, - const std::vector>& validation_shapes, - const TripleAssemblerHelper& triple_assembler_helper, - const std::vector>& reasoning_output_queries, - const ReasonerSettings& reasoner_settings); + ModelConfig(const std::map& supported_data_points, + const std::vector>& ontologies, + const std::string& output_path, + const std::vector>& reasoner_rules, + const std::vector>& validation_shapes, + const TripleAssemblerHelper& triple_assembler_helper, + const std::vector& reasoning_output_queries, + const ReasonerSettings& reasoner_settings); virtual std::map getObjectId() const; - virtual std::map> getInputs() const; + virtual std::map getInputs() const; virtual std::vector> getOntologies() const; virtual std::string getOutput() const; virtual std::vector> getReasonerRules() const; virtual std::vector> getValidationShapes() const; virtual TripleAssemblerHelper getQueriesTripleAssemblerHelper() const; - virtual std::vector> getReasoningOutputQueries() - const; + virtual std::vector getReasoningOutputQueries() const; virtual ReasonerSettings getReasonerSettings() const; friend std::ostream& operator<<(std::ostream& os, const ModelConfig& config); private: - std::map> inputs_; + std::map inputs_; std::vector> ontologies_; std::string output_path_; std::vector> reasoner_rules_; std::vector> validation_shapes_; ReasonerSettings reasoner_settings_; TripleAssemblerHelper triple_assembler_helper_; - std::vector> reasoning_output_queries_; + std::vector reasoning_output_queries_; std::map object_ids_; }; diff --git a/cdsp/knowledge-layer/connector/data-objects/bo/node.cpp b/cdsp/knowledge-layer/connector/data-objects/bo/node.cpp index e45420ce..02354821 100644 --- a/cdsp/knowledge-layer/connector/data-objects/bo/node.cpp +++ b/cdsp/knowledge-layer/connector/data-objects/bo/node.cpp @@ -1,6 +1,5 @@ #include "node.h" -#include #include /** * @brief Constructs a new Node object. @@ -8,22 +7,14 @@ * @param name The name of the node. Must not be empty. * @param value The optional value associated with the node. * @param metadata The metadata associated with the node. - * @param supported_data_points A vector of supported data points. * * @throws std::invalid_argument if the name is empty. - * @throws std::invalid_argument if the name is not supported. */ -Node::Node(const std::string& name, const std::optional& value, - const Metadata& metadata, const std::vector& supported_data_points) - : name_(name), value_(value), metadata_(metadata) { - if (name.empty()) { +Node::Node(std::string name, std::optional value, Metadata metadata) + : name_(std::move(name)), value_(std::move(value)), metadata_(std::move(metadata)) { + if (name_.empty()) { throw std::invalid_argument("Node name cannot be empty"); } - if (supported_data_points.empty() || - std::find(supported_data_points.begin(), supported_data_points.end(), name) == - supported_data_points.end()) { - throw std::invalid_argument("Node name `" + name + "` is not supported"); - } } /** @@ -36,7 +27,8 @@ std::string Node::getName() const { return name_; } /** * @brief Retrieves the value stored in the node. * - * @return std::optional An optional containing the value if it exists, + * @return std::optional An optional containing the value if it + * exists, */ std::optional Node::getValue() const { return value_; } diff --git a/cdsp/knowledge-layer/connector/data-objects/bo/node.h b/cdsp/knowledge-layer/connector/data-objects/bo/node.h index 18e8103c..05af52db 100644 --- a/cdsp/knowledge-layer/connector/data-objects/bo/node.h +++ b/cdsp/knowledge-layer/connector/data-objects/bo/node.h @@ -3,18 +3,16 @@ #include #include -#include #include "metadata.h" class Node { public: - Node(const std::string& name, const std::optional& value, const Metadata& metadata, - const std::vector& supported_data_points); + Node(std::string name, std::optional value, Metadata metadata); - std::string getName() const; - std::optional getValue() const; - Metadata getMetadata() const; + [[nodiscard]] std::string getName() const; + [[nodiscard]] std::optional getValue() const; + [[nodiscard]] Metadata getMetadata() const; private: std::string name_; diff --git a/cdsp/knowledge-layer/connector/data-objects/bo/set_message.cpp b/cdsp/knowledge-layer/connector/data-objects/bo/set_message.cpp index 5b9d879f..db511f12 100644 --- a/cdsp/knowledge-layer/connector/data-objects/bo/set_message.cpp +++ b/cdsp/knowledge-layer/connector/data-objects/bo/set_message.cpp @@ -42,7 +42,7 @@ std::vector SetMessage::getNodes() const { return nodes_; } std::ostream& operator<<(std::ostream& os, const SetMessage& message) { os << "SetMessage {" << "\n"; os << " Header: \n"; - os << " Id: " << message.getHeader().getId() << "\n"; + os << " Instance: " << message.getHeader().getInstance() << "\n"; os << " Schema Type: " << schemaTypeToString(message.getHeader().getSchemaType()) << "\n"; if (message.getNodes().empty()) { os << " Nodes: None" << "\n"; diff --git a/cdsp/knowledge-layer/connector/data-objects/bo/status_message.cpp b/cdsp/knowledge-layer/connector/data-objects/bo/status_message.cpp index e586df69..771c7ed0 100644 --- a/cdsp/knowledge-layer/connector/data-objects/bo/status_message.cpp +++ b/cdsp/knowledge-layer/connector/data-objects/bo/status_message.cpp @@ -3,67 +3,48 @@ /** * @brief Constructs a StatusMessage object with the given parameters. * - * @param code An integer representing the status code. - * @param message A string containing the status message. Must not be empty. - * @param requestId An optional string representing the request ID. - * @param timestamp A time_point representing the timestamp of the status message. Must not be - * empty. - * - * @throws std::invalid_argument if the message is empty. - * @throws std::invalid_argument if the timestamp is empty. + * @param identifier A string representing the request ID. + * @param error An optional Error object representing the error associated with + * the status message. */ -StatusMessage::StatusMessage(int code, const std::string& message, - const std::optional& requestId, - const std::chrono::system_clock::time_point& timestamp) - : code_(code), message_(message), request_id_(requestId), timestamp_(timestamp) { - if (message_.empty()) { - throw std::invalid_argument("StatusMessage message cannot be empty"); +StatusMessage::StatusMessage(int identifier, std::optional error) + : id_(identifier), error_(std::move(error)) { + if (id_ < 0) { + throw std::invalid_argument("StatusMessage ID cannot be negative"); } } /** - * @brief Returns the status code. - * - * @return An integer representing the status code. - */ -int StatusMessage::getCode() const { return code_; } - -/** - * @brief Returns the status message. + * @brief Returns the ID of the request. * - * @return A string containing the status message. + * @return An integer representing the request ID. */ -std::string StatusMessage::getMessage() const { return message_; } +int StatusMessage::getIdentifier() const { return id_; } /** - * @brief Returns the request ID. + * @brief Returns the optional Error object associated with the status message. * - * @return An optional string representing the request ID. + * @return An optional Error object. */ -std::optional StatusMessage::getRequestId() const { return request_id_; } - -/** - * @brief Returns the timestamp. - * - * @return A time_point representing the timestamp of the status message. - */ -std::chrono::system_clock::time_point StatusMessage::getTimestamp() const { return timestamp_; } +std::optional StatusMessage::getError() const { return error_; } /** * @brief Overloads the << operator to print the StatusMessage object. * * @param os The output stream to write to. * @param message The StatusMessage object to print. - * @return std::ostream& The output stream with the StatusMessage object printed to it. + * @return std::ostream& The output stream with the StatusMessage object printed + * to it. */ -std::ostream& operator<<(std::ostream& os, const StatusMessage& message) { - os << "StatusMessage {" << "\n"; - os << " code: " << message.getCode() << ",\n"; - os << " message: " << message.getMessage() << ",\n"; - os << " request_id: " - << (message.getRequestId() ? *message.getRequestId() : std::string("null")) << ",\n"; - os << " timestamp: " << message.getTimestamp().time_since_epoch().count() << " nanos\n"; - os << "}"; +std::ostream &operator<<(std::ostream &out_stream, const StatusMessage &message) { + out_stream << "StatusMessage {" << "\n"; + out_stream << " id: " << message.getIdentifier() << ",\n"; + if (message.getError()) { + out_stream << " error: " << *message.getError() << ",\n"; + } else { + out_stream << " error: null,\n"; + } + out_stream << "}"; - return os; + return out_stream; } \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/data-objects/bo/status_message.h b/cdsp/knowledge-layer/connector/data-objects/bo/status_message.h index 85cab35c..e91da6d6 100644 --- a/cdsp/knowledge-layer/connector/data-objects/bo/status_message.h +++ b/cdsp/knowledge-layer/connector/data-objects/bo/status_message.h @@ -1,26 +1,22 @@ #ifndef STATUS_MESSAGE_H #define STATUS_MESSAGE_H -#include #include #include -#include + +#include "error.h" class StatusMessage { public: - StatusMessage(int code, const std::string& message, const std::optional& requestId, - const std::chrono::system_clock::time_point& timestamp); + StatusMessage(int identifier, std::optional error); - int getCode() const; - std::string getMessage() const; - std::optional getRequestId() const; - std::chrono::system_clock::time_point getTimestamp() const; + [[nodiscard]] int getIdentifier() const; + [[nodiscard]] std::optional getError() const; friend std::ostream& operator<<(std::ostream& os, const StatusMessage& message); private: - int code_; - std::string message_; - std::optional request_id_; + int id_; + std::optional error_; std::chrono::system_clock::time_point timestamp_; }; diff --git a/cdsp/knowledge-layer/connector/data-objects/bo/subscribe_message.cpp b/cdsp/knowledge-layer/connector/data-objects/bo/subscribe_message.cpp index 891366b7..adc03885 100644 --- a/cdsp/knowledge-layer/connector/data-objects/bo/subscribe_message.cpp +++ b/cdsp/knowledge-layer/connector/data-objects/bo/subscribe_message.cpp @@ -45,7 +45,7 @@ std::vector SubscribeMessage::getNodes() const { return nodes_; } std::ostream& operator<<(std::ostream& os, const SubscribeMessage& message) { os << "SubscribeMessage {" << "\n"; os << " Header: \n"; - os << " Id: " << message.getHeader().getId() << "\n"; + os << " Instance: " << message.getHeader().getInstance() << "\n"; os << " Schema Type: " << schemaTypeToString(message.getHeader().getSchemaType()) << "\n"; if (message.getNodes().empty()) { os << " Nodes: None" << "\n"; diff --git a/cdsp/knowledge-layer/connector/data-objects/bo/tests/utils/mock_model_config.h b/cdsp/knowledge-layer/connector/data-objects/bo/tests/utils/mock_model_config.h index 5670e76c..0d3fcbef 100644 --- a/cdsp/knowledge-layer/connector/data-objects/bo/tests/utils/mock_model_config.h +++ b/cdsp/knowledge-layer/connector/data-objects/bo/tests/utils/mock_model_config.h @@ -3,6 +3,7 @@ #include +#include "data_types.h" #include "model_config.h" class MockModelConfig : public ModelConfig { @@ -10,8 +11,8 @@ class MockModelConfig : public ModelConfig { MockModelConfig() : ModelConfig( // Supported data points - std::map>{ - {SchemaType::VEHICLE, {"mocked_supported_data_points"}}}, + std::map{ + {SchemaType::VEHICLE, SchemaInputList{{"mocked_supported_data_points"}}}}, // Ontologies std::vector>{}, // Output path @@ -27,14 +28,14 @@ class MockModelConfig : public ModelConfig { {{QueryLanguageType::SPARQL, "mocked_query_object"}, {QueryLanguageType::SPARQL, "mocked_query_data"}}}}), // Reasoning output queries - std::vector>{ + std::vector{ {QueryLanguageType::SPARQL, "mocked_reasoning_output_queries"}}, // Reasoner settings ReasonerSettings(InferenceEngineType::RDFOX, ReasonerSyntaxType::TURTLE, {SchemaType::VEHICLE}, true)) {} MOCK_METHOD((std::map), getObjectId, (), (const, override)); - MOCK_METHOD((std::map>), getInputs, (), (const, override)); + MOCK_METHOD((std::map), getInputs, (), (const, override)); MOCK_METHOD((std::vector>), getOntologies, (), (const, override)); MOCK_METHOD((std::string), getOutput, (), (const, override)); @@ -43,8 +44,8 @@ class MockModelConfig : public ModelConfig { MOCK_METHOD((std::vector>), getValidationShapes, (), (const, override)); MOCK_METHOD((TripleAssemblerHelper), getQueriesTripleAssemblerHelper, (), (const, override)); - MOCK_METHOD((std::vector>), getReasoningOutputQueries, - (), (const, override)); + MOCK_METHOD((std::vector), getReasoningOutputQueries, (), + (const, override)); MOCK_METHOD((ReasonerSettings), getReasonerSettings, (), (const, override)); }; diff --git a/cdsp/knowledge-layer/connector/data-objects/bo/unsubscribe_message.cpp b/cdsp/knowledge-layer/connector/data-objects/bo/unsubscribe_message.cpp new file mode 100644 index 00000000..e62ad27e --- /dev/null +++ b/cdsp/knowledge-layer/connector/data-objects/bo/unsubscribe_message.cpp @@ -0,0 +1,53 @@ +#include "unsubscribe_message.h" + +UnsubscribeMessage::UnsubscribeMessage(MessageHeader header, std::vector nodes) + : header_(std::move(header)), nodes_(std::move(nodes)) {} + +/** + * @brief Retrieves the message header associated with the unsubscribe message. + * + * This function returns the header of the unsubscribe message, which contains + * metadata and information relevant to the message. The header is returned as + * a MessageHeader object. + * + * @return MessageHeader The header of the unsubscribe message. + */ +MessageHeader UnsubscribeMessage::getHeader() const { return header_; } + +/** + * @brief Retrieves the list of nodes associated with the UnsubscribeMessage. + * + * @return A vector containing Node objects. + */ +std::vector UnsubscribeMessage::getNodes() const { return nodes_; } + +/** + * Overloads the insertion (<<) operator for the UnsubscribeMessage class. + * + * This function formats and outputs the contents of an UnsubscribeMessage + * object to the provided output stream. It includes the header information + * and the list of nodes associated with the message. + * + * @param out_stream The output stream to which the UnsubscribeMessage will be + * written. + * @param message The UnsubscribeMessage object to be output. + * @return A reference to the output stream after the UnsubscribeMessage has + * been written. + */ +std::ostream &operator<<(std::ostream &out_stream, const UnsubscribeMessage &message) { + out_stream << "UnsubscribeMessage {" << "\n"; + out_stream << " Header: \n"; + out_stream << " Instance: " << message.getHeader().getInstance() << "\n"; + out_stream << " Schema Type: " << schemaTypeToString(message.getHeader().getSchemaType()) + << "\n"; + if (message.getNodes().empty()) { + out_stream << " Nodes: None" << "\n"; + } else { + out_stream << " Nodes: " << "\n"; + for (const auto &node : message.getNodes()) { + out_stream << " " << node.getName() << "\n"; + } + } + out_stream << "}" << "\n"; + return out_stream; +} \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/data-objects/bo/unsubscribe_message.h b/cdsp/knowledge-layer/connector/data-objects/bo/unsubscribe_message.h new file mode 100644 index 00000000..e8c36cac --- /dev/null +++ b/cdsp/knowledge-layer/connector/data-objects/bo/unsubscribe_message.h @@ -0,0 +1,20 @@ +#ifndef UNSUBSCRIBE_MESSAGE_H +#define UNSUBSCRIBE_MESSAGE_H + +#include "message_header.h" +#include "node.h" + +class UnsubscribeMessage { + public: + UnsubscribeMessage(MessageHeader header, std::vector nodes); + + [[nodiscard]] MessageHeader getHeader() const; + [[nodiscard]] std::vector getNodes() const; + friend std::ostream &operator<<(std::ostream &out_stream, const UnsubscribeMessage &message); + + private: + MessageHeader header_; + std::vector nodes_; +}; + +#endif // UNSUBSCRIBE_MESSAGE_H \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/data-objects/dto/data_message_dto.h b/cdsp/knowledge-layer/connector/data-objects/dto/data_message_dto.h index 7053645e..26f6c62d 100644 --- a/cdsp/knowledge-layer/connector/data-objects/dto/data_message_dto.h +++ b/cdsp/knowledge-layer/connector/data-objects/dto/data_message_dto.h @@ -3,40 +3,32 @@ #include #include -#include #include "metadata_dto.h" /** * @brief Data Transfer Object for Websocket Messages */ struct DataMessageDTO { - std::string type; - std::string schema; - std::string instance; - std::optional path; + int id; nlohmann::json data; std::optional metadata; - std::optional requestId; // Overload the << operator to print the DTO - friend std::ostream& operator<<(std::ostream& os, const DataMessageDTO& dto) { - os << "DataMessageDTO {\n" - << " type: " << dto.type << "\n" - << " schema: " << dto.schema << "\n" - << " instance: " << dto.instance << "\n" - << " data: " << dto.data.dump(4) << "\n" - << " path: " << (dto.path ? *dto.path : "null") << "\n" - << " metadata: "; + friend std::ostream &operator<<(std::ostream &out_stream, const DataMessageDTO &dto) { + out_stream << "DataMessageDTO {\n" + << " id: " << dto.id << "\n" + << " data: " << dto.data.dump(4) << "\n" + << " metadata: "; if (dto.metadata) { - os << dto.metadata.value(); + out_stream << dto.metadata.value(); } else { - os << "null"; + out_stream << "null"; } - os << "\n requestId: " << (dto.requestId ? *dto.requestId : "null") << "\n" - << "}"; - return os; + out_stream << "\n" + << "}\n"; + return out_stream; } }; diff --git a/cdsp/knowledge-layer/connector/data-objects/dto/get_message_dto.h b/cdsp/knowledge-layer/connector/data-objects/dto/get_message_dto.h index e8c67b18..07159118 100644 --- a/cdsp/knowledge-layer/connector/data-objects/dto/get_message_dto.h +++ b/cdsp/knowledge-layer/connector/data-objects/dto/get_message_dto.h @@ -5,15 +5,16 @@ #include #include +#include "globals.h" + /** * @brief Data Transfer Object for GetMessage. */ struct GetMessageDTO { - std::string type; + int id; std::string schema; std::string instance; std::optional path; - std::optional requestId; std::optional format; std::optional root; }; @@ -22,21 +23,24 @@ struct GetMessageDTO { * @brief Define the `to_json` function for `nlohmann::json` * * This function converts a GetMessageDTO object into a JSON representation. - * It includes mandatory fields such as type, schema, and instance, and conditionally - * includes optional fields if they are present. * * @param j The JSON object to which the GetMessageDTO will be serialized. * @param dto The GetMessageDTO object containing the data to be serialized. */ -inline void to_json(nlohmann::json& j, const GetMessageDTO& dto) { - j = nlohmann::json{{"type", dto.type}, {"schema", dto.schema}, {"instance", dto.instance}}; - if (dto.path) - j["path"] = *dto.path; - if (dto.requestId) - j["requestId"] = *dto.requestId; - if (dto.format) - j["format"] = *dto.format; - if (dto.root) - j["root"] = *dto.root; +inline void to_json(nlohmann::json &json_obj, const GetMessageDTO &dto) { + nlohmann::json::object_t params_obj; + params_obj = nlohmann::json{{"schema", dto.schema}, {"instance", dto.instance}}; + if (dto.path) { + params_obj["path"] = *dto.path; + } + if (dto.format) { + params_obj["format"] = *dto.format; + } + if (dto.root) { + params_obj["root"] = *dto.root; + } + + json_obj = + nlohmann::json{{"jsonrpc", getJsonRpcVersion()}, {"id", dto.id}, {"params", params_obj}}; } #endif // GET_MESSAGE_DTO_H \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/data-objects/dto/metadata_dto.h b/cdsp/knowledge-layer/connector/data-objects/dto/metadata_dto.h index fad4b9d6..768084c3 100644 --- a/cdsp/knowledge-layer/connector/data-objects/dto/metadata_dto.h +++ b/cdsp/knowledge-layer/connector/data-objects/dto/metadata_dto.h @@ -1,6 +1,8 @@ #ifndef METADATA_DTO_H #define METADATA_DTO_H +#include +#include #include #include @@ -13,27 +15,58 @@ struct MetadataDTO { int64_t nanos = 0; }; + struct OriginType { + std::string name; + std::optional uri; + }; + + struct Confidence { + std::string type; + int value = 0; + }; + struct NodeMetadata { Timestamp received; Timestamp generated; + std::optional origin_type; + std::optional confidence; }; std::unordered_map nodes; // Overload the << operator to print the DTO - friend std::ostream& operator<<(std::ostream& os, const MetadataDTO& dto) { - os << "MetadataDTO: {\n"; - for (const auto& node : dto.nodes) { - os << " " << node.first << ": {\n"; - os << " received: { Seconds: " << node.second.received.seconds << ", " - << " nanos: " << node.second.received.nanos << " },\n"; - os << " generated: { Seconds: " << node.second.generated.seconds << ", " - << " nanos: " << node.second.generated.nanos << " }\n"; - os << " }\n"; + friend std::ostream &operator<<(std::ostream &out_stream, const MetadataDTO &dto) { + out_stream << "MetadataDTO: {\n"; + for (const auto &node : dto.nodes) { + out_stream << " " << node.first << ": {\n"; + out_stream << " timestamps: {"; + out_stream << " received: { Seconds: " << node.second.received.seconds << ", " + << " nanos: " << node.second.received.nanos << " },\n"; + out_stream << " generated: { Seconds: " << node.second.generated.seconds << ", " + << " nanos: " << node.second.generated.nanos << " }\n"; + out_stream << " }\n"; + + if (node.second.origin_type.has_value() && node.second.origin_type->name != "") { + out_stream << " origin: { " + << " type: { name: " << node.second.origin_type->name; + if (node.second.origin_type->uri) { + out_stream << ", uri: " << *node.second.origin_type->uri; + } else { + out_stream << ", uri: "; + } + out_stream << " } \n" + << " }\n"; + } + + if (node.second.confidence) { + out_stream << " confidence: { type: " << node.second.confidence->type + << ", value: " << node.second.confidence->value << " }\n"; + } + out_stream << " }\n"; } - os << "}"; - return os; + out_stream << "}"; + return out_stream; } }; @@ -42,19 +75,31 @@ struct MetadataDTO { * * This function converts a MetadataDTO object into a JSON representation. * - * @param j The JSON object to which the MetadataDTO will be serialized. + * @param json_object The JSON object to which the MetadataDTO will be serialized. * @param dto The MetadataDTO object containing the data to be serialized. */ -inline void to_json(nlohmann::json& j, const MetadataDTO& dto) { - j = nlohmann::json::object(); - for (const auto& [node_name, metadata] : dto.nodes) { +inline void to_json(nlohmann::json &json_object, const MetadataDTO &dto) { + json_object = nlohmann::json::object(); + for (const auto &[node_name, metadata] : dto.nodes) { if (metadata.received.seconds != 0 || metadata.received.nanos != 0) { - j[node_name]["received"] = {{"seconds", metadata.received.seconds}, - {"nanos", metadata.received.nanos}}; + json_object[node_name]["timestamps"]["received"] = { + {"seconds", metadata.received.seconds}, {"nanos", metadata.received.nanos}}; } if (metadata.generated.seconds != 0 || metadata.generated.nanos != 0) { - j[node_name]["generated"] = {{"seconds", metadata.generated.seconds}, - {"nanos", metadata.generated.nanos}}; + json_object[node_name]["timestamps"]["generated"] = { + {"seconds", metadata.generated.seconds}, {"nanos", metadata.generated.nanos}}; + } + if (metadata.origin_type.has_value() && metadata.origin_type->name != "") { + json_object[node_name]["origin"]["type"] = {{"name", metadata.origin_type->name}}; + if (metadata.origin_type->uri) { + json_object[node_name]["origin"]["type"]["uri"] = *metadata.origin_type->uri; + } else { + json_object[node_name]["origin"]["type"]["uri"] = ""; + } + } + if (metadata.confidence) { + json_object[node_name]["confidence"] = {{"type", metadata.confidence->type}, + {"value", metadata.confidence->value}}; } } } diff --git a/cdsp/knowledge-layer/connector/data-objects/dto/model_config_dto.h b/cdsp/knowledge-layer/connector/data-objects/dto/model_config_dto.h index a7549e2a..d6212ac0 100644 --- a/cdsp/knowledge-layer/connector/data-objects/dto/model_config_dto.h +++ b/cdsp/knowledge-layer/connector/data-objects/dto/model_config_dto.h @@ -2,6 +2,7 @@ #define MODEL_CONFIG_DTO_H #include +#include #include #include @@ -13,21 +14,22 @@ struct QueriesDTO { std::string reasoning_output_queries_path; // Overload the << operator to print the QueriesDTO - friend std::ostream& operator<<(std::ostream& os, const QueriesDTO& dto) { - os << "QueriesDTO {\n"; - os << " triple_assembler_helper: {\n"; + friend std::ostream& operator<<(std::ostream& out_stream, const QueriesDTO& dto) { + out_stream << "QueriesDTO {\n"; + out_stream << " triple_assembler_helper: {\n"; for (const auto& [key, value] : dto.triple_assembler_helper) { - os << " " << key << ": [\n"; + out_stream << " " << key << ": [\n"; for (const auto& query : value) { - os << " " << query << ",\n"; + out_stream << " " << query << ",\n"; } - os << " ]\n"; + out_stream << " ]\n"; } - os << " }\n"; - os << " reasoning_output_queries_path: " << dto.reasoning_output_queries_path << "\n"; - os << "}"; + out_stream << " }\n"; + out_stream << " reasoning_output_queries_path: " << dto.reasoning_output_queries_path + << "\n"; + out_stream << "}"; - return os; + return out_stream; } }; diff --git a/cdsp/knowledge-layer/connector/data-objects/dto/set_message_dto.h b/cdsp/knowledge-layer/connector/data-objects/dto/set_message_dto.h index 710575af..c09c8e7c 100644 --- a/cdsp/knowledge-layer/connector/data-objects/dto/set_message_dto.h +++ b/cdsp/knowledge-layer/connector/data-objects/dto/set_message_dto.h @@ -5,6 +5,7 @@ #include #include +#include "globals.h" #include "metadata_dto.h" /** @@ -15,12 +16,12 @@ struct DataDTO { nlohmann::json value; // Overload the << operator to print the DataDTO - friend std::ostream& operator<<(std::ostream& os, const DataDTO& dto) { - os << " DataDTO {\n" - << " name: " << dto.name << "\n" - << " value: " << dto.value.dump() << "\n" - << " }"; - return os; + friend std::ostream &operator<<(std::ostream &out_stream, const DataDTO &dto) { + out_stream << " DataDTO {\n" + << " name: " << dto.name << "\n" + << " value: " << dto.value.dump() << "\n" + << " }"; + return out_stream; } }; @@ -29,39 +30,38 @@ struct DataDTO { * * This function converts a DataDTO object into a JSON representation. * - * @param j The JSON object to which the DataDTO will be serialized. + * @param json_obj The JSON object to which the DataDTO will be serialized. * @param dto The DataDTO object containing the data to be serialized. */ -inline void to_json(nlohmann::json& j, const DataDTO& dto) { - j = nlohmann::json{{dto.name, dto.value}}; +inline void to_json(nlohmann::json &json_obj, const DataDTO &dto) { + json_obj = nlohmann::json{{dto.name, dto.value}}; } - /** * @brief Data Transfer Object for SetMessage. */ struct SetMessageDTO { + int id; std::string schema; std::string instance; std::vector data; std::optional path; - std::optional requestId; MetadataDTO metadata; // Overload the << operator to print the SetMessageDTO - friend std::ostream& operator<<(std::ostream& os, const SetMessageDTO& dto) { - os << "SetMessageDTO {\n" - << " schema: " << dto.schema << "\n" - << " instance: " << dto.instance << "\n" - << " path: " << (dto.path ? *dto.path : "null") << "\n" - << " requestId: " << (dto.requestId ? *dto.requestId : "null") << "\n" - << " data: [\n"; - for (const auto& data : dto.data) { - os << data << ",\n"; + friend std::ostream &operator<<(std::ostream &out_stream, const SetMessageDTO &dto) { + out_stream << "SetMessageDTO {\n" + << " id: " << dto.id << "\n" + << " schema: " << dto.schema << "\n" + << " instance: " << dto.instance << "\n" + << " path: " << (dto.path ? *dto.path : "null") << "\n" + << " data: [\n"; + for (const auto &data : dto.data) { + out_stream << data << ",\n"; } - os << " ]\n"; - os << " metadata: " << dto.metadata << "\n"; - os << "}"; - return os; + out_stream << " ]\n"; + out_stream << " metadata: " << dto.metadata << "\n"; + out_stream << "}"; + return out_stream; } }; @@ -69,27 +69,29 @@ struct SetMessageDTO { * @brief Define the `to_json` function for `nlohmann::json` * * This function converts a SetMessageDTO object into a JSON representation. - * It includes mandatory fields such as schema, instance, and data, and conditionally - * includes optional fields if they are present. * * @param j The JSON object to which the SetMessageDTO will be serialized. * @param dto The SetMessageDTO object containing the data to be serialized. */ -inline void to_json(nlohmann::json& j, const SetMessageDTO& dto) { +inline void to_json(nlohmann::json &json_obj, const SetMessageDTO &dto) { nlohmann::json data_obj = nlohmann::json::object(); - for (const auto& item : dto.data) { + for (const auto &item : dto.data) { data_obj[item.name] = item.value; } - j = nlohmann::json{{"type", "set"}, - {"schema", dto.schema}, - {"instance", dto.instance}, - {"data", data_obj}, - {"metadata", dto.metadata}}; - if (dto.path) - j["path"] = *dto.path; - if (dto.requestId) - j["requestId"] = *dto.requestId; + nlohmann::json params_obj = nlohmann::json::object(); + params_obj = nlohmann::json{{"schema", dto.schema}, + {"instance", dto.instance}, + {"data", data_obj}, + {"metadata", dto.metadata}}; + if (dto.path) { + params_obj["path"] = *dto.path; + } + + json_obj = nlohmann::json{{"jsonrpc", getJsonRpcVersion()}, + {"method", "set"}, + {"id", dto.id}, + {"params", params_obj}}; } #endif // SET_MESSAGE_DTO_H \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/data-objects/dto/status_message_dto.h b/cdsp/knowledge-layer/connector/data-objects/dto/status_message_dto.h index 7f8cfe90..f306e4ec 100644 --- a/cdsp/knowledge-layer/connector/data-objects/dto/status_message_dto.h +++ b/cdsp/knowledge-layer/connector/data-objects/dto/status_message_dto.h @@ -1,31 +1,45 @@ #ifndef STATUS_MESSAGE_DTO_H #define STATUS_MESSAGE_DTO_H +#include #include +#include #include +struct StatusMessageErrorDTO { + int code; + std::string message; + nlohmann::json data; + + friend std::ostream &operator<<(std::ostream &out_stream, const StatusMessageErrorDTO &error) { + out_stream << "{\n" + << " code: " << error.code << "\n" + << " message: " << error.message << "\n" + << " data: " << error.data.dump(2) << "\n" + << "}"; + return out_stream; + } +}; + /** * @brief Data Transfer Object for Websocket Status Messages */ struct StatusMessageDTO { - int code; - std::string message; - std::optional requestId; - struct { - int seconds; - int nanos; - } timestamp; + int id; + std::optional error; // Overload the << operator to print the DTO - friend std::ostream& operator<<(std::ostream& os, const StatusMessageDTO& dto) { - os << "StatusMessageDTO {\n" - << " code: " << dto.code << "\n" - << " message: " << dto.message << "\n" - << " requestId: " << (dto.requestId ? *dto.requestId : "null") << "\n" - << " timestamp: { seconds: " << dto.timestamp.seconds - << ", nanos: " << dto.timestamp.nanos << " }\n" - << "}"; - return os; + friend std::ostream &operator<<(std::ostream &out_stream, const StatusMessageDTO &dto) { + out_stream << "StatusMessageDTO {\n" + << " id: " << dto.id << "\n" + << " error: "; + if (dto.error.has_value()) { + out_stream << *dto.error << "\n"; + } else { + out_stream << "null\n"; + } + out_stream << "}"; + return out_stream; } }; diff --git a/cdsp/knowledge-layer/connector/data-objects/dto/subscribe_message_dto.h b/cdsp/knowledge-layer/connector/data-objects/dto/subscribe_message_dto.h index 5069ffc2..b153383f 100644 --- a/cdsp/knowledge-layer/connector/data-objects/dto/subscribe_message_dto.h +++ b/cdsp/knowledge-layer/connector/data-objects/dto/subscribe_message_dto.h @@ -5,15 +5,16 @@ #include #include +#include "globals.h" + /** * @brief Data Transfer Object for SubscribeMessage. */ struct SubscribeMessageDTO { - std::string type; + int id; std::string schema; std::string instance; std::optional path; - std::optional requestId; std::optional format; std::optional root; }; @@ -21,23 +22,27 @@ struct SubscribeMessageDTO { /** * @brief Define the `to_json` function for `nlohmann::json` * - * This function converts a SubscribeMessageDTO object into a JSON representation. - * It includes mandatory fields such as type, schema, and instance, and conditionally - * includes optional fields if they are present. + * This function converts a SubscribeMessageDTO object into a JSON + * representation. * * @param j The JSON object to which the SubscribeMessageDTO will be serialized. - * @param dto The SubscribeMessageDTO object containing the data to be serialized. + * @param dto The SubscribeMessageDTO object containing the data to be + * serialized. */ -inline void to_json(nlohmann::json& j, const SubscribeMessageDTO& dto) { - j = nlohmann::json{{"type", dto.type}, {"schema", dto.schema}, {"instance", dto.instance}}; - if (dto.path) - j["path"] = *dto.path; - if (dto.requestId) - j["requestId"] = *dto.requestId; - if (dto.format) - j["format"] = *dto.format; - if (dto.root) - j["root"] = *dto.root; +inline void to_json(nlohmann::json &json_obj, const SubscribeMessageDTO &dto) { + nlohmann::json::object_t params_obj; + params_obj = nlohmann::json{{"schema", dto.schema}, {"instance", dto.instance}}; + if (dto.path) { + params_obj["path"] = *dto.path; + } + if (dto.format) { + params_obj["format"] = *dto.format; + } + if (dto.root) { + params_obj["root"] = *dto.root; + } + json_obj = + nlohmann::json{{"jsonrpc", getJsonRpcVersion()}, {"id", dto.id}, {"params", params_obj}}; } #endif // SUBSCRIBE_MESSAGE_DTO_H \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/data-objects/dto/unsubscribe_message_dto.h b/cdsp/knowledge-layer/connector/data-objects/dto/unsubscribe_message_dto.h new file mode 100644 index 00000000..9eb7dffd --- /dev/null +++ b/cdsp/knowledge-layer/connector/data-objects/dto/unsubscribe_message_dto.h @@ -0,0 +1,43 @@ +#ifndef UNSUBSCRIBE_MESSAGE_DTO +#define UNSUBSCRIBE_MESSAGE_DTO + +#include +#include +#include + +#include "globals.h" + +/** + * @brief Data Transfer Object for UnsubscribeMessage. + */ +struct UnsubscribeMessageDTO { + int id; + std::string schema; + std::string instance; + std::optional path; +}; + +/** + * @brief Define the `to_json` function for `nlohmann::json` + * + * This function converts a UnsubscribeMessageDTO object into a JSON + * representation. + * + * @param json_obj The JSON object to which the UnsubscribeMessageDTO will be + * serialized. + * @param dto The UnsubscribeMessageDTO object containing the data to be + * serialized. + */ +inline void to_json(nlohmann::json &json_obj, const UnsubscribeMessageDTO &dto) { + nlohmann::json::object_t params_obj; + params_obj = nlohmann::json{{"schema", dto.schema}, {"instance", dto.instance}}; + if (dto.path) { + params_obj["path"] = *dto.path; + } + json_obj = nlohmann::json{{"jsonrpc", getJsonRpcVersion()}, + {"method", "unsubscribe"}, + {"id", dto.id}, + {"params", params_obj}}; +} + +#endif // UNSUBSCRIBE_MESSAGE_DTO \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/json-rdf-convertor/json-writer/src/json_writer.cpp b/cdsp/knowledge-layer/connector/json-rdf-convertor/json-writer/src/json_writer.cpp index 78863ced..641db852 100644 --- a/cdsp/knowledge-layer/connector/json-rdf-convertor/json-writer/src/json_writer.cpp +++ b/cdsp/knowledge-layer/connector/json-rdf-convertor/json-writer/src/json_writer.cpp @@ -10,88 +10,185 @@ /** * Writes the query result to a JSON object and optionally stores it in a file. * - * This function takes a string representing the result of a query, the format of the result, - * and an optional output file path. It converts the query result into a JSON object and stores - * it in a file if an output file path is provided. The JSON object contains the query result - * data in a structured format, along with a timestamp indicating when the object was created. + * This function takes a string representing the result of a query, the format + * of the result, and an optional output file path. It converts the query result + * into a JSON object and stores it in a file if an output file path is + * provided. The JSON object contains the query result data in a structured + * format, along with a timestamp indicating when the object was created. * * @param query_result A string containing the result of a query. * @param result_format_type The format of the query result. - * @param is_ai_reasoner_inference_results A boolean indicating whether the reasoning results are - * inferred. + * @param is_ai_reasoner_inference_results A boolean indicating whether the + * reasoning results are inferred. * @param output_file_path An optional file path to store the JSON object. - * @param file_handler An optional file handler to use for writing the JSON object to a file. + * @param file_handler An optional file handler to use for writing the JSON + * object to a file. * @return A JSON object containing the query result data and metadata. * @throws std::runtime_error If the query result format is not supported. */ -nlohmann::json JSONWriter::writeToJson(const std::string& query_result, - const DataQueryAcceptType& result_format_type, - const bool is_ai_reasoner_inference_results, +nlohmann::json JSONWriter::writeToJson(const std::string &query_result, + const DataQueryAcceptType &result_format_type, + bool is_ai_reasoner_inference_results, std::optional output_file_path, - std::shared_ptr file_handler) { - nlohmann::json flat_result; + const std::shared_ptr &file_handler) { + nlohmann::json flat_result = parseQueryResult(query_result, result_format_type); + nlohmann::json grouped_result = groupResult(flat_result, is_ai_reasoner_inference_results); + if (!grouped_result.empty()) { + if (output_file_path.has_value() && !output_file_path->empty()) { + storeJsonToFile(grouped_result, *output_file_path, file_handler); + } + return grouped_result; + } + return nlohmann::json::object(); // Return an empty JSON object if no results +} + +/** + * @brief Parses a query result string into a JSON object based on the specified + * result format type. + * + * This function takes a query result string and a format type, then parses the + * string into a JSON object using the appropriate parsing method for the given + * format. + * + * @param query_result The query result string to be parsed. + * @param result_format_type The format type of the query result. Supported + * types are: + * - DataQueryAcceptType::TEXT_CSV: Parses the result as a CSV table. + * - DataQueryAcceptType::TEXT_TSV: Parses the result as a TSV table. + * - DataQueryAcceptType::SPARQL_JSON: Parses the result as SPARQL JSON. + * - DataQueryAcceptType::SPARQL_XML: Parses the result as SPARQL XML. + * + * @return nlohmann::json The parsed query result as a JSON object. + * + * @throws std::runtime_error If the result format type is unsupported. + */ +nlohmann::json JSONWriter::parseQueryResult(const std::string &query_result, + const DataQueryAcceptType &result_format_type) { if (result_format_type == DataQueryAcceptType::TEXT_CSV) { - flat_result = parseTableFormat(query_result, ','); - } else if (result_format_type == DataQueryAcceptType::TEXT_TSV) { - flat_result = parseTableFormat(query_result, '\t'); - } else if (result_format_type == DataQueryAcceptType::SPARQL_JSON) { - flat_result = parseSparqlJson(query_result); - } else if (result_format_type == DataQueryAcceptType::SPARQL_XML) { - flat_result = parseSparqlXml(query_result); - } else { - throw std::runtime_error("Unsupported query result format"); + return parseTableFormat(query_result, ','); + } + + if (result_format_type == DataQueryAcceptType::TEXT_TSV) { + return parseTableFormat(query_result, '\t'); + } + + if (result_format_type == DataQueryAcceptType::SPARQL_JSON) { + return parseSparqlJson(query_result); } + if (result_format_type == DataQueryAcceptType::SPARQL_XML) { + return parseSparqlXml(query_result); + } + + throw std::runtime_error("Unsupported query result format"); +} + +/** + * @brief Groups the flat JSON result into a structured format. + * + * This function processes a flat JSON result and groups its items into a more + * structured format based on the provided parameters. It iterates through each + * item in the flat result, applies the grouping logic, and appends the grouped + * result to the output array. + * + * @param flat_result The flat JSON result to be grouped. This is expected to be + * an array of JSON objects. + * @param is_ai_reasoner_inference_results A flag indicating whether the input + * data is related to AI reasoner + * inference results. + * @return A JSON array containing the grouped results. + */ +nlohmann::json JSONWriter::groupResult(const nlohmann::json &flat_result, + bool is_ai_reasoner_inference_results) { nlohmann::json grouped_result = nlohmann::json::array(); - for (const auto& item : flat_result) { - nlohmann::json grouped; - - for (auto it = item.begin(); it != item.end(); ++it) { - std::string key = it.key(); - auto value = it.value(); - - size_t dot_pos = key.find('.'); - if (dot_pos != std::string::npos) { - std::string schema = key.substr(0, dot_pos); - std::string flat_data_point = key.substr(dot_pos + 1); - if (is_ai_reasoner_inference_results) { - grouped[schema]["AI.Reasoner.InferenceResults"][flat_data_point] = value; - } else { - grouped[schema][flat_data_point] = value; - } - } else { - std::cerr << "Warning parsing reasoning query to JSON - No schema found for key: " - << key << std::endl; - } - } + for (const auto &item : flat_result) { + nlohmann::json grouped = groupItem(item, is_ai_reasoner_inference_results); + grouped_result.push_back(grouped); + } - if (is_ai_reasoner_inference_results) { - // Convert the nested object to a JSON string - for (auto& [schema, section] : grouped.items()) { - if (section.contains("AI.Reasoner.InferenceResults")) { - section["AI.Reasoner.InferenceResults"] = - section["AI.Reasoner.InferenceResults"].dump(); - } + return grouped_result; +} + +/** + * @brief Groups a flat JSON object into a structured format based on key + * schemas. + * + * This function processes a flat JSON object and organizes its key-value pairs + * into a hierarchical structure. Keys containing a dot ('.') are split into a + * schema and a data point, and the values are grouped under the corresponding + * schema. If the `is_ai_reasoner_inference_results` flag is set, additional + * nesting under "AI.Reasoner.InferenceResults" is applied. + * + * @param item The flat JSON object to be grouped. Keys in the object may + * contain dot-separated schemas. + * @param is_ai_reasoner_inference_results A flag indicating whether the input + * data is related to AI reasoner + * inference results. If true, the grouped structure will include an additional + * "AI.Reasoner.InferenceResults" level. + * @return A JSON object containing the grouped data in a structured format. + * + * @note If a key does not contain a dot ('.'), a warning is logged, and the key + * is ignored in the grouping process. + * @warning Ensure that keys in the input JSON object follow the expected schema + * format (e.g., "schema.data_point") to avoid warnings or data loss. + */ +nlohmann::json JSONWriter::groupItem(const nlohmann::json &item, + bool is_ai_reasoner_inference_results) { + nlohmann::json grouped; + + for (auto it = item.begin(); it != item.end(); ++it) { + const std::string &key = it.key(); + const auto &value = it.value(); + + size_t dot_pos = key.find('.'); + if (dot_pos != std::string::npos) { + std::string schema = key.substr(0, dot_pos); + std::string flat_data_point = key.substr(dot_pos + 1); + if (is_ai_reasoner_inference_results) { + grouped[schema]["AI.Reasoner.InferenceResults"][flat_data_point] = value; + } else { + grouped[schema][flat_data_point] = value; } + } else { + std::cerr << "Warning parsing reasoning query to JSON - No schema found for key: " + << key << std::endl; } + } - grouped_result.push_back(grouped); + if (is_ai_reasoner_inference_results) { + handleAIReasonerInferenceResults(grouped); } - if (!grouped_result.empty()) { - if (output_file_path.has_value() && !output_file_path->empty()) { - storeJsonToFile(grouped_result, *output_file_path, file_handler); - } + return grouped; +} - return grouped_result; +/** + * @brief Processes and modifies the AI Reasoner inference results within a JSON + * object. + * + * This function iterates over the items in the provided JSON object and checks + * if the "AI.Reasoner.InferenceResults" key exists in each section. If the key + * is found, the corresponding value is serialized into a JSON string using the + * `dump()` method. + * + * @param grouped A reference to a JSON object containing grouped data. Each + * item in the object is expected to have a schema as the key and a section as + * the value. + */ +void JSONWriter::handleAIReasonerInferenceResults(nlohmann::json &grouped) { + for (const auto &[schema, section] : grouped.items()) { + if (section.contains("AI.Reasoner.InferenceResults")) { + section["AI.Reasoner.InferenceResults"] = + section["AI.Reasoner.InferenceResults"].dump(); + } } - return nlohmann::json::object(); // Return an empty JSON object if no results } /** - * Parses a delimited table format string and converts it into a JSON array string. + * Parses a delimited table format string and converts it into a JSON array + * string. * * This function takes a string representing a table with a specified delimiter, * parses it, and converts it into a JSON array format. Each row in the table is @@ -101,52 +198,56 @@ nlohmann::json JSONWriter::writeToJson(const std::string& query_result, * * @param query_result A string containing the table data to be parsed. * @param delimiter A character used to separate values in the table. - * @return A string representing the JSON array of the parsed table rows, formatted with an - * indentation of 2 spaces. + * @return A string representing the JSON array of the parsed table rows, + * formatted with an indentation of 2 spaces. */ -nlohmann::json JSONWriter::parseTableFormat(const std::string& query_result, char delimiter) { - std::stringstream ss(query_result); - std::string line; - std::vector headers; - nlohmann::json json_array = nlohmann::json::array(); - - // Read headers from the first line - if (std::getline(ss, line)) { - std::stringstream header_stream(line); - std::string header; - while (std::getline(header_stream, header, delimiter)) { - header.erase(std::remove(header.begin(), header.end(), '\r'), header.end()); - std::replace(header.begin(), header.end(), '_', '.'); // Convert _ to . - headers.push_back(header); +nlohmann::json JSONWriter::parseTableFormat(const std::string &query_result, char delimiter) { + try { + std::stringstream str_stream(query_result); + std::string line; + std::vector headers; + nlohmann::json json_array = nlohmann::json::array(); + + // Read headers from the first line + if (std::getline(str_stream, line)) { + std::stringstream header_stream(line); + std::string header; + while (std::getline(header_stream, header, delimiter)) { + header.erase(std::remove(header.begin(), header.end(), '\r'), header.end()); + std::replace(header.begin(), header.end(), '_', '.'); // Convert _ to . + headers.push_back(header); + } } - } - // Process each row - while (std::getline(ss, line)) { - std::stringstream row_stream(line); - std::string value; - nlohmann::json row_object; - size_t col_idx = 0; - - while (std::getline(row_stream, value, delimiter)) { - value.erase(std::remove(value.begin(), value.end(), '\r'), value.end()); - if (col_idx < headers.size()) { - row_object[headers[col_idx]] = Helper::detectType(value); + // Process each row + while (std::getline(str_stream, line)) { + std::stringstream row_stream(line); + std::string value; + nlohmann::json row_object; + size_t col_idx = 0; + + while (std::getline(row_stream, value, delimiter)) { + value.erase(std::remove(value.begin(), value.end(), '\r'), value.end()); + if (col_idx < headers.size()) { + row_object[headers[col_idx]] = Helper::detectType(value); + } + col_idx++; } - col_idx++; - } - // Ensure all expected headers exist in the row - for (const auto& header : headers) { - if (!row_object.contains(header)) { - row_object[header] = ""; + // Ensure all expected headers exist in the row + for (const auto &header : headers) { + if (!row_object.contains(header)) { + row_object[header] = ""; + } } + + json_array.push_back(row_object); } - json_array.push_back(row_object); + return json_array; + } catch (const std::exception &e) { + throw std::runtime_error("Failed to parse table format: " + std::string(e.what())); } - - return json_array; } /** @@ -154,39 +255,44 @@ nlohmann::json JSONWriter::parseTableFormat(const std::string& query_result, cha * * This function takes a JSON string representing the result of a SPARQL query, * parses it, and converts it into a JSON array format. Each SPARQL result is - * represented as a JSON object within the array, where the keys are the variable - * names from the SPARQL query and the values are the corresponding result values. - * Underscores in variable names are replaced with dots. + * represented as a JSON object within the array, where the keys are the + * variable names from the SPARQL query and the values are the corresponding + * result values. Underscores in variable names are replaced with dots. * * @param json_result A string containing the SPARQL JSON result to be parsed. - * @return A string representing the JSON array of the parsed SPARQL results, formatted with an - * indentation of 2 spaces. - * @throws std::runtime_error If the JSON string does not contain the expected SPARQL result format. + * @return A string representing the JSON array of the parsed SPARQL results, + * formatted with an indentation of 2 spaces. + * @throws std::runtime_error If the JSON string does not contain the expected + * SPARQL result format. */ -nlohmann::json JSONWriter::parseSparqlJson(const std::string& json_result) { - nlohmann::json sparql_json = nlohmann::json::parse(json_result); - nlohmann::json json_array = nlohmann::json::array(); +nlohmann::json JSONWriter::parseSparqlJson(const std::string &json_result) { + try { + nlohmann::json sparql_json = nlohmann::json::parse(json_result); + nlohmann::json json_array = nlohmann::json::array(); - if (!sparql_json.contains("results") || !sparql_json["results"].contains("bindings")) { - throw std::runtime_error("Invalid SPARQL JSON response format"); - } + if (!sparql_json.contains("results") || !sparql_json["results"].contains("bindings")) { + throw std::runtime_error("Invalid SPARQL JSON response format"); + } - for (const auto& binding : sparql_json["results"]["bindings"]) { - nlohmann::json row_object; + for (const auto &binding : sparql_json["results"]["bindings"]) { + nlohmann::json row_object; - for (auto it = binding.begin(); it != binding.end(); ++it) { - std::string var_name = it.key(); - std::string value = it.value()["value"].get(); + for (auto it = binding.begin(); it != binding.end(); ++it) { + std::string var_name = it.key(); + std::string value = it.value()["value"].get(); - std::replace(var_name.begin(), var_name.end(), '_', '.'); + std::replace(var_name.begin(), var_name.end(), '_', '.'); - row_object[var_name] = Helper::detectType(value); + row_object[var_name] = Helper::detectType(value); + } + + json_array.push_back(row_object); } - json_array.push_back(row_object); + return json_array; + } catch (const nlohmann::json::exception &e) { + throw std::runtime_error("Failed to parse SPARQL JSON response: " + std::string(e.what())); } - - return json_array; } /** @@ -194,45 +300,49 @@ nlohmann::json JSONWriter::parseSparqlJson(const std::string& json_result) { * * This function takes an XML string representing the result of a SPARQL query, * parses it, and converts it into a JSON array format. Each SPARQL result is - * represented as a JSON object within the array, where the keys are the variable - * names from the SPARQL query and the values are the corresponding result values. - * Underscores in variable names are replaced with dots. + * represented as a JSON object within the array, where the keys are the + * variable names from the SPARQL query and the values are the corresponding + * result values. Underscores in variable names are replaced with dots. * * @param xml_result A string containing the SPARQL XML result to be parsed. - * @return A string representing the JSON array of the parsed SPARQL results, formatted with an - * indentation of 2 spaces. + * @return A string representing the JSON array of the parsed SPARQL results, + * formatted with an indentation of 2 spaces. * @throws std::runtime_error If the XML string cannot be parsed. */ -nlohmann::json JSONWriter::parseSparqlXml(const std::string& xml_result) { - pugi::xml_document doc; - if (!doc.load_string(xml_result.c_str())) { - throw std::runtime_error("Failed to parse SPARQL XML response"); - } +nlohmann::json JSONWriter::parseSparqlXml(const std::string &xml_result) { + try { + pugi::xml_document doc; + if (!doc.load_string(xml_result.c_str())) { + throw std::runtime_error("Failed to parse SPARQL XML response"); + } - nlohmann::json json_array = nlohmann::json::array(); + nlohmann::json json_array = nlohmann::json::array(); - // Navigate to node - pugi::xml_node results_node = doc.child("sparql").child("results"); + // Navigate to node + pugi::xml_node results_node = doc.child("sparql").child("results"); - for (pugi::xml_node result : results_node.children("result")) { - nlohmann::json row_object; + for (pugi::xml_node result : results_node.children("result")) { + nlohmann::json row_object; - for (pugi::xml_node binding_node : result.children("binding")) { - std::string name = binding_node.attribute("name").value(); - std::string value; + for (pugi::xml_node binding_node : result.children("binding")) { + std::string name = binding_node.attribute("name").value(); + std::string value; - // Handle any value type (literal, URI, etc.) - for (pugi::xml_node child : binding_node.children()) { - value = child.child_value(); - break; + // Handle any value type (literal, URI, etc.) + for (pugi::xml_node child : binding_node.children()) { + value = child.child_value(); + break; + } + std::replace(name.begin(), name.end(), '_', '.'); + row_object[name] = Helper::detectType(value); } - std::replace(name.begin(), name.end(), '_', '.'); - row_object[name] = Helper::detectType(value); + json_array.push_back(row_object); } - json_array.push_back(row_object); - } - return json_array; + return json_array; + } catch (const std::exception &e) { + throw std::runtime_error("Failed to parse SPARQL XML response: " + std::string(e.what())); + } } /** @@ -243,14 +353,14 @@ nlohmann::json JSONWriter::parseSparqlXml(const std::string& xml_result) { * If a file handler is not provided, a default implementation is used. * * @param json_data The JSON object to be stored in the file. - * @param output_file_path The path to the directory where the JSON file will be stored. - * The function will create the directory if it does not exist. - * @param file_handler An optional shared pointer to an IFileHandler implementation. - * If not provided, a default FileHandlerImpl is used. + * @param output_file_path The path to the directory where the JSON file will be + * stored. The function will create the directory if it does not exist. + * @param file_handler An optional shared pointer to an IFileHandler + * implementation. If not provided, a default FileHandlerImpl is used. */ -void JSONWriter::storeJsonToFile(const nlohmann::json& json_data, - const std::string& output_file_path, - std::shared_ptr file_handler) { +void JSONWriter::storeJsonToFile(const nlohmann::json &json_data, + const std::string &output_file_path, + const std::shared_ptr &file_handler) { namespace fs = std::filesystem; // Ensure the directory exists @@ -265,10 +375,11 @@ void JSONWriter::storeJsonToFile(const nlohmann::json& json_data, ".json"; // Write the file - if (!file_handler) { - file_handler = std::make_shared(); + std::shared_ptr local_file_handler = file_handler; + if (!local_file_handler) { + local_file_handler = std::make_shared(); } - file_handler->writeFile(file_name, json_data.dump(2), false); + local_file_handler->writeFile(file_name, json_data.dump(2), false); std::cout << "A JSON SPARQL Output has been generated under: " << file_name << std::endl << std::endl; } \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/json-rdf-convertor/json-writer/src/json_writer.h b/cdsp/knowledge-layer/connector/json-rdf-convertor/json-writer/src/json_writer.h index 319e30b5..f0600ad9 100644 --- a/cdsp/knowledge-layer/connector/json-rdf-convertor/json-writer/src/json_writer.h +++ b/cdsp/knowledge-layer/connector/json-rdf-convertor/json-writer/src/json_writer.h @@ -1,29 +1,32 @@ -#ifndef JSON_WRITER_H -#define JSON_WRITER_H +#pragma once #include #include #include "data_types.h" #include "i_file_handler.h" -#include "nlohmann/json.hpp" class JSONWriter { public: - static nlohmann::json writeToJson(const std::string& query_result, - const DataQueryAcceptType& result_format_type, - const bool is_ai_reasoner_inference_results = false, + static nlohmann::json writeToJson(const std::string &query_result, + const DataQueryAcceptType &result_format_type, + bool is_ai_reasoner_inference_results = false, std::optional output_file_path = std::nullopt, - std::shared_ptr file_handler = nullptr); + const std::shared_ptr &file_handler = nullptr); private: - static nlohmann::json parseTableFormat(const std::string& query_result, char delimiter); - static nlohmann::json parseSparqlJson(const std::string& json_result); - static nlohmann::json parseSparqlXml(const std::string& xml_result); + static nlohmann::json parseQueryResult(const std::string &query_result, + const DataQueryAcceptType &result_format_type); + static nlohmann::json groupResult(const nlohmann::json &flat_result, + bool is_ai_reasoner_inference_results); + static nlohmann::json groupItem(const nlohmann::json &item, + bool is_ai_reasoner_inference_results); + static void handleAIReasonerInferenceResults(nlohmann::json &grouped); + static nlohmann::json parseTableFormat(const std::string &query_result, char delimiter); + static nlohmann::json parseSparqlJson(const std::string &json_result); + static nlohmann::json parseSparqlXml(const std::string &xml_result); - static void storeJsonToFile(const nlohmann::json& json_data, - const std::string& output_file_path, - std::shared_ptr file_handler); -}; - -#endif // JSON_WRITER_H \ No newline at end of file + static void storeJsonToFile(const nlohmann::json &json_data, + const std::string &output_file_path, + const std::shared_ptr &file_handler); +}; \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/json-rdf-convertor/json-writer/tests/json_writer_integration_test.cpp b/cdsp/knowledge-layer/connector/json-rdf-convertor/json-writer/tests/json_writer_integration_test.cpp index 2c5e20ee..53c9aea6 100644 --- a/cdsp/knowledge-layer/connector/json-rdf-convertor/json-writer/tests/json_writer_integration_test.cpp +++ b/cdsp/knowledge-layer/connector/json-rdf-convertor/json-writer/tests/json_writer_integration_test.cpp @@ -4,10 +4,13 @@ #include "random_utils.h" class JSONWriterIntegrationTest : public ::testing::Test { + // NOLINTBEGIN(cppcoreguidelines-non-private-member-variables-in-classes) protected: std::string data_point_1_; std::string data_point_2_; std::string data_point_3_; + static constexpr int MAX_RANDOM_BIG = 100; + static constexpr int MAX_RANDOM_SMALL = 10; int value_dp_1_row_1_; float value_dp_1_row_2_; std::string value_dp_2_row_1_; @@ -15,6 +18,7 @@ class JSONWriterIntegrationTest : public ::testing::Test { bool value_dp_3_row_1_; double value_dp_3_row_2_; nlohmann::json expected_json_; + // NOLINTEND(cppcoreguidelines-non-private-member-variables-in-classes) void SetUp() override { const std::string some_string_1 = RandomUtils::generateRandomString(4); @@ -26,12 +30,12 @@ class JSONWriterIntegrationTest : public ::testing::Test { data_point_1_ = some_string_1 + "_" + some_string_2 + "_" + some_string_3; data_point_2_ = some_string_1 + "_" + some_string_4; data_point_3_ = some_string_5 + "_" + some_string_6; - value_dp_1_row_1_ = RandomUtils::generateRandomInt(0, 100); - value_dp_1_row_2_ = RandomUtils::generateRandomFloat(0, 10); - value_dp_2_row_1_ = RandomUtils::generateRandomString(10); + value_dp_1_row_1_ = RandomUtils::generateRandomInt(0, MAX_RANDOM_BIG); + value_dp_1_row_2_ = RandomUtils::generateRandomFloat(0, MAX_RANDOM_SMALL); + value_dp_2_row_1_ = RandomUtils::generateRandomString(MAX_RANDOM_SMALL); value_dp_2_row_2_ = ""; - value_dp_3_row_1_ = RandomUtils::generateRandomInt(0, 1); - value_dp_3_row_2_ = RandomUtils::generateRandomDouble(0, 10); + value_dp_3_row_1_ = (RandomUtils::generateRandomInt(0, 1) != 0); + value_dp_3_row_2_ = RandomUtils::generateRandomDouble(0, MAX_RANDOM_SMALL); expected_json_ = nlohmann::json::array({{{some_string_1, @@ -45,19 +49,19 @@ class JSONWriterIntegrationTest : public ::testing::Test { } // Helper function to validate the JSON output - void validateJsonOutput(const nlohmann::json& result_json) { + void validateJsonOutput(const nlohmann::json &result_json) { ASSERT_EQ(result_json.size(), expected_json_.size()); for (size_t i = 0; i < expected_json_.size(); ++i) { - const auto& expected_group = expected_json_[i]; - const auto& actual_group = result_json[i]; + const auto &expected_group = expected_json_[i]; + const auto &actual_group = result_json[i]; ASSERT_EQ(expected_group.size(), actual_group.size()); - for (const auto& [expected_schema, expected_data_points] : expected_group.items()) { + for (const auto &[expected_schema, expected_data_points] : expected_group.items()) { ASSERT_TRUE(actual_group.contains(expected_schema)) << "Missing domain: " << expected_schema; - const auto& actual_schema_section = actual_group[expected_schema]; + const auto &actual_schema_section = actual_group[expected_schema]; // Detect AI Reasoner stringified format if (actual_schema_section.contains("AI.Reasoner.InferenceResults")) { @@ -75,24 +79,35 @@ class JSONWriterIntegrationTest : public ::testing::Test { } } - void validateJsonDataContent(const nlohmann::json& actual_data_points, - const nlohmann::json& expected_data_points, - const std::string& expected_schema) { + static void validateJsonDataContent(const nlohmann::json &actual_data_points, + const nlohmann::json &expected_data_points, + const std::string &expected_schema) { ASSERT_EQ(expected_data_points.size(), actual_data_points.size()); - for (const auto& [expected_data_point, expected_value] : expected_data_points.items()) { - ASSERT_TRUE(actual_data_points.contains(expected_data_point)) - << "Missing key: " << expected_schema << "." << expected_data_point; + for (const auto &[expected_data_point, expected_value] : expected_data_points.items()) { + validateJsonKeyExists(actual_data_points, expected_data_point, expected_schema); + validateJsonValue(actual_data_points[expected_data_point], expected_value, + expected_schema, expected_data_point); + } + } - const auto& actual_value = actual_data_points[expected_data_point]; + static void validateJsonKeyExists(const nlohmann::json &actual_data_points, + const std::string &expected_data_point, + const std::string &expected_schema) { + ASSERT_TRUE(actual_data_points.contains(expected_data_point)) + << "Missing key: " << expected_schema << "." << expected_data_point; + } - if (expected_value.is_number_float() && actual_value.is_number_float()) { - ASSERT_NEAR(actual_value.get(), expected_value.get(), 1e-6) - << "Mismatch in key: " << expected_schema << "." << expected_data_point; - } else { - ASSERT_EQ(actual_value, expected_value) - << "Mismatch in key: " << expected_schema << "." << expected_data_point; - } + static void validateJsonValue(const nlohmann::json &actual_value, + const nlohmann::json &expected_value, + const std::string &expected_schema, + const std::string &expected_data_point) { + if (expected_value.is_number_float() && actual_value.is_number_float()) { + ASSERT_NEAR(actual_value.get(), expected_value.get(), 1e-6) + << "Mismatch in key: " << expected_schema << "." << expected_data_point; + } else { + ASSERT_EQ(actual_value, expected_value) + << "Mismatch in key: " << expected_schema << "." << expected_data_point; } } }; @@ -113,14 +128,14 @@ TEST_F(JSONWriterIntegrationTest, ConvertSparqlCsvToJson) { bool is_ai_reasoner_inference_results = RandomUtils::generateRandomBool(); std::cout << "Inference results: " << (is_ai_reasoner_inference_results ? "true" : "false") - << std::endl; + << "\n"; - std::cout << "Input CSV:\n" << csv_result << std::endl; + std::cout << "Input CSV:\n" << csv_result << "\n"; nlohmann::json result_json = JSONWriter::writeToJson(csv_result, DataQueryAcceptType::TEXT_CSV, is_ai_reasoner_inference_results, TEST_OUTPUT_DIR); - std::cout << "Result JSON:\n" << result_json.dump(4) << std::endl; + std::cout << "Result JSON:\n" << result_json.dump(4) << "\n"; validateJsonOutput(result_json); }; @@ -140,15 +155,15 @@ TEST_F(JSONWriterIntegrationTest, ConvertSparqlTsvToJson) { bool is_ai_reasoner_inference_results = RandomUtils::generateRandomBool(); std::cout << "Inference results: " << (is_ai_reasoner_inference_results ? "true" : "false") - << std::endl; + << "\n"; - std::cout << "Input TSV:\n" << tsv_result << std::endl; + std::cout << "Input TSV:\n" << tsv_result << "\n"; nlohmann::json result_json = JSONWriter::writeToJson(tsv_result, DataQueryAcceptType::TEXT_TSV, is_ai_reasoner_inference_results, TEST_OUTPUT_DIR); - std::cout << "Result JSON:\n" << result_json.dump(4) << std::endl; + std::cout << "Result JSON:\n" << result_json.dump(4) << "\n"; validateJsonOutput(result_json); }; @@ -164,7 +179,8 @@ TEST_F(JSONWriterIntegrationTest, ConvertSparqlJsonToJson) { R"({ "head": { "vars": [" )" + - data_point_1_ + R"(", ")" + data_point_2_ + R"(", ")" + data_point_3_ + R"("] + data_point_1_ + R"(", ")" + data_point_2_ + R"(", ")" + data_point_3_ + + R"("] }, "results": { "bindings": [ @@ -173,7 +189,8 @@ TEST_F(JSONWriterIntegrationTest, ConvertSparqlJsonToJson) { data_point_1_ + R"(": { "type": "literal", "value": ")" + std::to_string(value_dp_1_row_1_) + R"(" }, ")" + - data_point_2_ + R"(": { "type": "uri", "value": ")" + value_dp_2_row_1_ + R"(" }, + data_point_2_ + R"(": { "type": "uri", "value": ")" + value_dp_2_row_1_ + + R"(" }, ")" + data_point_3_ + R"(": { "type": "literal", "value": ")" + (value_dp_3_row_1_ ? "true" : "false") + R"(" } @@ -193,15 +210,15 @@ TEST_F(JSONWriterIntegrationTest, ConvertSparqlJsonToJson) { })"; bool is_ai_reasoner_inference_results = RandomUtils::generateRandomBool(); std::cout << "Inference results: " << (is_ai_reasoner_inference_results ? "true" : "false") - << std::endl; + << "\n"; - std::cout << "Input SPARQL JSON:\n" << sparql_json_result << std::endl; + std::cout << "Input SPARQL JSON:\n" << sparql_json_result << "\n"; nlohmann::json result_json = JSONWriter::writeToJson(sparql_json_result, DataQueryAcceptType::SPARQL_JSON, is_ai_reasoner_inference_results, TEST_OUTPUT_DIR); - std::cout << "Result JSON:\n" << result_json.dump(4) << std::endl; + std::cout << "Result JSON:\n" << result_json.dump(4) << "\n"; validateJsonOutput(result_json); } @@ -277,14 +294,14 @@ TEST_F(JSONWriterIntegrationTest, ConvertSparqXmlToJson) { )"; bool is_ai_reasoner_inference_results = RandomUtils::generateRandomBool(); - std::cout << "Input SPARQL XML:\n" << xml_response_ << std::endl; + std::cout << "Input SPARQL XML:\n" << xml_response_ << "\n"; std::cout << "Inference results: " << (is_ai_reasoner_inference_results ? "true" : "false") - << std::endl; + << "\n"; nlohmann::json result_json = JSONWriter::writeToJson(xml_response_, DataQueryAcceptType::SPARQL_XML, is_ai_reasoner_inference_results, TEST_OUTPUT_DIR); - std::cout << "Result JSON:\n" << result_json.dump(4) << std::endl; + std::cout << "Result JSON:\n" << result_json.dump(4) << "\n"; validateJsonOutput(result_json); } \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/json-rdf-convertor/json-writer/tests/json_writer_unit_test.cpp b/cdsp/knowledge-layer/connector/json-rdf-convertor/json-writer/tests/json_writer_unit_test.cpp index 91b5fae6..9d97165e 100644 --- a/cdsp/knowledge-layer/connector/json-rdf-convertor/json-writer/tests/json_writer_unit_test.cpp +++ b/cdsp/knowledge-layer/connector/json-rdf-convertor/json-writer/tests/json_writer_unit_test.cpp @@ -4,10 +4,7 @@ #include "mock_i_file_handler.h" #include "random_utils.h" -class JSONWriterTest : public ::testing::Test { - protected: - JSONWriter jsonWriter; -}; +class JSONWriterTest : public ::testing::Test {}; /** * @brief Test case for writing JSON data to a file. @@ -27,9 +24,9 @@ TEST_F(JSONWriterTest, WriteJsonToFile) { auto mockFileHandler = std::make_shared(); EXPECT_CALL(*mockFileHandler, writeFile(::testing::_, ::testing::_, false)).Times(1); - nlohmann::json result = - jsonWriter.writeToJson(csv_input, DataQueryAcceptType::TEXT_CSV, - is_ai_reasoner_inference_results, output_file_path, mockFileHandler); + nlohmann::json result = JSONWriter::writeToJson(csv_input, DataQueryAcceptType::TEXT_CSV, + is_ai_reasoner_inference_results, + output_file_path, mockFileHandler); ASSERT_FALSE(result.empty()); } @@ -53,8 +50,8 @@ TEST_F(JSONWriterTest, WriteJsonWithoutFilePath) { EXPECT_CALL(*mockFileHandler, writeFile(::testing::_, ::testing::_, ::testing::_)).Times(0); nlohmann::json result = - jsonWriter.writeToJson(csv_input, DataQueryAcceptType::TEXT_CSV, - is_ai_reasoner_inference_results, output_file_path); + JSONWriter::writeToJson(csv_input, DataQueryAcceptType::TEXT_CSV, + is_ai_reasoner_inference_results, output_file_path); ASSERT_FALSE(result.empty()); } @@ -69,21 +66,20 @@ TEST_F(JSONWriterTest, WriteJsonWithoutFilePath) { * headers and data. The test specifically uses an invalid * DataQueryAcceptType (999) to trigger the error. */ + TEST_F(JSONWriterTest, WriteJsonWithInvalidFormat) { std::string query_result = "id,name,age\n1,Alice,30\n2,Bob,25"; bool is_ai_reasoner_inference_results = RandomUtils::generateRandomBool(); - ASSERT_THROW( - { - try { - jsonWriter.writeToJson(query_result, static_cast(999), - is_ai_reasoner_inference_results, std::nullopt); - } catch (const std::runtime_error& e) { - EXPECT_THAT(e.what(), testing::HasSubstr("Unsupported query result format")); - throw; - } + EXPECT_THAT( + [&]() { + JSONWriter::writeToJson(query_result, + // NOLINTNEXTLINE(clang-analyzer-optin.core.EnumCastOutOfRange) + static_cast(999), + is_ai_reasoner_inference_results, std::nullopt); }, - std::runtime_error); + ::testing::ThrowsMessage( + ::testing::HasSubstr("Unsupported query result format"))); } /** @@ -100,14 +96,15 @@ TEST_F(JSONWriterTest, WriteJsonWithEmptyValues) { ASSERT_NO_THROW({ nlohmann::json result = - jsonWriter.writeToJson(query_result, DataQueryAcceptType::TEXT_CSV, - is_ai_reasoner_inference_results, std::nullopt); + JSONWriter::writeToJson(query_result, DataQueryAcceptType::TEXT_CSV, + is_ai_reasoner_inference_results, std::nullopt); ASSERT_TRUE(result.empty()); }); } /** - * @brief Test case for JSONWriter's behavior when writing JSON with an empty result. + * @brief Test case for JSONWriter's behavior when writing JSON with an empty + * result. * * This test verifies that when an empty query result is provided to the * jsonWriter's writeToJson method, it throws a runtime_error. The test @@ -115,13 +112,13 @@ TEST_F(JSONWriterTest, WriteJsonWithEmptyValues) { * indicating that no results were found. */ TEST_F(JSONWriterTest, WriteJsonWithEmptyResult) { - std::string query_result = ""; + std::string query_result; bool is_ai_reasoner_inference_results = RandomUtils::generateRandomBool(); ASSERT_NO_THROW({ nlohmann::json result = - jsonWriter.writeToJson(query_result, DataQueryAcceptType::TEXT_CSV, - is_ai_reasoner_inference_results, std::nullopt); + JSONWriter::writeToJson(query_result, DataQueryAcceptType::TEXT_CSV, + is_ai_reasoner_inference_results, std::nullopt); ASSERT_TRUE(result.empty()); }); } @@ -138,17 +135,13 @@ TEST_F(JSONWriterTest, InvalidSparqlJson) { std::string invalid_sparql_json = R"({ "invalid": "format" })"; bool is_ai_reasoner_inference_results = RandomUtils::generateRandomBool(); - ASSERT_THROW( - { - try { - jsonWriter.writeToJson(invalid_sparql_json, DataQueryAcceptType::SPARQL_JSON, - is_ai_reasoner_inference_results, std::nullopt); - } catch (const std::runtime_error& e) { - EXPECT_THAT(e.what(), testing::HasSubstr("Invalid SPARQL JSON response format")); - throw; - } + EXPECT_THAT( + [&]() { + JSONWriter::writeToJson(invalid_sparql_json, DataQueryAcceptType::SPARQL_JSON, + is_ai_reasoner_inference_results, std::nullopt); }, - std::runtime_error); + ::testing::ThrowsMessage( + ::testing::HasSubstr("Invalid SPARQL JSON response format"))); } /** @@ -163,15 +156,11 @@ TEST_F(JSONWriterTest, InvalidSparqlXml) { std::string invalid_sparql_xml = R"()"; bool is_ai_reasoner_inference_results = RandomUtils::generateRandomBool(); - ASSERT_THROW( - { - try { - jsonWriter.writeToJson(invalid_sparql_xml, DataQueryAcceptType::SPARQL_XML, - is_ai_reasoner_inference_results, std::nullopt); - } catch (const std::runtime_error& e) { - EXPECT_THAT(e.what(), testing::HasSubstr("Failed to parse SPARQL XML response")); - throw; - } + EXPECT_THAT( + [&]() { + JSONWriter::writeToJson(invalid_sparql_xml, DataQueryAcceptType::SPARQL_XML, + is_ai_reasoner_inference_results, std::nullopt); }, - std::runtime_error); + ::testing::ThrowsMessage( + ::testing::HasSubstr("Failed to parse SPARQL XML response"))); } \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/json-rdf-convertor/rdf-writer/src/triple_assembler.cpp b/cdsp/knowledge-layer/connector/json-rdf-convertor/rdf-writer/src/triple_assembler.cpp index aa625a12..5752332f 100644 --- a/cdsp/knowledge-layer/connector/json-rdf-convertor/rdf-writer/src/triple_assembler.cpp +++ b/cdsp/knowledge-layer/connector/json-rdf-convertor/rdf-writer/src/triple_assembler.cpp @@ -1,6 +1,5 @@ #include "triple_assembler.h" -#include #include #include #include @@ -68,7 +67,7 @@ void TripleAssembler::transformMessageToTriple(const DataMessage& message) { std::vector nodes = message.getNodes(); // Add the identifier to the triples that will be generated - triple_writer_.initiateTriple(header.getId()); + triple_writer_.initiateTriple(header.getInstance()); if (nodes.empty()) { std::cout << "No nodes found in the message\n\n"; diff --git a/cdsp/knowledge-layer/connector/json-rdf-convertor/rdf-writer/tests/triple_assembler_unit_test.cpp b/cdsp/knowledge-layer/connector/json-rdf-convertor/rdf-writer/tests/triple_assembler_unit_test.cpp index 7500148f..76a670ad 100644 --- a/cdsp/knowledge-layer/connector/json-rdf-convertor/rdf-writer/tests/triple_assembler_unit_test.cpp +++ b/cdsp/knowledge-layer/connector/json-rdf-convertor/rdf-writer/tests/triple_assembler_unit_test.cpp @@ -14,7 +14,6 @@ #include "mock_reasoner_adapter.h" #include "mock_reasoner_service.h" #include "mock_triple_writer.h" -#include "model_config.h" #include "triple_assembler.h" #include "vin_utils.h" @@ -56,7 +55,7 @@ class TripleAssemblerUnitTest : public ::testing::Test { const std::string data_point = "Vehicle.Powertrain.TractionBattery.StateOfCharge.CurrentEnergy"; - nodes_.emplace_back(data_point, "98.6", Metadata(), std::vector{data_point}); + nodes_.emplace_back(data_point, "98.6", Metadata()); } /** @@ -268,15 +267,8 @@ TEST_F(TripleAssemblerUnitTest, setUpMessage(); // Add two nodes more to the `message_feature` - nodes_.emplace_back( - "Vehicle.FuelLevel", "75", Metadata(), - std::vector{"Vehicle.FuelLevel"}); // Add data point as supported, `Node` - // validation should not fail for this - // test - nodes_.emplace_back( - "InvalidNode", "error_value", Metadata(), - std::vector{"InvalidNode"}); // Add data point as supported, `Node` - // validation should not fail for this test + nodes_.emplace_back("Vehicle.FuelLevel", "75", Metadata()); + nodes_.emplace_back("InvalidNode", "error_value", Metadata()); auto message_header = MessageHeader(VIN, SchemaType::VEHICLE); DataMessage message_feature(message_header, nodes_); @@ -455,14 +447,9 @@ TEST_F(TripleAssemblerUnitTest, TransformMessageToTripleFailsWhenAnyDataStoreIsS TEST_F(TripleAssemblerUnitTest, TransformMessageToRDFTripleWithCoordinatesSuccess) { // Set up the initial message and model base for the test - std::vector supported_data_points = {"Vehicle.CurrentLocation.Latitude", - "Vehicle.CurrentLocation.Longitude"}; - // Set up the message with coordinates - nodes_.emplace_back("Vehicle.CurrentLocation.Latitude", "40.0", Metadata(), - supported_data_points); - nodes_.emplace_back("Vehicle.CurrentLocation.Longitude", "50.0", Metadata(), - supported_data_points); + nodes_.emplace_back("Vehicle.CurrentLocation.Latitude", "40.0", Metadata()); + nodes_.emplace_back("Vehicle.CurrentLocation.Longitude", "50.0", Metadata()); auto message_header = MessageHeader(VIN, SchemaType::VEHICLE); DataMessage message_feature(message_header, nodes_); @@ -516,10 +503,8 @@ TEST_F(TripleAssemblerUnitTest, TransformMessageToTripleFailsWhenCoordinatesAreN "Vehicle.CurrentLocation.Longitude"}; // Add two nodes more to the `message_feature_` with a wrong coordinate - nodes_.emplace_back("Vehicle.CurrentLocation.Latitude", "", Metadata(), - supported_data_points); // Invalid value - nodes_.emplace_back("Vehicle.CurrentLocation.Longitude", "50.0", Metadata(), - supported_data_points); + nodes_.emplace_back("Vehicle.CurrentLocation.Latitude", "", Metadata()); // Invalid value + nodes_.emplace_back("Vehicle.CurrentLocation.Longitude", "50.0", Metadata()); auto message_header = MessageHeader(VIN, SchemaType::VEHICLE); DataMessage message_feature(message_header, nodes_); diff --git a/cdsp/knowledge-layer/connector/json-rdf-convertor/rdf-writer/tests/triple_writer_integration_test.cpp b/cdsp/knowledge-layer/connector/json-rdf-convertor/rdf-writer/tests/triple_writer_integration_test.cpp index a1ed0d76..5315a3e5 100644 --- a/cdsp/knowledge-layer/connector/json-rdf-convertor/rdf-writer/tests/triple_writer_integration_test.cpp +++ b/cdsp/knowledge-layer/connector/json-rdf-convertor/rdf-writer/tests/triple_writer_integration_test.cpp @@ -17,7 +17,7 @@ class TripleWriterIntegrationTest : public ::testing::Test { const std::string VIN = VinUtils::getRandomVinString(); const std::string OBSERVATION_VALUE = std::to_string(RandomUtils::generateRandomFloat(0, 300)); const std::chrono::system_clock::time_point TIMESTAMP = - RandomUtils::generateRandomTimestamp(2000, 2030, true); + RandomUtils::generateRandomTimestamp({2000, 2030}, true); const std::string DATA_TIME = UtcDateUtils::formatCustomTimestampAsIso8601(TIMESTAMP); const std::string OBSERVATION_IDENTIFIER = diff --git a/cdsp/knowledge-layer/connector/json-rdf-convertor/services/src/reasoning_query_service.cpp b/cdsp/knowledge-layer/connector/json-rdf-convertor/services/src/reasoning_query_service.cpp index 75f64a09..742c0eef 100644 --- a/cdsp/knowledge-layer/connector/json-rdf-convertor/services/src/reasoning_query_service.cpp +++ b/cdsp/knowledge-layer/connector/json-rdf-convertor/services/src/reasoning_query_service.cpp @@ -1,8 +1,7 @@ #include "reasoning_query_service.h" -#include - -#include "helper.h" +#include "data_types.h" +#include "json_writer.h" ReasoningQueryService::ReasoningQueryService(std::shared_ptr reasoning_service) : reasoning_service_(reasoning_service) {} @@ -10,7 +9,8 @@ ReasoningQueryService::ReasoningQueryService(std::shared_ptr re /** * Processes a reasoning query and returns the result in JSON format. * - * @param reasoning_output_query A pair consisting of the query language type and the query string. + * @param reasoning_output_query The reasoning output query containing the query + * string, query language, and other relevant details. * @param is_ai_reasoner_inference_results A boolean indicating whether the reasoning results are * inferred. * @param output_file_path An optional string representing the path to an output file where results @@ -18,15 +18,15 @@ ReasoningQueryService::ReasoningQueryService(std::shared_ptr re * @return A nlohmann::json object containing the results of the reasoning query. */ nlohmann::json ReasoningQueryService::processReasoningQuery( - const std::pair& reasoning_output_query, - const bool is_ai_reasoner_inference_results, + const ReasoningOutputQuery& reasoning_output_query, const bool is_ai_reasoner_inference_results, const std::optional& output_file_path) { nlohmann::json result; // Process each query - std::string query_result = - reasoning_service_->queryData(reasoning_output_query.second, reasoning_output_query.first, - DataQueryAcceptType::SPARQL_JSON); + std::string query_result = reasoning_service_->queryData(reasoning_output_query.query, + reasoning_output_query.query_language, + DataQueryAcceptType::SPARQL_JSON); + return JSONWriter::writeToJson(query_result, DataQueryAcceptType::SPARQL_JSON, is_ai_reasoner_inference_results, output_file_path); } diff --git a/cdsp/knowledge-layer/connector/json-rdf-convertor/services/src/reasoning_query_service.h b/cdsp/knowledge-layer/connector/json-rdf-convertor/services/src/reasoning_query_service.h index fa141c96..22db3384 100644 --- a/cdsp/knowledge-layer/connector/json-rdf-convertor/services/src/reasoning_query_service.h +++ b/cdsp/knowledge-layer/connector/json-rdf-convertor/services/src/reasoning_query_service.h @@ -5,7 +5,7 @@ #include #include -#include "json_writer.h" +#include "data_types.h" #include "reasoner_service.h" class ReasoningQueryService { @@ -13,7 +13,7 @@ class ReasoningQueryService { ReasoningQueryService(std::shared_ptr reasoning_service); nlohmann::json processReasoningQuery( - const std::pair& reasoning_output_query, + const ReasoningOutputQuery& reasoning_output_query, const bool is_ai_reasoner_inference_results = false, const std::optional& output_file_path = std::nullopt); diff --git a/cdsp/knowledge-layer/connector/json-rdf-convertor/services/tests/CMakeLists.txt b/cdsp/knowledge-layer/connector/json-rdf-convertor/services/tests/CMakeLists.txt new file mode 100644 index 00000000..744cebdb --- /dev/null +++ b/cdsp/knowledge-layer/connector/json-rdf-convertor/services/tests/CMakeLists.txt @@ -0,0 +1,39 @@ +# Add unit test executable for ReasoningQueryService +add_executable(reasoning_query_service_unit_test reasoning_query_service_unit_test.cpp) + +target_include_directories(reasoning_query_service_unit_test + PRIVATE + ${PROJECT_ROOT_DIR}/symbolic-reasoner/services/tests/utils + ${PROJECT_ROOT_DIR}/symbolic-reasoner/interfaces/tests/utils +) + +target_link_libraries(reasoning_query_service_unit_test + PRIVATE + GTest::gtest_main + GTest::gmock + rdf_services + test_fixtures +) + +# Add reasoning query service integration test executable +add_executable(reasoning_query_service_integration_test + reasoning_query_service_integration_test.cpp +) + +target_link_libraries(reasoning_query_service_integration_test + PRIVATE + GTest::gtest_main + test_fixtures + websocket_client +) + +# Add integration test to CTest +add_test(NAME ReasoningQueryServiceUnitTests COMMAND reasoning_query_service_unit_test) +add_test(NAME ReasoningQueryServiceIntegrationTests COMMAND reasoning_query_service_integration_test) + +# Define custom output directory for test binaries +set_target_properties(reasoning_query_service_unit_test PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/tests") +set_target_properties(reasoning_query_service_integration_test PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/tests") + +# Ensure tests are built with the all target +add_custom_target(json_rdf_convertor_reasoning_query_tests ALL DEPENDS reasoning_query_service_unit_test reasoning_query_service_integration_test) \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/json-rdf-convertor/services/tests/reasoning_query_service_integration_test.cpp b/cdsp/knowledge-layer/connector/json-rdf-convertor/services/tests/reasoning_query_service_integration_test.cpp new file mode 100644 index 00000000..318ae056 --- /dev/null +++ b/cdsp/knowledge-layer/connector/json-rdf-convertor/services/tests/reasoning_query_service_integration_test.cpp @@ -0,0 +1,134 @@ +#include + +#include "file_handler_impl.h" +#include "globals.h" +#include "model_config.h" +#include "reasoner_factory.h" +#include "reasoner_service.h" +#include "reasoning_query_service.h" +#include "server_data_fixture.h" +#include "system_configuration_service.h" +#include "vin_utils.h" + +class ReasoningQueryServiceTest : public ::testing::Test { + protected: + // NOLINTBEGIN(cppcoreguidelines-non-private-member-variables-in-classes) + std::shared_ptr file_handler_; + const ReasonerServerData server_data_ = ServerDataFixture::getValidRDFoxServerData(); + std::shared_ptr reasoner_service_; + // NOLINTEND(cppcoreguidelines-non-private-member-variables-in-classes) + + void SetUp() override { + file_handler_ = std::make_shared(); + setenv("VEHICLE_OBJECT_ID", VinUtils::getRandomVinString().c_str(), 1); + } + + void TearDown() override { + // Clean up + if (reasoner_service_) { + reasoner_service_->deleteDataStore(); + } + } +}; + +/** + * @brief Tests the processing of a regular reasoning query. + * + * This test initializes the ReasonerService with a model configuration that + * does not use delta queries. It loads a set of triples into the reasoner and + * then processes the reasoning queries defined in the model configuration. + * The test verifies that the results match the expected output. + */ +TEST_F(ReasoningQueryServiceTest, ProcessRegularReasoningQuery_Success) { + // Arrange + setPathToUseCases( + "/common-test-resources/common_fixtures/" + "use_case_fixture/model/"); + + // Initialize System Configuration + SystemConfig system_config; + system_config.reasoner_server = ServerDataFixture::getValidRDFoxServerData(); + + const auto model_config = + std::make_shared(SystemConfigurationService::loadModelConfig( + getProjectRoot() + getPathToUseCases() + "model_config.json")); + + reasoner_service_ = ReasonerFactory::initReasoner( + model_config->getReasonerSettings().getInferenceEngine(), system_config.reasoner_server, + model_config->getReasonerRules(), model_config->getOntologies(), false); + + if (!reasoner_service_) { + throw std::runtime_error("Failed to initialize the reasoner service."); + } + + ReasoningQueryService reasoning_query_service(reasoner_service_); + + // ** Act 1 ** + // Load data and perform reasoning query + + std::vector expected_results; + expected_results.push_back(R"( + {"Vehicle": + {"AI.Reasoner.InferenceResults":"{\"Chassis.SteeringWheel.Angle\":55.0,\"Speed\":100.0}"} + } + )"_json); + + const std::string triple_data = R"( + @prefix bmw: . + @prefix sosa: . + @prefix xsd: . + + bmw:VehicleObservation_speed_20251021114517921301000 + a sosa:Observation ; + sosa:hasFeatureOfInterest bmw:VehicleVINABC_Vehicle ; + sosa:hasSimpleResult "100"^^xsd:float ; + sosa:observedProperty bmw:speed ; + sosa:phenomenonTime "2025-10-21T11:45:17.921301000Z"^^xsd:dateTime . + + bmw:VehicleVINABC_Vehicle + a bmw:Vehicle . + + bmw:ChassisVINABC_Vehicle + a bmw:Chassis ; + bmw:hasPart bmw:SteeringWheelVINABC_Vehicle . + + bmw:SteeringWheelVINABC_Vehicle + a bmw:SteeringWheel . + + bmw:VehicleObservation_angle_20251021114518921301000 + a sosa:Observation ; + sosa:hasFeatureOfInterest bmw:SteeringWheelVINABC_Vehicle ; + sosa:hasSimpleResult "55"^^xsd:float ; + sosa:observedProperty bmw:angle ; + sosa:phenomenonTime "2025-10-21T11:45:18.921301000Z"^^xsd:dateTime . + + bmw:VehicleVINABC_Vehicle + a bmw:Vehicle ; + bmw:hasPart bmw:ChassisVINABC_Vehicle . + )"; + + reasoner_service_->loadData(triple_data, model_config->getReasonerSettings().getOutputFormat()); + + auto results_1 = reasoning_query_service.processReasoningQuery( + model_config->getReasoningOutputQueries().at(0), + model_config->getReasonerSettings().isIsAiReasonerInferenceResults(), std::nullopt); + + // Assert: Expect that results contain the new delta changes + ASSERT_EQ(results_1.size(), expected_results.size()); + for (size_t i = 0; i < results_1.size(); ++i) { + EXPECT_EQ(results_1[i], expected_results[i]); + } + + // ** Act 2 ** + // Perform reasoning query again returning same results + + auto results_2 = reasoning_query_service.processReasoningQuery( + model_config->getReasoningOutputQueries().at(0), + model_config->getReasonerSettings().isIsAiReasonerInferenceResults(), std::nullopt); + + // Assert: Expect that results contain the new delta changes + ASSERT_EQ(results_2.size(), expected_results.size()); + for (size_t i = 0; i < results_2.size(); ++i) { + EXPECT_EQ(results_2[i], expected_results[i]); + } +} \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/json-rdf-convertor/services/tests/reasoning_query_service_unit_test.cpp b/cdsp/knowledge-layer/connector/json-rdf-convertor/services/tests/reasoning_query_service_unit_test.cpp new file mode 100644 index 00000000..1b635e6d --- /dev/null +++ b/cdsp/knowledge-layer/connector/json-rdf-convertor/services/tests/reasoning_query_service_unit_test.cpp @@ -0,0 +1,54 @@ +#include + +#include + +#include "mock_reasoner_adapter.h" +#include "mock_reasoner_service.h" +#include "reasoning_query_service.h" + +class ReasoningQueryServiceUnitTest : public ::testing::Test { + // NOLINTBEGIN(cppcoreguidelines-non-private-member-variables-in-classes) + protected: + std::shared_ptr mock_reasoner_service_; + std::unique_ptr reasoning_query_service_; + std::shared_ptr mock_adapter_; + // NOLINTEND(cppcoreguidelines-non-private-member-variables-in-classes) + void SetUp() override { + mock_adapter_ = std::make_shared(); + EXPECT_CALL(*mock_adapter_, initialize()).Times(1); + mock_reasoner_service_ = std::make_unique(mock_adapter_); + reasoning_query_service_ = std::make_unique(mock_reasoner_service_); + } +}; + +/** + * @brief Test case for processing a reasoning query where JSONWriter + * throws an exception. + * + * This test verifies that when the JSONWriter throws an exception during the + * processing of a reasoning query, the processReasoningQuery method + * handles the exception gracefully and does not propagate it. The test checks + * that the results are empty, indicating that no processing occurred due to + * the exception. + */ +TEST_F(ReasoningQueryServiceUnitTest, ProcessReasoningQuery_JSONWriterThrowsException) { + // Arrange + ReasoningOutputQuery regular_query; + regular_query.query = "SELECT ?s WHERE { ?s a . }"; + regular_query.query_language = QueryLanguageType::SPARQL; + ReasoningOutputQuery reasoning_output_queries = regular_query; + + bool is_ai_reasoner_inference_results = false; + std::optional reasoning_results_file_path = std::nullopt; + + EXPECT_CALL(*mock_reasoner_service_, + queryData(regular_query.query, regular_query.query_language, + DataQueryAcceptType::SPARQL_JSON)) + .WillOnce(testing::Return("Snapshot content not parsable to JSON")); + + // Act && Assert + EXPECT_THROW(reasoning_query_service_->processReasoningQuery(reasoning_output_queries, + is_ai_reasoner_inference_results, + reasoning_results_file_path), + std::runtime_error); +} \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/utils/data_types.h b/cdsp/knowledge-layer/connector/utils/data_types.h index bd93d98d..2d5b4d30 100644 --- a/cdsp/knowledge-layer/connector/utils/data_types.h +++ b/cdsp/knowledge-layer/connector/utils/data_types.h @@ -1,12 +1,8 @@ #ifndef DATA_TYPES_H #define DATA_TYPES_H -#include -#include #include #include -#include -#include #include "helper.h" @@ -36,6 +32,14 @@ enum class SchemaType { DEFAULT, }; +/** + * @brief Enum class for the supported confidence types in metadata + */ +enum class ConfidenceType : std::uint8_t { + DEDUCTIVE, + PROBABILISTIC, +}; + /** * @brief Enum class for the reasoner syntax types */ @@ -95,6 +99,7 @@ struct ReasonerServerData { std::string host; std::string port; std::string auth_base64; + std::string origin_system_name; std::optional data_store_name; }; @@ -107,6 +112,19 @@ struct SystemConfig { ReasonerServerData reasoner_server; }; +/** + * @brief Structure representing the data of a Reasoning Output Query in the + * `model_config` object. + */ +struct ReasoningOutputQuery { + QueryLanguageType query_language; + std::string query; + + bool operator==(const ReasoningOutputQuery& other) const { + return query_language == other.query_language && query == other.query; + } +}; + /** * @brief Converts a MessageType enum to its corresponding string representation. * @@ -240,6 +258,35 @@ std::string queryAcceptTypeToString(const DataQueryAcceptType& type); */ std::string schemaTypeToString(const SchemaType& type, bool capitalizeFirstLetter = false); +/** + * @brief Converts a ConfidenceType enumeration value to its corresponding + * string representation. + * + * This function takes a ConfidenceType value as input and returns a string that + * describes the confidence type. + * + * @param type The ConfidenceType value to be converted to a string. + * @return A string representation of the provided ConfidenceType. + * + * @throws std::runtime_error if the input ConfidenceType is not supported. + */ +std::string confidenceTypeToString(const ConfidenceType& type); + +/** + * @brief Converts a string to a ConfidenceType enum value. + * + * This function takes a string as input and converts it to the corresponding + * ConfidenceType enum value. The input string should match one of the + * predefined confidence types. + * + * @param type A string representing the confidence type. + * @return The corresponding ConfidenceType enum value for the given string. + * + * @throws std::runtime_error if the input string does not match any of the + * predefined confidence definitions. + */ +ConfidenceType stringToConfidenceType(const std::string& type); + /** * @brief Converts a string to a SchemaType enum value. * @@ -441,6 +488,28 @@ inline SchemaType stringToSchemaType(const std::string& type) { } } +inline std::string confidenceTypeToString(const ConfidenceType& type) { + switch (type) { + case ConfidenceType::DEDUCTIVE: + return "deductive"; + case ConfidenceType::PROBABILISTIC: + return "probabilistic"; + default: + throw std::invalid_argument("Unsupported confidence type"); + } +} + +inline ConfidenceType stringToConfidenceType(const std::string& type) { + std::string lowerCaseType = Helper::toLowerCase(type); + if (lowerCaseType == "deductive") { + return ConfidenceType::DEDUCTIVE; + } + if (lowerCaseType == "probabilistic") { + return ConfidenceType::PROBABILISTIC; + } + throw std::invalid_argument("Unsupported confidence type: " + type); +} + inline std::string MessageStructureFormatToString(const MessageStructureFormat& type) { switch (type) { case MessageStructureFormat::FLAT: diff --git a/cdsp/knowledge-layer/connector/websocket-client/CMakeLists.txt b/cdsp/knowledge-layer/connector/websocket-client/CMakeLists.txt index 3e13ab21..49e9687e 100644 --- a/cdsp/knowledge-layer/connector/websocket-client/CMakeLists.txt +++ b/cdsp/knowledge-layer/connector/websocket-client/CMakeLists.txt @@ -1,6 +1,6 @@ # Define the websocket_client library add_library(websocket_client - common/globals.cpp + runtime/request_registry.cpp src/websocket_client.cpp src/real_websocket_connection.cpp services/bo_service.cpp @@ -17,7 +17,7 @@ add_library(websocket_client # Include directories target_include_directories(websocket_client PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR}/common + ${CMAKE_CURRENT_SOURCE_DIR}/runtime ${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR}/services ${CMAKE_CURRENT_SOURCE_DIR}/services/converters @@ -33,6 +33,8 @@ target_link_libraries(websocket_client rdf_writer rdf_services reasoner + websocket_client_runtime + common PRIVATE Boost::system Boost::filesystem diff --git a/cdsp/knowledge-layer/connector/websocket-client/common/globals.cpp b/cdsp/knowledge-layer/connector/websocket-client/common/globals.cpp deleted file mode 100644 index f0cce6ac..00000000 --- a/cdsp/knowledge-layer/connector/websocket-client/common/globals.cpp +++ /dev/null @@ -1,42 +0,0 @@ -#include "globals.h" - -#include - -namespace { -std::string PROJECT_ROOT_RUNTIME; -bool isSet = false; -} // namespace - -/** - * @brief Retrieves the project root directory. - * - * This function returns a constant reference to a string representing - * the project root directory. If the project root is set, it returns - * the runtime project root; otherwise, it returns the default root. - * - * @return const std::string& A constant reference to the project root string. - */ -const std::string& getProjectRoot() { - static const std::string default_root = std::string(PROJECT_ROOT); - return isSet ? PROJECT_ROOT_RUNTIME : default_root; -} - -/** - * @brief Sets the project root directory. - * - * This function sets the runtime project root directory to the specified - * path if it has not already been set. If the project root is already - * set, the function returns false without making any changes. - * - * @param root A constant reference to a string representing the new - * project root directory. - * @return true if the project root was successfully set; - * false if it was already set. - */ -bool setProjectRoot(const std::string& root) { - if (isSet) - return false; - PROJECT_ROOT_RUNTIME = root; - isSet = true; - return true; -} \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/websocket-client/common/globals.h b/cdsp/knowledge-layer/connector/websocket-client/common/globals.h deleted file mode 100644 index 054e92ad..00000000 --- a/cdsp/knowledge-layer/connector/websocket-client/common/globals.h +++ /dev/null @@ -1,5 +0,0 @@ -#pragma once -#include - -const std::string& getProjectRoot(); -bool setProjectRoot(const std::string& root); \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/websocket-client/runtime/CMakeLists.txt b/cdsp/knowledge-layer/connector/websocket-client/runtime/CMakeLists.txt new file mode 100644 index 00000000..1cad35a3 --- /dev/null +++ b/cdsp/knowledge-layer/connector/websocket-client/runtime/CMakeLists.txt @@ -0,0 +1,18 @@ +# Define the websocket_client_runtime library +add_library(websocket_client_runtime + request_registry.cpp +) + +# Include directories +target_include_directories(websocket_client_runtime + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} +) + +# Link dependencies +target_link_libraries(websocket_client_runtime + PUBLIC + common + data_objects + utils +) \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/websocket-client/runtime/README.md b/cdsp/knowledge-layer/connector/websocket-client/runtime/README.md new file mode 100644 index 00000000..2d3c8f0c --- /dev/null +++ b/cdsp/knowledge-layer/connector/websocket-client/runtime/README.md @@ -0,0 +1,30 @@ +# WebSocket Runtime Utilities + +The runtime directory contains lightweight utilities that maintain state across +the WebSocket client’s lifetime. At present it hosts the `RequestRegistry`, a +central component for tracking in-flight subscribe/get/set/unsubscribe +operations and correlating responses. + +## RequestRegistry + +Defined in `request_registry.*`, this class assigns numeric identifiers to +outgoing requests and stores their metadata: + +- **Tracked types** – Standard subscribe/unsubscribe/get/set + flows. +- **Lookup helpers** – Retrieve request details (`getRequest`), search for a + request by its metadata (`findRequestId`), and remove entries once handled. + +`RequestInfo::typeToString` provides human-readable labels for logging. + +## Usage in Services + +`RequestRegistry` is injected into several services under +`websocket-client/services/`, including `MessageService`, converters, and +status handlers. Any component that enqueues outbound messages should register +the request to enable response matching. + +## Testing + +The registry is exercised indirectly through [service tests](../services/tests/) that rely on +request tracking. diff --git a/cdsp/knowledge-layer/connector/websocket-client/runtime/request_registry.cpp b/cdsp/knowledge-layer/connector/websocket-client/runtime/request_registry.cpp new file mode 100644 index 00000000..5cb9e414 --- /dev/null +++ b/cdsp/knowledge-layer/connector/websocket-client/runtime/request_registry.cpp @@ -0,0 +1,82 @@ +#include "request_registry.h" + +#include +/** + * @brief Adds a new request to the registry. + * + * This function takes a RequestInfo object and adds it to the + * requests list associated with the next available identifier. + * The identifier is then incremented for future requests. + * + * @param info The RequestInfo object containing details of the + * request to be added. + * @return The identifier of the newly added request. + */ +int RequestRegistry::addRequest(const RequestInfo &info) { + requests_[next_identifier_] = info; + return next_identifier_++; +} + +/** + * @brief Retrieves the request information associated with the given + * identifier. + * + * This function searches for the request in the registry using the + * provided identifier. If a request is found and it is not empty, the + * first request info is returned. Otherwise, it returns an empty optional. + * + * @param identifier The unique identifier for the request to retrieve. + * @return std::optional An optional containing the + * request info if found, or std::nullopt if no request exists for the + * given identifier. + */ +std::optional RequestRegistry::getRequest(int identifier) const { + auto iterator = requests_.find(identifier); + if (iterator != requests_.end()) { + return iterator->second; + } + return std::nullopt; +} + +/** + * @brief Finds the request ID associated with the given request information. + * + * This method iterates through the registered requests and compares the + * provided RequestInfo object with each request's information. If a match + * is found based on type, schema, instance, and path, the corresponding + * request ID is returned. If no match is found, -1 is returned to indicate + * that the request ID could not be found. + * + * @param info The RequestInfo object containing the details to match against. + * @return The identifier of the matching request if found; otherwise, -1. + */ +std::optional RequestRegistry::findRequestId(const RequestInfo &info) const { + for (const auto &[identifier, request_info] : requests_) { + if (request_info == info) { + return identifier; + } + } + return std::nullopt; // Not found +} + +/** + * @brief Removes a request from the registry using the given identifier. + * + * This function searches for the request in the registry using the + * provided identifier. If found, it removes the request and returns + * true. If not found, it returns false. + * + * @param identifier The unique identifier for the request to remove. + * @return bool True if the request was successfully removed, false + * otherwise. + */ +void RequestRegistry::removeRequest(int identifier) { + auto iterator = requests_.find(identifier); + if (iterator != requests_.end()) { + RequestInfo request_info = iterator->second; + + requests_.erase(iterator); + } else { + std::cout << "Request with identifier " << identifier << " not found in the registry.\n"; + } +} diff --git a/cdsp/knowledge-layer/connector/websocket-client/runtime/request_registry.h b/cdsp/knowledge-layer/connector/websocket-client/runtime/request_registry.h new file mode 100644 index 00000000..06371ff3 --- /dev/null +++ b/cdsp/knowledge-layer/connector/websocket-client/runtime/request_registry.h @@ -0,0 +1,48 @@ +#ifndef REQUEST_REGISTRY_H +#define REQUEST_REGISTRY_H + +#include +#include +#include +#include + +struct RequestInfo { + enum Type : std::uint8_t { SUBSCRIBE, UNSUBSCRIBE, GET, SET } type; + std::string schema; + std::string instance; + std::optional path; + + bool operator==(const RequestInfo &other) const { + return type == other.type && schema == other.schema && instance == other.instance && + path == other.path; + } + + static const char *typeToString(Type type) { + switch (type) { + case Type::SUBSCRIBE: + return "SUBSCRIBE"; + case Type::UNSUBSCRIBE: + return "UNSUBSCRIBE"; + case Type::GET: + return "GET"; + case Type::SET: + return "SET"; + default: + return "UNDEFINED"; + } + } +}; + +class RequestRegistry { + public: + int addRequest(const RequestInfo &info); + [[nodiscard]] std::optional getRequest(int identifier) const; + [[nodiscard]] std::optional findRequestId(const RequestInfo &info) const; + void removeRequest(int identifier); + + private: + std::map requests_; + int next_identifier_ = 0; +}; + +#endif // REQUEST_REGISTRY_H \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/websocket-client/services/bo_service.cpp b/cdsp/knowledge-layer/connector/websocket-client/services/bo_service.cpp index 2e8aa603..05fb2504 100644 --- a/cdsp/knowledge-layer/connector/websocket-client/services/bo_service.cpp +++ b/cdsp/knowledge-layer/connector/websocket-client/services/bo_service.cpp @@ -2,77 +2,107 @@ #include -#include "helper.h" #include "message_header.h" #include "node.h" /** - * @brief Creates a SubscribeMessage object for a given object ID and schema type. + * @brief Creates a SubscribeMessage object for a given object ID and schema + * type. * - * This function initializes a MessageHeader with the provided object ID and schema type, - * and returns a SubscribeMessage. Currently, it contains a TODO to refactor the method - * to allow subscribing to each node in the schema collection. + * This function initializes a MessageHeader with the provided object ID and + * schema type, and returns a SubscribeMessage. Currently, it contains a TODO to + * refactor the method to allow subscribing to each node in the schema + * collection. * - * @param object_id A string representing the unique identifier for the object to subscribe to. - * @param schema_type The SchemaType indicating the type of schema for the subscription. - * @return A SubscribeMessage object initialized with the created MessageHeader and an empty - * payload. + * @param object_id A string representing the unique identifier for the object + * to subscribe to. + * @param schema_type The SchemaType indicating the type of schema for the + * subscription. + * @param nodes A vector of Node objects representing the data points to + * subscribe to. + * @return A Message object containing the subscribe header and the provided + * nodes. */ SubscribeMessage BoService::createSubscribeMessage(const std::string& object_id, - SchemaType schema_type) { + SchemaType schema_type, + const std::vector& nodes) { MessageHeader subscribe_header(object_id, schema_type); - // TODO: Refactor to subscribe to each node in the schema collection - return SubscribeMessage(subscribe_header, {}); + return {subscribe_header, nodes}; +} + +/** + * Creates an unsubscribe message with the specified object ID, schema type, and + * nodes. + * + * @param object_id The unique identifier for the object to unsubscribe from. + * @param schema_type The schema type associated with the unsubscribe message. + * @param nodes A vector of nodes that are part of the unsubscribe message. + * @return A Message object containing the unsubscribe header and the provided + * nodes. + */ +UnsubscribeMessage BoService::createUnsubscribeMessage(const std::string& object_id, + SchemaType schema_type, + const std::vector& nodes) { + MessageHeader unsubscribe_header(object_id, schema_type); + return {unsubscribe_header, nodes}; } /** * @brief Creates a GetMessage object for a given object ID and schema type. * - * This function initializes a MessageHeader with the provided object ID and schema type, - * and constructs a vector of Node objects from the specified list of data points. - * It then returns a GetMessage object containing the header and the constructed nodes. + * This function initializes a MessageHeader with the provided object ID and + * schema type, and constructs a vector of Node objects from the specified list + * of data points. It then returns a GetMessage object containing the header and + * the constructed nodes. * - * @param object_id A string representing the unique identifier for the object to retrieve. - * @param schema_type The SchemaType indicating the type of schema for the message. - * @param list_data_points A vector of strings representing the data points to include in the + * @param object_id A string representing the unique identifier for the object + * to retrieve. + * @param schema_type The SchemaType indicating the type of schema for the * message. - * @return A GetMessage object initialized with the created MessageHeader and the corresponding - * nodes. + * @param list_data_points A vector of strings representing the data points to + * include in the message. + * @return A GetMessage object initialized with the created MessageHeader and + * the corresponding nodes. */ GetMessage BoService::createGetMessage(const std::string& object_id, SchemaType schema_type, const std::vector& list_data_points) { MessageHeader get_header(object_id, schema_type); std::vector nodes; + nodes.reserve(list_data_points.size()); for (const auto& data_point : list_data_points) { - nodes.push_back(Node(data_point, std::nullopt, Metadata(), {data_point})); + nodes.emplace_back(data_point, std::nullopt, Metadata()); } - return GetMessage(get_header, nodes); + return {get_header, nodes}; } /** - * @brief Creates a vector of SetMessage objects based on the provided object ID map and JSON data. + * @brief Creates a vector of SetMessage objects based on the provided object ID + * map and JSON data. * - * This function iterates through the given JSON data, extracting schema types and their - * corresponding data points. It constructs SetMessage objects for each schema type found in the - * JSON that matches an entry in the object ID map. If a schema type is not found in the object ID - * map, a warning is logged. + * This function iterates through the given JSON data, extracting schema types + * and their corresponding data points. It constructs SetMessage objects for + * each schema type found in the JSON that matches an entry in the object ID + * map. If a schema type is not found in the object ID map, a warning is logged. + * + * @param object_ids A map that associates SchemaType with their corresponding + * object IDs. + * @param json A JSON object containing the schema types and their associated + * data points. + * @param origin_system_name The name of the origin system sending the message. * - * @param object_id A map that associates SchemaType with their corresponding object IDs. - * @param json A JSON object containing the schema types and their associated data points. * @return A vector of SetMessage objects created from the provided JSON data. */ std::vector BoService::createSetMessage( - const std::map& object_id, const nlohmann::json& json) { + const std::map& object_ids, const nlohmann::json& json, + const std::string& origin_system_name) { std::vector set_messages; - Metadata metadata(std::nullopt, std::chrono::system_clock::now()); - for (const auto& groups : json) { for (const auto& [schema, data_points] : groups.items()) { SchemaType schema_type = stringToSchemaType(schema); - if (object_id.find(schema_type) == object_id.end()) { + if (object_ids.find(schema_type) == object_ids.end()) { std::cerr << "Warning: Schema type " << schema << " not found in object ID map." << std::endl; continue; @@ -80,12 +110,15 @@ std::vector BoService::createSetMessage( std::vector nodes; + Metadata::OriginType origin_system = {origin_system_name, std::nullopt}; + for (const auto& [data_point, value] : data_points.items()) { - nodes.push_back(Node(data_point, value.dump(), metadata, {data_point})); + Metadata metadata({std::nullopt, std::chrono::system_clock::now()}, origin_system); + nodes.emplace_back(data_point, value.dump(), metadata); } - MessageHeader header(object_id.at(schema_type), schema_type); - set_messages.push_back(SetMessage(header, nodes)); + MessageHeader header(object_ids.at(schema_type), schema_type); + set_messages.emplace_back(header, nodes); } } diff --git a/cdsp/knowledge-layer/connector/websocket-client/services/bo_service.h b/cdsp/knowledge-layer/connector/websocket-client/services/bo_service.h index 17012b41..d03687bd 100644 --- a/cdsp/knowledge-layer/connector/websocket-client/services/bo_service.h +++ b/cdsp/knowledge-layer/connector/websocket-client/services/bo_service.h @@ -10,17 +10,24 @@ #include "get_message.h" #include "set_message.h" #include "subscribe_message.h" +#include "unsubscribe_message.h" class BoService { public: - static SubscribeMessage createSubscribeMessage(const std::string& object_id, - SchemaType schema_type); + static SubscribeMessage createSubscribeMessage(const std::string &object_id, + SchemaType schema_type, + const std::vector &nodes); - static GetMessage createGetMessage(const std::string& object_id, SchemaType schema_type, - const std::vector& list_data_points); + static UnsubscribeMessage createUnsubscribeMessage(const std::string &object_id, + SchemaType schema_type, + const std::vector &nodes); + + static GetMessage createGetMessage(const std::string &object_id, SchemaType schema_type, + const std::vector &list_data_points); static std::vector createSetMessage( - const std::map& object_id, const nlohmann::json& json); + const std::map &object_id, const nlohmann::json &json, + const std::string &origin_system_name); }; #endif // BO_SERVICE_H \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/websocket-client/services/bo_to_dto.cpp b/cdsp/knowledge-layer/connector/websocket-client/services/bo_to_dto.cpp index a985258d..a9dec447 100644 --- a/cdsp/knowledge-layer/connector/websocket-client/services/bo_to_dto.cpp +++ b/cdsp/knowledge-layer/connector/websocket-client/services/bo_to_dto.cpp @@ -8,12 +8,12 @@ /** * Converts a GetMessage business object (BO) into a vector of GetMessageDTOs. * - * @param bo The GetMessage business object to be converted. + * @param b_obj The GetMessage business object to be converted. * @return A vector of GetMessageDTOs representing the converted data. */ -std::vector BoToDto::convert(const GetMessage& bo) { +std::vector BoToDto::convert(const GetMessage& b_obj) { std::vector dtos; - auto schema = schemaTypeToString(bo.getHeader().getSchemaType(), true); + auto schema = schemaTypeToString(b_obj.getHeader().getSchemaType(), true); /** * Helper lambda function to create a GetMessageDTO from a given path. @@ -22,21 +22,19 @@ std::vector BoToDto::convert(const GetMessage& bo) { */ auto createDto = [&](const std::optional& path) { GetMessageDTO dto; - dto.type = "get"; dto.schema = schema; - dto.instance = bo.getHeader().getId(); + dto.instance = b_obj.getHeader().getInstance(); dto.path = path; dto.format = MessageStructureFormatToString( MessageStructureFormat::FLAT); // Default format for GetMessageDTO dtos.push_back(dto); }; - if (bo.getNodes().empty()) { + if (b_obj.getNodes().empty()) { createDto(std::nullopt); } else { - for (const auto& node : bo.getNodes()) { - std::string node_name = node.getName().substr(schema.size() + 1); - createDto(node_name); + for (const auto& node : b_obj.getNodes()) { + createDto(node.getName()); } } @@ -44,12 +42,13 @@ std::vector BoToDto::convert(const GetMessage& bo) { } /** - * Converts a SubscribeMessage business object (BO) into a vector of SubscribeMessageDTOs. + * Converts a SubscribeMessage business object (BO) into a vector of + * SubscribeMessageDTOs. * - * @param bo The SubscribeMessage business object to be converted. + * @param b_obj The SubscribeMessage business object to be converted. * @return A vector of SubscribeMessageDTOs representing the converted data. */ -std::vector BoToDto::convert(const SubscribeMessage& bo) { +std::vector BoToDto::convert(const SubscribeMessage& b_obj) { std::vector dtos; /** @@ -59,19 +58,49 @@ std::vector BoToDto::convert(const SubscribeMessage& bo) { */ auto createDto = [&](const std::optional& path) { SubscribeMessageDTO dto; - dto.type = "subscribe"; - dto.schema = schemaTypeToString(bo.getHeader().getSchemaType(), true); - dto.instance = bo.getHeader().getId(); + dto.schema = schemaTypeToString(b_obj.getHeader().getSchemaType(), true); + dto.instance = b_obj.getHeader().getInstance(); dto.path = path; dto.format = MessageStructureFormatToString( MessageStructureFormat::FLAT); // Default format for SubscribeMessageDTO + dto.root = "relative"; // Default root for SubscribeMessageDTO + dtos.push_back(dto); + }; + + if (b_obj.getNodes().empty()) { + createDto(std::nullopt); + } else { + for (const auto& node : b_obj.getNodes()) { + createDto(node.getName()); + } + } + return dtos; +} + +/** + * Converts an UnsubscribeMessage business object (BO) into a vector of + * UnsubscribeMessageDTOs. + * + * @param b_obj The UnsubscribeMessage business object to be converted. + * @return A vector of UnsubscribeMessageDTOs representing the converted data. + */ +std::vector BoToDto::convert(const UnsubscribeMessage& b_obj) { + std::vector dtos; + /** Helper lambda function to create an UnsubscribeMessageDTO. + * @param path An optional string representing the path of the node. + */ + auto createDto = [&](const std::optional& path) { + UnsubscribeMessageDTO dto; + dto.schema = schemaTypeToString(b_obj.getHeader().getSchemaType(), true); + dto.instance = b_obj.getHeader().getInstance(); + dto.path = path; dtos.push_back(dto); }; - if (bo.getNodes().empty()) { + if (b_obj.getNodes().empty()) { createDto(std::nullopt); } else { - for (const auto& node : bo.getNodes()) { + for (const auto& node : b_obj.getNodes()) { createDto(node.getName()); } } @@ -81,18 +110,15 @@ std::vector BoToDto::convert(const SubscribeMessage& bo) { /** * Converts a SetMessage business object (BO) into a SetMessageDTO. * - * @param bo The SetMessage business object to be converted. - * @return A SetMessageDTO representing the converted data. + * @param b_obj The SetMessage business object to be converted. + * @return A vector with a single SetMessageDTO representing the converted data. */ -SetMessageDTO BoToDto::convert(const SetMessage& bo) { +std::vector BoToDto::convert(const SetMessage& b_obj) { SetMessageDTO dto; + dto.schema = schemaTypeToString(b_obj.getHeader().getSchemaType(), true); + dto.instance = b_obj.getHeader().getInstance(); - dto.schema = schemaTypeToString(bo.getHeader().getSchemaType(), true); - dto.instance = bo.getHeader().getId(); - dto.requestId = std::nullopt; - dto.schema = schemaTypeToString(bo.getHeader().getSchemaType(), true); - - for (const auto& node : bo.getNodes()) { + for (const auto& node : b_obj.getNodes()) { DataDTO data; data.name = node.getName(); if (node.getValue()) { @@ -106,12 +132,26 @@ SetMessageDTO BoToDto::convert(const SetMessage& bo) { } else { data.value = ""; } - auto [seconds, nanos] = - Helper::getSecondsAndNanosecondsSinceEpoch(node.getMetadata().getGenerated().value()); - dto.metadata.nodes[node.getName()] = - MetadataDTO::NodeMetadata{.generated = {seconds, nanos}}; + + // Populate the generated timestamp if it exists + if (auto generated = node.getMetadata().getGenerated()) { + auto [seconds, nanos] = Helper::getSecondsAndNanosecondsSinceEpoch(*generated); + dto.metadata.nodes[node.getName()] = + MetadataDTO::NodeMetadata{.generated = {seconds, nanos}}; + } + + // Populate the origin type if it exists + if (auto origin = node.getMetadata().getOriginType()) { + auto name = origin->name ? *origin->name : ""; + auto uri = origin->uri ? *origin->uri : ""; + + dto.metadata.nodes[node.getName()].origin_type = {.name = name, .uri = uri}; + } + dto.data.push_back(data); } - return dto; + // Returns a vector with a single SetMessageDTO object in order to match the + // logic of the other convert functions + return {dto}; } \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/websocket-client/services/bo_to_dto.h b/cdsp/knowledge-layer/connector/websocket-client/services/bo_to_dto.h index b2a495c6..dc4d0bd8 100644 --- a/cdsp/knowledge-layer/connector/websocket-client/services/bo_to_dto.h +++ b/cdsp/knowledge-layer/connector/websocket-client/services/bo_to_dto.h @@ -3,20 +3,24 @@ #include -#include "data_types.h" #include "get_message.h" #include "get_message_dto.h" #include "set_message.h" #include "set_message_dto.h" #include "subscribe_message.h" #include "subscribe_message_dto.h" +#include "unsubscribe_message.h" +#include "unsubscribe_message_dto.h" class BoToDto { public: - static std::vector convert(const GetMessage& bo); - static std::vector convert(const SubscribeMessage& bo); + static std::vector convert(const GetMessage &b_obj); - static SetMessageDTO convert(const SetMessage& bo); + static std::vector convert(const SubscribeMessage &b_obj); + + static std::vector convert(const UnsubscribeMessage &b_obj); + + static std::vector convert(const SetMessage &b_obj); }; #endif // BO_TO_DTO_H diff --git a/cdsp/knowledge-layer/connector/websocket-client/services/converters/data_message_converter.cpp b/cdsp/knowledge-layer/connector/websocket-client/services/converters/data_message_converter.cpp index 8c4304ab..32f95ee1 100644 --- a/cdsp/knowledge-layer/connector/websocket-client/services/converters/data_message_converter.cpp +++ b/cdsp/knowledge-layer/connector/websocket-client/services/converters/data_message_converter.cpp @@ -1,82 +1,122 @@ #include "data_message_converter.h" -#include #include #include "converter_helper.h" #include "node.h" /** + * @brief DataMessageConverter is responsible for converting DataMessageDTO + * objects, which are typically received as JSON payloads, into DataMessage + * objects that encapsulate structured data nodes and their associated metadata. + * * Converts a DataMessageDTO object to a DataMessage object. * - * @param dto The DataMessageDTO object to be converted. It must have a type of "data". - * @param system_data_points A map of schema types to data points used to validate the schema. + * @param dto The DataMessageDTO object to be converted. It must have a type of + * "data". + * @param registry The RequestRegistry object that contains information about + * the request associated with the DTO. * @return A DataMessage object constructed from the provided DataMessageDTO. * @throws std::invalid_argument if the dto type is not "data". */ -DataMessage DataMessageConverter::convert( - const DataMessageDTO& dto, - const std::map>& system_data_points) { - if (dto.type != "data") { - throw std::invalid_argument("Invalid DTO type"); - } - if ((!dto.path.has_value() || dto.path->empty()) && !dto.data.is_object()) { - throw std::invalid_argument("Path is missing"); - } - SchemaType schema_type = stringToSchemaType(dto.schema); - if (system_data_points.find(schema_type) == system_data_points.end()) { - throw std::invalid_argument("Schema not found in system data points"); +DataMessage DataMessageConverter::convert(const DataMessageDTO &dto, RequestRegistry ®istry) { + // Check if the message belongs to the request registry + auto request_registry = registry.getRequest(dto.id); + + if (!request_registry.has_value()) { + throw std::invalid_argument("Registry not found for the ID: " + std::to_string(dto.id)); } - const std::vector& supported_data_points = system_data_points.at(schema_type); - MessageHeader header(dto.instance, schema_type); + SchemaType schema_type = stringToSchemaType(request_registry->schema); + + MessageHeader header(request_registry->instance, schema_type); std::vector nodes; - std::string base_path = dto.path.value_or(""); - parseNodes(base_path, dto.data, nodes, dto.schema, dto.metadata, supported_data_points); + std::string base_path = request_registry->path.value_or(""); + + if (base_path.empty() && !dto.data.is_object()) { + throw std::invalid_argument("Path is missing and data is not an object"); + } + + parseNodes(base_path, dto.data, nodes, request_registry->schema); + + // Combine nodes with their associated metadata to produce the final node + // collection + std::vector nodes_with_metadata = + includeMetadata(request_registry->schema, base_path, dto.metadata, nodes); + + if (request_registry->type != RequestInfo::Type::SUBSCRIBE) { + // Remove the request from the registry after processing + registry.removeRequest(dto.id); + } - return DataMessage(header, nodes); + return {header, nodes_with_metadata}; } /** * @brief Parses a JSON object and converts it into a vector of Node objects. * - * This function recursively traverses a JSON object, constructing a path for each - * key-value pair. If a value is an object, the function calls itself recursively. - * Otherwise, it creates a Node object with the constructed path, the value converted - * to a string, and optional metadata, then adds it to the nodes vector. + * This function recursively traverses a JSON object, constructing a path for + * each key-value pair. If a value is an object, the function calls itself + * recursively. Otherwise, it creates a Node object with the constructed path, + * the value converted to a string, and optional metadata, then adds it to the + * nodes vector. * * @param base_path The base path used to construct the full path for each node. * @param data The JSON object to parse. - * @param nodes A reference to a vector where the resulting Node objects will be stored. - * @param schema_collection A string representing the schema used to prefix the node names. + * @param nodes A reference to a vector where the resulting Node objects will be + * stored. + * @param schema_collection A string representing the schema used to prefix the + * node names. * @param metadata Optional metadata that can be associated with each node. - * @param supported_data_points A vector of supported data points for the schema. */ -void DataMessageConverter::parseNodes(const std::string& base_path, const nlohmann::json& data, - std::vector& nodes, - const std::string& schema_collection, - const std::optional& metadata, - const std::vector& supported_data_points) { - for (const auto& [key, value] : data.items()) { +void DataMessageConverter::parseNodes(const std::string &base_path, const nlohmann::json &data, + std::vector &nodes, + const std::string &schema_collection) { + for (const auto &[key, value] : data.items()) { std::string path; if (key.empty()) { - // leaf message + // Leaf message path = base_path; + } else if (base_path.empty()) { + path = key; } else { - // nested or flat messages - path = base_path.empty() ? key : base_path + "." + key; + // Detect array index by checking if key is numeric (used to correctly + // format paths) + bool is_numeric = std::all_of(key.begin(), key.end(), ::isdigit); + path = base_path; + if (is_numeric) { + path.append("[").append(key).append("]"); + } else { + path.append(".").append(key); + } } if (value.is_object()) { - parseNodes(path, value, nodes, schema_collection, metadata, supported_data_points); + parseNodes(path, value, nodes, schema_collection); + } else if (value.is_array()) { + for (size_t i = 0; i < value.size(); ++i) { + std::string indexed_path = path + "[" + std::to_string(i) + "]"; + const auto &element = value[i]; + + if (element.is_primitive()) { + // Handle primitive values in the array + std::string name = schema_collection; + name.append(".").append(indexed_path); + auto node_value = Helper::jsonToString(element); + nodes.emplace_back(name, node_value, Metadata()); + } else { + // Recursively parse nested objects in the array + parseNodes(indexed_path, value[i], nodes, schema_collection); + } + } } else { try { - std::string name = schema_collection + "." + path; + std::string name = schema_collection; + name.append(".").append(path); auto node_value = Helper::jsonToString(value); - Metadata node_metadata = extractBoMetadata(metadata, path); - nodes.emplace_back(name, node_value, node_metadata, supported_data_points); - } catch (const std::invalid_argument& e) { + nodes.emplace_back(name, node_value, Metadata()); + } catch (const std::invalid_argument &e) { std::cerr << "Failed to create node: " << e.what() << std::endl; } } @@ -84,33 +124,123 @@ void DataMessageConverter::parseNodes(const std::string& base_path, const nlohma } /** - * Extracts business object metadata from a given MetadataDTO object. + * @brief Includes metadata for a collection of nodes based on the provided + * schema and metadata DTO. + * + * This function processes a vector of nodes and associates them with metadata + * if available. If no metadata is provided, it creates nodes with default + * metadata. For each node, it determines the appropriate metadata path and + * retrieves the corresponding metadata, if it exists. + * + * @param schema_collection The schema collection name used to construct the + * metadata path. + * @param base_path The base path used in conjunction with the schema collection + * to identify nodes. + * @param metadata_dto An optional MetadataDTO containing metadata information + * for the nodes. + * @param nodes A vector of Node objects to which metadata will be added. + * @return A vector of Node objects that includes metadata. + */ +std::vector DataMessageConverter::includeMetadata( + const std::string &schema_collection, const std::string &base_path, + const std::optional &metadata_dto, const std::vector &nodes) { + std::vector nodes_with_metadata; + + for (const auto &node : nodes) { + if (!metadata_dto.has_value()) { + nodes_with_metadata.emplace_back(node.getName(), node.getValue(), Metadata()); + } else { + std::string metadata_path = + normalizeMetadataPath(schema_collection, {base_path, node.getName()}); + const auto &metadata = findMetadata(metadata_dto->nodes, metadata_path); + nodes_with_metadata.emplace_back(node.getName(), node.getValue(), metadata); + } + } + + return nodes_with_metadata; +} +/** + * @brief Normalizes the metadata path by removing the schema collection prefix + * and any leading dots. * - * This function attempts to retrieve and parse metadata associated with a specific path - * from the provided MetadataDTO. If the metadata is not available or parsing fails, - * it returns an empty optional. + * This function takes a schema collection name and a MetadataPathComponents + * object, constructs the metadata path, and removes the schema collection + * prefix. It also handles leading dots and array indices in the path. * - * @param metadata_dto An optional MetadataDTO object containing metadata nodes. - * @param path A string representing the path to the desired metadata node. - * @return An optional Metadata object containing the parsed metadata if successful, - * or std::nullopt if the metadata is unavailable or parsing fails. + * @param schema_collection The schema collection name used to construct the + * metadata path. + * @param components The MetadataPathComponents object containing base path and + * node name. + * @return A normalized metadata path as a string. */ -Metadata DataMessageConverter::extractBoMetadata(const std::optional& metadata_dto, - const std::string& path) { - if (!metadata_dto.has_value() || - metadata_dto.value().nodes.find(path) == metadata_dto.value().nodes.end()) { - return Metadata(); + +std::string DataMessageConverter::normalizeMetadataPath(const std::string &schema_collection, + const MetadataPathComponents &components) { + std::string prefix_to_remove = schema_collection + "." + components.base_path; + std::string metadata_path = (components.node_name != prefix_to_remove) + ? components.node_name.substr(prefix_to_remove.size()) + : ""; + + if (!metadata_path.empty() && metadata_path[0] == '.') { + metadata_path = metadata_path.substr(1); } - const auto& node_metadata = metadata_dto.value().nodes.at(path); - if (node_metadata.received.seconds == 0 && node_metadata.received.nanos == 0 && - node_metadata.generated.seconds == 0 && node_metadata.generated.nanos == 0) { - return Metadata(); + auto pos = metadata_path.find('['); + if (pos != std::string::npos) { + metadata_path = metadata_path.substr(0, pos); } - auto received = ConverterHelper::parseTimestamp(node_metadata.received.seconds, - node_metadata.received.nanos); - auto generated = ConverterHelper::parseTimestamp(node_metadata.generated.seconds, - node_metadata.generated.nanos); - return Metadata(received, generated); + return metadata_path; } + +/** + * @brief Finds the metadata for a given metadata path in the provided nodes + * map. + * + * This function searches for the specified metadata path in the nodes map. If + * not found, it defaults to an empty string. It retrieves the received and + * generated timestamps, as well as confidence information if available. + * + * @param nodes A map of metadata paths to NodeMetadata objects. + * @param metadata_path The metadata path to search for. + * @return A Metadata object containing the received and generated timestamps, + * and confidence information if available. + */ +Metadata DataMessageConverter::findMetadata( + const std::unordered_map &nodes, + const std::string &metadata_path) { + auto iterator = nodes.find(metadata_path); + if (iterator == nodes.end()) { + iterator = nodes.find(""); + if (iterator == nodes.end()) { + return Metadata(); + } + } + + const auto &node_metadata = iterator->second; + auto received = (node_metadata.received.seconds != 0 || node_metadata.received.nanos != 0) + ? ConverterHelper::parseTimestamp(node_metadata.received.seconds, + node_metadata.received.nanos) + : std::nullopt; + + auto generated = (node_metadata.generated.seconds != 0 || node_metadata.generated.nanos != 0) + ? ConverterHelper::parseTimestamp(node_metadata.generated.seconds, + node_metadata.generated.nanos) + : std::nullopt; + + std::optional> confidence = std::nullopt; + if (node_metadata.confidence.has_value()) { + confidence = std::make_pair(stringToConfidenceType(node_metadata.confidence->type), + std::to_string(node_metadata.confidence->value)); + } + + std::optional origin_type = std::nullopt; + if (node_metadata.origin_type.has_value()) { + origin_type = Metadata::OriginType{.name = node_metadata.origin_type->name, + .uri = node_metadata.origin_type->uri}; + } + + Metadata::Timestamps timestamps{received, generated}; + + return Metadata(timestamps, origin_type, confidence); +} \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/websocket-client/services/converters/data_message_converter.h b/cdsp/knowledge-layer/connector/websocket-client/services/converters/data_message_converter.h index 80490b9a..029857b9 100644 --- a/cdsp/knowledge-layer/connector/websocket-client/services/converters/data_message_converter.h +++ b/cdsp/knowledge-layer/connector/websocket-client/services/converters/data_message_converter.h @@ -3,20 +3,29 @@ #include "data_message.h" #include "data_message_dto.h" +#include "request_registry.h" + +struct MetadataPathComponents { + std::string base_path; + std::string node_name; +}; class DataMessageConverter { public: - static DataMessage convert( - const DataMessageDTO& dto, - const std::map>& system_data_points); + static DataMessage convert(const DataMessageDTO &dto, RequestRegistry ®istry); private: - static void parseNodes(const std::string& base_path, const nlohmann::json& data, - std::vector& nodes, const std::string& schema_collection, - const std::optional& metadata, - const std::vector& supported_data_points); - static Metadata extractBoMetadata(const std::optional& metadata, - const std::string& path); + static void parseNodes(const std::string &base_path, const nlohmann::json &data, + std::vector &nodes, const std::string &schema_collection); + static std::vector includeMetadata(const std::string &schema_collection, + const std::string &base_path, + const std::optional &metadata_dto, + const std::vector &nodes); + static std::string normalizeMetadataPath(const std::string &schema_collection, + const MetadataPathComponents &components); + static Metadata findMetadata( + const std::unordered_map &nodes, + const std::string &metadata_path); }; #endif // DATA_MESSAGE_CONVERTER_H \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/websocket-client/services/converters/model_config_converter.cpp b/cdsp/knowledge-layer/connector/websocket-client/services/converters/model_config_converter.cpp index 3d9ba6f0..0d190ccc 100644 --- a/cdsp/knowledge-layer/connector/websocket-client/services/converters/model_config_converter.cpp +++ b/cdsp/knowledge-layer/connector/websocket-client/services/converters/model_config_converter.cpp @@ -1,9 +1,10 @@ #include "model_config_converter.h" -#include #include #include +#include "globals.h" +#include "model_config.h" #include "triple_assembler_helper.h" // Define the static constant variable @@ -123,30 +124,45 @@ TripleAssemblerHelper::QueryPair ModelConfigConverter::getQueriesToCreateTriples } /** - * @brief Converts a vector of query rules output file paths into a vector of pairs - * containing the query language type and the corresponding file content. + * @brief Converts a vector of query rules output file paths into a vector of + * pairs containing the query language type and the corresponding file content. * - * This function iterates over each file path in the provided vector, retrieves the - * full path of the model configuration file, reads its content, determines the - * query language type based on the file extension, and stores the results in a - * vector of pairs. + * This function iterates over each file path in the provided vector, retrieves + * the full path of the model configuration file, reads its content, determines + * the query language type based on the file extension, and stores the results + * in a vector of pairs. * - * @param reasoning_output_queries_path A vector of strings representing the file paths of - * query rules output. + * @param reasoning_output_queries_path A vector of strings representing the + * file paths of query rules output. * @return A vector of pairs, where each pair consists of a QueryLanguageType * and the corresponding file content as a string. */ -std::vector> -ModelConfigConverter::getReasoningOutputQueries(const std::string& reasoning_output_queries_path) { +std::vector ModelConfigConverter::getReasoningOutputQueries( + const std::string& reasoning_output_queries_path) { try { + if (reasoning_output_queries_path.empty()) { + throw std::invalid_argument("The provided reasoning output queries path is empty."); + } + std::vector file_list = file_handler_->readDirectory(getFullModelConfigPath(reasoning_output_queries_path)); - std::vector> reasoning_output_queries; + + if (file_list.empty()) { + throw std::invalid_argument("The provided reasoning output queries list is empty."); + } + + std::vector queries; + queries.reserve(file_list.size()); + for (const auto& file_path : file_list) { - reasoning_output_queries.push_back( - getQueryLanguageTypeAndContent(reasoning_output_queries_path + "/" + file_path)); + auto [query_type, file_content] = getQueryLanguageTypeAndContent( + (reasoning_output_queries_path + "/").append(file_path)); + + ReasoningOutputQuery query{query_type, file_content}; + queries.push_back(std::move(query)); } - return reasoning_output_queries; + return queries; + } catch (const std::runtime_error& e) { std::cerr << "Failed to read the query rules output directory: " << e.what() << std::endl; return {}; @@ -185,9 +201,9 @@ std::pair ModelConfigConverter::getQueryLanguage * @param inputs The map of input data points from the ModelConfigDTO object. * @return A map of SchemaType and vector of strings containing the converted data. */ -std::map> ModelConfigConverter::getInputsFromDto( +std::map ModelConfigConverter::getInputsFromDto( const std::map& inputs) { - std::map> inputs_map; + std::map inputs_map; for (const auto& [collection, data] : inputs) { auto collection_name = collection.substr(0, collection.find(INPUT_SUFFIX)); auto schema_type = stringToSchemaType(collection_name); @@ -232,20 +248,23 @@ ModelConfigConverter::getReasonerSyntaxTypeAndContent(const std::vector ModelConfigConverter::getSupportedDataPoints(std::string file_name) { +SchemaInputList ModelConfigConverter::getSupportedDataPoints(std::string file_name) { try { - std::vector required_data; std::string root = getFullModelConfigPath(file_name); auto file_content = file_handler_->readFile(root); + if (file_content.empty()) { + throw std::runtime_error(" - Model configuration file is empty"); + } + SchemaInputList schema_input_list; std::istringstream file(file_content); std::string line; while (std::getline(file, line)) { - required_data.push_back(line); + schema_input_list.subscribe.push_back(line); } - return required_data; + return schema_input_list; } catch (const std::exception& e) { std::cerr << "Failed to read the file " << file_name << ": " << e.what() << std::endl; return {}; @@ -311,5 +330,5 @@ std::string ModelConfigConverter::getFullModelConfigPath(const std::string& mode if (model_config_file.empty()) { return ""; // Failure will provide from the BO for each specific case } - return PATH_TO_MODEL_FILES + model_config_file; + return getProjectRoot() + getPathToUseCases() + model_config_file; } \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/websocket-client/services/converters/model_config_converter.h b/cdsp/knowledge-layer/connector/websocket-client/services/converters/model_config_converter.h index 6c1e05b4..c19cf62f 100644 --- a/cdsp/knowledge-layer/connector/websocket-client/services/converters/model_config_converter.h +++ b/cdsp/knowledge-layer/connector/websocket-client/services/converters/model_config_converter.h @@ -1,7 +1,6 @@ #ifndef MODEL_CONFIG_CONVERTER_H #define MODEL_CONFIG_CONVERTER_H -#include "globals.h" #include "i_file_handler.h" #include "model_config.h" #include "model_config_dto.h" @@ -15,18 +14,16 @@ class ModelConfigConverter { ModelConfig convert(const ModelConfigDTO& dto); private: - const std::string PATH_TO_MODEL_FILES = - getProjectRoot() + "/symbolic-reasoner/examples/use-case/model/"; std::shared_ptr file_handler_; ReasonerSettings convertReasonerSettings(const ReasonerSettingsDTO& dto); TripleAssemblerHelper convertTripleAssemblerHelper( const std::map>& queries_dto); - std::vector> getReasoningOutputQueries( + std::vector getReasoningOutputQueries( const std::string& reasoning_output_queries_path); - std::map> getInputsFromDto( + std::map getInputsFromDto( const std::map& inputs); - std::vector getSupportedDataPoints(std::string file_name); + SchemaInputList getSupportedDataPoints(std::string file_name); std::string getFullModelConfigPath(const std::string& model_config_file); std::vector> getReasonerSyntaxTypeAndContent( const std::vector& file_list); diff --git a/cdsp/knowledge-layer/connector/websocket-client/services/converters/status_message_converter.cpp b/cdsp/knowledge-layer/connector/websocket-client/services/converters/status_message_converter.cpp index 92324de4..9d24c1d3 100644 --- a/cdsp/knowledge-layer/connector/websocket-client/services/converters/status_message_converter.cpp +++ b/cdsp/knowledge-layer/connector/websocket-client/services/converters/status_message_converter.cpp @@ -1,17 +1,56 @@ #include "status_message_converter.h" -#include "converter_helper.h" +#include +#include "error.h" /** - * Converts a StatusMessageDTO object to a StatusMessage object. + * Converts a StatusMessageDTO to a StatusMessage. * - * @param dto The StatusMessageDTO object containing the data to be converted. - * @return A StatusMessage object initialized with the data from the dto. + * This function checks if the provided StatusMessageDTO belongs to the + * given RequestRegistry. If the request is found, it processes the error + * information if available and constructs a StatusMessage object. + * + * @param dto The StatusMessageDTO object containing the status message data. + * @param registry The RequestRegistry that holds the request information. + * @return A StatusMessage object constructed from the provided DTO. + * @throws std::invalid_argument If the request ID does not exist in the + * registry. */ -StatusMessage StatusMessageConverter::convert(const StatusMessageDTO& dto) { - auto timestamp = ConverterHelper::parseTimestamp(dto.timestamp.seconds, dto.timestamp.nanos); - if (!timestamp.has_value()) { - throw std::invalid_argument("Invalid timestamp"); +StatusMessage StatusMessageConverter::convert(const StatusMessageDTO &dto, + RequestRegistry ®istry) { + // Check if the message belongs to the request registry + auto request_registry = registry.getRequest(dto.id); + + if (!request_registry.has_value()) { + throw std::invalid_argument("Registry not found for the ID: " + std::to_string(dto.id)); + } + + std::optional error; + + if (dto.error.has_value()) { + std::optional data = std::nullopt; + if (!dto.error->data.empty()) { + data = dto.error->data; + } + error = Error(dto.error->code, dto.error->message, data); + } + + if (request_registry->type != RequestInfo::Type::SUBSCRIBE) { + if (request_registry->type == RequestInfo::Type::UNSUBSCRIBE) { + auto subscribe_identifier = registry.findRequestId( + RequestInfo{RequestInfo::Type::SUBSCRIBE, request_registry->schema, + request_registry->instance, request_registry->path}); + if (subscribe_identifier.has_value()) { + registry.removeRequest(*subscribe_identifier); + } else { + std::cout + << "Info: No matching subscribe request found for unsubscribe message ID: " + << dto.id << std::endl; + } + } + // Remove the request from the registry after processing + registry.removeRequest(dto.id); } - return StatusMessage(dto.code, dto.message, dto.requestId, timestamp.value()); -} \ No newline at end of file + + return {dto.id, error}; +} diff --git a/cdsp/knowledge-layer/connector/websocket-client/services/converters/status_message_converter.h b/cdsp/knowledge-layer/connector/websocket-client/services/converters/status_message_converter.h index c195172b..c3ec421b 100644 --- a/cdsp/knowledge-layer/connector/websocket-client/services/converters/status_message_converter.h +++ b/cdsp/knowledge-layer/connector/websocket-client/services/converters/status_message_converter.h @@ -1,12 +1,13 @@ #ifndef STATUS_MESSAGE_CONVERTER_H #define STATUS_MESSAGE_CONVERTER_H +#include "request_registry.h" #include "status_message.h" #include "status_message_dto.h" class StatusMessageConverter { public: - static StatusMessage convert(const StatusMessageDTO& dto); + static StatusMessage convert(const StatusMessageDTO &dto, RequestRegistry ®istry); }; #endif // STATUS_MESSAGE_CONVERTER_H \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/websocket-client/services/dto_service.cpp b/cdsp/knowledge-layer/connector/websocket-client/services/dto_service.cpp index 9ad3a7b9..7ba6df72 100644 --- a/cdsp/knowledge-layer/connector/websocket-client/services/dto_service.cpp +++ b/cdsp/knowledge-layer/connector/websocket-client/services/dto_service.cpp @@ -8,35 +8,28 @@ /** * Parses a JSON object into a DataMessageDTO object. * - * @param json The JSON object to parse, expected to contain keys: "type", "schema", "instance", - * and optionally "path", "metadata", and "requestId". - * @return A DataMessageDTO object populated with data extracted from the JSON object. + * @param json The JSON object to parse, expected to contain keys: "type", + * "schema", "instance", and optionally "path", "metadata", and "requestId". + * @return A DataMessageDTO object populated with data extracted from the JSON + * object. */ DataMessageDTO DtoService::parseDataJsonToDto(const nlohmann::json& json) { try { - if (!json.contains("type") || !json.contains("schema") || !json.contains("instance") || - !json.contains("data")) { - throw std::invalid_argument("Missing required fields in DataMessageDTO"); + if (!json.contains("id")) { + throw std::invalid_argument("Missing required id field in DataMessageDTO"); } - DataMessageDTO dto; - dto.type = json["type"]; - dto.schema = json["schema"]; - dto.instance = json["instance"]; - dto.path = json.contains("path") - ? std::optional(json["path"].get()) - : std::nullopt; - dto.data = json["data"]; - dto.metadata = json.contains("metadata") - ? std::optional(parseMetadataJsonToDto(json["metadata"])) - : std::nullopt; - - dto.requestId = json.contains("requestId") - ? std::optional(json["requestId"].get()) - : std::nullopt; - if (!dto.path.has_value() && !dto.data.is_object()) { - throw std::invalid_argument("DataMessageDTO must have a path if data is not an object"); + if (!json["result"].contains("data")) { + throw std::invalid_argument("Missing required result data field in DataMessageDTO"); } + + DataMessageDTO dto; + dto.id = json["id"].get(); + dto.data = json["result"]["data"].get(); + dto.metadata = + json["result"].contains("metadata") + ? std::optional(parseMetadataJsonToDto(json["result"]["metadata"])) + : std::nullopt; return dto; } catch (const nlohmann::json::exception& e) { throw std::invalid_argument("Failed to parse DataMessageDTO: " + std::string(e.what())); @@ -51,19 +44,33 @@ DataMessageDTO DtoService::parseDataJsonToDto(const nlohmann::json& json) { */ StatusMessageDTO DtoService::parseStatusJsonToDto(const nlohmann::json& json) { try { - if (!json.contains("code") || !json.contains("message") || !json.contains("timestamp") || - !json["timestamp"].contains("seconds") || !json["timestamp"].contains("nanos")) { - throw std::invalid_argument("Missing required fields in StatusMessageDTO"); - } StatusMessageDTO dto; - dto.code = json["code"]; - dto.message = json["message"]; - dto.requestId = json.contains("requestId") - ? std::optional(json["requestId"].get()) - : std::nullopt; - - dto.timestamp.seconds = json["timestamp"]["seconds"]; - dto.timestamp.nanos = json["timestamp"]["nanos"]; + dto.id = json["id"]; + if (json.contains("error")) { + StatusMessageErrorDTO error; + + if (json.contains("error") && json["error"].contains("code")) { + error.code = json["error"]["code"].get(); + } + + if (json["error"].contains("message")) { + error.message = json["error"]["message"].get(); + } + + if (json["error"].contains("data")) { + error.data = json["error"]["data"].get(); + } else { + error.data = ""; + } + dto.error = error; + } else if (json.contains("result")) { + dto.error = std::nullopt; + } else { + throw std::invalid_argument( + "Invalid StatusMessageDTO: missing error or " + "result field"); + } + return dto; } catch (const std::exception& e) { std::cout << Helper::getFormattedTimestampNow("%Y-%m-%dT%H:%M:%S", true, true) @@ -71,7 +78,7 @@ StatusMessageDTO DtoService::parseStatusJsonToDto(const nlohmann::json& json) { << json.dump() << std::endl << std::endl; std::cerr << " Failed to parse StatusMessageDTO: " << e.what() << std::endl; - return StatusMessageDTO(); + return {}; } } @@ -84,30 +91,55 @@ StatusMessageDTO DtoService::parseStatusJsonToDto(const nlohmann::json& json) { MetadataDTO DtoService::parseMetadataJsonToDto(const nlohmann::json& metadata_json) { MetadataDTO dto; try { - for (const auto& [node, timestamps] : metadata_json.items()) { + // Helper lambda to parse timestamp data + auto parseTimestamp = [](const nlohmann::json& timestamp_json, + const std::string& timestamp_type) -> MetadataDTO::Timestamp { + if (!timestamp_json.contains("seconds")) { + throw std::invalid_argument("Missing required seconds field into " + + timestamp_type + " timestamp in MetadataDTO"); + } + + MetadataDTO::Timestamp timestamp; + timestamp.seconds = timestamp_json["seconds"].get(); + + if (timestamp_json.contains("nanos")) { + timestamp.nanos = timestamp_json["nanos"].get(); + } + + return timestamp; + }; + + // Helper lambda to parse confidence data + auto parseConfidence = + [](const nlohmann::json& confidence_json) -> MetadataDTO::Confidence { + if (!confidence_json.contains("type") || !confidence_json.contains("value")) { + throw std::invalid_argument("Missing required confidence fields in MetadataDTO"); + } + + return MetadataDTO::Confidence{confidence_json["type"].get(), + confidence_json["value"].get()}; + }; + + for (const auto& [node, metadata] : metadata_json.items()) { MetadataDTO::NodeMetadata node_metadata; - // Check and parse received timestamps - if (timestamps.contains("timestamps") && - timestamps["timestamps"].contains("received")) { - if (!timestamps["timestamps"]["received"].contains("seconds") || - !timestamps["timestamps"]["received"].contains("nanos")) { - throw std::invalid_argument("Missing required fields in MetadataDTO"); - } - node_metadata.received.seconds = timestamps["timestamps"]["received"]["seconds"]; - node_metadata.received.nanos = timestamps["timestamps"]["received"]["nanos"]; + // Parse received timestamps + if (metadata.contains("timestamps") && metadata["timestamps"].contains("received")) { + node_metadata.received = + parseTimestamp(metadata["timestamps"]["received"], "received"); + } + + // Parse generated timestamps + if (metadata.contains("timestamps") && metadata["timestamps"].contains("generated")) { + node_metadata.generated = + parseTimestamp(metadata["timestamps"]["generated"], "generated"); } - // Check and parse generated timestamps - if (timestamps.contains("timestamps") && - timestamps["timestamps"].contains("generated")) { - if (!timestamps["timestamps"]["generated"].contains("seconds") || - !timestamps["timestamps"]["generated"].contains("nanos")) { - throw std::invalid_argument("Missing required fields in MetadataDTO"); - } - node_metadata.generated.seconds = timestamps["timestamps"]["generated"]["seconds"]; - node_metadata.generated.nanos = timestamps["timestamps"]["generated"]["nanos"]; + // Parse confidence + if (metadata.contains("confidence")) { + node_metadata.confidence = parseConfidence(metadata["confidence"]); } + dto.nodes[node] = node_metadata; } return dto; @@ -124,13 +156,35 @@ MetadataDTO DtoService::parseMetadataJsonToDto(const nlohmann::json& metadata_js */ ModelConfigDTO DtoService::parseModelConfigJsonToDto(const nlohmann::json& json) { try { - if (!json.contains("inputs") || !json.contains("ontologies") || !json.contains("output") || - !json.contains("rules") || !json.contains("shacl") || !json.contains("queries") || - !json.contains("reasoner_settings")) { - throw std::invalid_argument("Missing required fields in ModelConfigDTO"); + if (!json.contains("inputs")) { + throw std::invalid_argument("Missing required inputs field in ModelConfigDTO"); } - QueriesDTO queries_dto; - ReasonerSettingsDTO reasoner_settings_dto; + + if (!json.contains("ontologies")) { + throw std::invalid_argument("Missing required ontologies field in ModelConfigDTO"); + } + + if (!json.contains("output")) { + throw std::invalid_argument("Missing required output field in ModelConfigDTO"); + } + + if (!json.contains("rules")) { + throw std::invalid_argument("Missing required rules field in ModelConfigDTO"); + } + + if (!json.contains("shacl")) { + throw std::invalid_argument("Missing required shacl field in ModelConfigDTO"); + } + + if (!json.contains("queries")) { + throw std::invalid_argument("Missing required queries field in ModelConfigDTO"); + } + + if (!json.contains("reasoner_settings")) { + throw std::invalid_argument( + "Missing required reasoner_settings field in ModelConfigDTO"); + } + ModelConfigDTO model_config_dto; model_config_dto.inputs = json["inputs"]; @@ -139,6 +193,7 @@ ModelConfigDTO DtoService::parseModelConfigJsonToDto(const nlohmann::json& json) model_config_dto.rules = json["rules"]; model_config_dto.shacl_shapes = json["shacl"]; + QueriesDTO queries_dto; for (const auto& [key, value] : json["queries"]["triple_assembler_helper"].items()) { queries_dto.triple_assembler_helper[key] = value; } @@ -146,15 +201,56 @@ ModelConfigDTO DtoService::parseModelConfigJsonToDto(const nlohmann::json& json) queries_dto.reasoning_output_queries_path = json["queries"]["output"]; model_config_dto.queries = queries_dto; - reasoner_settings_dto.inference_engine = json["reasoner_settings"]["inference_engine"]; - reasoner_settings_dto.is_ai_reasoner_inference_results = - json["reasoner_settings"]["is_ai_reasoner_inference_results"]; - reasoner_settings_dto.output_format = json["reasoner_settings"]["output_format"]; - reasoner_settings_dto.supported_schema_collections = - json["reasoner_settings"]["supported_schema_collections"]; + ReasonerSettingsDTO reasoner_settings_dto; + reasoner_settings_dto = parseReasonerSettingsJsonToDto(json["reasoner_settings"]); model_config_dto.reasoner_settings = reasoner_settings_dto; + return model_config_dto; } catch (const nlohmann::json::exception& e) { throw std::invalid_argument("Failed to parse ModelConfigDTO: " + std::string(e.what())); } } + +/** + * Parses a JSON object containing reasoner settings into a ReasonerSettingsDTO. + * + * @param reasoner_settings_json The JSON object containing the reasoner + * settings data. + * @return A ReasonerSettingsDTO object populated with data from the JSON + * object. + */ +ReasonerSettingsDTO DtoService::parseReasonerSettingsJsonToDto( + const nlohmann::json& reasoner_settings_json) { + ReasonerSettingsDTO dto; + try { + if (!reasoner_settings_json.contains("inference_engine")) { + throw std::invalid_argument( + "Missing required inference_engine field in ReasonerSettingsDTO"); + } + + if (!reasoner_settings_json.contains("output_format")) { + throw std::invalid_argument( + "Missing required output_format field in ReasonerSettingsDTO"); + } + + if (!reasoner_settings_json.contains("supported_schema_collections")) { + throw std::invalid_argument( + "Missing required supported_schema_collections field in " + "ReasonerSettingsDTO"); + } + + dto.inference_engine = reasoner_settings_json["inference_engine"]; + dto.output_format = reasoner_settings_json["output_format"]; + dto.supported_schema_collections = + reasoner_settings_json["supported_schema_collections"].get>(); + + if (reasoner_settings_json.contains("is_ai_reasoner_inference_results")) { + dto.is_ai_reasoner_inference_results = + reasoner_settings_json["is_ai_reasoner_inference_results"]; + } + + return dto; + } catch (const nlohmann::json::exception& e) { + throw std::invalid_argument("ReasonerSettingsDTO: " + std::string(e.what())); + } +} diff --git a/cdsp/knowledge-layer/connector/websocket-client/services/dto_service.h b/cdsp/knowledge-layer/connector/websocket-client/services/dto_service.h index d970ab16..d71038f5 100644 --- a/cdsp/knowledge-layer/connector/websocket-client/services/dto_service.h +++ b/cdsp/knowledge-layer/connector/websocket-client/services/dto_service.h @@ -5,7 +5,6 @@ #include "data_message_dto.h" #include "model_config_dto.h" -#include "set_message_dto.h" #include "status_message_dto.h" class DtoService { @@ -16,6 +15,9 @@ class DtoService { private: static MetadataDTO parseMetadataJsonToDto(const nlohmann::json& metadata_json); + + static ReasonerSettingsDTO parseReasonerSettingsJsonToDto( + const nlohmann::json& reasoner_settings_json); }; #endif // DTO_SERVICE_H \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/websocket-client/services/dto_to_bo.h b/cdsp/knowledge-layer/connector/websocket-client/services/dto_to_bo.h index d95e21df..6df19719 100644 --- a/cdsp/knowledge-layer/connector/websocket-client/services/dto_to_bo.h +++ b/cdsp/knowledge-layer/connector/websocket-client/services/dto_to_bo.h @@ -1,16 +1,13 @@ #ifndef DTO_TO_BO_H #define DTO_TO_BO_H -#include -#include -#include - #include "data_message_converter.h" #include "data_message_dto.h" #include "i_file_handler.h" #include "model_config.h" #include "model_config_converter.h" #include "model_config_dto.h" +#include "request_registry.h" #include "status_message.h" #include "status_message_converter.h" #include "status_message_dto.h" @@ -20,14 +17,13 @@ class DtoToBo { explicit DtoToBo(std::shared_ptr file_handler = nullptr) : file_handler_(std::move(file_handler)) {} - DataMessage convert(const DataMessageDTO& dto, - const std::map>& system_data_points) { - return DataMessageConverter::convert(dto, system_data_points); + static DataMessage convert(const DataMessageDTO &dto, RequestRegistry ®istry) { + return DataMessageConverter::convert(dto, registry); } - StatusMessage convert(const StatusMessageDTO& dto) { - return StatusMessageConverter::convert(dto); + static StatusMessage convert(const StatusMessageDTO &dto, RequestRegistry ®istry) { + return StatusMessageConverter::convert(dto, registry); } - ModelConfig convert(const ModelConfigDTO& dto) { + ModelConfig convert(const ModelConfigDTO &dto) { if (file_handler_ == nullptr) { throw std::runtime_error("File handler is not initialized"); } diff --git a/cdsp/knowledge-layer/connector/websocket-client/services/message_service.cpp b/cdsp/knowledge-layer/connector/websocket-client/services/message_service.cpp index ba06142c..d80ef90f 100644 --- a/cdsp/knowledge-layer/connector/websocket-client/services/message_service.cpp +++ b/cdsp/knowledge-layer/connector/websocket-client/services/message_service.cpp @@ -2,125 +2,266 @@ #include +#include "bo_service.h" #include "bo_to_dto.h" #include "dto_service.h" #include "dto_to_bo.h" #include "message_header.h" +#include "unsubscribe_message_dto.h" -MessageService::MessageService( - const std::map>& system_data_point) - : system_data_point_(system_data_point) { - if (system_data_point_.empty()) { - throw std::invalid_argument("System data points cannot be empty"); +/** + * @brief Creates a SubscribeMessage and queues it for processing. + * + * This function creates a SubscribeMessage for the specified object ID and + * schema type, including all data points in the provided data point list. It + * then adds this message to the reply messages queue for further processing. + * + * @param object_id The ID of the object to subscribe to. + * @param schema_type The schema type for which to subscribe. + * @param data_point_list A vector of data points to subscribe to. + * @param registry A reference to the RequestRegistry used for tracking + * requests. + * @param reply_messages_queue A reference to the queue where messages will be + * added for processing. + */ +void MessageService::createAndQueueSubscribeMessage(const std::string &object_id, + const SchemaType &schema_type, + const std::vector &data_point_list, + RequestRegistry ®istry, + std::vector &reply_messages_queue) { + // Create a GetMessage to retrieve all data points for the schema type + auto get_message = BoService::createGetMessage(object_id, schema_type, data_point_list); + addMessageToQueue(get_message, registry, reply_messages_queue); + + // Start subscribing to data points into the schema type + std::vector nodes; + nodes.reserve(data_point_list.size()); + for (const auto &data_point : data_point_list) { + nodes.emplace_back(data_point, std::nullopt, Metadata()); } + + auto subscribe_message = BoService::createSubscribeMessage(object_id, schema_type, nodes); + addMessageToQueue(subscribe_message, registry, reply_messages_queue); } /** - * @brief Adds a message to the reply messages queue. + * @brief Creates an UnsubscribeMessage and queues it for processing. * - * This function takes a message variant and adds it to the reply messages queue. The message is - * converted to a DTO and then to a JSON object before being added to the queue. + * This function creates an UnsubscribeMessage for the specified object ID, + * schema type, and list of data points, and adds it to the reply messages queue + * for further processing. * - * @param message The message to be added to the queue. - * @param reply_messages_queue The queue to which the message will be added. + * @param object_id The ID of the object to unsubscribe from. + * @param schema_type The schema type for which to unsubscribe. + * @param data_point_list A vector of data points to unsubscribe from. + * @param registry A reference to the RequestRegistry used for tracking + * requests. + * @param reply_messages_queue A reference to the queue where messages will be + * added for processing. */ -void MessageService::addMessageToQueue( - const std::variant& message, - std::vector& reply_messages_queue) { - std::visit( - [&reply_messages_queue](const auto& specific_message) { - auto dto = BoToDto::convert(specific_message); - json json_dto = dto; - if (json_dto.is_array()) { - for (const auto& message_dto : json_dto) { - reply_messages_queue.push_back(message_dto); - } - } else { - reply_messages_queue.push_back(json_dto); - } - }, - message); +void MessageService::createAndQueueUnsubscribeMessage( + const std::string &object_id, const SchemaType &schema_type, + const std::vector &data_point_list, RequestRegistry ®istry, + std::vector &reply_messages_queue) { + // Create an UnsubscribeMessage to stop subscribing to data points for the + // schema type + std::vector nodes; + nodes.reserve(data_point_list.size()); + for (const auto &data_point : data_point_list) { + nodes.emplace_back(data_point, std::nullopt, Metadata()); + } + auto unsubscribe_message = BoService::createUnsubscribeMessage(object_id, schema_type, nodes); + addMessageToQueue(unsubscribe_message, registry, reply_messages_queue); } /** - * @brief Parses a JSON message and returns a variant containing either a DTODataMessage, - * DTOStatusMessage, or InternalErrorMessage. + * @brief Creates and queues SetMessage objects for setting data points. * - * This function attempts to parse the input JSON string and determine the type of message it - * contains. It supports "data" and "status" message types. If the message type is unknown or if - * there is a parsing error, an InternalErrorMessage is returned. + * This function generates SetMessage objects to set data points for a given + * schema type and queues them for processing. * - * @param message A JSON string representing the message to be parsed. - * @return A std::variant containing either a DTODataMessage, DTOStatusMessage, or ErrorMessage. + * @param object_ids A map associating SchemaType with its corresponding object + * ID. This identifies the schema type and object to be updated. + * @param json_body A JSON object containing the data to be set in the + * SetMessage. + * @param registry A reference to the RequestRegistry used to track and manage + * requests. + * @param reply_messages_queue A reference to a vector where the generated JSON + * messages (SetMessage and optional GetMessage) will be queued. + * @param origin_system_name A string representing the name of the originating + * system. This is used to identify the source of the messages. */ -std::variant MessageService::displayAndParseMessage( - const std::string& message) { - try { - // Parse the input JSON string into a JSON object - json json_message = json::parse(message); +void MessageService::createAndQueueSetMessage(const std::map &object_ids, + const json &json_body, RequestRegistry ®istry, + std::vector &reply_messages_queue, + const std::string &origin_system_name) { + // Create a SetMessage to set the data points for the schema type + auto set_messages = BoService::createSetMessage(object_ids, json_body, origin_system_name); - // Extract "type" field to determine the message type - std::string type = json_message["type"]; + for (auto &set_message : set_messages) { + const auto &schema_type = set_message.getHeader().getSchemaType(); - if (type == "data") { - // Parse and return a DataMessage - std::cout << Helper::getFormattedTimestampNow("%Y-%m-%dT%H:%M:%S", true, true) - << " Data message received:\n" - << json_message.dump() << std::endl - << std::endl; - return (DtoService::parseDataJsonToDto(json_message)); - } else if (type == "status") { - // Parse and return a StatusMessage - return DtoService::parseStatusJsonToDto(json_message); - } else { - // Log and return an ErrorMessage for unknown message types - throw std::runtime_error("Unknown message type: " + type); - } - } catch (const std::exception& e) { - // Log and return an ErrorMessage for JSON parsing errors - std::cerr << "Error parsing JSON message: " << e.what() << std::endl; - throw std::runtime_error("Error parsing JSON message: " + std::string(e.what())); + // Add the SetMessage to the processing queue + addMessageToQueue(set_message, registry, reply_messages_queue); } } /** - * @brief Retrieves a DataMessage from a given message string or logs the status if the message is a - * StatusMessageDTO. + * @brief Processes incoming WebSocket messages and extracts DataMessage if + * available. * - * This function attempts to parse the provided message string. If the parsed message is of type - * StatusMessageDTO, it converts it to a StatusMessage and logs its code and message. If the parsed - * message is of type DataMessageDTO, it converts it to a DataMessage and returns it. In case of - * any exceptions during the conversion process, an error message is logged. + * Parses JSON-RPC messages into DataMessageDTO or StatusMessageDTO. For data + * messages, converts and returns directly. For status messages logs + * errors/success. * - * @param message A constant reference to a string containing the message to be parsed. - * @return std::optional An optional containing the converted DataMessage if - * successful, or std::nullopt if the message is a StatusMessageDTO or an error occurs during - * conversion. + * @param message JSON-RPC formatted string from WebSocket server. + * @param registry RequestRegistry for tracking requests and DTO conversion. + * @return DataMessage if available from data response, + * std::nullopt for status-only messages or errors. */ -std::optional MessageService::getDataMessageOrLogStatus(const std::string& message) { +std::optional MessageService::getDataOrProcessStatusFromMessage( + const std::string &message, RequestRegistry ®istry) { const auto parsed_message = MessageService::displayAndParseMessage(message); - DtoToBo dto_to_bo; if (std::holds_alternative(parsed_message)) { StatusMessageDTO status_message_dto = std::get(parsed_message); try { - StatusMessage status_message = dto_to_bo.convert(status_message_dto); + StatusMessage status_message = DtoToBo::convert(status_message_dto, registry); std::cout << Helper::getFormattedTimestampNow("%Y-%m-%dT%H:%M:%S", true, true) - << " Status message received(" << status_message.getCode() << "):\n" - << status_message.getMessage() << std::endl - << std::endl; - } catch (const std::exception& e) { - std::cerr << "Error parsing status message: " << e.what() << std::endl; + << " Message received (Request ID:" << status_message.getIdentifier() + << "):\n"; + + auto error = status_message.getError(); + if (error.has_value()) { + std::cout << " Error during processing: \n"; + std::cout << " - Code: " << error->getCode() << "\n"; + std::cout << " - Message: " << error->getMessage() << "\n"; + if (error->getData()) { + std::cout << " - Data: " << *error->getData() << "\n"; + } + } else { + // Successful status message + std::cout << " Processed successfully!\n"; + } + } catch (const std::exception &e) { + std::cerr << "Error parsing status message: " << e.what() << "\n"; } } else { DataMessageDTO data_message_dto = std::get(parsed_message); try { - return dto_to_bo.convert(data_message_dto, system_data_point_); - } catch (const std::exception& e) { + return DtoToBo::convert(data_message_dto, registry); + } catch (const std::exception &e) { std::cerr << "Error parsing and transforming data message to RDF triple: " << e.what() - << std::endl; + << "\n"; } } return std::nullopt; } + +/** + * @brief Adds a message to the processing queue based on its type. + * + * This function takes a variant message which can be of type GetMessage, + * SetMessage, SubscribeMessage, or UnsubscribeMessage and processes it + * accordingly. It converts the message to its corresponding Data Transfer + * Object (DTO) and adds it to the reply messages queue. Additionally, it + * registers the request information in the provided RequestRegistry. + * + * @param message A variant containing the message to be processed. It can + * be one of the following types: GetMessage, SetMessage, SubscribeMessage, + * or UnsubscribeMessage. + * @param registry A reference to the RequestRegistry used to track requests + * and assign unique IDs to the DTOs. + * @param reply_messages_queue A reference to a vector where the converted + * JSON DTOs will be pushed for further + * processing. + * @throws std::runtime_error If the message type is unknown and cannot be + * processed. + */ +void MessageService::addMessageToQueue( + const std::variant &message, + RequestRegistry ®istry, std::vector &reply_messages_queue) { + std::visit( + [&reply_messages_queue, ®istry](const auto &specific_message) { + auto dto = BoToDto::convert(specific_message); + + for (auto &actual_dto : dto) { + using T = std::decay_t; + RequestInfo request_info; + + if constexpr (std::is_same_v) { + request_info.type = RequestInfo::Type::GET; + } else if constexpr (std::is_same_v) { + request_info.type = RequestInfo::Type::SUBSCRIBE; + } else if constexpr (std::is_same_v) { + request_info.type = RequestInfo::Type::UNSUBSCRIBE; + } else if constexpr (std::is_same_v) { + request_info.type = RequestInfo::Type::SET; + } else { + throw std::runtime_error("Unknown DTO type in addMessageToQueue"); + } + + request_info.schema = actual_dto.schema; + request_info.instance = actual_dto.instance; + request_info.path = actual_dto.path; + + actual_dto.id = registry.addRequest(request_info); + reply_messages_queue.push_back(json(actual_dto)); + } + }, + message); +} + +/** + * @brief Parses and displays a JSON-RPC message, returning either a + * DataMessageDTO or a StatusMessageDTO. + * + * This function attempts to parse the provided JSON string into a JSON object + * and checks for the presence of a valid JSON-RPC version. Depending on the + * content of the message, it either parses and returns a DataMessageDTO or a + * StatusMessageDTO. If the message cannot be parsed or is invalid, an exception + * is thrown. + * + * @param message The JSON string representing the message to be parsed. + * @return A std::variant containing either a DataMessageDTO or a + * StatusMessageDTO. + * @throws std::invalid_argument If the JSON-RPC version is invalid. + * @throws std::runtime_error If the message cannot be parsed or is of an + * unknown type. + */ +std::variant MessageService::displayAndParseMessage( + const std::string &message) { + try { + // Parse the input JSON string into a JSON object + json json_message = json::parse(message); + if (!json_message.contains("jsonrpc") || json_message["jsonrpc"] != getJsonRpcVersion()) { + throw std::invalid_argument("Invalid JSON-RPC version"); + } + + if (json_message.contains("result") && !json_message["result"].empty()) { + // Parse and return a DataMessage + std::cout << "(" << Helper::getFormattedTimestampNow("%Y-%m-%dT%H:%M:%S", true, true) + << ") Websocket-Server: Data message received correctly\n" + << "Message Content: " << json_message.dump() << "\n"; + return (DtoService::parseDataJsonToDto(json_message)); + } + + if (json_message.contains("error") || + (json_message.contains("result") && json_message["result"].empty())) { + std::cout << "(" << Helper::getFormattedTimestampNow("%Y-%m-%dT%H:%M:%S", true, true) + << ") Websocket-Server: Error status message received\n" + << "Message Content: " << json_message.dump() << "\n"; + return DtoService::parseStatusJsonToDto(json_message); + } + + // Log and return an ErrorMessage for unknown message types + throw std::runtime_error("The incoming message cannot be parsed: " + json_message.dump()); + + } catch (const std::exception &e) { + // Log and return an ErrorMessage for JSON parsing errors + std::cerr << "(" << Helper::getFormattedTimestampNow("%Y-%m-%dT%H:%M:%S", true, true) + << ") Websocket-Server: Error parsing JSON message: " << e.what() << "\n"; + throw std::runtime_error("Error parsing JSON message: " + std::string(e.what())); + } +} diff --git a/cdsp/knowledge-layer/connector/websocket-client/services/message_service.h b/cdsp/knowledge-layer/connector/websocket-client/services/message_service.h index 8398e117..916afb6e 100644 --- a/cdsp/knowledge-layer/connector/websocket-client/services/message_service.h +++ b/cdsp/knowledge-layer/connector/websocket-client/services/message_service.h @@ -1,7 +1,6 @@ #ifndef MESSAGE_UTILS_H #define MESSAGE_UTILS_H -#include #include #include #include @@ -12,26 +11,42 @@ #include "data_message_dto.h" #include "data_types.h" #include "get_message.h" -#include "model_config.h" +#include "request_registry.h" #include "set_message.h" #include "status_message_dto.h" #include "subscribe_message.h" +#include "unsubscribe_message.h" using json = nlohmann::json; class MessageService { public: - MessageService(const std::map>& system_data_point); - - void addMessageToQueue(const std::variant& message, - std::vector& reply_messages_queue); - - std::optional getDataMessageOrLogStatus(const std::string& message); + static void createAndQueueSubscribeMessage(const std::string &object_id, + const SchemaType &schema_type, + const std::vector &data_point_list, + RequestRegistry ®istry, + std::vector &reply_messages_queue); + + static void createAndQueueUnsubscribeMessage(const std::string &object_id, + const SchemaType &schema_type, + const std::vector &data_point_list, + RequestRegistry ®istry, + std::vector &reply_messages_queue); + + static void createAndQueueSetMessage(const std::map &object_id, + const json &json_body, RequestRegistry ®istry, + std::vector &reply_messages_queue, + const std::string &origin_system_name); + + static std::optional getDataOrProcessStatusFromMessage(const std::string &message, + RequestRegistry ®istry); private: - std::map> system_data_point_; + static void addMessageToQueue( + const std::variant &message, + RequestRegistry ®istry, std::vector &reply_messages_queue); - std::variant displayAndParseMessage( - const std::string& message); + static std::variant displayAndParseMessage( + const std::string &message); }; #endif // MESSAGE_UTILS_H \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/websocket-client/services/system_configuration_service.cpp b/cdsp/knowledge-layer/connector/websocket-client/services/system_configuration_service.cpp index 51dcaebd..3d377ed9 100644 --- a/cdsp/knowledge-layer/connector/websocket-client/services/system_configuration_service.cpp +++ b/cdsp/knowledge-layer/connector/websocket-client/services/system_configuration_service.cpp @@ -1,6 +1,5 @@ #include "system_configuration_service.h" -#include #include #include @@ -11,11 +10,14 @@ #include "model_config_dto.h" SystemConfig SystemConfigurationService::loadSystemConfig( - std::optional ws_server_host, std::optional ws_server_port, - std::optional ws_server_target, std::optional reasoner_server_host, - std::optional reasoner_server_port, - std::optional reasoner_server_auth_base64, - std::optional reasoner_server_data_store_name) { + const std::optional ws_server_host, + const std::optional ws_server_port, + const std::optional ws_server_target, + const std::optional reasoner_server_host, + const std::optional reasoner_server_port, + const std::optional reasoner_server_auth_base64, + const std::optional reasoner_server_data_store_name, + const std::optional& reasoner_server_origin_system) { SystemConfig system_config; system_config.websocket_server.host = Helper::getEnvVariable("HOST_WEBSOCKET_SERVER", ws_server_host); @@ -31,6 +33,8 @@ SystemConfig SystemConfigurationService::loadSystemConfig( Helper::getEnvVariable("AUTH_REASONER_SERVER_BASE64", reasoner_server_auth_base64); system_config.reasoner_server.data_store_name = Helper::getEnvVariable("REASONER_DATASTORE", reasoner_server_data_store_name); + system_config.reasoner_server.origin_system_name = + Helper::getEnvVariable("REASONER_ORIGIN_SYSTEM_NAME", reasoner_server_origin_system); return system_config; } @@ -48,6 +52,7 @@ ModelConfig SystemConfigurationService::loadModelConfig(const std::string& confi config_json = nlohmann::json::parse(file_content); ModelConfigDTO model_config_dto = DtoService::parseModelConfigJsonToDto(config_json); + std::cout << " - ModelConfigDTO parsed successfully\n"; DtoToBo dto_to_bo(file_handler); const auto model_config = dto_to_bo.convert(model_config_dto); std::cout << " - Model configuration loaded successfully\n\n"; diff --git a/cdsp/knowledge-layer/connector/websocket-client/services/system_configuration_service.h b/cdsp/knowledge-layer/connector/websocket-client/services/system_configuration_service.h index 291efc42..8dc7925e 100644 --- a/cdsp/knowledge-layer/connector/websocket-client/services/system_configuration_service.h +++ b/cdsp/knowledge-layer/connector/websocket-client/services/system_configuration_service.h @@ -10,12 +10,14 @@ class SystemConfigurationService { public: static SystemConfig loadSystemConfig( - std::optional ws_server_host, std::optional ws_server_port, - std::optional ws_server_target, - std::optional reasoner_server_host, - std::optional reasoner_server_port, - std::optional reasoner_server_auth_base64, - std::optional reasoner_server_data_store_name); + const std::optional ws_server_host, + const std::optional ws_server_port, + const std::optional ws_server_target, + const std::optional reasoner_server_host, + const std::optional reasoner_server_port, + const std::optional reasoner_server_auth_base64, + const std::optional reasoner_server_data_store_name, + const std::optional& reasoner_server_origin_system); static ModelConfig loadModelConfig(const std::string& config_file); }; diff --git a/cdsp/knowledge-layer/connector/websocket-client/services/tests/CMakeLists.txt b/cdsp/knowledge-layer/connector/websocket-client/services/tests/CMakeLists.txt index 75435f49..88cc4aff 100644 --- a/cdsp/knowledge-layer/connector/websocket-client/services/tests/CMakeLists.txt +++ b/cdsp/knowledge-layer/connector/websocket-client/services/tests/CMakeLists.txt @@ -1,35 +1,3 @@ -# Add the test for DataMessageDTO service -add_executable(data_message_dto_service_unit_test - data_message_dto_service_unit_test.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/utils/test_helper.cpp -) -target_link_libraries(data_message_dto_service_unit_test - PRIVATE - GTest::gtest_main - websocket_client - test_fixtures -) -target_include_directories(data_message_dto_service_unit_test - PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/utils -) - -# Add the test for StatusMessageDTO service -add_executable(status_message_dto_service_unit_test - status_message_dto_service_unit_test.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/utils/test_helper.cpp -) -target_link_libraries(status_message_dto_service_unit_test - PRIVATE - GTest::gtest_main - websocket_client - test_fixtures -) -target_include_directories(status_message_dto_service_unit_test - PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/utils -) - # Add the test for ModelConfigDTO service add_executable(model_config_dto_service_unit_test model_config_dto_service_unit_test.cpp @@ -46,40 +14,6 @@ target_include_directories(model_config_dto_service_unit_test ${CMAKE_CURRENT_SOURCE_DIR}/utils ) -# Add the test for DTO to Data Message BO conversion -add_executable(dto_to_data_message_integration_test - dto_to_data_message_integration_test.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/utils/test_helper.cpp -) -target_link_libraries(dto_to_data_message_integration_test - PRIVATE - GTest::gtest_main - websocket_client - test_fixtures -) -target_include_directories(dto_to_data_message_integration_test - PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/utils - ${PROJECT_ROOT_DIR}/connector/utils -) - -# Add the test for DTO to Status Message BO conversion -add_executable(dto_to_status_message_integration_test - dto_to_status_message_integration_test.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/utils/test_helper.cpp -) -target_link_libraries(dto_to_status_message_integration_test - PRIVATE - GTest::gtest_main - websocket_client - test_fixtures -) -target_include_directories(dto_to_status_message_integration_test - PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/utils - ${PROJECT_ROOT_DIR}/connector/utils -) - # Add the test for DTO to Model Config BO conversion add_executable(dto_to_model_config_integration_test dto_to_model_config_integration_test.cpp @@ -99,23 +33,6 @@ target_include_directories(dto_to_model_config_integration_test ${PROJECT_ROOT_DIR}/connector/utils/tests/utils ) -# Add the test for BO to DTO conversion -add_executable(bo_to_dto_integration_test - bo_to_dto_integration_test.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/utils/test_helper.cpp -) -target_link_libraries(bo_to_dto_integration_test - PRIVATE - GTest::gtest_main - websocket_client - test_fixtures - nlohmann_json::nlohmann_json -) -target_include_directories(bo_to_dto_integration_test - PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/utils -) - # Add the test for BO Service add_executable(bo_service_integration_test bo_service_integration_test.cpp @@ -134,31 +51,16 @@ target_include_directories(bo_service_integration_test # Add unit and integration tests to CTest add_test(NAME ModelConfigDtoServiceUnitTest COMMAND model_config_dto_service_unit_test) -add_test(NAME StatusMessageDtoServiceUnitTest COMMAND status_message_dto_service_unit_test) -add_test(NAME DataMessageDtoServiceUnitTest COMMAND data_message_dto_service_unit_test) -add_test(NAME DtoToDataMessageIntegrationTests COMMAND dto_to_data_message_integration_test) -add_test(NAME DtoToStatusMessageIntegrationTest COMMAND dto_to_status_message_integration_test) add_test(NAME DtoToModelConfigIntegrationTest COMMAND dto_to_model_config_integration_test) -add_test(NAME BoToDtoIntegrationTests COMMAND bo_to_dto_integration_test) add_test(NAME BoServiceIntegrationTest COMMAND bo_service_integration_test) # Define custom output directory for test binaries set_target_properties(model_config_dto_service_unit_test PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/tests") -set_target_properties(status_message_dto_service_unit_test PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/tests") -set_target_properties(data_message_dto_service_unit_test PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/tests") -set_target_properties(dto_to_data_message_integration_test PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/tests") -set_target_properties(dto_to_status_message_integration_test PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/tests") set_target_properties(dto_to_model_config_integration_test PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/tests") -set_target_properties(bo_to_dto_integration_test PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/tests") set_target_properties(bo_service_integration_test PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/tests") # Ensure tests are built with the all target add_custom_target(websocket_client_services_tests ALL DEPENDS - status_message_dto_service_unit_test - dto_to_status_message_integration_test - dto_to_data_message_integration_test dto_to_model_config_integration_test - bo_to_dto_integration_test - data_message_dto_service_unit_test model_config_dto_service_unit_test bo_service_integration_test ) \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/websocket-client/services/tests/bo_service_integration_test.cpp b/cdsp/knowledge-layer/connector/websocket-client/services/tests/bo_service_integration_test.cpp index 6412018f..2e1afda8 100644 --- a/cdsp/knowledge-layer/connector/websocket-client/services/tests/bo_service_integration_test.cpp +++ b/cdsp/knowledge-layer/connector/websocket-client/services/tests/bo_service_integration_test.cpp @@ -6,30 +6,96 @@ #include "data_points_utils.h" #include "random_utils.h" class BoServiceIntegrationTest : public ::testing::Test { + // NOLINTBEGIN(cppcoreguidelines-non-private-member-variables-in-classes) protected: - BoService bo_service_; const std::string OBJECT_ID = "object_id"; const SchemaType SCHEMA_TYPE = SchemaType::VEHICLE; + static constexpr int MAX_RANDOM_SIZE = 5; + // NOLINTEND(cppcoreguidelines-non-private-member-variables-in-classes) }; /** * @brief Test case for creating a SubscribeMessage. * - * This test verifies that the SubscribeMessage is created correctly with the provided - * object ID and schema type. + * This test verifies that the SubscribeMessage is created correctly with the + * provided object ID and schema type. */ TEST_F(BoServiceIntegrationTest, CreateSubscribeMessage) { + // Arrange + std::vector nodes; + int num_points = RandomUtils::generateRandomInt(1, MAX_RANDOM_SIZE); + nodes.reserve(num_points); + for (int i = 0; i < num_points; ++i) { + Node node{ + DataPointsUtils::generateRandomKey(RandomUtils::generateRandomInt(1, MAX_RANDOM_SIZE)), + std::nullopt, Metadata()}; + nodes.emplace_back(node); + } + // Act - SubscribeMessage subscribe_message = bo_service_.createSubscribeMessage(OBJECT_ID, SCHEMA_TYPE); + SubscribeMessage subscribe_message = + BoService::createSubscribeMessage(OBJECT_ID, SCHEMA_TYPE, nodes); - std::cout << "SubscribeMessage created:\n" << subscribe_message << std::endl; + std::cout << "SubscribeMessage created:\n" << subscribe_message << "\n"; // Assert - ASSERT_EQ(subscribe_message.getHeader().getId(), OBJECT_ID) << "Object ID mismatch"; + ASSERT_EQ(subscribe_message.getHeader().getInstance(), OBJECT_ID) << "Object ID mismatch"; ASSERT_EQ(subscribe_message.getHeader().getSchemaType(), SCHEMA_TYPE) << "Schema type mismatch"; - // TODO: Change when required, at this point, the subscribe message subscribes to all nodes in - // the schema collection. - ASSERT_TRUE(subscribe_message.getNodes().empty()) << "Nodes should be empty"; + ASSERT_EQ(subscribe_message.getNodes().size(), nodes.size()) << "Number of nodes mismatch"; + for (size_t i = 0; i < nodes.size(); ++i) { + ASSERT_EQ(subscribe_message.getNodes()[i].getName(), nodes[i].getName()) + << "Node name mismatch at index " << i; + ASSERT_FALSE(subscribe_message.getNodes()[i].getMetadata().getGenerated().has_value()) + << "Generated time should be empty at index " << i; + // This uses the default value of the Metadata class, which is set if the + // value is not provided + ASSERT_TRUE( + subscribe_message.getNodes()[i].getMetadata().getReceived().time_since_epoch().count() > + 0) + << "Received time should not be empty at index " << i; + } +} + +/** + * @brief Test case for creating an UnsubscribeMessage. + * + * This test verifies that the UnsubscribeMessage is created correctly with the + * provided object ID, schema type, and nodes. + */ +TEST_F(BoServiceIntegrationTest, CreateUnsubscribeMessage) { + // Arrange + std::vector nodes; + int num_points = RandomUtils::generateRandomInt(1, MAX_RANDOM_SIZE); + nodes.reserve(num_points); + for (int i = 0; i < num_points; ++i) { + Node node{ + DataPointsUtils::generateRandomKey(RandomUtils::generateRandomInt(1, MAX_RANDOM_SIZE)), + std::nullopt, Metadata()}; + nodes.emplace_back(node); + } + // Act + UnsubscribeMessage unsubscribe_message = + BoService::createUnsubscribeMessage(OBJECT_ID, SCHEMA_TYPE, nodes); + std::cout << "UnsubscribeMessage created:\n" << unsubscribe_message << "\n"; + // Assert + ASSERT_EQ(unsubscribe_message.getHeader().getInstance(), OBJECT_ID) << "Object ID mismatch"; + ASSERT_EQ(unsubscribe_message.getHeader().getSchemaType(), SCHEMA_TYPE) + << "Schema type mismatch"; + ASSERT_EQ(unsubscribe_message.getNodes().size(), nodes.size()) << "Number of nodes mismatch"; + for (size_t i = 0; i < nodes.size(); ++i) { + ASSERT_EQ(unsubscribe_message.getNodes()[i].getName(), nodes[i].getName()) + << "Node name mismatch at index " << i; + ASSERT_FALSE(unsubscribe_message.getNodes()[i].getMetadata().getGenerated().has_value()) + << "Generated time should be empty at index " << i; + // This uses the default value of the Metadata class, which is set if the + // value is not provided + ASSERT_TRUE(unsubscribe_message.getNodes()[i] + .getMetadata() + .getReceived() + .time_since_epoch() + .count() > 0) + << "Received time should not be empty at index " << i; + } } /** @@ -41,23 +107,25 @@ TEST_F(BoServiceIntegrationTest, CreateSubscribeMessage) { TEST_F(BoServiceIntegrationTest, CreateGetMessage) { // Arrange std::vector list_data_points; - for (int i = 0; i < RandomUtils::generateRandomInt(1, 5); ++i) { + int random_size = RandomUtils::generateRandomInt(1, MAX_RANDOM_SIZE); + list_data_points.reserve(random_size); + for (int i = 0; i < random_size; ++i) { list_data_points.push_back( - DataPointsUtils::generateRandomKey(RandomUtils::generateRandomInt(1, 5))); + DataPointsUtils::generateRandomKey(RandomUtils::generateRandomInt(1, MAX_RANDOM_SIZE))); } // Act - GetMessage get_message = bo_service_.createGetMessage(OBJECT_ID, SCHEMA_TYPE, list_data_points); + GetMessage get_message = BoService::createGetMessage(OBJECT_ID, SCHEMA_TYPE, list_data_points); std::cout << "List of data points:\n"; - for (const auto& data_point : list_data_points) { - std::cout << data_point << std::endl; + for (const auto &data_point : list_data_points) { + std::cout << data_point << "\n"; } - std::cout << "GetMessage created:\n" << get_message << std::endl; + std::cout << "GetMessage created:\n" << get_message << "\n"; // Assert - ASSERT_EQ(get_message.getHeader().getId(), OBJECT_ID) << "Object ID mismatch"; + ASSERT_EQ(get_message.getHeader().getInstance(), OBJECT_ID) << "Object ID mismatch"; ASSERT_EQ(get_message.getHeader().getSchemaType(), SCHEMA_TYPE) << "Schema type mismatch"; ASSERT_EQ(get_message.getNodes().size(), list_data_points.size()) << "Number of nodes mismatch"; for (size_t i = 0; i < list_data_points.size(); ++i) { @@ -67,8 +135,8 @@ TEST_F(BoServiceIntegrationTest, CreateGetMessage) { << "Any value should be included in the Get Message at index " << i; ASSERT_FALSE(get_message.getNodes()[i].getMetadata().getGenerated().has_value()) << "Generated time should be empty at index " << i; - // This uses the default value of the Metadata class, which is set if the value is not - // provided + // This uses the default value of the Metadata class, which is set if the + // value is not provided ASSERT_TRUE( get_message.getNodes()[i].getMetadata().getReceived().time_since_epoch().count() > 0) << "Received time should not be empty at index " << i; @@ -87,64 +155,70 @@ TEST_F(BoServiceIntegrationTest, CreateSetMessage) { object_id_map[SCHEMA_TYPE] = OBJECT_ID; nlohmann::json json_data = nlohmann::json::array(); - for (int i = 0; i < RandomUtils::generateRandomInt(1, 5); ++i) { + for (int i = 0; i < RandomUtils::generateRandomInt(1, MAX_RANDOM_SIZE); ++i) { std::unordered_map> - data_points = - DataPointsUtils::generateDataPointsWithValues(RandomUtils::generateRandomInt(1, 5)); + data_points = DataPointsUtils::generateDataPointsWithValues( + RandomUtils::generateRandomInt(1, MAX_RANDOM_SIZE)); nlohmann::json vehicle_data; - for (const auto& [key, value] : data_points) { + for (const auto &[key, value] : data_points) { vehicle_data[key] = Helper::variantToString(value); } json_data.push_back({{"Vehicle", vehicle_data}}); } + // Populate the origin system name if it exists + const std::string origin_system_name = "test_reasoner"; + // Act - std::vector set_messages = bo_service_.createSetMessage(object_id_map, json_data); + std::vector set_messages = + BoService::createSetMessage(object_id_map, json_data, origin_system_name); // Assert ASSERT_EQ(set_messages.size(), json_data.size()) << "Number of SetMessages mismatch"; for (size_t i = 0; i < json_data.size(); ++i) { - ASSERT_EQ(set_messages[i].getHeader().getId(), OBJECT_ID) + ASSERT_EQ(set_messages[i].getHeader().getInstance(), OBJECT_ID) << "Object ID mismatch at index " << i; ASSERT_EQ(set_messages[i].getHeader().getSchemaType(), SCHEMA_TYPE) << "Schema type mismatch at index " << i; std::cout << "Received JSON data[" << i << "]:\n" << json_data[i].dump(4) << "\n" - << std::endl; - std::cout << "Generated SetMessage[" << i << "]:\n" << set_messages[i] << "\n" << std::endl; + << "\n"; + std::cout << "Generated SetMessage[" << i << "]:\n" + << set_messages[i] << "\n" + << "\n"; ASSERT_EQ(set_messages[i].getNodes().size(), json_data[i]["Vehicle"].size()) << "Node size mismatch at index " << i; - const auto& vehicle_data = json_data[i]["Vehicle"]; - size_t j = 0; - for (const auto& [key, value] : vehicle_data.items()) { - ASSERT_EQ(set_messages[i].getNodes()[j].getName(), key) - << "Node name mismatch at index " << i << ", node " << j; - ASSERT_EQ(set_messages[i].getNodes()[j].getValue().value(), value.dump()) + const auto &vehicle_data = json_data[i]["Vehicle"]; + size_t index = 0; + for (const auto &[key, value] : vehicle_data.items()) { + ASSERT_EQ(set_messages[i].getNodes()[index].getName(), key) + << "Node name mismatch at index " << i << ", node " << index; + ASSERT_EQ(set_messages[i].getNodes()[index].getValue().value(), value.dump()) << "Node value mismatch at index " << i - << ", SetMessage value: " << set_messages[i].getNodes()[j].getValue().value() - << ", JSON: " << value << std::endl; + << ", SetMessage value: " << set_messages[i].getNodes()[index].getValue().value() + << ", JSON: " << value << "\n"; ASSERT_TRUE(set_messages[i] - .getNodes()[j] + .getNodes()[index] .getMetadata() .getGenerated() .value() .time_since_epoch() .count() > 0) - << "Generated time should not be empty at index " << i << ", node " << j; - // This uses the default value of the Metadata class, which is set if the value - // is not provided + << "Generated time should not be empty at index " << i << ", node " << index; + // This uses the default value of the Metadata class, which is set if the + // value is not provided ASSERT_TRUE(set_messages[i] - .getNodes()[j] + .getNodes()[index] .getMetadata() .getReceived() .time_since_epoch() .count() > 0) - << "Received time should not be empty at index " << i << ", node " << j; + << "Received time should not be empty at index " << i << ", node " << index; // } - ++j; + ++index; } } } \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/websocket-client/services/tests/bo_to_dto_integration_test.cpp b/cdsp/knowledge-layer/connector/websocket-client/services/tests/bo_to_dto_integration_test.cpp deleted file mode 100644 index 13e120cc..00000000 --- a/cdsp/knowledge-layer/connector/websocket-client/services/tests/bo_to_dto_integration_test.cpp +++ /dev/null @@ -1,211 +0,0 @@ -#include - -#include "bo_to_dto.h" -#include "data_points_utils.h" -#include "data_types.h" -#include "get_message.h" -#include "get_message_dto.h" -#include "nlohmann/json.hpp" -#include "node.h" -#include "random_utils.h" -#include "set_message.h" -#include "set_message_dto.h" -#include "subscribe_message.h" -#include "subscribe_message_dto.h" -#include "vin_utils.h" - -class BoToDtoIntegrationTest : public ::testing::Test { - protected: - BoToDto bo_to_dto_; -}; - -/** - * @brief Generates an optional timestamp. - * - * This function returns an optional timestamp. It uses a random boolean generator - * to decide whether to return a valid timestamp or an empty optional. - * - * @return std::optional - * An optional containing a randomly generated timestamp if the random - * boolean is true, or std::nullopt if false. - */ -std::optional getOptionalTimestamp( - const bool required_generated_timestamp = false) { - if (required_generated_timestamp) { - return std::optional( - RandomUtils::generateRandomTimestamp(2020, 2025, true)); - } else - return RandomUtils::generateRandomBool() - ? std::optional( - RandomUtils::generateRandomTimestamp()) - : std::nullopt; -} - -/** - * Generates a vector of optional Node objects based on the specified number of points and schema. - * - * @param num_points The number of data points to generate. - * @param schema The schema to be used for naming the nodes. - * @param add_nodes_values A boolean flag indicating whether to include node values in the Node - * objects. Defaults to false. - * @return A vector of Node objects, each representing an optional data point with associated - * metadata. - */ -std::vector getOptionalNodes(const int& num_points, const std::string& schema, - const bool add_nodes_values = false, - const bool required_generated_timestamp = false) { - std::vector random_bo_nodes; - auto random_nodes = DataPointsUtils::generateDataPointsWithValues(num_points); - std::vector supported_data_points; - - // Add the supported data points to the list - for (const auto& [node_name, _] : random_nodes) { - supported_data_points.push_back(schema + "." + node_name); - } - - for (const auto& [node_name, value] : random_nodes) { - Metadata random_metadata(getOptionalTimestamp(), getOptionalTimestamp(true)); - std::optional node_value = std::nullopt; - if (add_nodes_values) { - node_value = std::optional(Helper::variantToString(value)); - } - Node node(schema + "." + node_name, node_value, random_metadata, supported_data_points); - random_bo_nodes.push_back(node); - } - return random_bo_nodes; -} - -/** - * @brief Test case for converting a GetMessage business object to a GetMessageDTO. - * - * This test verifies the conversion of a GetMessage business object to a GetMessageDTO. - * It checks the integrity and correctness of the conversion process by comparing the fields - * of the converted GetMessageDTO with the original business object. - */ -TEST_F(BoToDtoIntegrationTest, ConvertGetMessageToDto) { - // Arrange - int num_points = RandomUtils::generateRandomInt(0, 5); - auto random_id = VinUtils::getRandomVinString(); - auto schema = schemaTypeToString(SchemaType::VEHICLE, true); - auto random_bo_nodes = getOptionalNodes(num_points, schema); - - GetMessage bo(MessageHeader(random_id, SchemaType::VEHICLE), random_bo_nodes); - std::cout << "GetMessage to convert:\n" << bo << std::endl; - - // Act - std::vector dtos = bo_to_dto_.convert(bo); - std::cout << "Converted JSON GetMessageDTO:\n" << nlohmann::json(dtos) << std::endl; - - // Assert - int expected_num_points = num_points == 0 ? 1 : num_points; - ASSERT_EQ(dtos.size(), expected_num_points); - for (size_t i = 0; i < expected_num_points; ++i) { - GetMessageDTO dto = dtos[i]; - ASSERT_EQ(dto.type, "get"); - ASSERT_EQ(dto.schema, schemaTypeToString(bo.getHeader().getSchemaType(), true)); - ASSERT_EQ(dto.instance, bo.getHeader().getId()); - ASSERT_EQ(dto.path, num_points == 0 - ? std::nullopt - : std::optional(bo.getNodes()[i].getName().substr( - schema.size() + 1))); // Schema is absent from path - ASSERT_EQ(dto.format, "flat"); // Default format for GetMessageDTO - } -} - -/** - * @brief Test case for converting a SubscribeMessage business object to a vector of - * SubscribeMessageDTO. - * - * This test verifies the conversion of a SubscribeMessage business object to a vector of - * SubscribeMessageDTO. It checks the integrity and correctness of the conversion process by - * comparing the fields of the converted SubscribeMessageDTO with the original business object. - */ -TEST_F(BoToDtoIntegrationTest, ConvertSubscribeMessageToDto) { - // Arrange - int num_points = RandomUtils::generateRandomInt(0, 5); - auto random_id = VinUtils::getRandomVinString(); - auto schema = schemaTypeToString(SchemaType::VEHICLE, true); - auto random_bo_nodes = getOptionalNodes(num_points, schema); - - SubscribeMessage bo(MessageHeader(random_id, SchemaType::VEHICLE), random_bo_nodes); - std::cout << "SubscribeMessage to convert:\n" << bo << std::endl; - - // Act - std::vector dtos = bo_to_dto_.convert(bo); - std::cout << "Converted JSON SubscribeMessageDTO:\n" << nlohmann::json(dtos) << std::endl; - - // Assert - int expected_num_points = num_points == 0 ? 1 : num_points; - ASSERT_EQ(dtos.size(), expected_num_points); - for (size_t i = 0; i < expected_num_points; ++i) { - SubscribeMessageDTO dto = dtos[i]; - ASSERT_EQ(dto.type, "subscribe"); - ASSERT_EQ(dto.schema, schemaTypeToString(bo.getHeader().getSchemaType(), true)); - ASSERT_EQ(dto.instance, bo.getHeader().getId()); - ASSERT_EQ(dto.path, num_points == 0 - ? std::nullopt - : std::optional(bo.getNodes()[i].getName())); - ASSERT_EQ(dto.format, "flat"); // Default format for SubscribeMessageDTO - } -} - -/** - * @brief Test case for converting a SetMessage business object to a SetMessageDTO. - * - * This test verifies the conversion of a SetMessage business object to a SetMessageDTO. - * It checks the integrity and correctness of the conversion process by comparing the fields - * of the converted SetMessageDTO with the original business object. - */ -TEST_F(BoToDtoIntegrationTest, ConvertSetMessageToDto) { - // Arrange - int num_points = RandomUtils::generateRandomInt(1, 5); - auto random_id = VinUtils::getRandomVinString(); - auto schema = schemaTypeToString(SchemaType::VEHICLE, true); - auto random_bo_nodes = getOptionalNodes(num_points, schema, true, true); - - SetMessage bo(MessageHeader(random_id, SchemaType::VEHICLE), random_bo_nodes); - std::cout << "SetMessage to convert:\n" << bo << std::endl; - - // Act - SetMessageDTO dto = bo_to_dto_.convert(bo); - std::cout << "Converted JSON SetMessageDTO:\n" << nlohmann::json(dto) << std::endl; - - // Assert - ASSERT_EQ(dto.schema, schemaTypeToString(bo.getHeader().getSchemaType(), true)); - ASSERT_EQ(dto.instance, bo.getHeader().getId()); - ASSERT_EQ(dto.data.size(), bo.getNodes().size()); - const auto& nodes = bo.getNodes(); - for (size_t i = 0; i < dto.data.size(); ++i) { - const DataDTO& data = dto.data[i]; - const Node& node = nodes[i]; - - ASSERT_EQ(data.name, node.getName()) << "Mismatch in node name at index " << i; - auto [seconds, nanos] = - Helper::getSecondsAndNanosecondsSinceEpoch(node.getMetadata().getGenerated().value()); - - MetadataDTO::NodeMetadata metadata{.generated = {nanos, seconds}}; - ASSERT_EQ(dto.metadata.nodes[data.name].generated.seconds, seconds) - << "Mismatch in generated seconds for node: " << node.getName(); - - ASSERT_EQ(dto.metadata.nodes[data.name].generated.nanos, nanos) - << "Mismatch in generated nanoseconds for node: " << node.getName(); - ASSERT_EQ(dto.metadata.nodes[data.name].received.seconds, 0) // This should not be set - << "Mismatch in received seconds for node: " << node.getName(); - ASSERT_EQ(dto.metadata.nodes[data.name].received.nanos, 0) // This should not be set - << "Mismatch in received nanoseconds for node: " << node.getName(); - if (node.getValue()) { - try { - auto expected_json = nlohmann::json::parse(*node.getValue()); - ASSERT_EQ(data.value, expected_json) - << "Parsed value mismatch for node: " << node.getName(); - } catch (const std::exception&) { - ASSERT_TRUE(data.value.is_string()) << "Expected fallback to string value"; - ASSERT_EQ(data.value, *node.getValue()) - << "Fallback value mismatch for node: " << node.getName(); - } - } else { - ASSERT_TRUE(data.value.is_string()) << "Expected empty string for nullopt value"; - ASSERT_EQ(data.value, "") << "Expected empty string for node with no value"; - } - } -} \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/websocket-client/services/tests/data_message_dto_service_unit_test.cpp b/cdsp/knowledge-layer/connector/websocket-client/services/tests/data_message_dto_service_unit_test.cpp deleted file mode 100644 index 48cdfb4e..00000000 --- a/cdsp/knowledge-layer/connector/websocket-client/services/tests/data_message_dto_service_unit_test.cpp +++ /dev/null @@ -1,242 +0,0 @@ -#include - -#include -#include -#include -#include -#include -#include - -#include "data_points_utils.h" -#include "dto_service.h" -#include "helper.h" -#include "metadata_dto.h" -#include "random_utils.h" -#include "test_helper.h" -#include "vin_utils.h" - -class ModelConfigDtoServiceUnitTest - : public ::testing::Test, - public ::testing::WithParamInterface> { - protected: - DtoService dto_service_; -}; - -INSTANTIATE_TEST_SUITE_P( - DataStructureVariants, ModelConfigDtoServiceUnitTest, - ::testing::Values(std::make_pair(TestHelper::DataStructure::Flat, "FlatNodes"), - std::make_pair(TestHelper::DataStructure::Nested, "NestedNodes"), - std::make_pair(TestHelper::DataStructure::Leaf, "LeafNodes")), - [](const ::testing::TestParamInfo>& info) { - return info.param.second; // test name - }); - -/** - * @brief Generates a JSON object representing metadata tags for nodes. - * - * This function takes a map of node metadata and converts it into a JSON object. - * Each node in the map is represented as a key in the JSON object, and its value - * is another JSON object containing timestamp information. - * - * @param nodes_metadata A map where each key is a node identifier (string) and - * the value is another map. This inner map's key is a - * timestamp (string), and its value is a pair of integers - * representing seconds and nanoseconds. - * @return nlohmann::json A JSON object where each key is a node identifier and - * its value is a JSON object containing timestamp data. - */ -nlohmann::json generateMetadataTag( - const std::map>>& - nodes_metadata) { - nlohmann::json json_metadata; - for (const auto& [node, timestamps] : nodes_metadata) { - nlohmann::json node_metadata; - if (!timestamps.empty()) { - nlohmann::json timestamps_json; - for (const auto& [timestamp, values] : timestamps) { - timestamps_json[timestamp] = {{"seconds", values.first}, {"nanos", values.second}}; - } - node_metadata["timestamps"] = timestamps_json; - } - json_metadata[node] = node_metadata; - } - return json_metadata; -} - -nlohmann::json generateValidDataMessageJson() { - return { - {"type", "data"}, - {"schema", "Vehicle"}, - {"instance", "1234"}, - {"data", {{"Speed", 120}}}, - {"metadata", - {{"Speed", - {{"timestamps", {{"generated", {{"seconds", 1234567890}, {"nanos", 123456789}}}}}}}}}}; -} - -// Tests for parsing DataMessageDTO - -/** - * @brief Test case for parsing a valid DataMessage JSON. - * - * This test verifies the parsing of a valid DataMessage JSON object into a DataMessageDTO. - * It checks the integrity and correctness of the parsing process by comparing the fields - * of the parsed DataMessageDTO with the original JSON object. - */ -TEST_P(ModelConfigDtoServiceUnitTest, ParseJsonDataMessage) { - auto data_structure_type = GetParam().first; - // Arrange - int num_points = RandomUtils::generateRandomInt(1, 5); - auto random_nodes = DataPointsUtils::generateDataPointsWithValues( - data_structure_type == TestHelper::DataStructure::Leaf ? 1 : num_points); - auto random_path = TestHelper::generatePathTag(data_structure_type); - auto random_request_id = TestHelper::generateRequestIdTag(); - auto random_vin = VinUtils::getRandomVinString(); - auto random_json_data = TestHelper::generateDataTag(random_nodes, data_structure_type); - std::map>> random_nodes_metadata; - if (data_structure_type == TestHelper::DataStructure::Leaf) { - random_nodes_metadata.insert({random_path.value(), TestHelper::generateMetadata()}); - } else { - for (const auto& [node, _] : random_nodes) { - random_nodes_metadata.insert({node, TestHelper::generateMetadata()}); - } - } - - // Create dynamic incoming JSON message - nlohmann::json json_message = {{"type", "data"}, - {"schema", "Vehicle"}, - {"instance", random_vin}, - {"data", random_json_data}}; - - if (random_path.has_value()) { - json_message["path"] = random_path.value(); - } - if (!random_nodes_metadata.empty()) { - json_message["metadata"] = generateMetadataTag(random_nodes_metadata); - } - if (random_request_id.has_value()) { - json_message["requestId"] = random_request_id.value(); - } - std::cout << "Incoming message: \n" << json_message.dump(4) << std::endl; - - // Act - DataMessageDTO dto = dto_service_.parseDataJsonToDto(json_message); - std::cout << "Parsed DataMessageDTO: \n" << dto << std::endl; - - // Assert - ASSERT_EQ(dto.type, "data"); - ASSERT_EQ(dto.schema, "Vehicle"); - ASSERT_EQ(dto.instance, random_vin); - ASSERT_EQ(dto.data, random_json_data); - ASSERT_EQ(dto.path, random_path); - ASSERT_EQ(dto.requestId, random_request_id); - ASSERT_EQ(dto.metadata.has_value(), !random_nodes_metadata.empty()); - - if (dto.metadata.has_value()) { - ASSERT_EQ(dto.metadata.value().nodes.size(), random_nodes_metadata.size()); - for (const auto& [node, timestamps] : dto.metadata.value().nodes) { - if (random_nodes_metadata.find(node) != random_nodes_metadata.end()) { - if (random_nodes_metadata[node].find("generated") != - random_nodes_metadata[node].end()) { - ASSERT_EQ(timestamps.generated.seconds, - random_nodes_metadata[node]["generated"].first); - ASSERT_EQ(timestamps.generated.nanos, - random_nodes_metadata[node]["generated"].second); - } - if (random_nodes_metadata[node].find("received") != - random_nodes_metadata[node].end()) { - ASSERT_EQ(timestamps.received.seconds, - random_nodes_metadata[node]["received"].first); - ASSERT_EQ(timestamps.received.nanos, - random_nodes_metadata[node]["received"].second); - } - } - } - } -} - -/** - * @brief Unit test for the DtoService class to verify exception handling. - * - * This test case checks that the `parseDataJsonToDto` method of the `dto_service_` object - * throws an `std::invalid_argument` exception when any of the required fields are missing - * from the JSON message. The required fields are "type", "schema", "instance", and "data". - * - * The test iterates over each required field, removes it from a valid JSON message, - * and asserts that an exception is thrown when the modified message is parsed. - */ -TEST_F(ModelConfigDtoServiceUnitTest, ParseDataDtoThrowsExceptionWhenRequiredFieldsAreMissing) { - // Define the complete JSON message - nlohmann::json json_message = generateValidDataMessageJson(); - - // List of required fields in the JSON message - std::vector required_fields = {"type", "schema", "instance", "data"}; - - // Iterate over each required field - for (const auto& field : required_fields) { - nlohmann::json test_message = json_message; - test_message.erase(field); // Remove the required field - - // Output the current test case details - std::cout << "\nTesting with missing field: " << field << "\n"; - std::cout << test_message.dump(4) << std::endl; - - // Act & Assert: Ensure exception is thrown when a required field is missing - ASSERT_THROW(dto_service_.parseDataJsonToDto(test_message), std::invalid_argument); - } -} - -/** - * @brief Unit test for the DtoService class to verify exception handling for incomplete metadata. - * - * This test case checks that the `parseDataJsonToDto` method of the `dto_service_` object - * throws an `std::invalid_argument` exception when any of the required fields in the metadata - * are missing from the JSON message. The required fields within the "generated" timestamps - * are "seconds" and "nanos". - * - * The test iterates over each required field, removes it from a valid JSON message's metadata, - * and asserts that an exception is thrown when the modified message is parsed. - */ -TEST_F(ModelConfigDtoServiceUnitTest, ParseDataDtoThrowsExceptionWhenMetadataIsIncomplete) { - // Define the complete JSON message - nlohmann::json json_message = generateValidDataMessageJson(); - - // List of required fields in the metadata's "generated" timestamps - std::vector required_fields = {"seconds", "nanos"}; - - // Iterate over each required field - for (const auto& field : required_fields) { - nlohmann::json test_message = json_message; - test_message["metadata"]["Speed"]["timestamps"]["generated"].erase( - field); // Remove the required field - - // Output the current test case details - std::cout << "\nTesting with missing field: " << field << "\n"; - std::cout << test_message.dump(4) << std::endl; - - // Act & Assert: Ensure exception is thrown when a required field is missing - ASSERT_THROW(dto_service_.parseDataJsonToDto(test_message), std::invalid_argument); - } -} - -/** - * @brief Unit test for DtoService to verify that an exception is thrown when the path is missing in - * the JSON message and data is not an object. - * - * This test constructs a JSON message with missing path information and verifies that the - * parseDataJsonToDto function throws an std::invalid_argument exception when data is not an object. - * The JSON message includes a type, schema, instance, and data, but lacks the necessary path field, - * which should trigger the exception. - */ -TEST_F(ModelConfigDtoServiceUnitTest, ParseDataDtoThrowsExceptionWhenPathIsMissing) { - // Define the complete JSON message - nlohmann::json json_message = { - {"type", "data"}, {"schema", "Vehicle"}, {"instance", "1234"}, {"data", 120} - // Data is not an object - }; - - std::cout << "\nTesting with missing path\n" << json_message.dump(4) << std::endl; - - // Act & Assert: Ensure exception is thrown when path is missing - ASSERT_THROW(dto_service_.parseDataJsonToDto(json_message), std::invalid_argument); -} diff --git a/cdsp/knowledge-layer/connector/websocket-client/services/tests/dto_to_data_message_integration_test.cpp b/cdsp/knowledge-layer/connector/websocket-client/services/tests/dto_to_data_message_integration_test.cpp deleted file mode 100644 index 84ce8193..00000000 --- a/cdsp/knowledge-layer/connector/websocket-client/services/tests/dto_to_data_message_integration_test.cpp +++ /dev/null @@ -1,429 +0,0 @@ -#include - -#include - -#include "data_points_utils.h" -#include "dto_to_bo.h" -#include "helper.h" -#include "random_utils.h" -#include "test_helper.h" -#include "vin_utils.h" -class DtoToDataMessageIntegrationTest - : public ::testing::Test, - public ::testing::WithParamInterface> { - protected: - DtoToBo dto_to_bo_; - std::chrono::system_clock::time_point test_execution_start_point_; - const SchemaType SCHEMA_TYPE = SchemaType::VEHICLE; - - void SetUp() override { test_execution_start_point_ = std::chrono::system_clock::now(); } -}; - -INSTANTIATE_TEST_SUITE_P( - DataStructureVariants, DtoToDataMessageIntegrationTest, - ::testing::Values(std::make_pair(TestHelper::DataStructure::Flat, "FlatNodes"), - std::make_pair(TestHelper::DataStructure::Nested, "NestedNodes"), - std::make_pair(TestHelper::DataStructure::Leaf, "LeafNodes")), - [](const ::testing::TestParamInfo>& info) { - return info.param.second; // test name - }); - -/** - * @brief Generates a MetadataDTO object from a map of node metadata. - * - * This function processes a map containing metadata for various nodes, where each node - * is associated with a map of timestamp pairs. It extracts the "generated" and "received" - * timestamps for each node and constructs a MetadataDTO object. If no nodes are present - * in the input map, the function returns an empty optional. - * - * @param nodes_metadata A map where the key is a node identifier (string) and the value - * is another map. This inner map has keys as tags ("generated" or "received") and values - * as pairs of integers representing seconds and nanos. - * - * @return An optional MetadataDTO object containing the processed metadata. Returns - * std::nullopt if no nodes are present in the input map. - */ -std::optional generateMetadataDTO( - const std::map>>& - nodes_metadata) { - MetadataDTO metadata_dto; - for (const auto& [node, timestamps] : nodes_metadata) { - MetadataDTO::NodeMetadata node_metadata; - for (const auto& [tag, timestamp] : timestamps) { - if (tag == "generated") { - node_metadata.generated.seconds = timestamp.first; - node_metadata.generated.nanos = timestamp.second; - } else if (tag == "received") { - node_metadata.received.seconds = timestamp.first; - node_metadata.received.nanos = timestamp.second; - } - } - metadata_dto.nodes[node] = node_metadata; - } - if (metadata_dto.nodes.empty()) { - return std::nullopt; - } - return metadata_dto; -} - -/** - * @brief Assembles and returns a pair consisting of a DataMessageDTO and a map of system data - * points. - * - * This function creates a DataMessageDTO object with predefined values for type, schema, instance, - * and data. It also initializes a MetadataDTO object for the metadata field. Additionally, it - * constructs a map of system data points, associating a vector of strings with a SchemaType key. - * The function returns these two objects as a pair. - * - * @return A std::pair containing: - * - A DataMessageDTO object with initialized fields. - * - A std::map where the key is of type SchemaType and the value is a vector of strings - * representing system data points. - */ -std::pair>> -assembleValidFunctionArgs() { - DataMessageDTO dto; - dto.type = "data"; - dto.schema = "Vehicle"; - dto.instance = "sampleVin"; - dto.data = {{"node_name", "value"}}; - dto.metadata = MetadataDTO(); - - // Initialize system data points map - std::map> system_data_points; - system_data_points[SchemaType::VEHICLE].push_back("Vehicle.node_name"); - - // Return the assembled DataMessageDTO and supported data points - return {dto, system_data_points}; -} - -// Test for converting DataMessageDTO to DataMessage - -/** - * @brief Test case for converting a DataMessageDTO to a DataMessage business object. - * - * This test verifies the conversion of a DataMessageDTO to a DataMessage business object (BO). - * It checks the integrity and correctness of the conversion process by comparing the fields - * and metadata of the resulting BO with the expected values derived from the DTO. - */ -TEST_P(DtoToDataMessageIntegrationTest, ConvertDataMessageDTOToBo) { - // Arrange - auto data_structure_type = GetParam().first; - - int num_points = RandomUtils::generateRandomInt(1, 5); - auto random_nodes = DataPointsUtils::generateDataPointsWithValues( - data_structure_type == TestHelper::DataStructure::Leaf ? 1 : num_points); - auto random_json_data = TestHelper::generateDataTag(random_nodes, data_structure_type); - auto random_path = TestHelper::generatePathTag(data_structure_type); - auto random_vin = VinUtils::getRandomVinString(); - - std::map>> random_nodes_metadata; - std::map> random_system_data_points; - - if (data_structure_type == TestHelper::DataStructure::Leaf) { - random_nodes_metadata.insert({random_path.value(), TestHelper::generateMetadata()}); - const auto supported_node = - schemaTypeToString(SCHEMA_TYPE, true) + "." + random_path.value(); - - random_system_data_points[SCHEMA_TYPE].push_back(supported_node); - } else { - for (const auto& [node, _] : random_nodes) { - random_nodes_metadata.insert({node, TestHelper::generateMetadata()}); - const auto supported_node = schemaTypeToString(SCHEMA_TYPE, true) + "." + - (random_path.has_value() ? random_path.value() + "." : "") + - node; - - random_system_data_points[SCHEMA_TYPE].push_back(supported_node); - } - } - auto random_request_id = TestHelper::generateRequestIdTag(); - - // Create dynamic DataMessageDTO - DataMessageDTO dto; - dto.type = "data"; - dto.schema = schemaTypeToString(SCHEMA_TYPE, true); - dto.instance = random_vin; - dto.data = random_json_data; - dto.path = random_path; - dto.metadata = generateMetadataDTO(random_nodes_metadata); - dto.requestId = random_request_id; - - std::cout << "DataMessageDTO to parse:\n" << dto << std::endl; - - // Act - DataMessage bo = dto_to_bo_.convert(dto, random_system_data_points); - std::cout << "DataMessage parsed:\n" << bo << std::endl; - - // Assert - ASSERT_EQ(bo.getHeader().getId(), dto.instance); - ASSERT_EQ(bo.getHeader().getSchemaType(), SchemaType::VEHICLE); - ASSERT_EQ(bo.getNodes().size(), random_nodes.size()); - for (const auto& bo_node : bo.getNodes()) { - const auto bo_node_name = bo_node.getName(); - const auto bo_node_value = bo_node.getValue().value(); - std::string random_metadata_node_name; - if (data_structure_type == TestHelper::DataStructure::Leaf) { - random_metadata_node_name = dto.path.value(); - ASSERT_EQ(bo_node_name, schemaTypeToString(SCHEMA_TYPE, true) + "." + dto.path.value()); - ASSERT_EQ(bo_node_value, Helper::jsonToString(dto.data)); - } else { - random_metadata_node_name = dto.path.has_value() ? dto.path.value() + "." : ""; - std::string expected_random_node_name = bo_node_name.substr( - (schemaTypeToString(SCHEMA_TYPE, true) + "." + random_metadata_node_name).size()); - - ASSERT_TRUE(random_nodes.find(expected_random_node_name) != random_nodes.end()); - ASSERT_EQ(Helper::variantToString(random_nodes[expected_random_node_name]), - bo_node_value); - } - - std::string expected_random_metadata_node_name = - bo_node_name.substr((schemaTypeToString(SCHEMA_TYPE, true) + ".").size()); - if (dto.metadata.has_value()) { - if (dto.metadata.value().nodes.find(expected_random_metadata_node_name) != - dto.metadata.value().nodes.end()) { - auto dto_time = dto.metadata.value().nodes.at(expected_random_metadata_node_name); - if (dto_time.generated.seconds != 0 || dto_time.generated.nanos != 0) { - auto timestamp_from_dto = Helper::convertToTimestamp(dto_time.generated.seconds, - dto_time.generated.nanos); - - ASSERT_EQ(timestamp_from_dto, bo_node.getMetadata().getGenerated()); - } else { - ASSERT_FALSE(bo_node.getMetadata().getGenerated().has_value()); - } - if (dto_time.received.seconds != 0 || dto_time.received.nanos != 0) { - auto timestamp_from_dto = Helper::convertToTimestamp(dto_time.received.seconds, - dto_time.received.nanos); - - ASSERT_EQ(bo_node.getMetadata().getReceived(), timestamp_from_dto); - } else { - // If received time is not set, it will be assigned the current time when - // the message was processed and should be after the test execution start - // point - ASSERT_TRUE(bo_node.getMetadata().getReceived() > test_execution_start_point_); - } - } - } else { - // If any time has been set, the current time when the message was processed will be - // assigned to received and it should be after the test execution start point - ASSERT_TRUE(bo_node.getMetadata().getReceived() > test_execution_start_point_); - } - } -} - -/** - * @brief Test case for converting a DataMessageDTO with a missing path and non-object data. - * - * This test verifies that attempting to convert a DataMessageDTO to a business object (BO) - * throws an invalid_argument exception when the path is missing and the data is not an object. - * It ensures that the conversion process correctly handles cases where essential fields are - * improperly formatted or missing. - */ -TEST_F(DtoToDataMessageIntegrationTest, - ConvertDataMessageDTOThrowsExceptionWhenPathIsMissingAndDataIsNotObject) { - // Define the system data points - std::map> system_data_points = { - {SCHEMA_TYPE, {"Vehicle.some_node"}}}; - - // Create a DataMessageDTO with missing path and non-object data - DataMessageDTO dto; - dto.type = "data"; // Set the type of the message - dto.schema = "Vehicle"; // Define the schema as "Vehicle" - dto.instance = "sampleVin"; // Set a sample VIN as the instance - dto.data = "some_value"; // Data must be an object if path is missing - dto.metadata = std::nullopt; // Metadata is not provided - - // Randomly decide if the path should be explicitly empty - if (RandomUtils::generateRandomBool()) { - dto.path = std::string(); // Empty path is also considered as missing - } - - // Output the DTO to be parsed - std::cout << "DataMessageDTO to parse with missing path:\n" << dto << std::endl; - - // Assert that converting the DTO throws an invalid_argument exception due to the missing path - ASSERT_THROW(dto_to_bo_.convert(dto, system_data_points), std::invalid_argument); -} - -/** - * @brief Test case for converting a DataMessageDTO with missing required fields. - * - * This test verifies that attempting to convert a DataMessageDTO to a business object (BO) - * throws an invalid_argument exception when any of the required fields are missing. - * It ensures that the conversion process correctly handles cases where essential fields are - * improperly formatted or missing. - */ -TEST_F(DtoToDataMessageIntegrationTest, - ConvertDataMessageDTOThrowsExceptionWhenRequiredFieldsAreMissing) { - // Define the complete DataMessageDTO - std::map> system_data_points = { - {SCHEMA_TYPE, {"Vehicle.some_node"}}}; - - std::vector required_fields = {"type", "schema", "instance", "data", - "supported_system_data_points"}; - - for (const auto& field : required_fields) { - auto [dto, system_data_points] = assembleValidFunctionArgs(); - if (field == "type") { - dto.type = std::string(); - } else if (field == "schema") { - dto.schema = std::string(); - } else if (field == "instance") { - dto.instance = std::string(); - } else if (field == "data") { - dto.data = nlohmann::json(); - } else if (field == "supported_system_data_points") { - system_data_points.clear(); - } - - std::cout << "\nTesting with missing field: " << field << "\n"; - std::cout << dto << std::endl; - - // Act & Assert: Ensure exception is thrown when a required field is missing - ASSERT_THROW(dto_to_bo_.convert(dto, system_data_points), std::invalid_argument); - } -} - -/** - * @brief Test case for converting a DataMessageDTO with incomplete metadata. - * - * This test verifies that attempting to convert a DataMessageDTO to a business object (BO) - * does not throw an exception when the metadata is incomplete. - * It ensures that the conversion process correctly handles cases where metadata fields are - * improperly formatted or missing. - */ -TEST_F(DtoToDataMessageIntegrationTest, ConvertDataMessageDTONotThrowExceptionWhenMetadataIsWrong) { - // Define the complete DataMessageDTO - auto [dto, system_data_points] = assembleValidFunctionArgs(); - - std::vector required_fields = {"seconds", "nanos"}; - std::string time_type = RandomUtils::generateRandomBool() ? "generated" : "received"; - - for (const auto& field : required_fields) { - dto.metadata->nodes["node_name"].generated.seconds = 0; - dto.metadata->nodes["node_name"].generated.nanos = 0; - dto.metadata->nodes["node_name"].received.seconds = 0; - dto.metadata->nodes["node_name"].received.nanos = 0; - dto.metadata->nodes["node_name"].generated.nanos = 0; - - // Set wrong field value - if (field == "seconds") { - time_type == "generated" ? dto.metadata->nodes["node_name"].generated.seconds = -1 - : dto.metadata->nodes["node_name"].received.seconds = -1; - } else if (field == "nanos") { - time_type == "generated" ? dto.metadata->nodes["node_name"].generated.nanos = -1 - : dto.metadata->nodes["node_name"].received.nanos = -1; - } - - std::cout << "\nTesting with missing field: " << field << "\n"; - std::cout << dto << std::endl; - - // Act - DataMessage bo = dto_to_bo_.convert(dto, system_data_points); - std::cout << "DataMessage parsed:\n" << bo << std::endl; - - if (time_type == "generated") { - // Assert: Ensure the generated time is not set - ASSERT_FALSE(bo.getNodes().front().getMetadata().getGenerated().has_value()); - } else { - // Assert: Ensure the received time is set to the current time - ASSERT_TRUE(bo.getNodes().front().getMetadata().getReceived() > - test_execution_start_point_); - } - } -} - -/** - * @brief Test case for converting a DataMessageDTO with an unsupported data point. - * - * This test verifies that attempting to convert a DataMessageDTO to a business object (BO) - * does log the unsupported node but does not throw an exception when a data point is not supported. - */ -TEST_F(DtoToDataMessageIntegrationTest, - ConvertDataMessageDTONotThrowsExceptionWhenADataPointIsNotSupported) { - // Define the complete DataMessageDTO - auto [dto, system_data_points] = assembleValidFunctionArgs(); - - // Add an unsupported data point to the DTO, which is not included in the system data points - dto.data["unsupported_node"] = "value"; - - // Output the DTO to be parsed - std::cout << "DataMessageDTO to parse with missing supported data points:\n" - << dto << std::endl; - - DataMessage bo = dto_to_bo_.convert(dto, system_data_points); - std::cout << "DataMessage parsed:\n" << bo << std::endl; - - // Assert: Ensure the unsupported data point is not included in the BO - ASSERT_EQ(bo.getNodes().size(), 1); // Only the supported data point should be included - ASSERT_EQ(bo.getNodes().front().getName(), "Vehicle.node_name"); - ASSERT_EQ(bo.getNodes().front().getValue().value(), "value"); -} - -/** - * @brief Test case for converting a StatusMessageDTO to a StatusMessage business object. - * - * This test verifies that the conversion from a StatusMessageDTO to a StatusMessage - * business object is performed correctly. It checks that all fields are accurately - * transferred from the DTO to the BO. - */ -TEST_F(DtoToDataMessageIntegrationTest, ConvertStatusMessageDtoToBo) { - // Arrange - int random_code = RandomUtils::generateRandomInt(100, 999); - std::string random_message = RandomUtils::generateRandomString(10); - auto random_timestamps = TestHelper::getSecondsAndNanoseconds(); - auto random_requestId = TestHelper::generateRequestIdTag(); - - // Create dynamic StatusMessageDTO - StatusMessageDTO dto; - dto.code = random_code; - dto.message = random_message; - dto.timestamp.seconds = random_timestamps.first; - dto.timestamp.nanos = random_timestamps.second; - dto.requestId = random_requestId; - - std::cout << "StatusMessageDTO to parse:\n" << dto << std::endl; - - // Act - StatusMessage bo = dto_to_bo_.convert(dto); - std::cout << "StatusMessage parsed:\n" << bo << std::endl; - - // Assert - ASSERT_EQ(bo.getCode(), dto.code); - ASSERT_EQ(bo.getMessage(), dto.message); - ASSERT_EQ(bo.getRequestId(), dto.requestId); - ASSERT_EQ(bo.getTimestamp(), - Helper::convertToTimestamp(dto.timestamp.seconds, dto.timestamp.nanos)); -} - -/** - * @brief Test case for converting a StatusMessageDTO with missing required fields. - * - * This test verifies that attempting to convert a StatusMessageDTO to a business object (BO) - * throws an invalid_argument exception when any of the required fields are missing. - */ -TEST_F(DtoToDataMessageIntegrationTest, - ConvertStatusMessageDtoThrowsExceptionWhenRequiredFieldsAreMissing) { - std::vector required_fields = {"message", "timestamp"}; - - for (const auto& field : required_fields) { - // Define the complete StatusMessageDTO - StatusMessageDTO dto; - dto.code = 200; - dto.message = "OK"; - dto.timestamp.seconds = 1234567890; - dto.timestamp.nanos = 123456789; - - if (field == "message") { - dto.message = std::string(); - } else if (field == "timestamp") { - dto.timestamp.seconds = 0; - dto.timestamp.nanos = 0; - } - - std::cout << "\nTesting with missing field: " << field << "\n"; - std::cout << dto << std::endl; - - // Act & Assert: Ensure exception is thrown when a required field is missing - ASSERT_THROW(dto_to_bo_.convert(dto), std::invalid_argument); - } -} \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/websocket-client/services/tests/dto_to_model_config_integration_test.cpp b/cdsp/knowledge-layer/connector/websocket-client/services/tests/dto_to_model_config_integration_test.cpp index dbeb92fd..ba78a22a 100644 --- a/cdsp/knowledge-layer/connector/websocket-client/services/tests/dto_to_model_config_integration_test.cpp +++ b/cdsp/knowledge-layer/connector/websocket-client/services/tests/dto_to_model_config_integration_test.cpp @@ -2,6 +2,7 @@ #include #include "data_points_utils.h" +#include "data_types.h" #include "dto_to_bo.h" #include "globals.h" #include "mock_i_file_handler.h" @@ -10,6 +11,7 @@ #include "vin_utils.h" class DtoToModelConfigIntegrationTest : public ::testing::Test { + // NOLINTBEGIN(cppcoreguidelines-non-private-member-variables-in-classes) protected: std::shared_ptr mock_i_file_handler_; std::unique_ptr dto_to_bo_; @@ -26,7 +28,9 @@ class DtoToModelConfigIntegrationTest : public ::testing::Test { const std::string MODEL_CONFIG_PATH = getProjectRoot() + "/symbolic-reasoner/examples/use-case/model/"; + static constexpr int MAX_RANDOM_POINTS = 5; + // NOLINTEND(cppcoreguidelines-non-private-member-variables-in-classes) void SetUp() override { // ** Initialize Main Services ** setenv("VEHICLE_OBJECT_ID", VinUtils::getRandomVinString().c_str(), 1); @@ -46,39 +50,41 @@ class DtoToModelConfigIntegrationTest : public ::testing::Test { * one randomly selected pair from the list. The selection is made using a * random index generated within the bounds of the list size. * - * @param types_list_to_use A vector containing pairs of strings, where each pair - * consists of a type and its corresponding file extension. + * @param types_list_to_use A vector containing pairs of strings, where each + * pair consists of a type and its corresponding file extension. * - * @return std::pair A randomly selected pair from the input list, - * containing a type and its corresponding file extension. + * @return std::pair A randomly selected pair from the + * input list, containing a type and its corresponding file extension. */ std::pair getRandomTypeAndFileExtension( std::vector> types_list_to_use) { - int random_index = RandomUtils::generateRandomInt(0, types_list_to_use.size() - 1); + int random_index = + RandomUtils::generateRandomInt(0, static_cast(types_list_to_use.size() - 1)); return types_list_to_use[random_index]; } /** * Generates a random list of file types and file paths. * - * This function creates a vector of pairs, where each pair consists of a file type - * and a randomly generated file path with an appropriate extension. The number of - * files generated is random, ranging from 1 to the specified maximum number of files. + * This function creates a vector of pairs, where each pair consists of a file + * type and a randomly generated file path with an appropriate extension. The + * number of files generated is random, ranging from 1 to the specified maximum + * number of files. * - * @param type_list_to_use A vector of pairs, where each pair contains a file type - * and its corresponding file extension. + * @param type_list_to_use A vector of pairs, where each pair contains a file + * type and its corresponding file extension. * @param max_files The maximum number of files to generate. Defaults to 5. - * @return A vector of pairs, where each pair contains a file type and a randomly - * generated file path with the corresponding file extension. + * @return A vector of pairs, where each pair contains a file type and a + * randomly generated file path with the corresponding file extension. */ std::vector> generateRandomListOfTypeAndFilePaths( - std::vector> type_list_to_use, int max_files = 5) { + const std::vector> &type_list_to_use, int max_files = 5) { int num_files = RandomUtils::generateRandomInt(1, max_files); std::vector> file_paths; for (int i = 0; i < num_files; i++) { auto rdf_extension = getRandomTypeAndFileExtension(type_list_to_use); - file_paths.push_back({rdf_extension.first, - RandomUtils::generateRandomString() + "." + rdf_extension.second}); + file_paths.emplace_back(rdf_extension.first, + RandomUtils::generateRandomString() + "." + rdf_extension.second); } return file_paths; } @@ -101,23 +107,26 @@ ModelConfigDTO createValidDto() { } /** - * @brief Test case for converting a ModelConfigDTO to a ModelConfig business object. + * @brief Test case for converting a ModelConfigDTO to a ModelConfig business + * object. * * This test verifies that the conversion from a ModelConfigDTO to a ModelConfig - * business object is performed correctly. It checks that all fields are accurately - * transferred from the DTO to the BO. + * business object is performed correctly. It checks that all fields are + * accurately transferred from the DTO to the BO. */ TEST_F(DtoToModelConfigIntegrationTest, ConvertModelConfigDtoToBo) { // Arrange // Generate model config inputs std::map dto_random_inputs; - std::map> expected_bo_inputs; - for (const auto& schema : SCHEMAS_IN_TEST) { + std::map expected_bo_inputs; + for (const auto &schema : SCHEMAS_IN_TEST) { std::string input_path = RandomUtils::generateRandomString() + ".txt"; std::string return_from_file; - for (int i = 0; i < RandomUtils::generateRandomInt(1, 5); i++) { - expected_bo_inputs[schema].push_back(DataPointsUtils::generateRandomKey()); - return_from_file += expected_bo_inputs[schema].back() + "\n"; + + // Generate random subscribe inputs + for (int i = 0; i < RandomUtils::generateRandomInt(1, MAX_RANDOM_POINTS); i++) { + expected_bo_inputs[schema].subscribe.push_back(DataPointsUtils::generateRandomKey()); + return_from_file += expected_bo_inputs[schema].subscribe.back() + "\n"; } dto_random_inputs[schemaTypeToString(schema, true) + ModelConfigConverter::INPUT_SUFFIX] = @@ -131,7 +140,7 @@ TEST_F(DtoToModelConfigIntegrationTest, ConvertModelConfigDtoToBo) { // Generate model config ontologies std::vector dto_ontologies; std::vector> expected_bo_ontologies; - for (const auto& ontology_type_and_path : + for (const auto &ontology_type_and_path : generateRandomListOfTypeAndFilePaths(SYNTAX_TYPES_AND_EXTENSION_IN_TEST)) { dto_ontologies.push_back(ontology_type_and_path.second); expected_bo_ontologies.push_back( @@ -152,7 +161,7 @@ TEST_F(DtoToModelConfigIntegrationTest, ConvertModelConfigDtoToBo) { std::map expected_bo_triple_assembler_helper_queries; - for (const auto& schema : SCHEMAS_IN_TEST) { + for (const auto &schema : SCHEMAS_IN_TEST) { auto random_query_extension = getRandomTypeAndFileExtension(QUERY_LANGUAGE_TYPES_AND_EXTENSION_IN_TEST); @@ -228,15 +237,15 @@ TEST_F(DtoToModelConfigIntegrationTest, ConvertModelConfigDtoToBo) { // Generate reasoning output queries dto_queries.reasoning_output_queries_path = RandomUtils::generateRandomString(); std::vector dto_reasoning_output_queries_paths; - std::vector> expected_bo_reasoning_output_queries; + std::vector expected_bo_reasoning_output_queries; - for (int i = 0; i < RandomUtils::generateRandomInt(1, 5); i++) { + for (int i = 0; i < RandomUtils::generateRandomInt(1, MAX_RANDOM_POINTS); i++) { auto random_query_extension = getRandomTypeAndFileExtension(QUERY_LANGUAGE_TYPES_AND_EXTENSION_IN_TEST); expected_bo_reasoning_output_queries.push_back( - std::make_pair(fileExtensionToQueryLanguageType("." + random_query_extension.second), - RandomUtils::generateRandomString())); + {fileExtensionToQueryLanguageType("." + random_query_extension.second), + RandomUtils::generateRandomString()}); dto_reasoning_output_queries_paths.push_back(RandomUtils::generateRandomString() + "." + random_query_extension.second); @@ -247,7 +256,7 @@ TEST_F(DtoToModelConfigIntegrationTest, ConvertModelConfigDtoToBo) { EXPECT_CALL(*mock_i_file_handler_, readFile(::testing::StrEq(expected_bo_reasoning_output_query))) - .WillOnce(testing::Return(expected_bo_reasoning_output_queries.back().second)); + .WillOnce(testing::Return(expected_bo_reasoning_output_queries.back().query)); } EXPECT_CALL(*mock_i_file_handler_, @@ -258,7 +267,7 @@ TEST_F(DtoToModelConfigIntegrationTest, ConvertModelConfigDtoToBo) { // Generate model config rules std::vector dto_rules; std::vector> expected_bo_rules; - for (int i = 0; i < RandomUtils::generateRandomInt(1, 5); i++) { + for (int i = 0; i < RandomUtils::generateRandomInt(1, MAX_RANDOM_POINTS); i++) { auto random_role_extension = getRandomTypeAndFileExtension(REASONER_RULES_AND_EXTENSIONS_IN_TEST); @@ -277,7 +286,7 @@ TEST_F(DtoToModelConfigIntegrationTest, ConvertModelConfigDtoToBo) { // Generate model config validation shapes std::vector dto_shacl_shapes; std::vector> expected_bo_validation_shapes; - for (const auto& shacl_file_path : + for (const auto &shacl_file_path : generateRandomListOfTypeAndFilePaths(SYNTAX_TYPES_AND_EXTENSION_IN_TEST)) { dto_shacl_shapes.push_back(shacl_file_path.second); expected_bo_validation_shapes.push_back( @@ -291,8 +300,8 @@ TEST_F(DtoToModelConfigIntegrationTest, ConvertModelConfigDtoToBo) { // Generate model config reasoner settings ReasonerSettingsDTO dto_reasoner_settings; - dto_reasoner_settings.inference_engine = - ENGINES_IN_TEST[RandomUtils::generateRandomInt(0, ENGINES_IN_TEST.size() - 1)]; + dto_reasoner_settings.inference_engine = ENGINES_IN_TEST[RandomUtils::generateRandomInt( + 0, static_cast(ENGINES_IN_TEST.size()) - 1)]; InferenceEngineType expected_bo_reasoner_inference_engine = stringToInferenceEngineType(dto_reasoner_settings.inference_engine); @@ -320,21 +329,21 @@ TEST_F(DtoToModelConfigIntegrationTest, ConvertModelConfigDtoToBo) { dto.rules = dto_rules; dto.shacl_shapes = dto_shacl_shapes; dto.reasoner_settings = dto_reasoner_settings; - std::cout << "ModelConfigDTO to parse: \n" << dto << std::endl; + std::cout << "ModelConfigDTO to parse: \n" << dto << "\n"; // Act - auto bo = dto_to_bo_->convert(dto); - std::cout << "ModelConfig parsed: \n" << bo << std::endl; + auto b_obj = dto_to_bo_->convert(dto); + std::cout << "ModelConfig parsed: \n" << b_obj << "\n"; // Assert - EXPECT_EQ(bo.getInputs(), expected_bo_inputs); - EXPECT_EQ(bo.getOntologies(), expected_bo_ontologies); - EXPECT_EQ(bo.getOutput(), expected_bo_output); - EXPECT_EQ(bo.getReasonerRules(), expected_bo_rules); - EXPECT_EQ(bo.getValidationShapes(), expected_bo_validation_shapes); - - TripleAssemblerHelper bo_queries_config = bo.getQueriesTripleAssemblerHelper(); - for (const auto& [expected_bo_schema, expected_bo_query_pair] : + EXPECT_EQ(b_obj.getInputs(), expected_bo_inputs); + EXPECT_EQ(b_obj.getOntologies(), expected_bo_ontologies); + EXPECT_EQ(b_obj.getOutput(), expected_bo_output); + EXPECT_EQ(b_obj.getReasonerRules(), expected_bo_rules); + EXPECT_EQ(b_obj.getValidationShapes(), expected_bo_validation_shapes); + + TripleAssemblerHelper bo_queries_config = b_obj.getQueriesTripleAssemblerHelper(); + for (const auto &[expected_bo_schema, expected_bo_query_pair] : expected_bo_triple_assembler_helper_queries) { EXPECT_EQ(bo_queries_config.getQueries()[expected_bo_schema].data_property, expected_bo_query_pair.data_property); @@ -342,23 +351,25 @@ TEST_F(DtoToModelConfigIntegrationTest, ConvertModelConfigDtoToBo) { EXPECT_EQ(bo_queries_config.getQueries()[expected_bo_schema].object_property, expected_bo_query_pair.object_property); } - EXPECT_EQ(bo.getReasoningOutputQueries(), expected_bo_reasoning_output_queries); + EXPECT_EQ(b_obj.getReasoningOutputQueries(), expected_bo_reasoning_output_queries); - ReasonerSettings bo_reasoner_settings = bo.getReasonerSettings(); + ReasonerSettings bo_reasoner_settings = b_obj.getReasonerSettings(); EXPECT_EQ(bo_reasoner_settings.getInferenceEngine(), expected_bo_reasoner_inference_engine); EXPECT_EQ(bo_reasoner_settings.getOutputFormat(), expected_bo_reasoner_output_format); - for (const SchemaType& schema : expected_bo_reasoner_supported_schema_collections) { + for (const SchemaType &schema : expected_bo_reasoner_supported_schema_collections) { EXPECT_THAT(bo_reasoner_settings.getSupportedSchemaCollections(), testing::Contains(schema)); } } /** - * @brief Test case for converting a ModelConfigDTO with missing required fields. + * @brief Test case for converting a ModelConfigDTO with missing required + * fields. * - * This test verifies that attempting to convert a ModelConfigDTO to a business object (BO) - * throws an invalid_argument exception when any of the required fields are missing. + * This test verifies that attempting to convert a ModelConfigDTO to a business + * object (BO) throws an invalid_argument exception when any of the required + * fields are missing. */ TEST_F(DtoToModelConfigIntegrationTest, ConvertModelConfigDtoThrowsExceptionWhenRequiredFieldsAreMissing) { @@ -377,12 +388,19 @@ TEST_F(DtoToModelConfigIntegrationTest, // For this test, the file handler is irrelevant and return a dummy string EXPECT_CALL(*mock_i_file_handler_, readFile(::testing::_)) - .WillRepeatedly(testing::Return("some_data")); + .WillRepeatedly([](const std::string &path) { + if (path.find(".json") != std::string::npos) { + // Minimal valid JSON for inputs + return std::string(R"({"subscribe":["foo"],"callback":[]})"); + } + // For .rq or others, just return dummy string + return std::string("some_data"); + }); EXPECT_CALL(*mock_i_file_handler_, readDirectory(::testing::_)) - .WillRepeatedly(testing::Return(std::vector())); + .WillRepeatedly(testing::Return(std::vector({"some_file.rq"}))); - for (const auto& field : required_fields) { + for (const auto &field : required_fields) { // Define the complete ModelConfigDTO ModelConfigDTO dto = createValidDto(); std::string error_message; @@ -412,27 +430,21 @@ TEST_F(DtoToModelConfigIntegrationTest, } else if (field == "triple_assembler_helper") { dto.queries.triple_assembler_helper.clear(); error_message = - "Queries for the triple assembler helper cannot be empty. At least one query must " + "Queries for the triple assembler helper cannot be " + "empty. At least one query must " "be provided for each schema or default"; } else if (field == "reasoning_output_queries") { dto.queries.reasoning_output_queries_path = std::string(); - error_message = "Reasoning output queries cannot be empty"; + error_message = "The provided reasoning output queries path is empty."; } std::cout << "\nTesting with missing field: " << field << "\n"; - std::cout << dto << std::endl; + std::cout << dto << "\n"; // Act & Assert: Ensure exception is thrown when a required field is missing - ASSERT_THROW( - { - try { - dto_to_bo_->convert(dto); - } catch (const std::invalid_argument& e) { - EXPECT_THAT(e.what(), testing::HasSubstr(error_message)); - throw; - } - }, - std::invalid_argument); + EXPECT_THAT( + [&]() { dto_to_bo_->convert(dto); }, + ::testing::ThrowsMessage(::testing::HasSubstr(error_message))); } } @@ -452,18 +464,25 @@ TEST_F(DtoToModelConfigIntegrationTest, ConvertModelConfigDtoHandlesIncompleteQu // For this test, the file handler is irrelevant and return a dummy string EXPECT_CALL(*mock_i_file_handler_, readFile(::testing::_)) - .WillRepeatedly(testing::Return("some_data")); + .WillRepeatedly([](const std::string &path) { + if (path.find(".json") != std::string::npos) { + // Minimal valid JSON for inputs + return std::string(R"({"subscribe":["foo"],"callback":[]})"); + } + // For .rq or others, just return dummy string + return std::string("some_data"); + }); EXPECT_CALL(*mock_i_file_handler_, readDirectory(::testing::_)) .WillRepeatedly(testing::Return(std::vector({"some_file.rq"}))); - for (const auto& schema : schema_types) { - for (const auto& field : required_fields) { + for (const auto &schema : schema_types) { + for (const auto &field : required_fields) { // Define the complete ModelConfigDTO ModelConfigDTO dto = createValidDto(); // Set the field to an empty value - auto& files = dto.queries.triple_assembler_helper[schema]; + auto &files = dto.queries.triple_assembler_helper[schema]; if (field == "data_property") { files.erase(files.begin()); } else if (field == "object_property") { @@ -475,15 +494,16 @@ TEST_F(DtoToModelConfigIntegrationTest, ConvertModelConfigDtoHandlesIncompleteQu std::cout << "\nTesting with failing " + field + " queries for schema: " << schema << "\n"; - std::cout << dto << std::endl; + std::cout << dto << "\n"; - // Act & Assert: Ensure exception is thrown when a required field is missing + // Act & Assert: Ensure exception is thrown when a required field is + // missing if (schema == "default") { ASSERT_NO_THROW({ - auto bo = dto_to_bo_->convert(dto); - std::cout << "ModelConfig parsed: \n" << bo << std::endl; - auto query = bo.getQueriesTripleAssemblerHelper().getQueries(); - for (const auto& [schema, query_pair] : query) { + auto b_obj = dto_to_bo_->convert(dto); + std::cout << "ModelConfig parsed: \n" << b_obj << "\n"; + auto query = b_obj.getQueriesTripleAssemblerHelper().getQueries(); + for (const auto &[schema, query_pair] : query) { std::cout << "Schema: " << schemaTypeToString(schema) << "\n"; } EXPECT_NE(query.find(SchemaType::VEHICLE), query.end()); @@ -491,16 +511,9 @@ TEST_F(DtoToModelConfigIntegrationTest, ConvertModelConfigDtoHandlesIncompleteQu }); } else { - ASSERT_THROW( - { - try { - dto_to_bo_->convert(dto); - } catch (const std::invalid_argument& e) { - EXPECT_THAT(e.what(), testing::HasSubstr(error_message)); - throw; - } - }, - std::invalid_argument); + EXPECT_THAT([&]() { dto_to_bo_->convert(dto); }, + ::testing::ThrowsMessage( + ::testing::HasSubstr(error_message))); } } } diff --git a/cdsp/knowledge-layer/connector/websocket-client/services/tests/dto_to_status_message_integration_test.cpp b/cdsp/knowledge-layer/connector/websocket-client/services/tests/dto_to_status_message_integration_test.cpp deleted file mode 100644 index 4fbbb62e..00000000 --- a/cdsp/knowledge-layer/connector/websocket-client/services/tests/dto_to_status_message_integration_test.cpp +++ /dev/null @@ -1,83 +0,0 @@ -#include - -#include - -#include "dto_to_bo.h" -#include "helper.h" -#include "random_utils.h" -#include "test_helper.h" - -class DtoToStatusMessageIntegrationTest : public ::testing::Test { - protected: - DtoToBo dto_to_bo_; - const SchemaType SCHEMA_TYPE = SchemaType::VEHICLE; -}; - -/** - * @brief Test case for converting a StatusMessageDTO to a StatusMessage business object. - * - * This test verifies that the conversion from a StatusMessageDTO to a StatusMessage - * business object is performed correctly. It checks that all fields are accurately - * transferred from the DTO to the BO. - */ -TEST_F(DtoToStatusMessageIntegrationTest, ConvertStatusMessageDtoToBo) { - // Arrange - int random_code = RandomUtils::generateRandomInt(100, 999); - std::string random_message = RandomUtils::generateRandomString(10); - auto random_timestamps = TestHelper::getSecondsAndNanoseconds(); - auto random_requestId = TestHelper::generateRequestIdTag(); - - // Create dynamic StatusMessageDTO - StatusMessageDTO dto; - dto.code = random_code; - dto.message = random_message; - dto.timestamp.seconds = random_timestamps.first; - dto.timestamp.nanos = random_timestamps.second; - dto.requestId = random_requestId; - - std::cout << "StatusMessageDTO to parse:\n" << dto << std::endl; - - // Act - StatusMessage bo = dto_to_bo_.convert(dto); - std::cout << "StatusMessage parsed:\n" << bo << std::endl; - - // Assert - ASSERT_EQ(bo.getCode(), dto.code); - ASSERT_EQ(bo.getMessage(), dto.message); - ASSERT_EQ(bo.getRequestId(), dto.requestId); - ASSERT_EQ(bo.getTimestamp(), - Helper::convertToTimestamp(dto.timestamp.seconds, dto.timestamp.nanos)); -} - -/** - * @brief Test case for converting a StatusMessageDTO with missing required fields. - * - * This test verifies that attempting to convert a StatusMessageDTO to a business object (BO) - * throws an invalid_argument exception when any of the required fields are missing. - */ -TEST_F(DtoToStatusMessageIntegrationTest, - ConvertStatusMessageDtoThrowsExceptionWhenRequiredFieldsAreMissing) { - std::vector required_fields = {"message", "timestamp"}; - - for (const auto& field : required_fields) { - // Define the complete StatusMessageDTO - StatusMessageDTO dto; - dto.code = 200; - dto.message = "OK"; - dto.timestamp.seconds = 1234567890; - dto.timestamp.nanos = 123456789; - - if (field == "message") { - dto.message = std::string(); - } else if (field == "timestamp") { - dto.timestamp.seconds = 0; - dto.timestamp.nanos = 0; - } - - std::cout << "\nTesting with missing field: " << field << "\n"; - std::cout << dto << std::endl; - - // Act & Assert: Ensure exception is thrown when a required field is missing - ASSERT_THROW(dto_to_bo_.convert(dto), std::invalid_argument); - } -} \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/websocket-client/services/tests/model_config_dto_service_unit_test.cpp b/cdsp/knowledge-layer/connector/websocket-client/services/tests/model_config_dto_service_unit_test.cpp index 8af50199..d00c4795 100644 --- a/cdsp/knowledge-layer/connector/websocket-client/services/tests/model_config_dto_service_unit_test.cpp +++ b/cdsp/knowledge-layer/connector/websocket-client/services/tests/model_config_dto_service_unit_test.cpp @@ -4,16 +4,10 @@ #include #include #include -#include #include -#include "data_points_utils.h" #include "dto_service.h" -#include "helper.h" -#include "metadata_dto.h" #include "random_utils.h" -#include "test_helper.h" -#include "vin_utils.h" class ModelConfigDtoServiceUnitTest : public ::testing::Test { protected: diff --git a/cdsp/knowledge-layer/connector/websocket-client/services/tests/status_message_dto_service_unit_test.cpp b/cdsp/knowledge-layer/connector/websocket-client/services/tests/status_message_dto_service_unit_test.cpp deleted file mode 100644 index 2d1c88eb..00000000 --- a/cdsp/knowledge-layer/connector/websocket-client/services/tests/status_message_dto_service_unit_test.cpp +++ /dev/null @@ -1,140 +0,0 @@ -#include - -#include -#include -#include -#include -#include -#include - -#include "data_points_utils.h" -#include "dto_service.h" -#include "helper.h" -#include "metadata_dto.h" -#include "random_utils.h" -#include "test_helper.h" -#include "vin_utils.h" - -class ModelConfigDtoServiceUnitTest : public ::testing::Test { - protected: - DtoService dto_service_; -}; - -nlohmann::json generateValidStatusMessageJson() { - return {{"code", 200}, - {"message", "OK"}, - {"timestamp", {{"seconds", 1234567890}, {"nanos", 123456789}}}}; -} - -// Test for parsing a Status Message JSON - -/** - * @brief Test case for parsing a valid StatusMessage JSON. - * - * This test verifies the parsing of a valid StatusMessage JSON object into a StatusMessageDTO. - * It checks the integrity and correctness of the parsing process by comparing the fields - * of the parsed StatusMessageDTO with the original JSON object. - */ -TEST_F(ModelConfigDtoServiceUnitTest, ParseStatusMessage) { - // Arrange - int random_code = RandomUtils::generateRandomInt(100, 999); - std::string random_message = RandomUtils::generateRandomString(10); - auto random_timestamps = TestHelper::getSecondsAndNanoseconds(); - auto random_requestId = TestHelper::generateRequestIdTag(); - - nlohmann::json json_message = { - {"code", random_code}, - {"message", random_message}, - {"timestamp", {{"seconds", random_timestamps.first}, {"nanos", random_timestamps.second}}}}; - if (random_requestId.has_value()) { - json_message["requestId"] = random_requestId.value(); - } - - std::cout << "Incoming message: \n" << json_message.dump(4) << std::endl; - - // Act - StatusMessageDTO dto = dto_service_.parseStatusJsonToDto(json_message); - std::cout << "Parsed StatusMessageDTO: \n" << dto << std::endl; - - // Assert - ASSERT_EQ(dto.code, random_code); - ASSERT_EQ(dto.message, random_message); - ASSERT_EQ(dto.requestId, random_requestId); - ASSERT_EQ(dto.timestamp.seconds, random_timestamps.first); - ASSERT_EQ(dto.timestamp.nanos, random_timestamps.second); -} - -/** - * @brief Unit test for the DtoService class to verify exception handling for missing required - * fields for the status message. - * - * This test case checks that the `parseStatusJsonToDto` method of the `dto_service_` object - * throws an `std::invalid_argument` exception when any of the required fields are missing - * from the JSON message. The required fields are "code", "message", and "timestamp". - * - * The test iterates over each required field, removes it from a valid JSON message, - * and asserts that an exception is thrown when the modified message is parsed. - */ -TEST_F(ModelConfigDtoServiceUnitTest, ParseStatusDtoThrowsExceptionWhenRequiredFieldsAreMissing) { - // Define the complete JSON message - nlohmann::json json_message = generateValidStatusMessageJson(); - - // List of required fields in the JSON message - std::vector required_fields = {"code", "message", "timestamp"}; - - // Iterate over each required field - for (const auto& field : required_fields) { - nlohmann::json test_message = json_message; - test_message.erase(field); // Remove the required field - - // Output the current test case details - std::cout << "\nTesting with missing field: " << field << "\n"; - std::cout << test_message.dump(4) << std::endl; - StatusMessageDTO expected_dto; - ASSERT_NO_THROW({ - StatusMessageDTO result = dto_service_.parseStatusJsonToDto(test_message); - ASSERT_EQ(result.code, 0); - ASSERT_TRUE(result.message.empty()); - ASSERT_EQ(result.timestamp.seconds, 0); - ASSERT_EQ(result.timestamp.nanos, 0); - }); - } -} - -/** - * @brief Unit test for the DtoService class to verify exception handling for incomplete timestamps. - * - * This test case checks that the `parseStatusJsonToDto` method of the `dto_service_` object - * throws an `std::invalid_argument` exception when any of the required fields in the timestamp - * are missing from the JSON message. The required fields within the "timestamp" are "seconds" and - * "nanos". - * - * The test iterates over each required field, removes it from a valid JSON message's timestamp, - * and asserts that an exception is thrown when the modified message is parsed. - */ -TEST_F(ModelConfigDtoServiceUnitTest, ParseStatusDtoThrowsExceptionWhenTimestampIsIncomplete) { - // Define the complete JSON message - nlohmann::json json_message = generateValidStatusMessageJson(); - - // List of required fields in the timestamp - std::vector required_fields = {"seconds", "nanos"}; - - // Iterate over each required field - for (const auto& field : required_fields) { - nlohmann::json test_message = json_message; - test_message["timestamp"].erase(field); // Remove the required field - - // Output the current test case details - std::cout << "\nTesting with missing field: " << field << "\n"; - std::cout << test_message.dump(4) << std::endl; - - StatusMessageDTO expected_dto; - ASSERT_NO_THROW({ - StatusMessageDTO result = dto_service_.parseStatusJsonToDto(test_message); - ASSERT_EQ(result.code, 0); - ASSERT_TRUE(result.message.empty()); - ASSERT_EQ(result.timestamp.seconds, 0); - ASSERT_EQ(result.timestamp.nanos, 0); - }); - } -} diff --git a/cdsp/knowledge-layer/connector/websocket-client/src/main.cpp b/cdsp/knowledge-layer/connector/websocket-client/src/main.cpp index 6155de77..a1073e57 100644 --- a/cdsp/knowledge-layer/connector/websocket-client/src/main.cpp +++ b/cdsp/knowledge-layer/connector/websocket-client/src/main.cpp @@ -24,11 +24,12 @@ constexpr char DEFAULT_PORT_WEB_SOCKET_SERVER[] = "8080"; const std::string DEFAULT_TARGET_WEB_SOCKET_SERVER = ""; const std::string MODEL_CONFIGURATION_FILE = getProjectRoot() + "/symbolic-reasoner/examples/use-case/model/model_config.json"; -constexpr char DEFAULT_REASONER_SERVER[] = "127.0.0.1"; -constexpr char DEFAULT_PORT_REASONER_SERVER[] = "12110"; -constexpr char DEFAULT_AUTH_REASONER_SERVER_BASE64[] = +const std::string DEFAULT_REASONER_SERVER = "127.0.0.1"; +const std::string DEFAULT_PORT_REASONER_SERVER = "12110"; +const std::string DEFAULT_AUTH_REASONER_SERVER_BASE64 = "cm9vdDphZG1pbg=="; // 'root:admin' in base64 -constexpr char DEFAULT_REASONER_DATASTORE_NAME[] = "ds-test"; +const std::string DEFAULT_REASONER_DATASTORE_NAME = "ds-test"; +const std::string DEFAULT_REASONER_ORIGIN_SYSTEM_NAME = "SemanticReasoner"; bool RESET_REASONER_DATASTORE = false; void printBanner() { @@ -126,6 +127,12 @@ void displayEnvVariables() { << "Datastore name of the reasoner server" << std::setw(40) << Helper::getEnvVariable("REASONER_DATASTORE_NAME", DEFAULT_REASONER_DATASTORE_NAME) << "\n"; + + std::cout << std::left << std::setw(35) << "REASONER_ORIGIN_SYSTEM_NAME" << std::setw(65) + << "Origin system name for the reasoner server" << std::setw(40) + << Helper::getEnvVariable("REASONER_ORIGIN_SYSTEM_NAME", + DEFAULT_REASONER_ORIGIN_SYSTEM_NAME) + << "\n"; } void displayHelpXOptions() { @@ -184,7 +191,8 @@ int main(int argc, char* argv[]) { SystemConfig system_config = SystemConfigurationService::loadSystemConfig( DEFAULT_HOST_WEB_SOCKET_SERVER, DEFAULT_PORT_WEB_SOCKET_SERVER, DEFAULT_TARGET_WEB_SOCKET_SERVER, DEFAULT_REASONER_SERVER, DEFAULT_PORT_REASONER_SERVER, - DEFAULT_AUTH_REASONER_SERVER_BASE64, DEFAULT_REASONER_DATASTORE_NAME); + DEFAULT_AUTH_REASONER_SERVER_BASE64, DEFAULT_REASONER_DATASTORE_NAME, + DEFAULT_REASONER_ORIGIN_SYSTEM_NAME); // Initialize Model Configuration std::shared_ptr model_config = std::make_shared( diff --git a/cdsp/knowledge-layer/connector/websocket-client/src/tests/CMakeLists.txt b/cdsp/knowledge-layer/connector/websocket-client/src/tests/CMakeLists.txt deleted file mode 100644 index 067f8dd4..00000000 --- a/cdsp/knowledge-layer/connector/websocket-client/src/tests/CMakeLists.txt +++ /dev/null @@ -1,27 +0,0 @@ -# Add the integration test executable for WebsocketClient -add_executable(websocket_client_integration_tests - websocket_client_base_integration_test.cpp - websocket_client_integration_test.cpp - websocket_client_coordinates_integration_test.cpp -) - -target_link_libraries(websocket_client_integration_tests - PRIVATE - GTest::gtest_main - GTest::gmock - websocket_client - test_fixtures -) -target_include_directories(websocket_client_integration_tests - PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/utils -) - -# Add unit and integration tests to CTest -add_test(NAME WebsocketClientIntegrationTests COMMAND websocket_client_integration_tests) - -# Define custom output directory for test binaries -set_target_properties(websocket_client_integration_tests PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/tests") - -# Ensure tests are built with the all target -add_custom_target(websocket_client_tests ALL DEPENDS websocket_client_integration_tests) \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/websocket-client/src/tests/utils/mock_websocket_connection.h b/cdsp/knowledge-layer/connector/websocket-client/src/tests/utils/mock_websocket_connection.h deleted file mode 100644 index 2b591588..00000000 --- a/cdsp/knowledge-layer/connector/websocket-client/src/tests/utils/mock_websocket_connection.h +++ /dev/null @@ -1,86 +0,0 @@ -#ifndef MOCK_WEBSOCKET_CONNECTION_H -#define MOCK_WEBSOCKET_CONNECTION_H - -#include -#include -#include - -#include "websocket_interface.h" - -class MockWebSocketConnection : public WebSocketClientInterface { - public: - MockWebSocketConnection() : resolved_(false), connected_(false) {}; - - void asyncResolve(const std::string& host, const std::string& port) override { - resolved_ = true; - if (onResolveCallback_) { - onResolveCallback_(); - } - } - - void asyncConnect() override { - if (resolved_) { - connected_ = true; - if (onConnectCallback_) - onConnectCallback_(); - } - } - - void asyncHandshake() override { - if (connected_) { - if (onHandshakeCallback_) - onHandshakeCallback_(); - } - } - - void asyncWrite(const json& message) override { - sent_messages_.push(message.dump()); - if (onWriteCallback_) - onWriteCallback_(message.dump()); - } - - void asyncRead() override { - if (!received_messages_.empty()) { - buffer_ = received_messages_.front(); - received_messages_.pop(); - if (onReadCallback_) - onReadCallback_(); - } - } - - std::string getReceivedMessage() override { return buffer_; } - - void consumeBuffer(std::size_t bytes) override { - // Mock: no-op - } - - // Test setup methods - void addReceivedMessage(const std::string& message) { received_messages_.push(message); } - - void setOnResolveCallback(std::function callback) { onResolveCallback_ = callback; } - - void setOnConnectCallback(std::function callback) { onConnectCallback_ = callback; } - - void setOnHandshakeCallback(std::function callback) { onHandshakeCallback_ = callback; } - - void setOnWriteCallback(std::function callback) { - onWriteCallback_ = callback; - } - - void setOnReadCallback(std::function callback) { onReadCallback_ = callback; } - - private: - bool resolved_; - bool connected_; - std::queue received_messages_; - std::queue sent_messages_; - std::string buffer_; - - std::function onResolveCallback_; - std::function onConnectCallback_; - std::function onHandshakeCallback_; - std::function onWriteCallback_; - std::function onReadCallback_; -}; - -#endif // MOCK_WEBSOCKET_CONNECTION_H \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/websocket-client/src/tests/websocket_client_base_integration_test.cpp b/cdsp/knowledge-layer/connector/websocket-client/src/tests/websocket_client_base_integration_test.cpp deleted file mode 100644 index ff1425c9..00000000 --- a/cdsp/knowledge-layer/connector/websocket-client/src/tests/websocket_client_base_integration_test.cpp +++ /dev/null @@ -1,163 +0,0 @@ -#include "websocket_client_base_integration_test.h" - -#include -#include - -#include "rdfox_adapter.h" -#include "reasoner_factory.h" -#include "server_data_fixture.h" -#include "system_configuration_service.h" -#include "vin_utils.h" - -void WebSocketClientBaseIntegrationTest::SetUp() { - // ** Initialize Main Services ** - setenv("VEHICLE_OBJECT_ID", VinUtils::getRandomVinString().c_str(), 1); - - // Initialize System Configuration - SystemConfig system_config; - system_config.reasoner_server = ServerDataFixture::getValidRDFoxServerData(); - - // Initialize Model Configuration - model_config_ = std::make_shared( - SystemConfigurationService::loadModelConfig(MODEL_CONFIGURATION_FILE)); - - // Initialize Reasoner Service - reasoner_service_ = ReasonerFactory::initReasoner( - model_config_->getReasonerSettings().getInferenceEngine(), system_config.reasoner_server, - model_config_->getReasonerRules(), model_config_->getOntologies(), false); - if (!reasoner_service_) { - throw std::runtime_error("Failed to initialize the reasoner service."); - } - - // Initialize WebSocket Client - mock_connection_ = std::make_shared(); - - client_ = std::make_shared(system_config, model_config_, reasoner_service_, - mock_connection_); -} - -void WebSocketClientBaseIntegrationTest::TearDown() { - ASSERT_TRUE(reasoner_service_->deleteDataStore()) << "Failed to clean up the test data store."; -} - -/** - * @brief Mocks the behavior of a WebSocket connection for integration testing. - * - * This function sets up mock callbacks for various stages of a WebSocket connection, - * including resolution, connection, handshake, and reading messages. Each callback - * logs a message to the console and triggers the next step in the WebSocket process. - * Finally, it runs the WebSocket client to simulate the connection behavior. - */ -void WebSocketClientBaseIntegrationTest::mockWebSocketBehavior() { - mock_connection_->setOnResolveCallback([&]() { - std::cout << "Mock: Resolved WebSocket Server." << std::endl; - mock_connection_->asyncConnect(); - }); - - mock_connection_->setOnConnectCallback([&]() { - std::cout << "Mock: Connected to Websocket server." << std::endl; - mock_connection_->asyncHandshake(); - }); - - mock_connection_->setOnHandshakeCallback([&]() { - std::cout << "Mock: Handshake completed." << std::endl; - mock_connection_->asyncRead(); - }); - - mock_connection_->setOnReadCallback([&]() { - std::cout << "Mock: Read message from server." << std::endl; - client_->onReceiveMessage({}, mock_connection_->getReceivedMessage().size()); - }); - - client_->run(); -} - -/** - * @brief Creates a JSON message containing data and metadata for a WebSocket client. - * - * This function constructs a JSON message string that includes data from a list of nodes - * and associated metadata. The message is structured to include the type, instance, schema, - * and metadata with timestamps for each node. - * - * @param vin The vehicle identification number (VIN) associated with the data. - * @param nodes A vector of Node objects, each containing data and metadata. - * @return A JSON-formatted string representing the data and metadata. - */ -const std::string WebSocketClientBaseIntegrationTest::createDataJsonMessage( - const std::string& vin, const std::vector& nodes) { - std::ostringstream message; - message << R"({ - "type": "data", - "data": {)"; - - for (size_t i = 0; i < nodes.size(); ++i) { - const auto& node = nodes[i]; - message << R"(")" << node.name << R"(": )" << node.value; - - if (i < nodes.size() - 1) { - message << ","; - } - } - - message << R"(}, - "instance": ")" - << vin << R"(", - "schema": "Vehicle", - "metadata": {)"; - - bool first_metadata = true; - for (const auto& node : nodes) { - bool has_generated = node.metadata.getGenerated().has_value(); - bool has_received = node.metadata.getReceived().time_since_epoch().count() > 0; - - if (has_generated || has_received) { - if (!first_metadata) { - message << ","; - } - first_metadata = false; - - message << R"(")" << node.name << R"(": { - "timestamps": {)"; - - bool first_timestamp = true; - - if (has_generated) { - if (!first_timestamp) { - message << ","; - } - first_timestamp = false; - - auto generated_time = node.metadata.getGenerated().value(); - message << R"("generated": { - "seconds": )" - << std::chrono::duration_cast( - generated_time.time_since_epoch()) - .count() - << R"(, - "nanos": )" - << std::stoi(Helper::extractNanoseconds(generated_time)) << R"(})"; - } - - if (has_received) { - if (!first_timestamp) { - message << ","; - } - - auto received_time = node.metadata.getReceived(); - message << R"("received": { - "seconds": )" - << std::chrono::duration_cast( - received_time.time_since_epoch()) - .count() - << R"(, - "nanos": )" - << std::stoi(Helper::extractNanoseconds(received_time)) << R"(})"; - } - - message << R"(}})"; - } - } - - message << R"(}})"; - return message.str(); -} \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/websocket-client/src/tests/websocket_client_base_integration_test.h b/cdsp/knowledge-layer/connector/websocket-client/src/tests/websocket_client_base_integration_test.h deleted file mode 100644 index 80bd77f4..00000000 --- a/cdsp/knowledge-layer/connector/websocket-client/src/tests/websocket_client_base_integration_test.h +++ /dev/null @@ -1,39 +0,0 @@ -// WebSocketClientBaseIntegrationTest.h -#ifndef WEBSOCKET_CLIENT_BASE_INTEGRATION_TEST_H -#define WEBSOCKET_CLIENT_BASE_INTEGRATION_TEST_H - -#include - -#include - -#include "file_handler_impl.h" -#include "globals.h" -#include "mock_websocket_connection.h" -#include "reasoner_service.h" -#include "triple_assembler.h" -#include "triple_writer.h" -#include "websocket_client.h" - -class WebSocketClientBaseIntegrationTest : public ::testing::Test { - protected: - const std::string MODEL_CONFIGURATION_FILE = - getProjectRoot() + "/symbolic-reasoner/examples/use-case/model/model_config.json"; - struct MessageNodeData { - std::string name; - std::string value; - Metadata metadata; - }; - - void SetUp() override; - void TearDown() override; - void mockWebSocketBehavior(); - const std::string createDataJsonMessage(const std::string& vin, - const std::vector& nodes); - SystemConfig system_config_; - std::shared_ptr mock_connection_; - std::shared_ptr client_; - std::shared_ptr reasoner_service_; - std::shared_ptr model_config_; -}; - -#endif // WEBSOCKET_CLIENT_BASE_INTEGRATION_TEST_H \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/websocket-client/src/tests/websocket_client_coordinates_integration_test.cpp b/cdsp/knowledge-layer/connector/websocket-client/src/tests/websocket_client_coordinates_integration_test.cpp deleted file mode 100644 index 0adc305e..00000000 --- a/cdsp/knowledge-layer/connector/websocket-client/src/tests/websocket_client_coordinates_integration_test.cpp +++ /dev/null @@ -1,328 +0,0 @@ -#include -#include - -#include "helper.h" -#include "random_utils.h" -#include "utc_date_utils.h" -#include "websocket_client_base_integration_test.h" - -#define ASSERT_CONDITIONAL(condition, expected, message) \ - do { \ - if (expected) { \ - ASSERT_TRUE(condition) << message; \ - } else { \ - ASSERT_FALSE(condition) << message; \ - } \ - } while (0) - -class WebSocketClientCoordinatesIntegrationTest : public WebSocketClientBaseIntegrationTest { - protected: - const std::string extractNumericTimestamp(const std::string& timestamp) { - std::string compact_time; - for (char c : timestamp) { - if (std::isdigit(c)) { - compact_time += c; - } - } - return compact_time; - } - const std::string createObservationQuery(const std::string& vin, const std::string& class_name, - const std::string& observed_property, - const std::string& node_value, - const std::string& node_type, - const std::string& data_time) { - return R"( - PREFIX car: - PREFIX sosa: - PREFIX xsd: - - SELECT ?obs ?ntmValue { - ?obs a sosa:Observation ; - sosa:hasFeatureOfInterest car:)" + - class_name + vin + R"( ; - sosa:hasSimpleResult ")" + - node_value + R"("^^xsd:)" + node_type + R"( ; - sosa:observedProperty car:)" + - observed_property + R"( ; - sosa:phenomenonTime ")" + - data_time + R"("^^xsd:dateTime ; - car:hasSimpleResultNTM ?ntmValue. - } LIMIT 1 - )"; - } - - /** - * Processes and verifies messages by adding them to a mock connection, executing a mock - * WebSocket behavior, and querying RDFox to verify the existence of latitude and longitude - * observations. - * - * @param messages_nodes A vector of vectors containing Node objects representing the messages - * to be processed. - * @param latitude The expected latitude value to verify in the RDFox query. - * @param longitude The expected longitude value to verify in the RDFox query. - * @param expect_exists A boolean indicating whether the observations are expected to exist in - * the RDFox query results. - * @param consecutive_observation_latitude An integer representing the expected consecutive - * observation ID for latitude. Defaults to 0. - * @param consecutive_observation_longitude An integer representing the expected consecutive - * observation ID for longitude. Defaults to 1. - */ - void processAndVerifyMessages(const std::vector>& messages_nodes, - const std::string& latitude, const std::string& longitude, - const std::string& data_time_latitude, - const std::string& data_time_longitude, bool expect_exists) { - // Add received messages_nodes - for (const auto& nodes : messages_nodes) { - std::string vin = model_config_->getObjectId()[SchemaType::VEHICLE]; - mock_connection_->addReceivedMessage(createDataJsonMessage(vin, nodes)); - } - - // Use the inherited mockWebSocketBehavior - mockWebSocketBehavior(); - - // Verify triples in RDFox - std::string vin = model_config_->getObjectId()[SchemaType::VEHICLE]; - std::string sparql_query_latitude = createObservationQuery( - vin, "CurrentLocation", "latitude", latitude, "double", data_time_latitude); - std::string sparql_query_longitude = createObservationQuery( - vin, "CurrentLocation", "longitude", longitude, "double", data_time_longitude); - - std::string result_latitude = reasoner_service_->queryData( - sparql_query_latitude, QueryLanguageType::SPARQL, DataQueryAcceptType::SPARQL_JSON); - std::string result_longitude = reasoner_service_->queryData( - sparql_query_longitude, QueryLanguageType::SPARQL, DataQueryAcceptType::SPARQL_JSON); - - // Helper function to check if the observation exists and `ntmValue` is valid - auto verifyObservation = [](const std::string& result, - const std::string& expected_observation_id, bool expect_exist) { - try { - json json_result = json::parse(result); - - // Check if the expected structure exists - if (!json_result.contains("results") || - !json_result["results"].contains("bindings")) { - FAIL() << "Missing expected observation data in RDFox response!"; - } - if (!expect_exist) { - ASSERT_TRUE(json_result["results"]["bindings"].empty()) - << "Unexpected observation data found in RDFox response!"; - } else { - json first_binding = json_result["results"]["bindings"][0]; - - // Assert observation ID exists - ASSERT_TRUE(first_binding.contains("obs") && - first_binding["obs"].contains("value")) - << "Observation ID is missing in RDFox response!"; - std::string observation_id = first_binding["obs"]["value"]; - ASSERT_TRUE(observation_id.find(expected_observation_id) != std::string::npos) - << "Expected observation ID `" << expected_observation_id - << "` not found! Found: " << observation_id; - - // Assert `ntmValue` exists and is a valid float/double - ASSERT_TRUE(first_binding.contains("ntmValue") && - first_binding["ntmValue"].contains("value")) - << "ntmValue is missing in RDFox response!"; - std::string ntm_value_str = first_binding["ntmValue"]["value"]; - - std::regex float_regex(R"([-+]?\d*\.?\d+([eE][-+]?\d+)?)"); - ASSERT_TRUE(std::regex_match(ntm_value_str, float_regex)) - << "Invalid ntmValue format: " << ntm_value_str; - } - - } catch (const std::exception& e) { - FAIL() << "JSON Parsing failed: " << e.what(); - } - }; - // Check latitude and longitude observations - verifyObservation(result_latitude, extractNumericTimestamp(data_time_latitude), - expect_exists); - verifyObservation(result_longitude, extractNumericTimestamp(data_time_longitude), - expect_exists); - } -}; - -/** - * @brief Test case for verifying that coordinates in the same message generate triples. - * - * This test generates random latitude and longitude values, constructs message nodes - * with these coordinates, and verifies that the messages are processed correctly to - * generate triples. - */ -TEST_F(WebSocketClientCoordinatesIntegrationTest, CoordinatesInSameMessageGenerateTriples) { - // Generate random latitude and longitude values as strings - const std::string latitude = std::to_string(RandomUtils::generateRandomDouble(-90, 90)); - const std::string longitude = std::to_string(RandomUtils::generateRandomDouble(-63, 86)); - - // Create message nodes with latitude and longitude data - const std::vector nodes = { - {"CurrentLocation.Latitude", latitude, Metadata()}, - {"CurrentLocation.Longitude", longitude, Metadata()}}; - - // Format the timestamps for latitude and longitude data - std::string data_time_latitude = Helper::getFormattedTimestampCustom( - "%Y-%m-%dT%H:%M:%S", nodes.at(0).metadata.getReceived(), true); - std::string data_time_longitude = Helper::getFormattedTimestampCustom( - "%Y-%m-%dT%H:%M:%S", nodes.at(1).metadata.getReceived(), true); - - // Process and verify the messages with the generated coordinates and timestamps - processAndVerifyMessages({nodes}, latitude, longitude, data_time_latitude, data_time_longitude, - true); -} - -/** - * @brief Test to verify that coordinates received in different messages within two seconds - * generate triples. - * - * This test simulates the reception of coordinates in separate messages with specific timestamps - * and verifies that they are processed correctly to form triples. The timestamps are generated - * such that some messages are within the two-second window required to form a triple, while others - * are not. - */ -TEST_F(WebSocketClientCoordinatesIntegrationTest, - CoordinatesInDifferentMessagesWithinTwoSecondsGenerateTriples) { - // Generate timestamps for each message - std::chrono::system_clock::time_point timestamp_msg1 = std::chrono::system_clock::now(); - std::chrono::system_clock::time_point timestamp_msg2 = timestamp_msg1 + std::chrono::seconds(3); - std::chrono::system_clock::time_point timestamp_msg3 = timestamp_msg2 + std::chrono::seconds(1); - std::chrono::system_clock::time_point timestamp_msg4 = - timestamp_msg3 + std::chrono::milliseconds(1500); - - // Create Metadata for each message using generated timestamps - Metadata metadata_msg1 = Metadata(std::nullopt, timestamp_msg1); - Metadata metadata_msg2 = Metadata(std::nullopt, timestamp_msg2); - Metadata metadata_msg3 = Metadata(std::nullopt, timestamp_msg3); - Metadata metadata_msg4 = Metadata(std::nullopt, timestamp_msg4); - - // Generate random coordinates - const std::string latitude_msg1 = std::to_string(RandomUtils::generateRandomDouble(-90, 90)); - const std::string longitude_msg2 = std::to_string(RandomUtils::generateRandomDouble(-63, 86)); - const std::string longitude_msg3 = std::to_string(RandomUtils::generateRandomDouble(-63, 86)); - const std::string latitude_msg4 = std::to_string(RandomUtils::generateRandomDouble(-90, 90)); - - const std::vector node_msg_1 = { - {"CurrentLocation.Latitude", latitude_msg1, metadata_msg1}}; - const std::vector node_msg_2 = { - {"CurrentLocation.Longitude", longitude_msg2, metadata_msg2}}; - const std::vector node_msg_3 = { - {"CurrentLocation.Longitude", longitude_msg3, metadata_msg3}}; - const std::vector node_msg_4 = { - {"CurrentLocation.Latitude", latitude_msg4, metadata_msg4}}; - - std::string data_time_longitude_msg3 = Helper::getFormattedTimestampCustom( - "%Y-%m-%dT%H:%M:%S", node_msg_3.at(0).metadata.getGenerated().value(), true); - std::string data_time_latitude_msg4 = Helper::getFormattedTimestampCustom( - "%Y-%m-%dT%H:%M:%S", node_msg_4.at(0).metadata.getGenerated().value(), true); - - // Process and verify the messages with the generated coordinates and timestamps - processAndVerifyMessages({node_msg_1, node_msg_2, node_msg_3, node_msg_4}, latitude_msg4, - longitude_msg3, data_time_latitude_msg4, data_time_longitude_msg3, - true); -} - -/** - * @brief Test case for verifying that coordinates in different messages after a two-second interval - * generate any triples. - * - * This test simulates the reception of coordinates in separate messages with specific timestamps - * and verifies that they are processed correctly to form triples. The timestamps are generated such - * that the messages are not within the two-second window required to form a triple. - */ -TEST_F(WebSocketClientCoordinatesIntegrationTest, - CoordinatesInDifferentMessagesAfterTwoSecondsGenerateAnyTriples) { - // Generate timestamps for each message, with a 3-second interval between them - std::chrono::system_clock::time_point timestamp_msg1 = std::chrono::system_clock::now(); - std::chrono::system_clock::time_point timestamp_msg2 = timestamp_msg1 + std::chrono::seconds(3); - - // Create Metadata objects for each message using the generated timestamps - Metadata metadata_msg1 = Metadata(std::nullopt, timestamp_msg1); - Metadata metadata_msg2 = Metadata(std::nullopt, timestamp_msg2); - - // Generate random latitude and longitude coordinates as strings - const std::string latitude = std::to_string(RandomUtils::generateRandomDouble(-90, 90)); - const std::string longitude = std::to_string(RandomUtils::generateRandomDouble(-63, 86)); - - // Create message node data for latitude and longitude with associated metadata - const std::vector node_msg_1 = { - {"CurrentLocation.Latitude", latitude, metadata_msg1}}; - const std::vector node_msg_2 = { - {"CurrentLocation.Longitude", longitude, metadata_msg2}}; - - // Format the timestamps for latitude and longitude messages into a custom string format - std::string data_time_latitude_msg1 = Helper::getFormattedTimestampCustom( - "%Y-%m-%dT%H:%M:%S", node_msg_1.at(0).metadata.getGenerated().value(), true); - std::string data_time_longitude_msg2 = Helper::getFormattedTimestampCustom( - "%Y-%m-%dT%H:%M:%S", node_msg_2.at(0).metadata.getGenerated().value(), true); - - // Process and verify the messages with the generated coordinates and timestamps - processAndVerifyMessages({node_msg_1, node_msg_2}, latitude, longitude, data_time_latitude_msg1, - data_time_longitude_msg2, false); -} - -/** - * @brief Test case for WebSocketClientCoordinatesIntegrationTest. - * - * This test verifies the behavior of the WebSocket client when handling messages - * with missing coordinates. It waits for the message processing to complete and - * checks if the messages are processed correctly. - */ -TEST_F(WebSocketClientCoordinatesIntegrationTest, - MessageWithMissingCoordinatesWaitForMessageToComplete) { - // Generate timestamps for each message - std::chrono::system_clock::time_point timestamp_msg1 = std::chrono::system_clock::now(); - std::chrono::system_clock::time_point timestamp_msg2 = timestamp_msg1 + std::chrono::seconds(1); - - // Create Metadata for each message using generated timestamps - Metadata metadata_msg1 = Metadata(std::nullopt, timestamp_msg1); - Metadata metadata_msg2 = Metadata(std::nullopt, timestamp_msg2); - - // Generate random coordinates - const std::string latitude_1 = std::to_string(RandomUtils::generateRandomDouble(-90, 90)); - const std::string latitude_2 = std::to_string(RandomUtils::generateRandomDouble(-90, 90)); - const std::string longitude = std::to_string(RandomUtils::generateRandomDouble(-63, 86)); - - // Create message node data for the first message, which should be ignored - const std::vector node_msg_1 = { - {"CurrentLocation.Latitude", latitude_1, metadata_msg1}}; - - // Create message node data for the second message - const std::vector node_msg_2 = { - {"CurrentLocation.Longitude", longitude, metadata_msg2}, - {"CurrentLocation.Latitude", latitude_2, metadata_msg2}}; - - // Format the timestamp for the second message - std::string data_time_both = Helper::getFormattedTimestampCustom( - "%Y-%m-%dT%H:%M:%S", node_msg_2.at(0).metadata.getGenerated().value(), true); - - // Process and verify the messages - processAndVerifyMessages({node_msg_1, node_msg_2}, latitude_2, longitude, data_time_both, - data_time_both, true); -} - -/** - * @brief Test case for verifying the processing of mixed content messages in WebSocket client. - * - * This test simulates the sending of mixed content messages containing latitude, longitude, - * and speed data points. It generates random latitude and longitude values, constructs - * message nodes with these values, and then processes and verifies the messages. - */ -TEST_F(WebSocketClientCoordinatesIntegrationTest, MixedContentMessages) { - // Generate random latitude and longitude values as strings. - const std::string latitude = std::to_string(RandomUtils::generateRandomDouble(-90, 90)); - const std::string longitude = std::to_string(RandomUtils::generateRandomDouble(-63, 86)); - - // Construct message nodes with latitude, longitude, and speed data points. - const std::vector node_msg_1 = { - {"CurrentLocation.Latitude", latitude, Metadata()}, {"Speed", "100", Metadata()}}; - const std::vector node_msg_2 = { - {"CurrentLocation.Longitude", longitude, Metadata()}, {"Speed", "200", Metadata()}}; - - // Format timestamps for the latitude and longitude data points. - std::string data_time_latitude = Helper::getFormattedTimestampCustom( - "%Y-%m-%dT%H:%M:%S", node_msg_1.at(0).metadata.getReceived(), true); - std::string data_time_longitude = Helper::getFormattedTimestampCustom( - "%Y-%m-%dT%H:%M:%S", node_msg_2.at(0).metadata.getReceived(), true); - - // Process and verify the constructed messages with the generated data. - processAndVerifyMessages({node_msg_1, node_msg_2}, latitude, longitude, data_time_latitude, - data_time_longitude, true); -} \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/websocket-client/src/tests/websocket_client_integration_test.cpp b/cdsp/knowledge-layer/connector/websocket-client/src/tests/websocket_client_integration_test.cpp deleted file mode 100644 index 5a4fb685..00000000 --- a/cdsp/knowledge-layer/connector/websocket-client/src/tests/websocket_client_integration_test.cpp +++ /dev/null @@ -1,213 +0,0 @@ -#include - -#include "random_utils.h" -#include "utc_date_utils.h" -#include "websocket_client_base_integration_test.h" - -class WebSocketClientIntegrationTest : public WebSocketClientBaseIntegrationTest { - protected: - const std::string createObservationQuery(const std::string& observation_id, - const std::string& vin, const std::string& class_name, - const std::string& observed_property, - const std::string& node_value, - const std::string& node_type, - const std::string& phenomenon_time) { - return R"( - PREFIX car: - PREFIX sosa: - PREFIX xsd: - - ASK { - car:ob_)" + - observed_property + "_" + observation_id + R"( a sosa:Observation ; - sosa:hasFeatureOfInterest car:)" + - class_name + vin + R"( ; - sosa:hasSimpleResult ")" + - node_value + R"("^^xsd:)" + node_type + R"( ; - sosa:observedProperty car:)" + - observed_property + R"( ; - sosa:phenomenonTime ")" + - phenomenon_time + R"("^^xsd:dateTime . - } - )"; - }; -}; - -/** - * @brief Test case for the full flow of WebSocket client integration. - * - * This test simulates a message from the server and verifies that the data - * is correctly processed and stored in RDFox. It uses a start and end time - * to query the data and checks that the expected observation entry exists. - */ -TEST_F(WebSocketClientIntegrationTest, FullFlowTest) { - // Simulate a message from the server - const Metadata metadata = Metadata(RandomUtils::generateRandomTimestamp(2020, 2030, true), - RandomUtils::generateRandomTimestamp(2020, 2030, true)); - const std::vector nodes = { - {"Speed", std::to_string(RandomUtils::generateRandomFloat(0, 300)), metadata}}; - mock_connection_->addReceivedMessage( - createDataJsonMessage(model_config_->getObjectId()[SchemaType::VEHICLE], nodes)); - - // Mock WebSocket behavior - mockWebSocketBehavior(); - - // Verify RDFox Data - std::string timestamp = - Helper::getFormattedTimestampCustom("%Y%m%d%H%M%S", metadata.getGenerated().value()); - - std::string phenomenon_time = Helper::getFormattedTimestampCustom( - "%Y-%m-%dT%H:%M:%S", metadata.getGenerated().value(), true); - - std::string observation_identifier = - timestamp + Helper::extractNanoseconds(metadata.getGenerated().value()); - - std::string sparql_query(createObservationQuery( - observation_identifier, model_config_->getObjectId()[SchemaType::VEHICLE], "Vehicle", - "speed", nodes.at(0).value, "float", phenomenon_time)); - - // Assert data exists in RDFox - std::string result = reasoner_service_->queryData(sparql_query, QueryLanguageType::SPARQL, - DataQueryAcceptType::SPARQL_JSON); - - // Parse the JSON result to check if the observation exists - nlohmann::json json_result = nlohmann::json::parse(result); - ASSERT_TRUE(json_result.contains("boolean")) - << "RDFox returned an empty result. Expected an observation entry."; - - ASSERT_TRUE(json_result["boolean"]) << "Observation was not found in RDFox!\n" - << "Query Result: " << result << "\n"; -} - -/** - * @brief Test case for the full flow of WebSocket client integration with multiple nodes. - * - * This test simulates two messages from the server and verifies that the data - * is correctly processed and stored in RDFox. It uses a start and end time - * to query the data and checks that the expected observation entries exist. - */ -TEST_F(WebSocketClientIntegrationTest, FullFlowTestMultiNodes) { - // Simulate a first message from the server - const Metadata metadata_msg_1 = - Metadata(RandomUtils::generateRandomTimestamp(2020, 2030, true), - RandomUtils::generateRandomTimestamp(2020, 2030, true)); - const std::vector nodes_msg_1 = { - {"Speed", std::to_string(RandomUtils::generateRandomFloat(0, 300)), metadata_msg_1}, - {"Powertrain.TractionBattery.StateOfCharge.CurrentEnergy", - std::to_string(RandomUtils::generateRandomFloat(0, 100)), metadata_msg_1}}; - - // Add the first simulated message to the mock connection - mock_connection_->addReceivedMessage( - createDataJsonMessage(model_config_->getObjectId()[SchemaType::VEHICLE], nodes_msg_1)); - - // Simulate a second message from the server - const Metadata metadata_msg_2 = - Metadata(RandomUtils::generateRandomTimestamp(2020, 2030, true), - RandomUtils::generateRandomTimestamp(2020, 2030, true)); - const std::vector nodes_msg_2 = { - {"Chassis.SteeringWheel.Angle", std::to_string(RandomUtils::generateRandomInt(0, 100)), - metadata_msg_2}, - {"Powertrain.TractionBattery.StateOfCharge.CurrentEnergy", - std::to_string(RandomUtils::generateRandomFloat(0, 100)), metadata_msg_2}}; - - // Add the second simulated message to the mock connection - mock_connection_->addReceivedMessage( - createDataJsonMessage(model_config_->getObjectId()[SchemaType::VEHICLE], nodes_msg_2)); - - // Mock WebSocket behavior - mockWebSocketBehavior(); - - // ** Verify RDFox Data Nodes for first message ** - - std::string timestamp_msg_1 = - Helper::getFormattedTimestampCustom("%Y%m%d%H%M%S", metadata_msg_1.getGenerated().value()); - - std::string phenomenon_time_msg_1 = Helper::getFormattedTimestampCustom( - "%Y-%m-%dT%H:%M:%S", metadata_msg_1.getGenerated().value(), true); - - std::string observation_identifier_msg_1 = - timestamp_msg_1 + Helper::extractNanoseconds(metadata_msg_1.getGenerated().value()); - - std::string sparql_query_msg_1_qry_1(createObservationQuery( - observation_identifier_msg_1, model_config_->getObjectId()[SchemaType::VEHICLE], - "StateOfCharge", "currentEnergy", nodes_msg_1.at(1).value, "float", phenomenon_time_msg_1)); - - std::string sparql_query_msg_1_qry_2(createObservationQuery( - observation_identifier_msg_1, model_config_->getObjectId()[SchemaType::VEHICLE], "Vehicle", - "speed", nodes_msg_1.at(0).value, "float", phenomenon_time_msg_1)); - - // ** Verify RDFox Data Nodes for second message ** - - std::string timestamp_msg_2 = - Helper::getFormattedTimestampCustom("%Y%m%d%H%M%S", metadata_msg_2.getGenerated().value()); - - std::string phenomenon_time_msg_2 = Helper::getFormattedTimestampCustom( - "%Y-%m-%dT%H:%M:%S", metadata_msg_2.getGenerated().value(), true); - - std::string observation_identifier_msg_2 = - timestamp_msg_2 + Helper::extractNanoseconds(metadata_msg_2.getGenerated().value()); - - std::string sparql_query_msg_2_qry_1(createObservationQuery( - observation_identifier_msg_2, model_config_->getObjectId()[SchemaType::VEHICLE], - "SteeringWheel", "angle", nodes_msg_2.at(0).value, "int", phenomenon_time_msg_2)); - - std::string sparql_query_msg_2_qry_2(createObservationQuery( - observation_identifier_msg_2, model_config_->getObjectId()[SchemaType::VEHICLE], - "StateOfCharge", "currentEnergy", nodes_msg_2.at(1).value, "float", phenomenon_time_msg_2)); - - // Assert data exists in RDFox - - // ** Assert data exists in RDFox for first message and first node ** - std::string result_msg_1_qry_1 = reasoner_service_->queryData( - sparql_query_msg_1_qry_1, QueryLanguageType::SPARQL, DataQueryAcceptType::SPARQL_JSON); - - // Parse the JSON result to check if the observation exists for the first message and first node - nlohmann::json json_result_msg_1_qry_1 = nlohmann::json::parse(result_msg_1_qry_1); - ASSERT_TRUE(json_result_msg_1_qry_1.contains("boolean")) - << "RDFox returned an empty result. Expected an observation entry."; - - ASSERT_TRUE(json_result_msg_1_qry_1["boolean"]) - << "Observation was not found in RDFox!\n" - << "Query Result: " << result_msg_1_qry_1 << "\n"; - - // ** Assert data exists in RDFox for first message and second node ** - std::string result_msg_1_qry_2 = reasoner_service_->queryData( - sparql_query_msg_1_qry_2, QueryLanguageType::SPARQL, DataQueryAcceptType::SPARQL_JSON); - - // Parse the JSON result to check if the observation exists for the first message and second - // node - nlohmann::json json_result_msg_1_qry_2 = nlohmann::json::parse(result_msg_1_qry_2); - ASSERT_TRUE(json_result_msg_1_qry_2.contains("boolean")) - << "RDFox returned an empty result. Expected an observation entry."; - - ASSERT_TRUE(json_result_msg_1_qry_2["boolean"]) - << "Observation was not found in RDFox!\n" - << "Query Result: " << result_msg_1_qry_2 << "\n"; - - // ** Assert data exists in RDFox for second message and first node ** - std::string result_msg_2_qry_1 = reasoner_service_->queryData( - sparql_query_msg_2_qry_1, QueryLanguageType::SPARQL, DataQueryAcceptType::SPARQL_JSON); - - // Parse the JSON result to check if the observation exists for the second message and first - // node - nlohmann::json json_result_msg_2_qry_1 = nlohmann::json::parse(result_msg_2_qry_1); - ASSERT_TRUE(json_result_msg_2_qry_1.contains("boolean")) - << "RDFox returned an empty result. Expected an observation entry."; - - ASSERT_TRUE(json_result_msg_2_qry_1["boolean"]) - << "Observation was not found in RDFox!\n" - << "Query Result: " << result_msg_2_qry_1 << "\n"; - - // ** Assert data exists in RDFox for second message and second node ** - std::string result_msg_2_qry_2 = reasoner_service_->queryData( - sparql_query_msg_2_qry_2, QueryLanguageType::SPARQL, DataQueryAcceptType::SPARQL_JSON); - - // Parse the JSON result to check if the observation exists for the second message and second - // node - nlohmann::json json_result_msg_2_qry_2 = nlohmann::json::parse(result_msg_2_qry_2); - ASSERT_TRUE(json_result_msg_2_qry_2.contains("boolean")) - << "RDFox returned an empty result. Expected an observation entry."; - ASSERT_TRUE(json_result_msg_2_qry_2["boolean"]) - << "Observation was not found in RDFox!\n" - << "Query Result: " << result_msg_2_qry_2 << "\n"; -} \ No newline at end of file diff --git a/cdsp/knowledge-layer/connector/websocket-client/src/websocket_client.cpp b/cdsp/knowledge-layer/connector/websocket-client/src/websocket_client.cpp index 9d7d7e8d..4dd57d65 100644 --- a/cdsp/knowledge-layer/connector/websocket-client/src/websocket_client.cpp +++ b/cdsp/knowledge-layer/connector/websocket-client/src/websocket_client.cpp @@ -1,15 +1,9 @@ #include "websocket_client.h" -#include #include -#include -#include -#include "bo_service.h" -#include "data_message.h" #include "helper.h" #include "real_websocket_connection.h" -#include "status_message.h" namespace { /** @@ -36,17 +30,15 @@ void Fail(const boost::system::error_code& ec, const std::string& what) { * @param connection A shared pointer to a WebSocketClientInterface, representing the connection to * be used. */ -WebSocketClient::WebSocketClient(const SystemConfig& system_config, +WebSocketClient::WebSocketClient(SystemConfig system_config, std::shared_ptr model_config, std::shared_ptr reasoner_service, std::shared_ptr connection) - : system_config_(system_config), + : system_config_(std::move(system_config)), reasoner_service_(std::move(reasoner_service)), model_config_(std::move(model_config)), connection_(std::move(connection)), - io_context_(), triple_assembler_(model_config_, *reasoner_service_, file_handler_, triple_writer_), - message_service_(model_config_->getInputs()), reasoner_query_service_(std::make_shared(reasoner_service_)) { triple_assembler_.initialize(); } @@ -86,10 +78,11 @@ void WebSocketClient::run() { const SystemConfig& WebSocketClient::getInitConfig() const { return system_config_; } // Callbacks for connection lifecycle and message handling -void WebSocketClient::onConnect(boost::system::error_code ec, +void WebSocketClient::onConnect(boost::system::error_code error_code, const boost::asio::ip::tcp::endpoint& endpoint) { - if (ec) { - return Fail(ec, " - Connection failed:"); + if (error_code) { + Fail(error_code, " - Connection failed:"); + return; } std::cout << " - Connected to Websocket Server: " << endpoint.address() << ":" @@ -107,24 +100,20 @@ void WebSocketClient::onConnect(boost::system::error_code ec, * * @param ec The error code indicating the result of the handshake attempt. */ -void WebSocketClient::handshake(boost::system::error_code ec) { - if (ec) { - return Fail(ec, " - Handshake failed:"); +void WebSocketClient::handshake(boost::system::error_code error_code) { + if (error_code) { + return Fail(error_code, " - Handshake failed:"); } for (const SchemaType& schema_type : model_config_->getReasonerSettings().getSupportedSchemaCollections()) { // Start subscribing to supported schemas const auto object_id = model_config_->getObjectId().at(schema_type); - auto subscribe_message = BoService::createSubscribeMessage(object_id, schema_type); - message_service_.addMessageToQueue(subscribe_message, reply_messages_queue_); - - // TODO: Check if this block is needed - /* - const auto data_point_list = model_config_->getInputs().at(schema_type); - auto get_message = BoService::createGetMessage(object_id, schema_type, data_point_list); - message_service_.addMessageToQueue(get_message, reply_messages_queue_); - */ + auto data_point_list = model_config_->getInputs().at(schema_type); + + MessageService::createAndQueueSubscribeMessage(object_id, schema_type, + data_point_list.subscribe, + *request_registry_, reply_messages_queue_); } std::cout << " - Handshake succeeded!\n\n"; @@ -139,17 +128,21 @@ void WebSocketClient::handshake(boost::system::error_code ec) { */ void WebSocketClient::sendMessage(const json& message) { connection_->asyncWrite(message); } -void WebSocketClient::onSendMessage(boost::system::error_code ec, std::size_t bytes_transferred) { - if (ec) { - return Fail(ec, "write"); +void WebSocketClient::onSendMessage(boost::system::error_code error_code, + std::size_t bytes_transferred) { + if (error_code) { + Fail(error_code, "write"); + return; } std::cout << "Message sent! " << bytes_transferred << " bytes transferred\n\n"; connection_->asyncRead(); } -void WebSocketClient::onReceiveMessage(boost::beast::error_code ec, std::size_t bytes_transferred) { - if (ec) { - return Fail(ec, "read"); +void WebSocketClient::onReceiveMessage(boost::beast::error_code error_code, + std::size_t bytes_transferred) { + if (error_code) { + Fail(error_code, "read"); + return; } const auto received_message = std::make_shared(connection_->getReceivedMessage()); @@ -172,8 +165,15 @@ void WebSocketClient::processMessage(const std::shared_ptr& m auto prio_message = *response_messages_queue_.front(); response_messages_queue_.erase(response_messages_queue_.begin()); - const auto data_message = message_service_.getDataMessageOrLogStatus(prio_message); + // Attempt to extract a data message from the priority message + // or process the status message and log any errors if present + const auto data_message = + MessageService::getDataOrProcessStatusFromMessage(prio_message, *request_registry_); + + // Process the data message if it is valid if (data_message.has_value()) { + // If the message is a regular data message, process it as a triple and + // handle the reasoning queries triple_assembler_.transformMessageToTriple(data_message.value()); // Process reasoning query @@ -185,11 +185,11 @@ void WebSocketClient::processMessage(const std::shared_ptr& m model_config_->getOutput() + "/reasoning_output/"); if (!result.empty()) { - auto set_messages = - BoService::createSetMessage(model_config_->getObjectId(), result); - for (auto& set_message : set_messages) { - message_service_.addMessageToQueue(set_message, reply_messages_queue_); - } + auto data_point_list = model_config_->getInputs(); + + MessageService::createAndQueueSetMessage( + model_config_->getObjectId(), result, *request_registry_, + reply_messages_queue_, system_config_.reasoner_server.origin_system_name); } } catch (const std::exception& e) { std::cerr << "Error processing reasoning query: " << e.what() << std::endl; diff --git a/cdsp/knowledge-layer/connector/websocket-client/src/websocket_client.h b/cdsp/knowledge-layer/connector/websocket-client/src/websocket_client.h index 6391cc5c..b0ca3571 100644 --- a/cdsp/knowledge-layer/connector/websocket-client/src/websocket_client.h +++ b/cdsp/knowledge-layer/connector/websocket-client/src/websocket_client.h @@ -10,11 +10,11 @@ #include "data_types.h" #include "file_handler_impl.h" -#include "json_writer.h" #include "message_service.h" #include "model_config.h" #include "reasoner_service.h" #include "reasoning_query_service.h" +#include "request_registry.h" #include "triple_assembler.h" #include "triple_writer.h" #include "websocket_interface.h" @@ -26,7 +26,7 @@ using json = nlohmann::json; class WebSocketClient : public std::enable_shared_from_this { public: - WebSocketClient(const SystemConfig& system_config, std::shared_ptr model_config, + WebSocketClient(SystemConfig system_config, std::shared_ptr model_config, std::shared_ptr reasoner_service, std::shared_ptr connection = nullptr); @@ -43,14 +43,13 @@ class WebSocketClient : public std::enable_shared_from_this { SystemConfig system_config_; net::io_context io_context_; std::shared_ptr connection_; - std::shared_ptr reasoner_service_; std::shared_ptr model_config_; - TripleAssembler triple_assembler_; + std::shared_ptr reasoner_service_; std::shared_ptr reasoner_query_service_; - FileHandlerImpl file_handler_; + std::shared_ptr request_registry_; TripleWriter triple_writer_; - MessageService message_service_; - + TripleAssembler triple_assembler_; + FileHandlerImpl file_handler_; std::vector reply_messages_queue_; std::vector> response_messages_queue_; diff --git a/cdsp/knowledge-layer/symbolic-reasoner/rdfox/tests/rdfox_adapter_unit_test.cpp b/cdsp/knowledge-layer/symbolic-reasoner/rdfox/tests/rdfox_adapter_unit_test.cpp index 953b1289..b048f0aa 100644 --- a/cdsp/knowledge-layer/symbolic-reasoner/rdfox/tests/rdfox_adapter_unit_test.cpp +++ b/cdsp/knowledge-layer/symbolic-reasoner/rdfox/tests/rdfox_adapter_unit_test.cpp @@ -1,28 +1,30 @@ #include #include -#include - #include "mock_rdfox_adapter.h" #include "mock_request_builder.h" #include "random_utils.h" #include "rdfox_adapter.h" class RDFoxAdapterTest : public ::testing::Test { + // NOLINTBEGIN(cppcoreguidelines-non-private-member-variables-in-classes) protected: const std::string DATASTORE = "ds-test"; - const ReasonerServerData server_data_{"localhost", "8080", "auth", DATASTORE}; + const ReasonerServerData server_data_{"localhost", "8080", "auth", "", DATASTORE}; + + std::shared_ptr mock_rdfox_adapter_; + std::unique_ptr mock_request_builder_; + // NOLINTEND(cppcoreguidelines-non-private-member-variables-in-classes) void SetUp() override { mock_rdfox_adapter_ = std::make_shared(server_data_); mock_request_builder_ = std::make_unique( server_data_.host, server_data_.port, server_data_.auth_base64); } - - std::shared_ptr mock_rdfox_adapter_; - std::unique_ptr mock_request_builder_; }; +// Unit tests for RDFoxAdapter to verify success regular operations + /** * @brief Test fixture for RDFoxAdapter to check the existence of a datastore. * @@ -35,7 +37,7 @@ TEST_F(RDFoxAdapterTest, CheckExistentDatastore) { const std::string expected_response = "datastore ds-test"; // Create a mock RequestBuilder - MockRequestBuilder* mock_request_builder_ptr = mock_request_builder_.get(); + MockRequestBuilder *mock_request_builder_ptr = mock_request_builder_.get(); // Mock `createRequestBuilder` to return the mock RequestBuilder EXPECT_CALL(*mock_rdfox_adapter_, createRequestBuilder()) @@ -57,7 +59,8 @@ TEST_F(RDFoxAdapterTest, CheckExistentDatastore) { } /** - * @brief Unit test for RDFoxAdapter to verify the initialization process creates a datastore. + * @brief Unit test for RDFoxAdapter to verify the initialization process + * creates a datastore. */ TEST_F(RDFoxAdapterTest, InitializationCreatesDatastore) { const std::string target_create_data = "/datastores/" + DATASTORE; @@ -66,7 +69,7 @@ TEST_F(RDFoxAdapterTest, InitializationCreatesDatastore) { EXPECT_CALL(*mock_rdfox_adapter_, checkDataStore()).WillOnce(testing::Return(false)); // Create a mock RequestBuilder - MockRequestBuilder* mock_request_builder_ptr = mock_request_builder_.get(); + MockRequestBuilder *mock_request_builder_ptr = mock_request_builder_.get(); // Mock createRequestBuilder to return the shared_ptr to MockRequestBuilder EXPECT_CALL(*mock_rdfox_adapter_, createRequestBuilder()) @@ -87,8 +90,8 @@ TEST_F(RDFoxAdapterTest, InitializationCreatesDatastore) { } /** - * @brief Unit test for RDFoxAdapter to verify the behavior during initialization - * when a datastore already exists. + * @brief Unit test for RDFoxAdapter to verify the behavior during + * initialization when a datastore already exists. */ TEST_F(RDFoxAdapterTest, InitializationWithExistingDatastore) { // Mock check for the existence of the datastore to return true @@ -102,7 +105,8 @@ TEST_F(RDFoxAdapterTest, InitializationWithExistingDatastore) { } /** - * @brief Unit test for RDFoxAdapter to verify the behavior when loading data into a datastore. + * @brief Unit test for RDFoxAdapter to verify the behavior when loading data + * into a datastore. */ TEST_F(RDFoxAdapterTest, LoadDataSuccess) { const std::string target = "/datastores/" + DATASTORE + "/content"; @@ -110,7 +114,7 @@ TEST_F(RDFoxAdapterTest, LoadDataSuccess) { const ReasonerSyntaxType content_type = ReasonerSyntaxType::TURTLE; // Create a mock RequestBuilder - MockRequestBuilder* mock_request_builder_ptr = mock_request_builder_.get(); + MockRequestBuilder *mock_request_builder_ptr = mock_request_builder_.get(); // Mock createRequestBuilder to return the shared_ptr to MockRequestBuilder EXPECT_CALL(*mock_rdfox_adapter_, createRequestBuilder()) @@ -143,7 +147,7 @@ TEST_F(RDFoxAdapterTest, QueryDataSuccess) { const std::string mock_response = ""; // Create a mock RequestBuilder - MockRequestBuilder* mock_request_builder_ptr = mock_request_builder_.get(); + MockRequestBuilder *mock_request_builder_ptr = mock_request_builder_.get(); // Mock createRequestBuilder to return the shared_ptr to MockRequestBuilder EXPECT_CALL(*mock_rdfox_adapter_, createRequestBuilder()) @@ -171,7 +175,8 @@ TEST_F(RDFoxAdapterTest, QueryDataSuccess) { } /** - * @brief Unit test for RDFoxAdapter to verify successful deletion of a datastore. + * @brief Unit test for RDFoxAdapter to verify successful deletion of a + * datastore. */ TEST_F(RDFoxAdapterTest, DeleteDataStoreSuccess) { const std::string target = "/datastores/" + DATASTORE; @@ -180,7 +185,7 @@ TEST_F(RDFoxAdapterTest, DeleteDataStoreSuccess) { EXPECT_CALL(*mock_rdfox_adapter_, checkDataStore()).WillOnce(testing::Return(true)); // Create a mock RequestBuilder - MockRequestBuilder* mock_request_builder_ptr = mock_request_builder_.get(); + MockRequestBuilder *mock_request_builder_ptr = mock_request_builder_.get(); // Mock createRequestBuilder to return the shared_ptr to MockRequestBuilder EXPECT_CALL(*mock_rdfox_adapter_, createRequestBuilder()) @@ -199,13 +204,11 @@ TEST_F(RDFoxAdapterTest, DeleteDataStoreSuccess) { } /** - * @brief Unit test for RDFoxAdapter to verify the behavior when attempting to delete a - nonexistent + * @brief Unit test for RDFoxAdapter to verify the behavior when attempting to + delete a nonexistent * datastore. */ TEST_F(RDFoxAdapterTest, DeleteNonexistentDataStoreSuccess) { - const std::string target = "/datastores/" + DATASTORE; - // Mock check for the existence of the datastore to return false EXPECT_CALL(*mock_rdfox_adapter_, checkDataStore()).WillOnce(testing::Return(false)); @@ -216,7 +219,7 @@ TEST_F(RDFoxAdapterTest, DeleteNonexistentDataStoreSuccess) { EXPECT_TRUE(mock_rdfox_adapter_->deleteDataStore()); } -// Unit tests for RDFoxAdapter to verify cursor-related operations +// Unit tests for RDFoxAdapter to verify success cursor-related operations /** * @brief Unit test for RDFoxAdapter to verify successful connection creation. @@ -231,7 +234,7 @@ TEST_F(RDFoxAdapterTest, CreateConnectionSuccess) { {"RDFox-Authentication-Token", random_auth}}; // Get the raw pointer to the mock_request_builder_ - MockRequestBuilder* mock_request_builder_ptr = mock_request_builder_.get(); + MockRequestBuilder *mock_request_builder_ptr = mock_request_builder_.get(); // Mock createRequestBuilder to return the mock RequestBuilder EXPECT_CALL(*mock_rdfox_adapter_, createRequestBuilder()) @@ -273,7 +276,7 @@ TEST_F(RDFoxAdapterTest, CreateCursorSuccess) { const std::map response_headers = {{"Location", location_header}}; // Get the raw pointer to the mock_request_builder_ - MockRequestBuilder* mock_request_builder_ptr = mock_request_builder_.get(); + MockRequestBuilder *mock_request_builder_ptr = mock_request_builder_.get(); ASSERT_TRUE(mock_request_builder_); @@ -322,7 +325,7 @@ TEST_F(RDFoxAdapterTest, AdvanceCursorSuccess) { "&limit=" + std::to_string(limit); // Create a mock RequestBuilder - MockRequestBuilder* mock_request_builder_ptr = mock_request_builder_.get(); + MockRequestBuilder *mock_request_builder_ptr = mock_request_builder_.get(); // Mock createRequestBuilder to return the mock RequestBuilder EXPECT_CALL(*mock_rdfox_adapter_, createRequestBuilder()) @@ -360,7 +363,7 @@ TEST_F(RDFoxAdapterTest, DeleteCursorSuccess) { "/datastores/" + DATASTORE + "/connections/" + connection_id + "/cursors/" + cursor_id; // Create a mock RequestBuilder - MockRequestBuilder* mock_request_builder_ptr = mock_request_builder_.get(); + MockRequestBuilder *mock_request_builder_ptr = mock_request_builder_.get(); // Mock createRequestBuilder to return the mock RequestBuilder EXPECT_CALL(*mock_rdfox_adapter_, createRequestBuilder()) @@ -381,10 +384,11 @@ TEST_F(RDFoxAdapterTest, DeleteCursorSuccess) { EXPECT_TRUE(success); } -// Unit tests for RDFoxAdapter to verify error handling +// Unit tests for RDFoxAdapter to verify error handling of generic operations /** - * @brief Unit test for RDFoxAdapter to verify behavior when datastore creation fails. + * @brief Unit test for RDFoxAdapter to verify behavior when datastore creation + * fails. */ TEST_F(RDFoxAdapterTest, FailedToCreateDataStore) { const std::string target_create_data = "/datastores/" + DATASTORE; @@ -393,7 +397,7 @@ TEST_F(RDFoxAdapterTest, FailedToCreateDataStore) { EXPECT_CALL(*mock_rdfox_adapter_, checkDataStore()).WillOnce(testing::Return(false)); // Create a mock RequestBuilder - MockRequestBuilder* mock_request_builder_ptr = mock_request_builder_.get(); + MockRequestBuilder *mock_request_builder_ptr = mock_request_builder_.get(); // Mock createRequestBuilder to return the shared_ptr to MockRequestBuilder EXPECT_CALL(*mock_rdfox_adapter_, createRequestBuilder()) @@ -422,7 +426,7 @@ TEST_F(RDFoxAdapterTest, LoadDataFailure) { const ReasonerSyntaxType content_type = ReasonerSyntaxType::TURTLE; // Create a mock RequestBuilder - MockRequestBuilder* mock_request_builder_ptr = mock_request_builder_.get(); + MockRequestBuilder *mock_request_builder_ptr = mock_request_builder_.get(); // Mock createRequestBuilder to return the shared_ptr to MockRequestBuilder EXPECT_CALL(*mock_rdfox_adapter_, createRequestBuilder()) @@ -447,7 +451,8 @@ TEST_F(RDFoxAdapterTest, LoadDataFailure) { } /** - * @brief Unit test for RDFoxAdapter to verify behavior when a SPARQL query fails. + * @brief Unit test for RDFoxAdapter to verify behavior when a SPARQL query + * fails. */ TEST_F(RDFoxAdapterTest, QueryDataFailure) { const std::string target = "/datastores/" + DATASTORE + "/sparql"; @@ -455,7 +460,7 @@ TEST_F(RDFoxAdapterTest, QueryDataFailure) { const DataQueryAcceptType accept_type = DataQueryAcceptType::TEXT_TSV; // Create a mock RequestBuilder - MockRequestBuilder* mock_request_builder_ptr = mock_request_builder_.get(); + MockRequestBuilder *mock_request_builder_ptr = mock_request_builder_.get(); // Mock createRequestBuilder to return the shared_ptr to MockRequestBuilder EXPECT_CALL(*mock_rdfox_adapter_, createRequestBuilder()) @@ -482,8 +487,8 @@ TEST_F(RDFoxAdapterTest, QueryDataFailure) { } /** - * @brief Unit test for RDFoxAdapter to verify the behavior when attempting to delete a - * datastore fails. + * @brief Unit test for RDFoxAdapter to verify the behavior when attempting to + * delete a datastore fails. */ TEST_F(RDFoxAdapterTest, DeleteDataStoreFailure) { const std::string target = "/datastores/" + DATASTORE; @@ -492,7 +497,7 @@ TEST_F(RDFoxAdapterTest, DeleteDataStoreFailure) { EXPECT_CALL(*mock_rdfox_adapter_, checkDataStore()).WillOnce(testing::Return(true)); // Create a mock RequestBuilder - MockRequestBuilder* mock_request_builder_ptr = mock_request_builder_.get(); + MockRequestBuilder *mock_request_builder_ptr = mock_request_builder_.get(); // Mock createRequestBuilder to return the shared_ptr to MockRequestBuilder EXPECT_CALL(*mock_rdfox_adapter_, createRequestBuilder()) @@ -513,13 +518,14 @@ TEST_F(RDFoxAdapterTest, DeleteDataStoreFailure) { // Unit tests for RDFoxAdapter to verify cursor-related operations /** - * @brief Unit test for RDFoxAdapter to verify the behavior when a connection cannot be created. + * @brief Unit test for RDFoxAdapter to verify the behavior when a connection + * cannot be created. */ TEST_F(RDFoxAdapterTest, CreateConnectionFailure) { const std::string target = "/datastores/" + DATASTORE + "/connections"; // Create a mock RequestBuilder - MockRequestBuilder* mock_request_builder_ptr = mock_request_builder_.get(); + MockRequestBuilder *mock_request_builder_ptr = mock_request_builder_.get(); // Mock createRequestBuilder to return the mock RequestBuilder EXPECT_CALL(*mock_rdfox_adapter_, createRequestBuilder()) @@ -542,13 +548,11 @@ TEST_F(RDFoxAdapterTest, CreateConnectionFailure) { } /** - * @brief Unit test for RDFoxAdapter to verify the behavior when header values are missing after - * create connection. + * @brief Unit test for RDFoxAdapter to verify the behavior when header values + * are missing after create connection. */ TEST_F(RDFoxAdapterTest, CreateConnectionMissingHeader) { const std::string target = "/datastores/" + DATASTORE + "/connections"; - const std::string random_connection_id = - std::to_string(RandomUtils::generateRandomInt(0, 1000)); const std::string random_header_value = RandomUtils::generateRandomString(8); const std::pair valid_headers = {"Location", "RDFox-Authentication-Token"}; @@ -558,7 +562,7 @@ TEST_F(RDFoxAdapterTest, CreateConnectionMissingHeader) { {header, random_header_value}}; // Missing header required values // Get the raw pointer to the mock_request_builder_ - MockRequestBuilder* mock_request_builder_ptr = mock_request_builder_.get(); + MockRequestBuilder *mock_request_builder_ptr = mock_request_builder_.get(); ASSERT_TRUE(mock_request_builder_); @@ -582,8 +586,8 @@ TEST_F(RDFoxAdapterTest, CreateConnectionMissingHeader) { } /** - * @brief Unit test for RDFoxAdapter to verify the behavior when the location header format is - * incorrect after creating a connection. + * @brief Unit test for RDFoxAdapter to verify the behavior when the location + * header format is incorrect after creating a connection. */ TEST_F(RDFoxAdapterTest, CreateConnectionWrongLocationHeaderFormat) { const std::string target = "/datastores/" + DATASTORE + "/connections"; @@ -592,7 +596,7 @@ TEST_F(RDFoxAdapterTest, CreateConnectionWrongLocationHeaderFormat) { {"Location", "wrong_format_without_slashes"}, {"RDFox-Authentication-Token", random_auth}}; // Get the raw pointer to the mock_request_builder_ - MockRequestBuilder* mock_request_builder_ptr = mock_request_builder_.get(); + MockRequestBuilder *mock_request_builder_ptr = mock_request_builder_.get(); // Mock createRequestBuilder to return the mock RequestBuilder EXPECT_CALL(*mock_rdfox_adapter_, createRequestBuilder()) @@ -614,7 +618,8 @@ TEST_F(RDFoxAdapterTest, CreateConnectionWrongLocationHeaderFormat) { } /** - * @brief Unit test for RDFoxAdapter to verify the behavior when a cursor cannot be created. + * @brief Unit test for RDFoxAdapter to verify the behavior when a cursor cannot + * be created. */ TEST_F(RDFoxAdapterTest, CreateCursorFailure) { const std::string connection_id = RandomUtils::generateRandomString(8); @@ -624,7 +629,7 @@ TEST_F(RDFoxAdapterTest, CreateCursorFailure) { "/datastores/" + DATASTORE + "/connections/" + connection_id + "/cursors"; // Create a mock RequestBuilder - MockRequestBuilder* mock_request_builder_ptr = mock_request_builder_.get(); + MockRequestBuilder *mock_request_builder_ptr = mock_request_builder_.get(); // Mock createRequestBuilder to return the mock RequestBuilder EXPECT_CALL(*mock_rdfox_adapter_, createRequestBuilder()) @@ -650,8 +655,8 @@ TEST_F(RDFoxAdapterTest, CreateCursorFailure) { } /** - * @brief Unit test for RDFoxAdapter to verify the behavior when location header is missing after - * cursor creation. + * @brief Unit test for RDFoxAdapter to verify the behavior when location header + * is missing after cursor creation. */ TEST_F(RDFoxAdapterTest, CreateCursorMissingLocationHeader) { const std::string connection_id = RandomUtils::generateRandomString(8); @@ -664,7 +669,7 @@ TEST_F(RDFoxAdapterTest, CreateCursorMissingLocationHeader) { std::map response_headers = {}; // Get the raw pointer to the mock_request_builder_ - MockRequestBuilder* mock_request_builder_ptr = mock_request_builder_.get(); + MockRequestBuilder *mock_request_builder_ptr = mock_request_builder_.get(); ASSERT_TRUE(mock_request_builder_); @@ -693,8 +698,8 @@ TEST_F(RDFoxAdapterTest, CreateCursorMissingLocationHeader) { } /** - * @brief Unit test for RDFoxAdapter to verify the behavior when the location header format is - * incorrect after creating a cursor. + * @brief Unit test for RDFoxAdapter to verify the behavior when the location + * header format is incorrect after creating a cursor. */ TEST_F(RDFoxAdapterTest, CreateCursorWrongLocationHeaderFormat) { const std::string connection_id = RandomUtils::generateRandomString(8); @@ -705,7 +710,7 @@ TEST_F(RDFoxAdapterTest, CreateCursorWrongLocationHeaderFormat) { std::map response_headers = {{"Location", "wrong_format"}}; // Get the raw pointer to the mock_request_builder_ - MockRequestBuilder* mock_request_builder_ptr = mock_request_builder_.get(); + MockRequestBuilder *mock_request_builder_ptr = mock_request_builder_.get(); ASSERT_TRUE(mock_request_builder_); @@ -734,7 +739,8 @@ TEST_F(RDFoxAdapterTest, CreateCursorWrongLocationHeaderFormat) { } /** - * @brief Unit test for RDFoxAdapter to verify the behavior when cursor advance fails. + * @brief Unit test for RDFoxAdapter to verify the behavior when cursor advance + * fails. */ TEST_F(RDFoxAdapterTest, AdvanceCursorFailure) { const std::string connection_id = RandomUtils::generateRandomString(8); @@ -751,7 +757,7 @@ TEST_F(RDFoxAdapterTest, AdvanceCursorFailure) { "&limit=" + std::to_string(limit); // Create a mock RequestBuilder - MockRequestBuilder* mock_request_builder_ptr = mock_request_builder_.get(); + MockRequestBuilder *mock_request_builder_ptr = mock_request_builder_.get(); // Mock createRequestBuilder to return the mock RequestBuilder EXPECT_CALL(*mock_rdfox_adapter_, createRequestBuilder()) @@ -777,8 +783,8 @@ TEST_F(RDFoxAdapterTest, AdvanceCursorFailure) { } /** - * @brief Unit test for RDFoxAdapter to verify the behavior when cursor advance fails due to - * invalid operation value. + * @brief Unit test for RDFoxAdapter to verify the behavior when cursor advance + * fails due to invalid operation value. */ TEST_F(RDFoxAdapterTest, AdvanceCursorInvalidOperation) { const std::string connection_id = RandomUtils::generateRandomString(8); @@ -787,9 +793,6 @@ TEST_F(RDFoxAdapterTest, AdvanceCursorInvalidOperation) { const DataQueryAcceptType accept_type = DataQueryAcceptType::SPARQL_JSON; const std::string operation = "invalid_operation"; - const std::string target = "/datastores/" + DATASTORE + "/connections/" + connection_id + - "/cursors/" + cursor_id + "?operation=" + operation; - // Ensure that any request to create the datastore is not made. EXPECT_CALL(*mock_rdfox_adapter_, createRequestBuilder()).Times(0); @@ -801,7 +804,8 @@ TEST_F(RDFoxAdapterTest, AdvanceCursorInvalidOperation) { } /** - * @brief Unit test for RDFoxAdapter to verify the behavior when cursor deletion fails. + * @brief Unit test for RDFoxAdapter to verify the behavior when cursor deletion + * fails. */ TEST_F(RDFoxAdapterTest, DeleteCursorFailure) { const std::string connection_id = RandomUtils::generateRandomString(8); @@ -810,7 +814,7 @@ TEST_F(RDFoxAdapterTest, DeleteCursorFailure) { "/datastores/" + DATASTORE + "/connections/" + connection_id + "/cursors/" + cursor_id; // Create a mock RequestBuilder - MockRequestBuilder* mock_request_builder_ptr = mock_request_builder_.get(); + MockRequestBuilder *mock_request_builder_ptr = mock_request_builder_.get(); // Mock createRequestBuilder to return the mock RequestBuilder EXPECT_CALL(*mock_rdfox_adapter_, createRequestBuilder()) From 5d003a9f5d4e2080e20d59568ad8922291f54b3e Mon Sep 17 00:00:00 2001 From: Haonan Qiu Date: Mon, 16 Feb 2026 18:22:24 +0100 Subject: [PATCH 4/7] Support format/root in JSON-RPC get/subscribe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Allows strings or integer for the field id in IL request validator * Applies method field to json message in KL * Extract schema type e.g. Vehicle from data points list in txt file in the KL * Fix missing class declaration in the constructor in KL * Add implementation for root and format options on the GET endpoint in IL * Apply format * Apply format * Add Test to cover all escenarios for the root and format functionality * - Subscription state now includes request metadata (requestId, path, root, format) and stores them per client subscription. - Replacement on same id: when a client subscribes again with the same requestId, the existing subscription is replaced (vin, datapoints, path, root, format updated). - Updates now respect get‑style formatting: emitted data changes use stored root, format, path, and set response id to requestId. - The Unsubscription method finds all matching subscriptions by filtering for both: - VIN matching (message.instance) - Exact datapoints (same count and all items match) - Removes all matching subscriptions, if there were multiple subscriptions for the same VIN with identical datapoint sets, they'll all be removed. - Cleans up correctly, if no subscriptions remain for the client, removes the websocket from the map. - Some code optimation. - Tests has updated for new fields and replacement behavior for subscription and unsubscription. * Update the README with the current supported messages and parameters. * Fix wrong default parameter for root into the message parameters * Fix console message, when status message is received in the KL --------- Update IL Readme to reflect the new API Move ErrorMessage.ts content into NewMessage.ts where all other BO objects are defined. - add DTOs for data, ok, and error responses - map BOs to response DTOs - propagate requestId into responses - separate ok vs error handling in websocket server Migrate IL websocket request messages to new API version. * Add jsonrpc envelope to requests * Add DTOs for request messages (GET/SET/SUBSCRIBE/UNSUBSCRIBE) * Adjust validation for this DTOs * Add DTO to BO mapper Signed-off-by: Haonan Qiu --- cdsp/information-layer/README.md | 201 +++--- .../handlers/src/HandlerBase.ts | 448 +++++++++++--- .../handlers/src/HandlerCreator.ts | 8 +- .../handlers/src/iotdb/src/IoTDBHandler.ts | 43 +- .../src/iotdb/src/SubscriptionSimulator.ts | 406 ++++++++---- .../src/iotdb/test/IoTDBHandler.test.ts | 15 +- .../iotdb/test/SubscriptionSimulator.test.ts | 581 ++++++++++++------ .../handlers/test/HandlerBase.format.test.ts | 425 +++++++++++++ .../router/src/websocket-server.ts | 52 +- .../router/test/RequestValidator.test.ts | 183 +++--- .../router/utils/ErrorMessage.ts | 17 - .../router/utils/MessageDTO.ts | 76 +++ .../router/utils/MessageMapper.ts | 155 +++++ .../router/utils/NewMessage.ts | 44 +- .../router/utils/RequestValidator.ts | 237 ++++--- .../utils/error-message-helper.ts | 19 - .../model/ontologies/test_ontology.ttl | 18 +- .../model/queries/output/test_query.rq | 10 +- .../model/rules/test_rule.dlog | 21 +- .../model/shacl/vehicle_shacl.ttl | 126 ++-- .../data-objects/dto/get_message_dto.h | 8 +- .../data-objects/dto/subscribe_message_dto.h | 8 +- ...asoning_query_service_integration_test.cpp | 34 +- .../converters/model_config_converter.cpp | 16 +- .../converters/model_config_converter.h | 2 +- .../services/message_service.cpp | 2 +- .../websocket-client/src/websocket_client.cpp | 3 +- examples/remotivelabs-feeder/rl-bridge.py | 34 +- 28 files changed, 2284 insertions(+), 908 deletions(-) create mode 100644 cdsp/information-layer/handlers/test/HandlerBase.format.test.ts create mode 100644 cdsp/information-layer/router/utils/MessageDTO.ts create mode 100644 cdsp/information-layer/router/utils/MessageMapper.ts delete mode 100644 cdsp/information-layer/utils/error-message-helper.ts diff --git a/cdsp/information-layer/README.md b/cdsp/information-layer/README.md index ea4082e9..a14fc1e9 100644 --- a/cdsp/information-layer/README.md +++ b/cdsp/information-layer/README.md @@ -5,8 +5,8 @@ Its intention is to abstract the underlying data storage database technology. It Clients can interact with it using websockets and JSON payload. - The IL consists of two logical components: + - database handler: Interaction with the chosen database, configurable - router: API provider that connects to the database handler @@ -20,23 +20,30 @@ flowchart LR iotdb_handler <--> iotdb ``` -# Information Layer Server - Hello World Setup +# Information Layer Server - Hello World Setup ### Build the typescript application natively Make sure all libraries are installed + ```bash npm install ``` + Build the application + ```bash npm run build ``` + Run the unit tests + ```bash npx jest ``` + or + ```bash npx jest --verbose ``` @@ -46,23 +53,30 @@ npx jest --verbose ### Start the database Start the db: + ```bash docker run -d --rm --name iotdb-service -p 6667:6667 -p 9003:9003 apache/iotdb:latest ``` + if you plan on running the Information Layer also with docker, IotDB and IL container need to be in the same network: + ```bash docker network create cdsp-net docker run -d --rm --name iotdb-service --network cdsp-net -p 6667:6667 -p 9003:9003 apache/iotdb:latest ``` Connect to it via cli to create or view the data (optional): + ```bash docker exec -it iotdb ./start-cli.sh -h 127.0.0.1 -p 6667 -u root -pw root ``` + Create a database (optional) + ```bash create database root.Vehicles ``` + Create desired timeseries (optional) ```bash @@ -73,10 +87,13 @@ create timeseries root.Vehicles.Vehicle_Speed WITH DATATYPE=FLOAT, ENCODING=RLE ### Start Information Layer Server Build the Information Layer image + ```bash docker build -t information-layer . ``` + Run IL + ```bash # Docker docker run --rm --name information-layer --network cdsp-net -p 8080:8080 -e HANDLER_TYPE=iotdb -e IOTDB_HOST=iotdb-service information-layer @@ -87,7 +104,6 @@ HANDLER_TYPE=iotdb IOTDB_HOST=localhost npm start ## Run IL with backend option 2: Document Data Service RealmDB - ### Prepare cloud - Ensure that in your [ATLAS cloud](https://cloud.mongodb.com/) app there is a vehicle _document_ with an @@ -104,11 +120,13 @@ Vehicle_TraveledDistance: "" (Double) ### Start Information Layer Server Build the Information Layer image + ```bash docker build -t information-layer . ``` Run the Information Layer + ```bash # Docker docker run -d --rm --name information-layer --network cdsp-net -p 8080:8080 -e HANDLER_TYPE=realmdb -e VERSION_REALMDB_SCHEMA=0 -e REALMDB_APP_ID= -e REALMDB_API_KEY= information-layer @@ -130,139 +148,156 @@ The examples use [websocat](https://github.com/vi/websocat) and [jq](https://git ## Get Schema: + ```yaml # only the root node provided to get all data points { - "type": "get", - "instance": "VIN_123", - "schema": "Vehicle", - "requestId": "123-456" + "jsonrpc": "2.0", + "method": "get", + "id": "123-456", # this can be any string or number, it is used to match the response with the request + "params": { + "instance": "VIN_123", + "schema": "Vehicle", + "format": "nested", # this is an optional parameter and sets how the received data should be formatted. It can be either "nested" or "flat". If not provided, the default is "nested" + "root": "relative" # this is an optional parameter and sets whether the received data should be to the requested path. It can be either "relative" or "absolute". If not provided, the default is "relative" + } } # with path to non-leaf node to get multiple data points below this node { - "type": "get", - "instance": "VIN_123", - "schema": "Vehicle", - "path": "CurrentLocation", - "requestId": "123-456" + "jsonrpc": "2.0", + "method": "get", + "id": "123-456", + "params": { + "instance": "VIN_123", + "schema": "Vehicle", + "path": "CurrentLocation" + } } # with path to leaf node to get one data point { - "type": "get", - "instance": "VIN_123", - "schema": "Vehicle", - "path": "CurrentLocation.Latitude", - "requestId": "123-456" + "jsonrpc": "2.0", + "method": "get", + "id": "123-456", + "params": { + "instance": "VIN_123", + "schema": "Vehicle", + "path": "CurrentLocation.Latitude" } +} ``` Example: + ```bash -echo '{"type": "get","instance": "VIN_123","schema": "Vehicle","path": "Speed","requestId": "123-456"}' | websocat ws://localhost:8080 -n1 | jq +echo '{"jsonrpc":"2.0","method":"get","id":"123-456","params":{"instance":"VIN_123","schema":"Vehicle","path":"Speed"}}' | websocat ws://localhost:8080 -n1 | jq ``` ## Set Schema: + ```yaml -# leaf node with single value in data +# Leaf node with single value in data { - "type": "set", - "path": "CurrentLocation.Latitude", - "instance": "VIN_123", - "schema": "Vehicle", - "data": 21, - "requestId": "235-183" + "jsonrpc": "2.0", + "method": "set", + "id": "123-456", + "params": { + "instance": "VIN_123", + "schema": "Vehicle", + "path": "CurrentLocation.Latitude", + "data": 21 + } } -# root node with multiple values in data nested and flat (no path provided) + +# Root node with multiple values in data nested and flat (no path provided) { - "type": "set", - "instance": "VIN_123", - "schema": "Vehicle", - "data": { - "CurrentLocation": { - "Latitude": 22, - "Longitude": 46 - }, - "Chassis.SteeringWheel.Angle": 22 - }, - "requestId": "235-183" + "jsonrpc": "2.0", + "method": "set", + "id": "123-456", + "params": { + "instance": "VIN_123", + "schema": "Vehicle", + "data": { + "CurrentLocation": { + "Latitude": 22, + "Longitude": 46 + }, + "Chassis.SteeringWheel.Angle": 22 + } + } } -# non-leaf node with multiple values in data + +# Non-leaf node with multiple values in data { - "type": "set", - "instance": "VIN_123", - "schema": "Vehicle", - "path": "CurrentLocation", - "data": { - "Latitude": 22, - "Longitude": 46 - }, - "requestId": "235-183" + "jsonrpc": "2.0", + "method": "set", + "id": "123-456", + "params": { + "instance": "VIN_123", + "schema": "Vehicle", + "path": "CurrentLocation", + "data": { + "Latitude": 22, + "Longitude": 46 + } + } } ``` + Example: + ```bash -echo '{"type": "set","path": "CurrentLocation.Latitude","instance": "VIN_123","schema": "Vehicle","data": 21}' | websocat ws://localhost:8080 -n1 | jq +echo '{"jsonrpc":"2.0","method":"set","id":"123-456","params":{"instance":"VIN_123","schema":"Vehicle","path":"CurrentLocation.Latitude","data":22}}' | websocat ws://localhost:8080 -n1 | jq ``` + ```yaml -{ - "type": "status", - "code": 200, - "message": "Successfully wrote data to database.", - "timestamp": { - "seconds": 1738613888, - "nanos": 143000000 - } -} +{ "jsonrpc": "2.0", "id": "123-456", "result": {} } ``` ## Subscribe Schema: + ```yaml { - "type": "subscribe", - "instance": "VIN_123", - "schema": "Vehicle" + "jsonrpc": "2.0", + "method": "subscribe", + "id": "123-456", # this can be any string or number, it is used to match the response with the request + "params": + { "instance": "VIN_123", "schema": "Vehicle" } + # "path": "CurrentLocation", # this is an optional parameter and sets the path to subscribe to. If not provided, it subscribes to the root node of the schema, + "format": "nested", # this is an optional parameter and sets how the received data should be formatted. It can be either "nested" or "flat". If not provided, the default is "nested" + "root": "relative", # this is an optional parameter and sets whether the received data should be to the requested path. It can be either "relative" or "absolute". If not provided, the default is "relative" } ``` + ```bash -echo '{"type": "subscribe", "instance": "VIN_123", "schema": "Vehicle"}' | websocat ws://localhost:8080 -n | jq +echo '{"jsonrpc":"2.0","method":"subscribe","id":"123-456","params":{"instance":"VIN_123","schema":"Vehicle"}}' | websocat ws://localhost:8080 -n | jq ``` On success: + ```yaml -{ - "type": "status", - "code": 200, - "message": "Successfully subscribed", - "timestamp": { - "seconds": 1738613971, - "nanos": 955000000 - } -} +{ "jsonrpc": "2.0", "id": "123-456", "result": {} } ``` + ## Unsubscribe ```yaml { - "type": "unsubscribe", - "schema": "Vehicle", - "instance": "VIN_123" + "jsonrpc": "2.0", + "method": "unsubscribe", + "id": "123-456", + "params": { "instance": "VIN_123", "schema": "Vehicle" }, } ``` +> [!Note] +> The unsubscribe method matches the subscription based on the provided parameters with the active subscriptions. This means that if the subscription was created with a path, the same path needs to be provided to unsubscribe successfully. Also the instance and schema parameters are required to match the subscription, otherwise the unsubscribe request will not be successful. + On success: + ```yaml -{ - "type": "status", - "code": 200, - "message": "Successfully unsubscribed", - "timestamp": { - "seconds": 1738614001, - "nanos": 562000000 - } -} +{ "jsonrpc": "2.0", "id": "123-456", "result": {} } ``` - diff --git a/cdsp/information-layer/handlers/src/HandlerBase.ts b/cdsp/information-layer/handlers/src/HandlerBase.ts index 9d519e02..41cc9818 100644 --- a/cdsp/information-layer/handlers/src/HandlerBase.ts +++ b/cdsp/information-layer/handlers/src/HandlerBase.ts @@ -3,67 +3,128 @@ import yaml from "js-yaml"; import { DataContentMessage, ensureMessageType, + ErrorMessage, + STATUS_ERRORS, GetMessageType, NewMessage, NewMessageType, SetMessageType, StatusMessage, - SubscribeMessageType, UnsubscribeMessageType -} from "../../router/utils/NewMessage" -import {DataPointSchema, WebSocketWithId} from "../../utils/database-params" -import {ErrorMessage, STATUS_ERRORS} from "../../router/utils/ErrorMessage"; -import {getDataPointsPath} from "../config/config"; -import {logMessage, LogMessageType} from "../../utils/logger"; -import {replaceDotsWithUnderscore, replaceUnderscoresWithDots, toResponseFormat} from "../utils/transformations"; -import {databaseParams} from "./iotdb/config/database-params"; + SubscribeMessageType, + UnsubscribeMessageType, +} from "../../router/utils/NewMessage"; +import { DataPointSchema, WebSocketWithId } from "../../utils/database-params"; +import { getDataPointsPath } from "../config/config"; +import { logMessage, LogMessageType } from "../../utils/logger"; +import { + replaceDotsWithUnderscore, + replaceUnderscoresWithDots, + toResponseFormat, +} from "../utils/transformations"; +import { databaseParams } from "./iotdb/config/database-params"; export abstract class HandlerBase { + /** + * Function to use for sending messages to the client, so that formating and mapping is applied to all messages. + * @private + */ + private readonly sendMessage: ( + ws: WebSocketWithId, + message: StatusMessage | DataContentMessage | ErrorMessage, + ) => void; + + protected constructor( + sendMessageFn: ( + ws: WebSocketWithId, + message: StatusMessage | DataContentMessage | ErrorMessage, + ) => void, + ) { + this.sendMessage = sendMessageFn; + } /** * Returns a list of all known data point names that begin with the specified prefix. */ - abstract getKnownDatapointsByPrefix(prefix: string): string[] + abstract getKnownDatapointsByPrefix(prefix: string): string[]; /** * Returns the dataPoints from DB based on the provided list of dataPoints and VIN. * @param dataPoints to extract from DB * @param vin VIN */ - abstract getDataPointsFromDB(dataPoints: string[], vin: string): Promise; + abstract getDataPointsFromDB( + dataPoints: string[], + vin: string, + ): Promise; // Default implementations of required functions async authenticateAndConnect(): Promise { logMessage( "authenticateAndConnect() is not implemented", - LogMessageType.WARNING + LogMessageType.WARNING, ); } async get(message: GetMessageType, ws: WebSocketWithId): Promise { const requestedDataPoints = this.getKnownDatapointsByPrefix(message.path); - if (requestedDataPoints.length === 0) { // no valid datapoints found for requested path - this.sendRequestedDataPointsNotFoundErrorMsg(ws, message.path); + if (requestedDataPoints.length === 0) { + // no valid datapoints found for requested path + this.sendRequestedDataPointsNotFoundErrorMsg( + ws, + message.path, + message.requestId, + ); return; } // Access the DB for the datapoints and their values - const queryResult = await this.getDataPointsFromDB(requestedDataPoints, message.instance); + const queryResult = await this.getDataPointsFromDB( + requestedDataPoints, + message.instance, + ); - this.sendGetResponseToClient(queryResult, message.instance, requestedDataPoints, ws); + this.sendGetResponseToClient( + queryResult, + message.instance, + requestedDataPoints, + ws, + message.requestId, + message.path, + message.root, + message.format, + ); } - protected sendRequestedDataPointsNotFoundErrorMsg(ws: WebSocketWithId, path: string) { - this.sendMessageToClient(ws, this.createStatusMessage( - STATUS_ERRORS.BAD_REQUEST, - `There are no known datapoints with prefix ${replaceUnderscoresWithDots(path)}` - )); + protected sendRequestedDataPointsNotFoundErrorMsg( + ws: WebSocketWithId, + path: string, + requestId: string, + ) { + this.sendMessageToClient( + ws, + this.createErrorMessage( + STATUS_ERRORS.BAD_REQUEST, + `Not found`, + `There are no known datapoints with prefix ${replaceUnderscoresWithDots(path)}`, + requestId, + ), + ); } - protected sendAlreadySubscribedErrorMsg(ws: WebSocketWithId, instance: string, newDataPoints: string[]) { - const statusMessage = this.createStatusMessage(STATUS_ERRORS.BAD_REQUEST, - `Subscription already done to instance '${instance}' ` + - `and datapoints [${toResponseFormat(newDataPoints)}].`); + protected sendAlreadySubscribedErrorMsg( + ws: WebSocketWithId, + instance: string, + newDataPoints: string[], + requestId: string, + ) { + const statusMessage = this.createErrorMessage( + STATUS_ERRORS.BAD_REQUEST, + `Subscription failed`, + `Already subscribed to instance '${instance}' ` + + `and datapoints [${toResponseFormat(newDataPoints)}]`, + requestId, + ); this.sendMessageToClient(ws, statusMessage); } @@ -71,71 +132,123 @@ export abstract class HandlerBase { logMessage("set() is not implemented", LogMessageType.WARNING); } - protected subscribe(message: SubscribeMessageType, ws: WebSocketWithId): void { + protected subscribe( + message: SubscribeMessageType, + ws: WebSocketWithId, + ): void { logMessage("subscribe() is not implemented", LogMessageType.WARNING); } - protected unsubscribe(message: UnsubscribeMessageType, ws: WebSocketWithId): void { + protected unsubscribe( + message: UnsubscribeMessageType, + ws: WebSocketWithId, + ): void { logMessage("unsubscribe() is not implemented", LogMessageType.WARNING); } unsubscribe_client(ws: WebSocketWithId): void { - logMessage("unsubscribe_client() is not implemented", LogMessageType.WARNING); + logMessage( + "unsubscribe_client() is not implemented", + LogMessageType.WARNING, + ); } handleMessage(message: NewMessage, ws: WebSocketWithId): void { try { switch (message.type) { case "get": - const aGetMessage = ensureMessageType(message, NewMessageType.Get); + const aGetMessage = ensureMessageType( + message, + NewMessageType.Get, + ); void this.get(aGetMessage, ws); break; case "set": - const aSetMessage = ensureMessageType(message, NewMessageType.Set); + const aSetMessage = ensureMessageType( + message, + NewMessageType.Set, + ); this.set(aSetMessage, ws); break; case "subscribe": - const aSubscribeMessage = ensureMessageType(message, NewMessageType.Subscribe); + const aSubscribeMessage = ensureMessageType( + message, + NewMessageType.Subscribe, + ); this.subscribe(aSubscribeMessage, ws); break; case "unsubscribe": - const anUnsubscribeMessage = ensureMessageType(message, NewMessageType.Unsubscribe); + const anUnsubscribeMessage = + ensureMessageType( + message, + NewMessageType.Unsubscribe, + ); this.unsubscribe(anUnsubscribeMessage, ws); break; default: - ws.send(JSON.stringify({error: "Unknown message type"})); + throw new Error("Unknown message type."); } } catch (error: unknown) { const errMsg = error instanceof Error ? error.message : "Unknown error"; - ws.send(errMsg); + this.sendMessage( + ws, + this.createErrorMessage( + STATUS_ERRORS.INTERNAL_SERVER_ERROR, + "Internal server error", + errMsg, + message.requestId, + ), + ); } } /** * Takes the queryResults and sends a response to the client. 3 different responses are possible: - * 1. Valida response with all available datapoints in the DB (number of datapoints can be less than requested) + * 1. Valid response with all available datapoints in the DB (number of datapoints can be less than requested) * 2. Client Error 404 if no requested datapoint could be found. * 3. Internal Server Error 500 if the DB access was not successful. - * */ protected sendGetResponseToClient( queryResult: QueryResult, vin: string, requestedDataPoints: string[], - ws: WebSocketWithId): void { + ws: WebSocketWithId, + requestId: string, + path: string, + root: "absolute" | "relative", + format: "nested" | "flat", + ): void { // There was an issue accessing the DB, notify the client and return if (!queryResult.success) { - this.sendMessageToClient(ws, this.createStatusMessage(STATUS_ERRORS.INTERNAL_SERVER_ERROR, queryResult.error)); + this.sendMessageToClient( + ws, + this.createErrorMessage( + STATUS_ERRORS.INTERNAL_SERVER_ERROR, + "Internal server error", + queryResult.error, + requestId, + ), + ); return; } - let responseMessage: DataContentMessage | StatusMessage; + let responseMessage: DataContentMessage | ErrorMessage; if (queryResult.dataPoints.length > 0) { - responseMessage = this.createDataContentMessage(vin, queryResult.dataPoints, queryResult.metadata); + responseMessage = this.createDataContentMessage( + vin, + queryResult.dataPoints, + root, + format, + path, + queryResult.metadata, + requestId, + ); } else { - responseMessage = this.createStatusMessage( + responseMessage = this.createErrorMessage( STATUS_ERRORS.NOT_FOUND, - `No values found for dataPoints [${replaceUnderscoresWithDots(requestedDataPoints.join(", "))}] for the instance: ${vin}` + `Data not found`, + `No values found for dataPoints [${replaceUnderscoresWithDots(requestedDataPoints.join(", "))}] for the instance: ${vin}`, + requestId, ); } @@ -147,10 +260,9 @@ export abstract class HandlerBase { */ protected sendMessageToClient( ws: WebSocketWithId, - message: StatusMessage | DataContentMessage | ErrorMessage + message: StatusMessage | DataContentMessage | ErrorMessage, ): void { - logMessage(JSON.stringify(message), LogMessageType.SENT, ws.id); - ws.send(JSON.stringify(message)); + this.sendMessage(ws, message); } /** @@ -163,14 +275,37 @@ export abstract class HandlerBase { protected createStatusMessage( code: number, statusMessage: string, - requestId?: string + requestId: string, ): StatusMessage { return { - type: "status", // Constant string - code, // Status code - message: statusMessage, // Status message - ...(requestId && {requestId}), // Include requestId only if provided - timestamp: getCurrentTimestamp(), // Generated timestamp + type: "status", + code: code, + message: statusMessage, + requestId: requestId, + timestamp: getCurrentTimestamp(), + }; + } + + /** + * Generic function to create an error message. + * @param code - status code (http-based). + * @param message - A short message. + * @param reason - A descriptive reason for the error. + * @param requestId - The ID of the corresponding request. + * @returns - A error message. + */ + protected createErrorMessage( + code: number, + message: string, + reason: string, + requestId: string, + ): ErrorMessage { + return { + type: "error", + code: code, + message: message, + reason: reason, + requestId: requestId, }; } @@ -178,38 +313,66 @@ export abstract class HandlerBase { * Generic function to create a data content message. * @param instance - The ID of the element in the tree. * @param dataPoints - the data points with values. + * @param root - "absolute" or "relative" paths. + * @param format - "nested" or "flat" data structure. + * @param path - The path associated with the data points. * @param metadata - available metadata for the data points * @param requestId - The ID of the corresponding request. - * @param shouldBeNested - Controls format of the data response structure: Nested if true, flat if false. * @returns - A data content message. */ protected createDataContentMessage( instance: string, dataPoints: Array<{ name: string; value: any }>, + root: "absolute" | "relative", + format: "nested" | "flat", + path: string, metadata?: Array<{ name: string; value: any }>, requestId?: string, - shouldBeNested: boolean = false ): DataContentMessage { // Ensure nodes are valid if (dataPoints.length === 0) { throw new Error("Nodes array cannot be empty."); } + // Process root first: determine the base path for each datapoint + // Then process format: structure the output as flat or nested + const schema = getSchemaOrThrow(dataPoints); + const requestedPath = normalizeRequestedPath( + replaceUnderscoresWithDots(path), + schema, + ); + return { type: "data", instance: instance, - schema: getSchemaOrThrow(dataPoints), - data: shouldBeNested ? buildDataStructureAsTreeWithoutRootElement(dataPoints) : buildDataStructureFlatWithoutRootElement(dataPoints), + schema: schema, + data: + format === "nested" + ? buildDataStructureAsTree(dataPoints, root, requestedPath, schema) + : buildDataStructureFlat(dataPoints, root, requestedPath, schema), requestId: requestId, - ...(metadata && Object.keys(metadata).length && {metadata: buildDataStructureFlatWithoutRootElement(metadata)}), + ...(metadata && + Object.keys(metadata).length && { + metadata: buildDataStructureFlat( + metadata, + root, + requestedPath, + schema, + ), + }), }; } - - protected extractNodesFromData(path: string, data: Record): Record { + protected extractNodesFromData( + path: string, + data: Record, + ): Record { const result: Record = {}; - function traverse(currentPath: string, currentData: Record): void { + function traverse( + currentPath: string, + currentData: Record, + ): void { for (const [key, value] of Object.entries(currentData)) { const fullPath = `${currentPath}.${key}`; if (typeof value === "object" && value !== null) { @@ -253,7 +416,7 @@ export abstract class HandlerBase { private extractDataTypes( dataPointsObj: any, parentKey = "", - result: { [key: string]: any } = {} + result: { [key: string]: any } = {}, ): { [key: string]: any } { for (const key in dataPointsObj) { if (dataPointsObj.hasOwnProperty(key)) { @@ -300,7 +463,7 @@ export abstract class HandlerBase { */ protected validateNodesAgainstSchema( message: NewMessage, - dataPointsSchema: DataPointSchema + dataPointsSchema: DataPointSchema, ): string | null { // build the nodes from path and data if (message.type == NewMessageType.PermissionsEdit) { @@ -309,17 +472,22 @@ export abstract class HandlerBase { const data = message.type === NewMessageType.Set ? message.data : undefined; const requestNodes = extractLeafNodesFromRequest(message.path, data); - const unknownFields = Array.from(requestNodes).filter(node => { + const unknownFields = Array.from(requestNodes).filter((node) => { return !dataPointsSchema.hasOwnProperty(node); }); if (unknownFields.length > 0) { - return "Could not find node: " + replaceUnderscoresWithDots(unknownFields.join(", ")); + return ( + "Could not find node: " + + replaceUnderscoresWithDots(unknownFields.join(", ")) + ); } return null; } - protected extractNodesFromMessage(message: SetMessageType): Record { + protected extractNodesFromMessage( + message: SetMessageType, + ): Record { const result: Record = {}; if (typeof message.data === "object" && message.data !== null) { @@ -336,10 +504,12 @@ export abstract class HandlerBase { return result; } - protected extractNodesFromMessageWithVinAsNode(message: SetMessageType): Record { + protected extractNodesFromMessageWithVinAsNode( + message: SetMessageType, + ): Record { const result = this.extractNodesFromMessage(message); // Add VIN as Node - const {dataPointId} = databaseParams["VSS"]; + const { dataPointId } = databaseParams["VSS"]; result[dataPointId] = message.instance; return result; } @@ -368,7 +538,7 @@ function extractLeafNodesFromRequest(path: string, data?: any): Set { } return new Set( - Array.from(leafNodes, (value) => replaceDotsWithUnderscore(value)) + Array.from(leafNodes, (value) => replaceDotsWithUnderscore(value)), ); } @@ -378,38 +548,119 @@ function extractLeafNodesFromRequest(path: string, data?: any): Set { * @param nodes list of nodes with names and values */ function getSchemaOrThrow(nodes: Array<{ name: string; value: any }>) { - return nodes[0]?.name.split(".")[0] ?? (() => { - throw new Error("Nodes array is empty"); - })(); + return ( + nodes[0]?.name.split(".")[0] ?? + (() => { + throw new Error("Nodes array is empty"); + })() + ); } -// Helper function to build the flat data structure string without the root element -function buildDataStructureFlatWithoutRootElement( - nodes: Array<{ name: string; value: any }> +/** + * Build flat data structure. + * Process root first: determine base path relative to requested path. + * Then format as flat: single-level key-value pairs. + */ +function buildDataStructureFlat( + nodes: Array<{ name: string; value: any }>, + root: "absolute" | "relative", + requestedPath: string, + schema: string, ): Record { - return nodes.reduce((accumulator, {name, value}) => { - const nameWithoutRoot = name.includes(".") ? name.split(".").slice(1).join(".") : name; - accumulator[nameWithoutRoot] = value; - return accumulator; - }, {} as Record); + return nodes.reduce( + (accumulator, { name, value }) => { + let processedName: string; + + if (root === "relative") { + // Remove everything up to and including the requested path + // E.g., "Vehicle.CurrentLocation.Latitude" with path "CurrentLocation" -> "Latitude" + // E.g., "Vehicle.Speed" with path "" -> "Speed" + const fullPath = requestedPath ? `${schema}.${requestedPath}` : schema; + if (name.startsWith(fullPath + ".")) { + processedName = name.substring(fullPath.length + 1); + } else if (name === fullPath) { + processedName = ""; // Exact match - use empty string + } else if (name.startsWith(schema + ".")) { + // Fall back to removing the schema prefix + processedName = name.substring(schema.length + 1); + } else { + processedName = name; + } + } else { + // Absolute: keep full path excluding only the schema prefix + // E.g., "Vehicle.CurrentLocation.Latitude" -> "CurrentLocation.Latitude" + processedName = name.includes(".") + ? name.split(".").slice(1).join(".") + : name; + } + + accumulator[processedName] = value; + return accumulator; + }, + {} as Record, + ); } -// Helper function to build the nested data structure string without the root element -function buildDataStructureAsTreeWithoutRootElement( +/** + * Build nested data structure. + * Process root first: determine base path relative to requested path. + * Then format as nested: hierarchical object structure. + */ +function buildDataStructureAsTree( nodes: Array<{ name: string; value: any }>, + root: "absolute" | "relative", + requestedPath: string, + schema: string, ): any { + // First, process each node name based on root setting + const processedNodes = nodes.map(({ name, value }) => { + let processedName: string; + + if (root === "relative") { + // Remove everything up to and including the requested path + const fullPath = requestedPath ? `${schema}.${requestedPath}` : schema; + if (name.startsWith(fullPath + ".")) { + processedName = name.substring(fullPath.length + 1); + } else if (name === fullPath) { + processedName = ""; // Exact match - return value directly + } else if (name.startsWith(schema + ".")) { + // Fall back to removing the schema prefix + processedName = name.substring(schema.length + 1); + } else { + processedName = name; + } + } else { + // Absolute: keep full path excluding only the schema prefix + processedName = name.includes(".") + ? name.split(".").slice(1).join(".") + : name; + } + + return { name: processedName, value }; + }); + + // Special case: if there's only one node with empty name, return the value directly + if (processedNodes.length === 1 && processedNodes[0].name === "") { + return processedNodes[0].value; + } + + // Build nested structure from processed names const data: any = {}; - const splitNames = nodes.map((node) => node.name.split(".")); - for (let i = 0; i < nodes.length; i++) { + for (const { name, value } of processedNodes) { + if (name === "") { + // Empty path - this shouldn't happen in nested format with multiple nodes + continue; + } + + const pathParts = name.split("."); let currentLevel = data; - const remainingPath = splitNames[i].slice(1); // Remaining part of the path after the common prefix - for (let j = 0; j < remainingPath.length; j++) { - const key = remainingPath[j]; - if (j === remainingPath.length - 1) { + for (let j = 0; j < pathParts.length; j++) { + const key = pathParts[j]; + if (j === pathParts.length - 1) { // Leaf node - currentLevel[key] = nodes[i].value; + currentLevel[key] = value; } else { // Intermediate node if (!currentLevel[key]) { @@ -423,14 +674,33 @@ function buildDataStructureAsTreeWithoutRootElement( return data; } +function normalizeRequestedPath(path: string, schema: string): string { + if (!path) { + return ""; + } + + if (path === schema) { + return ""; + } + + if (path.startsWith(schema + ".")) { + return path.substring(schema.length + 1); + } + + return path; +} + function getCurrentTimestamp(): { seconds: number; nanos: number } { const now = new Date(); const seconds = Math.floor(now.getTime() / 1000); // Convert milliseconds to seconds const nanos = (now.getTime() % 1000) * 1e6; // Remainder in milliseconds converted to nanoseconds - return {seconds, nanos: nanos}; + return { seconds, nanos: nanos }; } export type QueryResult = - | { success: true; dataPoints: Array<{ name: string; value: any }>; metadata: Array<{ name: string; value: any }> } + | { + success: true; + dataPoints: Array<{ name: string; value: any }>; + metadata: Array<{ name: string; value: any }>; + } | { success: false; error: string }; - diff --git a/cdsp/information-layer/handlers/src/HandlerCreator.ts b/cdsp/information-layer/handlers/src/HandlerCreator.ts index 754b4690..43b797a3 100644 --- a/cdsp/information-layer/handlers/src/HandlerCreator.ts +++ b/cdsp/information-layer/handlers/src/HandlerCreator.ts @@ -2,8 +2,12 @@ import { getHandlerType } from "../config/config"; import { HandlerBase } from "./HandlerBase"; import { IoTDBHandler } from "./iotdb/src/IoTDBHandler"; import { logWithColor, COLORS } from "../../utils/logger"; +import {StatusMessage, DataContentMessage, ErrorMessage} from "../../router/utils/NewMessage"; +import { WebSocketWithId } from "../../utils/database-params"; -export function createHandler() : HandlerBase { +export function createHandler( + sendMessage: (ws: WebSocketWithId, message: StatusMessage | DataContentMessage | ErrorMessage) => void +) : HandlerBase { const handlerType: string = getHandlerType(); logWithColor(`\n ** Handler: ${handlerType} ** \n`, COLORS.BOLD); @@ -12,7 +16,7 @@ export function createHandler() : HandlerBase { // Instantiate the correct handler based on the handler type switch (handlerType) { case "iotdb": - handler = new IoTDBHandler(); + handler = new IoTDBHandler(sendMessage); break; default: throw new Error("Unsupported handler type"); diff --git a/cdsp/information-layer/handlers/src/iotdb/src/IoTDBHandler.ts b/cdsp/information-layer/handlers/src/iotdb/src/IoTDBHandler.ts index f98fa212..f735a908 100644 --- a/cdsp/information-layer/handlers/src/iotdb/src/IoTDBHandler.ts +++ b/cdsp/information-layer/handlers/src/iotdb/src/IoTDBHandler.ts @@ -7,14 +7,18 @@ import {IoTDBDataInterpreter} from "../utils/IoTDBDataInterpreter"; import {createDataPointsSchema, isSupportedDataPoint, SupportedDataPoints,} from "../config/iotdb-config"; import {databaseConfig, databaseParams} from "../config/database-params"; import {COLORS, logErrorStr, logWithColor,} from "../../../../utils/logger"; -import {STATUS_ERRORS, STATUS_SUCCESS} from "../../../../router/utils/ErrorMessage"; import {WebSocketWithId} from "../../../../utils/database-params"; import {transformSessionDataSet} from "../utils/database-helper"; import { + DataContentMessage, + ErrorMessage, NewMessage, SetMessageType, + STATUS_ERRORS, + STATUS_SUCCESS, StatusMessage, - SubscribeMessageType, UnsubscribeMessageType + SubscribeMessageType, + UnsubscribeMessageType } from "../../../../router/utils/NewMessage"; import {removeSuffixFromString, replaceDotsWithUnderscore, replaceUnderscoresWithDots} from "../../../utils/transformations"; @@ -26,17 +30,18 @@ export class IoTDBHandler extends HandlerBase { private subscriptionSimulator: SubscriptionSimulator; private dataPointsSchema: SupportedDataPoints = {}; - constructor() { - super(); + constructor(sendMessage: (ws: WebSocketWithId, message: StatusMessage | DataContentMessage | ErrorMessage) => void) { + super(sendMessage); if (!databaseConfig) { throw new Error("Invalid database configuration."); } this.session = new Session(); this.subscriptionSimulator = getSubscriptionSimulator( - this.sendMessageToClient, + this.sendMessageToClient.bind(this), this.createDataContentMessage, this.createStatusMessage, - this.sendAlreadySubscribedErrorMsg); + this.createErrorMessage, + this.sendAlreadySubscribedErrorMsg.bind(this)); } async authenticateAndConnect(): Promise { @@ -60,7 +65,7 @@ export class IoTDBHandler extends HandlerBase { const newDataPoints = this.getKnownDatapointsByPrefix(message.path); if (newDataPoints.length === 0) { - this.sendRequestedDataPointsNotFoundErrorMsg(ws, message.path) + this.sendRequestedDataPointsNotFoundErrorMsg(ws, message.path, message.requestId) return; } @@ -70,7 +75,7 @@ export class IoTDBHandler extends HandlerBase { protected unsubscribe(message: UnsubscribeMessageType, ws: WebSocketWithId): void { const dataPointsToUnsub = this.getKnownDatapointsByPrefix(message.path); if (dataPointsToUnsub.length === 0) { - this.sendRequestedDataPointsNotFoundErrorMsg(ws, message.path) + this.sendRequestedDataPointsNotFoundErrorMsg(ws, message.path, message.requestId) return; } @@ -84,7 +89,7 @@ export class IoTDBHandler extends HandlerBase { protected async set(message: SetMessageType, ws: WebSocketWithId): Promise { if (this.areNodesValid(message, ws)) { - let statusMessage: StatusMessage; + let statusMessage: StatusMessage | ErrorMessage; try { const data = { ...this.extractNodesFromMessageWithVinAsNode(message), @@ -113,10 +118,13 @@ export class IoTDBHandler extends HandlerBase { COLORS.GREY ); - statusMessage = this.createStatusMessage(STATUS_SUCCESS.OK, "Successfully wrote data to database."); + statusMessage = this.createStatusMessage(STATUS_SUCCESS.OK, "Successfully wrote data to database.", message.requestId); } catch (error: unknown) { const errMsg = error instanceof Error ? error.message : "Unknown error"; - statusMessage = this.createStatusMessage(STATUS_ERRORS.SERVICE_UNAVAILABLE, `Failed writing data. ${errMsg}`); + statusMessage = this.createErrorMessage(STATUS_ERRORS.SERVICE_UNAVAILABLE, + `Database service unavailable`, + `Failed writing data. ${errMsg}`, + message.requestId); } this.sendMessageToClient(ws, statusMessage); } @@ -160,15 +168,18 @@ export class IoTDBHandler extends HandlerBase { * @returns - Returns true if all nodes are valid against the schema, otherwise false. */ private areNodesValid(message: NewMessage, ws: WebSocketWithId): boolean { - const errorMessage = this.validateNodesAgainstSchema( + const errorReason = this.validateNodesAgainstSchema( message, this.dataPointsSchema ); - if (errorMessage) { - logErrorStr(`Error validating message nodes against schema: ${errorMessage}`); - const statusMessage = this.createStatusMessage(STATUS_ERRORS.NOT_FOUND, errorMessage); - this.sendMessageToClient(ws, statusMessage); + if (errorReason) { + logErrorStr(`Error validating message nodes against schema: ${errorReason}`); + const errorMessage = this.createErrorMessage(STATUS_ERRORS.NOT_FOUND, + 'Nodes not found in schema', + errorReason, + message.requestId); + this.sendMessageToClient(ws, errorMessage); return false; } return true; diff --git a/cdsp/information-layer/handlers/src/iotdb/src/SubscriptionSimulator.ts b/cdsp/information-layer/handlers/src/iotdb/src/SubscriptionSimulator.ts index f4f29b6e..aaa55c6f 100644 --- a/cdsp/information-layer/handlers/src/iotdb/src/SubscriptionSimulator.ts +++ b/cdsp/information-layer/handlers/src/iotdb/src/SubscriptionSimulator.ts @@ -1,20 +1,30 @@ -import {logMessage, logError, logErrorStr} from "../../../../utils/logger"; -import {databaseConfig, databaseParams} from "../config/database-params"; -import {Session} from "./Session"; -import {SessionDataSet} from "../utils/SessionDataSet"; -import {WebSocketWithId} from "../../../../utils/database-params"; +import { logMessage, logError, logErrorStr } from "../../../../utils/logger"; +import { databaseConfig, databaseParams } from "../config/database-params"; +import { Session } from "./Session"; +import { SessionDataSet } from "../utils/SessionDataSet"; +import { WebSocketWithId } from "../../../../utils/database-params"; import { DataContentMessage, + ErrorMessage, + STATUS_ERRORS, + STATUS_SUCCESS, StatusMessage, SubscribeMessageType, - UnsubscribeMessageType + UnsubscribeMessageType, } from "../../../../router/utils/NewMessage"; -import {ErrorMessage, STATUS_ERRORS, STATUS_SUCCESS} from "../../../../router/utils/ErrorMessage"; -import {transformSessionDataSet} from "../utils/database-helper"; -import {toResponseFormat} from "../../../utils/transformations"; -import {METADATA_SUFFIX} from "../utils/iotdb-constants"; +import { transformSessionDataSet } from "../utils/database-helper"; +import { toResponseFormat } from "../../../utils/transformations"; +import { METADATA_SUFFIX } from "../utils/iotdb-constants"; + +export type Subscription = { + vin: string; + dataPoints: Set; + requestId: string; + path: string; + root: "absolute" | "relative"; + format: "nested" | "flat"; +}; -export type Subscription = { vin: string, dataPoints: Set } export type WebsocketToSubscriptionsMap = Map; const TREE_VSS = "VSS"; @@ -23,20 +33,44 @@ let subscriptionSimulatorInstance: SubscriptionSimulator | null = null; // Function to get or initialize the singleton instance export function getSubscriptionSimulator( - sendMessageToClient: (ws: WebSocketWithId, message: StatusMessage | DataContentMessage | ErrorMessage) => void, - createDataContentMessage: (instance: string, - dataPoints: Array<{ name: string; value: any; }>, - metadata: Array<{ name: string; value: any; }>, - requestId?: string) => DataContentMessage, - createStatusMessage: (code: number, statusMessage: string, requestId?: string) => StatusMessage, - sendAlreadySubscribedErrorMsg: (ws: WebSocketWithId, vin: string, newDataPoints: string[]) => void) - : SubscriptionSimulator { + sendMessageToClient: ( + ws: WebSocketWithId, + message: StatusMessage | DataContentMessage | ErrorMessage, + ) => void, + createDataContentMessage: ( + instance: string, + dataPoints: Array<{ name: string; value: any }>, + root: "absolute" | "relative", + format: "nested" | "flat", + path: string, + metadata?: Array<{ name: string; value: any }>, // FIXME: HandleBase contains optional metadata + requestId?: string, + ) => DataContentMessage, + createStatusMessage: ( + code: number, + statusMessage: string, + requestId: string, + ) => StatusMessage, + createErrorMessage: ( + code: number, + message: string, + reason: string, + requestId: string, + ) => ErrorMessage, + sendAlreadySubscribedErrorMsg: ( + ws: WebSocketWithId, + vin: string, + newDataPoints: string[], + requestId: string, + ) => void, +): SubscriptionSimulator { if (!subscriptionSimulatorInstance) { subscriptionSimulatorInstance = new SubscriptionSimulator( sendMessageToClient, createDataContentMessage, createStatusMessage, - sendAlreadySubscribedErrorMsg + createErrorMessage, + sendAlreadySubscribedErrorMsg, ); logMessage("SubscriptionSimulator instance created."); } @@ -48,28 +82,75 @@ export class SubscriptionSimulator { private session: Session; private timeIntervalLowerLimit: number | undefined = undefined; private websocketToSubscriptionsMap: WebsocketToSubscriptionsMap = new Map(); - private readonly sendMessageToClient: (ws: WebSocketWithId, message: StatusMessage | DataContentMessage | ErrorMessage) => void; + private readonly sendMessageToClient: ( + ws: WebSocketWithId, + message: StatusMessage | DataContentMessage | ErrorMessage, + ) => void; private readonly createDataContentMessage: ( instance: string, dataPoints: Array<{ name: string; value: any }>, - metadata: Array<{ name: string; value: any }>, - requestId?: string) => DataContentMessage; - private readonly createStatusMessage: (code: number, statusMessage: string, requestId?: string) => StatusMessage; - private readonly sendAlreadySubscribedErrorMsg: (ws: WebSocketWithId, vin: string, newDataPoints: string[]) => void; + root: "absolute" | "relative", + format: "nested" | "flat", + path: string, + metadata?: Array<{ name: string; value: any }>, // FIXME: HandleBase contains optional metadata + requestId?: string, + ) => DataContentMessage; + private readonly createStatusMessage: ( + code: number, + statusMessage: string, + requestId: string, + ) => StatusMessage; + private readonly createErrorMessage: ( + code: number, + message: string, + reason: string, + requestId: string, + ) => ErrorMessage; + private readonly sendAlreadySubscribedErrorMsg: ( + ws: WebSocketWithId, + vin: string, + newDataPoints: string[], + requestId: string, + ) => void; constructor( - sendMessageToClient: (ws: WebSocketWithId, message: StatusMessage | DataContentMessage | ErrorMessage) => void, - createDataContentMessage: (instance: string, - dataPoints: Array<{ name: string; value: any }>, - metadata: Array<{ name: string; value: any }>, - requestId?: string) => DataContentMessage, - createStatusMessage: (code: number, statusMessage: string, requestId?: string) => StatusMessage, - sendAlreadySubscribedErrorMsg: (ws: WebSocketWithId, vin: string, newDataPoints: string[]) => void) { + sendMessageToClient: ( + ws: WebSocketWithId, + message: StatusMessage | DataContentMessage | ErrorMessage, + ) => void, + createDataContentMessage: ( + instance: string, + dataPoints: Array<{ name: string; value: any }>, + root: "absolute" | "relative", + format: "nested" | "flat", + path: string, + metadata?: Array<{ name: string; value: any }>, // FIXME: HandleBase contains optional metadata + requestId?: string, + ) => DataContentMessage, + createStatusMessage: ( + code: number, + statusMessage: string, + requestId: string, + ) => StatusMessage, + createErrorMessage: ( + code: number, + message: string, + reason: string, + requestId: string, + ) => ErrorMessage, + sendAlreadySubscribedErrorMsg: ( + ws: WebSocketWithId, + vin: string, + newDataPoints: string[], + requestId: string, + ) => void, + ) { this.session = new Session(); this.notifyDatabaseChanges = this.notifyDatabaseChanges.bind(this); this.sendMessageToClient = sendMessageToClient; this.createDataContentMessage = createDataContentMessage; this.createStatusMessage = createStatusMessage; + this.createErrorMessage = createErrorMessage; this.sendAlreadySubscribedErrorMsg = sendAlreadySubscribedErrorMsg; void this.session.authenticateAndConnect(); @@ -97,28 +178,66 @@ export class SubscriptionSimulator { * @param newDataPoints list of data points the client wants to subscribe to * @returns void */ - async subscribe(message: SubscribeMessageType, wsOfNewSubscription: WebSocketWithId, newDataPoints: string[]): Promise { - const subscriptions: Subscription[] = this.websocketToSubscriptionsMap.get(wsOfNewSubscription) ?? []; - let subscription = subscriptions.find(value => value.vin === message.instance); + async subscribe( + message: SubscribeMessageType, + wsOfNewSubscription: WebSocketWithId, + newDataPoints: string[], // FIXME: Check if the data points are extracted from the path as array before calling this method + ): Promise { + const subscriptions: Subscription[] = + this.websocketToSubscriptionsMap.get(wsOfNewSubscription) ?? []; + let subscription = subscriptions.find( + (value) => value.requestId === message.requestId, + ); + if (!subscription) { - subscription = {vin: message.instance, dataPoints: new Set()}; + subscription = { + vin: message.instance, + dataPoints: new Set(), + requestId: message.requestId, + path: message.path, + root: message.root, + format: message.format, + }; subscriptions.push(subscription); // Add to the array + } else { + // Replace existing subscription for the same requestId + subscription.vin = message.instance; + subscription.path = message.path; + subscription.root = message.root; + subscription.format = message.format; + subscription.dataPoints = new Set(); } - const alreadySubscribed = newDataPoints.every(dataPoint => subscription.dataPoints.has(dataPoint)); + // FIXME: Check if this is required, seems that subscription.dataPoints is always empty here + const alreadySubscribed = newDataPoints.every((dataPoint) => + subscription.dataPoints.has(dataPoint), + ); + if (alreadySubscribed) { - this.sendAlreadySubscribedErrorMsg(wsOfNewSubscription, message.instance, newDataPoints); + this.sendAlreadySubscribedErrorMsg( + wsOfNewSubscription, + message.instance, + newDataPoints, + message.requestId, + ); return; } // Update subscribed data points - newDataPoints.forEach(dataPoint => subscription.dataPoints.add(dataPoint)); - this.websocketToSubscriptionsMap.set(wsOfNewSubscription, subscriptions) + newDataPoints.forEach((dataPoint) => + subscription.dataPoints.add(dataPoint), + ); + this.websocketToSubscriptionsMap.set(wsOfNewSubscription, subscriptions); this.startSubscriptionListenerIfNeeded(); - this.sendMessageToClient(wsOfNewSubscription, this.createStatusMessage(STATUS_SUCCESS.OK, - `Successfully subscribed to '${message.instance}' [${toResponseFormat(newDataPoints)}].` - )); + this.sendMessageToClient( + wsOfNewSubscription, + this.createStatusMessage( + STATUS_SUCCESS.OK, + `Successfully subscribed to '${message.instance}' [${toResponseFormat(newDataPoints)}].`, + message.requestId, + ), + ); logMessage("subscribed"); this.logSubscriptions(); @@ -126,7 +245,10 @@ export class SubscriptionSimulator { private startSubscriptionListenerIfNeeded() { if (this.intervalId === null) { - this.intervalId = setInterval(this.notifyDatabaseChanges, databaseConfig!.pollIntervalLenInSec * 1000); + this.intervalId = setInterval( + this.notifyDatabaseChanges, + databaseConfig!.pollIntervalLenInSec * 1000, + ); this.intervalId.unref(); // Ensure timer doesn't keep the process alive this.timeIntervalLowerLimit = Date.now(); logMessage("started timer"); @@ -134,9 +256,8 @@ export class SubscriptionSimulator { } /** - * Unsubscribes the client from the provided data points for given a given VIN (message.instance). - * If a subscription with this exists for the given client, the client is removed from the subscription. - * If no subscription exists, an error message is sent. + * Unsubscribes the client from all subscriptions that match the given VIN and exact datapoints. + * All subscriptions with the given VIN and containing exactly the same datapoints are removed. * If no subscriptions are left for the client, it is removed. * If no clients are left, the periodic database listener is disabled. * @param message unsubscribe message @@ -144,67 +265,84 @@ export class SubscriptionSimulator { * @param dataPointsToUnsub list of data point strings to unsubscribe the client from * @returns void */ - unsubscribe(message: UnsubscribeMessageType, wsOfSubscriptionToBeDeleted: WebSocketWithId, dataPointsToUnsub: string[]): void { - - let subscription = - this.websocketToSubscriptionsMap.get(wsOfSubscriptionToBeDeleted)?.find(value => value.vin === message.instance); + unsubscribe( + message: UnsubscribeMessageType, + wsOfSubscriptionToBeDeleted: WebSocketWithId, + dataPointsToUnsub: string[], + ): void { + const subscriptions = this.websocketToSubscriptionsMap.get( + wsOfSubscriptionToBeDeleted, + ); - if (!subscription) { - this.sendMessageToClient(wsOfSubscriptionToBeDeleted, this.createStatusMessage( - STATUS_ERRORS.NOT_FOUND, - `Cannot unsubscribe. No subscription for instance '${message.instance}'`) + if (!subscriptions) { + this.sendMessageToClient( + wsOfSubscriptionToBeDeleted, + this.createErrorMessage( + STATUS_ERRORS.NOT_FOUND, + `Subscription not found`, + `Cannot unsubscribe. No subscription for instance '${message.instance}'`, + message.requestId, + ), ); return; } - const notSubscribed = dataPointsToUnsub.every(dataPoint => !subscription.dataPoints.has(dataPoint)); + // Find all subscriptions matching the VIN and exact datapoints + const subscriptionsToRemove = subscriptions.filter((sub) => { + // Check if VIN matches + if (sub.vin !== message.instance) { + return false; + } + + // Check if subscription has exactly the same datapoints (same count and same items) + if (sub.dataPoints.size !== dataPointsToUnsub.length) { + return false; + } + + // Check if all datapoints to unsubscribe are in the subscription + return dataPointsToUnsub.every((dp) => sub.dataPoints.has(dp)); + }); - if (notSubscribed) { - this.sendMessageToClient(wsOfSubscriptionToBeDeleted, this.createStatusMessage( - STATUS_ERRORS.NOT_FOUND, - `Cannot unsubscribe. No subscription for instance '${message.instance}' ` + - `and datapoints [${toResponseFormat(dataPointsToUnsub)}].` - )); + // If no matching subscriptions found, return error + if (subscriptionsToRemove.length === 0) { + this.sendMessageToClient( + wsOfSubscriptionToBeDeleted, + this.createErrorMessage( + STATUS_ERRORS.NOT_FOUND, + `Subscription not found`, + `Cannot unsubscribe. No subscription for instance '${message.instance}' with the provided data points.`, + message.requestId, + ), + ); return; } - // Remove the data points from the subscription - dataPointsToUnsub.forEach((dataPoint) => subscription.dataPoints.delete(dataPoint)); - - this.cleanUpEmptySubscriptions(subscription, wsOfSubscriptionToBeDeleted, message.instance); - this.logSubscriptions(); - } + // Remove the matching subscriptions + const updatedSubscriptions = subscriptions.filter( + (sub) => !subscriptionsToRemove.includes(sub), + ); - /** - * Cleans up client and/or it's subscriptions if they are not needed any more. - * Stops the database listener if no clients are subscribed anymore. - * @param subscription latest modified subscription - * @param wsOfSubscriptionToBeDeleted websocket client - * @param vin VIN the clients subscribed to - * @private - */ - private cleanUpEmptySubscriptions(subscription: any, wsOfSubscriptionToBeDeleted: WebSocketWithId, vin: string) { - if (subscription.dataPoints.size == 0) { - const subscriptions = this.websocketToSubscriptionsMap.get(wsOfSubscriptionToBeDeleted); - if (subscriptions) { - // Filter out the subscription to be deleted - const updatedSubscriptions = subscriptions.filter(sub => sub.vin !== vin); - - if (updatedSubscriptions.length > 0) { - // Update the Map with the new subscriptions array - this.websocketToSubscriptionsMap.set(wsOfSubscriptionToBeDeleted, updatedSubscriptions); - } else { - // Remove the key from the Map if no subscriptions are left - this.websocketToSubscriptionsMap.delete(wsOfSubscriptionToBeDeleted); - } - - logMessage("unsubscribed"); - this.removeTimerIfNoSubscription(); - this.sendMessageToClient( - wsOfSubscriptionToBeDeleted, - this.createStatusMessage(STATUS_SUCCESS.OK, "Successfully unsubscribed")); - } + if (updatedSubscriptions.length > 0) { + this.websocketToSubscriptionsMap.set( + wsOfSubscriptionToBeDeleted, + updatedSubscriptions, + ); + } else { + // Remove the websocket from the map if no subscriptions are left + this.websocketToSubscriptionsMap.delete(wsOfSubscriptionToBeDeleted); } + + this.removeTimerIfNoSubscription(); + this.logSubscriptions(); + + this.sendMessageToClient( + wsOfSubscriptionToBeDeleted, + this.createStatusMessage( + STATUS_SUCCESS.OK, + "Successfully unsubscribed", + message.requestId, + ), + ); } /** @@ -226,8 +364,10 @@ export class SubscriptionSimulator { * Removes the timer if there is no subscription left. */ private removeTimerIfNoSubscription() { - if (this.intervalId !== null && - this.websocketToSubscriptionsMap.size === 0) { + if ( + this.intervalId !== null && + this.websocketToSubscriptionsMap.size === 0 + ) { clearInterval(this.intervalId); this.intervalId = null; this.timeIntervalLowerLimit = undefined; @@ -241,11 +381,15 @@ export class SubscriptionSimulator { */ private logSubscriptions(): void { let logString = "subscriptions:\n"; - for (const [ws, subscriptions] of this.websocketToSubscriptionsMap.entries()) { + for (const [ + ws, + subscriptions, + ] of this.websocketToSubscriptionsMap.entries()) { logString += `WS id: ${ws.id} is subscribed to: \n`; - subscriptions.forEach(sub => - logString += ` ${sub.vin} => [${Array.from(sub.dataPoints).join(", ")}] \n` - ) + subscriptions.forEach( + (sub) => + (logString += ` ${sub.vin} => [${Array.from(sub.dataPoints).join(", ")}] \n`), + ); } logMessage(logString); } @@ -260,21 +404,27 @@ export class SubscriptionSimulator { */ private async notifyDatabaseChanges(): Promise { if (!this.timeIntervalLowerLimit) { - logErrorStr("time of last check is undefined. Cannot go for notifications."); + logErrorStr( + "time of last check is undefined. Cannot go for notifications.", + ); return; } const timeIntervalUpperLimit = Date.now(); - for (const [ws, subscriptions] of this.websocketToSubscriptionsMap.entries()) { + for (const [ + ws, + subscriptions, + ] of this.websocketToSubscriptionsMap.entries()) { await Promise.all( subscriptions.map(async (subscription) => { let dataContentMessage = await this.checkForChanges( - subscription.vin, subscription.dataPoints, timeIntervalUpperLimit + subscription, + timeIntervalUpperLimit, ); if (dataContentMessage) { this.sendMessageToClient(ws, dataContentMessage); } - }) + }), ); } this.timeIntervalLowerLimit = timeIntervalUpperLimit; @@ -285,18 +435,25 @@ export class SubscriptionSimulator { * For each change it creates an update message for each client subscribed for this data point * and returns the update message. * - * @param vin VIN for checking value changes - * @param dataPoints data points that are changes relevant + * @param subscription subscription containing the vin and data points to check for changes * @param timeIntervalUpperLimit the upper limit of the time interval to be checked * @returns generated update message */ - private async checkForChanges(vin: string, dataPoints: Set, timeIntervalUpperLimit: number): Promise { - const {databaseName, dataPointId} = databaseParams[TREE_VSS as keyof typeof databaseParams]; - const metadataPoints = [...dataPoints].map((dataPoint) => dataPoint + METADATA_SUFFIX) - const fieldsToSearch = [...dataPoints, ...metadataPoints].join(", "); + private async checkForChanges( + subscription: Subscription, + timeIntervalUpperLimit: number, + ): Promise { + const { databaseName, dataPointId } = + databaseParams[TREE_VSS as keyof typeof databaseParams]; + const metadataPoints = [...subscription.dataPoints].map( + (dataPoint) => dataPoint + METADATA_SUFFIX, + ); + const fieldsToSearch = [...subscription.dataPoints, ...metadataPoints].join( + ", ", + ); const sql = `SELECT ${fieldsToSearch} FROM ${databaseName} - WHERE ${dataPointId} = '${vin}' + WHERE ${dataPointId} = '${subscription.vin}' AND Time > ${this.timeIntervalLowerLimit} AND Time <= ${timeIntervalUpperLimit} @@ -309,23 +466,32 @@ export class SubscriptionSimulator { // Check if sessionDataSet is not an instance of SessionDataSet, and handle the error if (!(sessionDataSet instanceof SessionDataSet)) { throw new Error( - "Failed to retrieve session data. Invalid session dataset." + "Failed to retrieve session data. Invalid session dataset.", ); } - const [data, metadata] = transformSessionDataSet(sessionDataSet, databaseName); + const [data, metadata] = transformSessionDataSet( + sessionDataSet, + databaseName, + ); if (data.length > 0) { dataContentMessage = this.createDataContentMessage( - vin, data, metadata + subscription.vin, + data, + subscription.root, + subscription.format, + subscription.path, + metadata, + subscription.requestId, + ); + logMessage( + `Processed ${data.length} changes for id ${subscription.vin}`, ); - logMessage(`Processed ${data.length} changes for id ${vin}`); } - } catch (error: unknown) { logError("Unknown error", error); } finally { return dataContentMessage; } } - } diff --git a/cdsp/information-layer/handlers/src/iotdb/test/IoTDBHandler.test.ts b/cdsp/information-layer/handlers/src/iotdb/test/IoTDBHandler.test.ts index 28099470..c81f5fd8 100644 --- a/cdsp/information-layer/handlers/src/iotdb/test/IoTDBHandler.test.ts +++ b/cdsp/information-layer/handlers/src/iotdb/test/IoTDBHandler.test.ts @@ -2,9 +2,8 @@ import {IoTDBHandler} from "../src/IoTDBHandler"; import {Session} from "../src/Session"; import {SessionDataSet} from "../utils/SessionDataSet"; import {WebSocketWithId} from "../../../../utils/database-params"; -import {NewMessageType, SetMessageType} from "../../../../router/utils/NewMessage"; +import {NewMessageType, SetMessageType, STATUS_SUCCESS} from "../../../../router/utils/NewMessage"; import {SupportedMessageDataTypes} from "../utils/iotdb-constants"; -import {STATUS_SUCCESS} from "../../../../router/utils/ErrorMessage"; jest.mock("../src/Session"); jest.mock("../src/SubscriptionSimulator"); @@ -29,7 +28,7 @@ describe("IoTDBHandler", () => { mockSession.authenticateAndConnect = jest.fn(); mockSession.executeQueryStatement = jest.fn(); - handler = new IoTDBHandler(); + handler = new IoTDBHandler(function(p1: WebSocketWithId,p2: any){}); (handler as any).session = mockSession; // Inject mocked session mockWebSocket = {id: "test-socket"} as jest.Mocked; handler["sendMessageToClient"] = jest.fn(); @@ -40,7 +39,7 @@ describe("IoTDBHandler", () => { expect(mockSession.authenticateAndConnect).toHaveBeenCalled(); }); - test("should query data points and metadata from IoTDB individually", async () => { + test("should query data points and metadata from IoTDB", async () => { const mockDataPoints = ["Temperature", "Speed"]; const vin = "TEST_VIN"; mockSession.executeQueryStatement.mockResolvedValueOnce(new SessionDataSet([], [], {}, 0, {}, 0, {}, {}, false)); @@ -48,10 +47,8 @@ describe("IoTDBHandler", () => { const result = await handler.getDataPointsFromDB(mockDataPoints, vin); const expectedCalls = [ - expect.stringContaining("SELECT Temperature"), - expect.stringContaining("SELECT Speed"), - expect.stringContaining("SELECT Temperature_Metadata"), - expect.stringContaining("SELECT Speed_Metadata"), + expect.stringContaining("SELECT Temperature,Temperature_Metadata"), + expect.stringContaining("SELECT Speed,Speed_Metadata"), ]; expectedCalls.forEach((expected, index) => { @@ -67,6 +64,7 @@ describe("IoTDBHandler", () => { type: NewMessageType.Set, instance: "TEST_VIN", path: "Vehicle", + requestId: "requestId", data: {Speed: 60, Temperature: 42.42}, metadata: {Speed: {unit: "km/h"}}, }; @@ -107,6 +105,7 @@ describe("IoTDBHandler", () => { instance: "", type: NewMessageType.Set, path: "Vehicle.Speed", + requestId: "requestId", data: {}, metadata: {VIN: "123456", Unit: "km/h"} }; diff --git a/cdsp/information-layer/handlers/src/iotdb/test/SubscriptionSimulator.test.ts b/cdsp/information-layer/handlers/src/iotdb/test/SubscriptionSimulator.test.ts index 58c94644..82f3571b 100644 --- a/cdsp/information-layer/handlers/src/iotdb/test/SubscriptionSimulator.test.ts +++ b/cdsp/information-layer/handlers/src/iotdb/test/SubscriptionSimulator.test.ts @@ -1,3 +1,14 @@ +import { SubscriptionSimulator } from "../src/SubscriptionSimulator"; +import { WebSocketWithId } from "../../../../utils/database-params"; +import { Session } from "../src/Session"; +import { + DataContentMessage, + ErrorMessage, + StatusMessage, + SubscribeMessageType, + UnsubscribeMessageType, +} from "../../../../router/utils/NewMessage"; + jest.mock("../config/database-params", () => ({ ...jest.requireActual("../config/database-params"), databaseConfig: { @@ -11,7 +22,7 @@ jest.mock("../config/database-params", () => ({ }, })); -jest.mock('thrift', () => ({ +jest.mock("thrift", () => ({ createConnection: jest.fn(() => ({ on: jest.fn(), end: jest.fn(), @@ -20,21 +31,11 @@ jest.mock('thrift', () => ({ })); // Mock the module where transformSessionDataSet is defined -jest.mock('../utils/database-helper', () => ({ - transformSessionDataSet: jest.fn(), // Ensure transformSessionDataSet is a mock function -})); - -// Mock the module where transformSessionDataSet is defined -jest.mock('../utils/database-helper', () => ({ - transformSessionDataSet: jest.fn(), // Ensure transformSessionDataSet is a mock function +jest.mock("../utils/database-helper", () => ({ + transformSessionDataSet: jest.fn(), // Ensure transformSessionDataSet is a mock function })); -import {Subscription, SubscriptionSimulator} from '../src/SubscriptionSimulator'; -import {WebSocketWithId} from '../../../../utils/database-params'; -import {Session} from '../src/Session'; -import {DataContentMessage, StatusMessage, SubscribeMessageType, UnsubscribeMessageType} from '../../../../router/utils/NewMessage'; - -describe('SubscriptionSimulator', () => { +describe("SubscriptionSimulator", () => { let simulator: SubscriptionSimulator; let mockSession: Session; let mockWebSocket: WebSocketWithId; @@ -50,28 +51,74 @@ describe('SubscriptionSimulator', () => { const createStatusMessageMock = jest.fn< StatusMessage, // Return type - [number, string, string?] // Parameters: code, message, optional requestId + [number, string, string] // Parameters: code, message, requestId >((code, statusMessage, requestId) => ({ type: "status", - code, + code: code, message: statusMessage, - ...(requestId && {requestId}), - timestamp: {seconds: 1715253322, nanos: 123000000}, // Mocked timestamp + requestId: requestId, + timestamp: { seconds: 1715253322, nanos: 123000000 }, // Mocked timestamp + })); + + const createErrorMessageMock = jest.fn< + ErrorMessage, // Return type + [number, string, string, string] // Parameters: code, message, reason, requestId + >((code, statusMessage, reason, requestId) => ({ + type: "error", + code: code, + message: statusMessage, + reason: reason, + requestId: requestId, })); sendAlreadySubscribedErrorMsg = jest.fn(); sendMessageToClientMock = jest.fn(); - simulator = new SubscriptionSimulator(sendMessageToClientMock, jest.fn(), createStatusMessageMock, sendAlreadySubscribedErrorMsg); - - mockWebSocket = {id: 'ws1'} as WebSocketWithId; - mockSubscribeMessageVIN1 = {instance: 'VIN_1'} as SubscribeMessageType; - mockSubscribeMessageVIN2 = {instance: 'VIN_2'} as SubscribeMessageType; - mockUnsubscribeMessageVIN1 = {instance: 'VIN_1'} as UnsubscribeMessageType; - mockUnsubscribeMessageVIN2 = {instance: 'VIN_2'} as UnsubscribeMessageType; - // Create already a subscription for 2 data points - let subscription = {vin: "VIN_1", dataPoints: new Set(['datapoint1', 'datapoint2'])}; - simulator['websocketToSubscriptionsMap'].set(mockWebSocket, [subscription]); + simulator = new SubscriptionSimulator( + sendMessageToClientMock, + jest.fn(), + createStatusMessageMock, + createErrorMessageMock, + sendAlreadySubscribedErrorMsg, + ); + mockWebSocket = { id: "ws1" } as WebSocketWithId; + mockSubscribeMessageVIN1 = { + instance: "VIN_1", + requestId: "requestId1", + path: "Vehicle_CurrentLocation", + root: "relative", + format: "flat", + type: "subscribe", + } as SubscribeMessageType; + + mockSubscribeMessageVIN2 = { + instance: "VIN_2", + requestId: "requestId2", + path: "Vehicle_CurrentLocation", + root: "relative", + format: "flat", + type: "subscribe", + } as SubscribeMessageType; + + mockUnsubscribeMessageVIN1 = { + instance: "VIN_1", + requestId: "requestId3", + } as UnsubscribeMessageType; + + mockUnsubscribeMessageVIN2 = { + instance: "VIN_2", + requestId: "requestId4", + } as UnsubscribeMessageType; + + const subscription = { + vin: "VIN_1", + dataPoints: new Set(["datapoint1", "datapoint2"]), + requestId: "requestId1", + path: "Vehicle_CurrentLocation", + root: "relative" as const, + format: "flat" as const, + }; + simulator["websocketToSubscriptionsMap"].set(mockWebSocket, [subscription]); }); afterEach(() => { @@ -81,187 +128,351 @@ describe('SubscriptionSimulator', () => { simulator["intervalId"] = null; // Reset intervalId } jest.clearAllTimers(); // Clear any remaining active timers - process.removeAllListeners("exit"); process.removeAllListeners("SIGINT"); }); - /* - * subscribe - */ - - test('Respond with BAD_REQUEST if subscription (vin & datapoints) with same webSocket already exists', () => { - // Act: Call subscribe with the existing WebSocket and message key - simulator.subscribe(mockSubscribeMessageVIN1, mockWebSocket, ["datapoint1"]); - - // Assert: Check that sendMessageToClient was called with the expected error message - expect(sendAlreadySubscribedErrorMsg).toHaveBeenCalledWith( - mockWebSocket, - mockSubscribeMessageVIN1.instance, - ["datapoint1"] - ); - }); - - test('Creates a new subscription if client is not subscribed to provided VIN', () => { - // Act: Call subscribe with the existing WebSocket and another message key - simulator.subscribe(mockSubscribeMessageVIN2, mockWebSocket, ['datapoint1', 'datapoint2']); - - // Assert: Check that a new subscription was added - expect(simulator['websocketToSubscriptionsMap'].size).toBe(1); - expect(simulator['websocketToSubscriptionsMap'].get(mockWebSocket)!.length).toBe(2); - expect(simulator['websocketToSubscriptionsMap'].get(mockWebSocket)!.find( - subscription => subscription.vin === mockSubscribeMessageVIN2.instance - )?.dataPoints.size).toBe(2); - }); - - test('Creates a new subscription if client is not subscribed to provided datapoint', () => { - // Act: Call subscribe with the existing WebSocket and another message key - simulator.subscribe(mockSubscribeMessageVIN1, mockWebSocket, ['datapointNew', 'datapoint2']); - - // Assert: Check that a new subscription was added - expect(simulator['websocketToSubscriptionsMap'].size).toBe(1); - expect(simulator['websocketToSubscriptionsMap'].get(mockWebSocket)!.length).toBe(1); - expect(simulator['websocketToSubscriptionsMap'].get(mockWebSocket)!.find( - subscription => subscription.vin === mockSubscribeMessageVIN1.instance - )?.dataPoints.size).toBe(3); - }); - - test('starts a periodic database listener if no intervalId is set', () => { - // Arrange: Ensure there is no active interval - simulator['intervalId'] = null; - - // Act: Call subscribe to trigger the timer start - simulator.subscribe(mockSubscribeMessageVIN2, mockWebSocket, ['datapoint1', 'datapoint2']); - - // Assert: Check that intervalId is no longer null - expect(simulator['intervalId']).not.toBeNull(); - }); - - /* - * unsubscribe - */ - - test('remove datapoint from already available subscription', () => { - // Act: Call subscribe with existing WebSocket and message key - simulator.unsubscribe(mockUnsubscribeMessageVIN1, mockWebSocket, ["datapoint1"]); - - // Assert: Check that the subscription exists with only the other websocket left - expect(simulator['websocketToSubscriptionsMap'].size).toBe(1); - expect(simulator['websocketToSubscriptionsMap'].get(mockWebSocket)!.length).toBe(1); - expect(simulator['websocketToSubscriptionsMap'].get(mockWebSocket)!.find( - subscription => subscription.vin === mockSubscribeMessageVIN1.instance - )?.dataPoints.size).toBe(1); - expect(simulator['websocketToSubscriptionsMap'].get(mockWebSocket)!.find( - subscription => subscription.vin === mockSubscribeMessageVIN1.instance - )?.dataPoints.has('datapoint2')).toBeTruthy(); + describe("subscribe", () => { + test("does not send already-subscribed error on first subscribe", () => { + simulator.subscribe(mockSubscribeMessageVIN1, mockWebSocket, [ + "datapoint1", + ]); + + expect(sendAlreadySubscribedErrorMsg).not.toHaveBeenCalled(); + }); + + test("creates a new subscription if client is not subscribed to provided VIN", () => { + simulator.subscribe(mockSubscribeMessageVIN2, mockWebSocket, [ + "datapoint1", + "datapoint2", + ]); + + expect(simulator["websocketToSubscriptionsMap"].size).toBe(1); + expect( + simulator["websocketToSubscriptionsMap"].get(mockWebSocket)!.length, + ).toBe(2); + expect( + simulator["websocketToSubscriptionsMap"] + .get(mockWebSocket)! + .find( + (subscription) => + subscription.vin === mockSubscribeMessageVIN2.instance, + )?.dataPoints.size, + ).toBe(2); + }); + + test("replaces existing subscription when requestId matches", () => { + const replacementMessage = { + ...mockSubscribeMessageVIN1, + instance: "VIN_1", + requestId: "requestId1", + path: "Vehicle_CurrentLocation_Latitude", + root: "absolute", + format: "nested", + } as SubscribeMessageType; + + // Subscribe with the replacement message + simulator.subscribe(replacementMessage, mockWebSocket, ["datapointNew"]); + + const subscriptions = + simulator["websocketToSubscriptionsMap"].get(mockWebSocket)!; + const replaced = subscriptions.find( + (sub) => sub.requestId === "requestId1", + ); + + expect(replaced).toBeDefined(); + expect(replaced?.vin).toBe("VIN_1"); + expect(replaced?.path).toBe("Vehicle_CurrentLocation_Latitude"); + expect(replaced?.root).toBe("absolute"); + expect(replaced?.format).toBe("nested"); + expect(replaced?.dataPoints.size).toBe(1); + expect(replaced?.dataPoints.has("datapointNew")).toBeTruthy(); + }); + + test("updates subscription datapoints when requestId matches", () => { + simulator.subscribe(mockSubscribeMessageVIN1, mockWebSocket, [ + "datapointNew", + "datapoint2", + ]); + + expect(simulator["websocketToSubscriptionsMap"].size).toBe(1); + expect( + simulator["websocketToSubscriptionsMap"].get(mockWebSocket)!.length, + ).toBe(1); + expect( + simulator["websocketToSubscriptionsMap"] + .get(mockWebSocket)! + .find( + (subscription) => + subscription.vin === mockSubscribeMessageVIN1.instance, + )?.dataPoints.size, + ).toBe(2); + }); + + test("starts a periodic database listener if no intervalId is set", () => { + simulator["intervalId"] = null; + + simulator.subscribe(mockSubscribeMessageVIN2, mockWebSocket, [ + "datapoint1", + "datapoint2", + ]); + + expect(simulator["intervalId"]).not.toBeNull(); + }); }); + describe("unsubscribe", () => { + test("remove subscription with exact matching datapoints", () => { + const subscriptions = + simulator["websocketToSubscriptionsMap"].get(mockWebSocket)!; + subscriptions.push({ + vin: "VIN_1", + dataPoints: new Set(["datapoint3", "datapoint4"]), + requestId: "requestId2", + path: "Vehicle_Speed", + root: "relative" as const, + format: "flat" as const, + }); - test('remove subscription if unsubscribing to all datapoints', () => { - // Act: Call unsubscribe with existing WebSocket and message key - simulator.unsubscribe(mockUnsubscribeMessageVIN1, mockWebSocket, ['datapoint1', 'datapoint2']); - - // Assert: Check that the subscription has been removed - expect(simulator['websocketToSubscriptionsMap'].size).toBe(0); - }); - - test('calls sendMessageToClient if unsubscribe is done on non-existing subscription', () => { - // Act: Call unsubscribe with existing WebSocket and non-existing message key - simulator.unsubscribe(mockUnsubscribeMessageVIN1, mockWebSocket, ['datapointUnknown']); - - // Assert: Check that sendMessageToClient was called with the expected error message - expect(sendMessageToClientMock).toHaveBeenCalledWith( - mockWebSocket, - expect.objectContaining({ - type: 'status', - code: 404, - message: expect.stringContaining('Cannot unsubscribe') - }) - ); - }); - - test('stops the timer if there is no subscription left', () => { - // Arrange: create a subscription to start the timer - simulator.subscribe(mockSubscribeMessageVIN1, mockWebSocket, ['datapointNew']); - expect(simulator['intervalId']).not.toBeNull(); - - // Act: Call unsubscribe to trigger the timer removal - simulator.unsubscribe(mockUnsubscribeMessageVIN1, mockWebSocket, ['datapoint1', 'datapoint2', 'datapointNew']); + // Unsubscribe with datapoints that match the first mocked subscription + simulator.unsubscribe(mockUnsubscribeMessageVIN1, mockWebSocket, [ + "datapoint1", + "datapoint2", + ]); + + expect(simulator["websocketToSubscriptionsMap"].size).toBe(1); + expect( + simulator["websocketToSubscriptionsMap"].get(mockWebSocket)!.length, + ).toBe(1); + expect( + simulator["websocketToSubscriptionsMap"] + .get(mockWebSocket)! + .find((sub) => sub.requestId === "requestId2"), + ).toBeDefined(); + expect( + simulator["websocketToSubscriptionsMap"] + .get(mockWebSocket)! + .find((sub) => sub.requestId === "requestId1"), + ).toBeUndefined(); + }); + + test("remove multiple subscriptions if they have exact matching datapoints", () => { + const subscriptions = + simulator["websocketToSubscriptionsMap"].get(mockWebSocket)!; + subscriptions.push({ + vin: "VIN_1", + dataPoints: new Set(["datapoint1", "datapoint2"]), + requestId: "requestId3", + path: "Vehicle_CurrentLocation", + root: "relative" as const, + format: "flat" as const, + }); - // Assert: Check that intervalId is null - expect(simulator['intervalId']).toBeNull(); + simulator.unsubscribe(mockUnsubscribeMessageVIN1, mockWebSocket, [ + "datapoint1", + "datapoint2", + ]); + + expect(simulator["websocketToSubscriptionsMap"].size).toBe(0); + }); + + test("calls sendMessageToClient if unsubscribe datapoints do not match exactly", () => { + simulator.unsubscribe(mockUnsubscribeMessageVIN1, mockWebSocket, [ + "datapointUnknown", + ]); + + expect(sendMessageToClientMock).toHaveBeenCalledWith( + mockWebSocket, + expect.objectContaining({ + type: "error", + code: 404, + requestId: "requestId3", + message: expect.stringContaining("Subscription not found"), + reason: expect.stringContaining( + "Cannot unsubscribe. No subscription for instance 'VIN_1' with the provided data points.", + ), + }), + ); + }); + + test("calls sendMessageToClient if unsubscribe with partial datapoints", () => { + simulator.unsubscribe(mockUnsubscribeMessageVIN1, mockWebSocket, [ + "datapoint1", + ]); + + expect(sendMessageToClientMock).toHaveBeenCalledWith( + mockWebSocket, + expect.objectContaining({ + type: "error", + code: 404, + requestId: "requestId3", + message: expect.stringContaining("Subscription not found"), + reason: expect.stringContaining( + "Cannot unsubscribe. No subscription for instance 'VIN_1' with the provided data points.", + ), + }), + ); + }); + + test("stops the timer if there is no subscription left", () => { + simulator.subscribe(mockSubscribeMessageVIN1, mockWebSocket, [ + "datapoint1", + "datapoint2", + ]); + expect(simulator["intervalId"]).not.toBeNull(); + + simulator.unsubscribe(mockUnsubscribeMessageVIN1, mockWebSocket, [ + "datapoint1", + "datapoint2", + ]); + + expect(simulator["intervalId"]).toBeNull(); + }); + + test("does not stop timer if other subscriptions exist", () => { + simulator.subscribe(mockSubscribeMessageVIN2, mockWebSocket, [ + "datapoint3", + ]); + const intervalId = simulator["intervalId"]; + expect(intervalId).not.toBeNull(); + + simulator.unsubscribe(mockUnsubscribeMessageVIN1, mockWebSocket, [ + "datapoint1", + "datapoint2", + ]); + + expect(simulator["intervalId"]).toBe(intervalId); + expect( + simulator["websocketToSubscriptionsMap"].get(mockWebSocket)!.length, + ).toBe(1); + expect( + simulator["websocketToSubscriptionsMap"] + .get(mockWebSocket)! + .find((sub) => sub.vin === "VIN_2"), + ).toBeDefined(); + }); + + test("returns error when unsubscribing from non-existent VIN", () => { + const nonExistentVINMessage = { + instance: "VIN_NONEXISTENT", + requestId: "requestId5", + } as UnsubscribeMessageType; + + simulator.unsubscribe(nonExistentVINMessage, mockWebSocket, [ + "datapoint1", + ]); + + expect(sendMessageToClientMock).toHaveBeenCalledWith( + mockWebSocket, + expect.objectContaining({ + type: "error", + code: 404, + requestId: "requestId5", + message: expect.stringContaining("Subscription not found"), + reason: expect.stringContaining( + "Cannot unsubscribe. No subscription for instance 'VIN_NONEXISTENT' with the provided data points.", + ), + }), + ); + }); + + test("returns error when unsubscribing from non-existent websocket", () => { + const nonExistentWebSocket = { id: "ws_unknown" } as WebSocketWithId; + + simulator.unsubscribe(mockUnsubscribeMessageVIN1, nonExistentWebSocket, [ + "datapoint1", + ]); + + expect(sendMessageToClientMock).toHaveBeenCalledWith( + nonExistentWebSocket, + expect.objectContaining({ + type: "error", + code: 404, + requestId: "requestId3", + message: expect.stringContaining("Subscription not found"), + reason: expect.stringContaining( + "Cannot unsubscribe. No subscription for instance 'VIN_1'", + ), + }), + ); + }); }); - /* - * notifyDatabaseChanges - */ + describe("notifyDatabaseChanges", () => { + test("timeIntervalUpperLimit and timeIntervalLowerLimit form a series", async () => { + simulator["timeIntervalLowerLimit"] = Date.now(); - test('timeIntervalUpperLimit and timeIntervalLowerLimit form a series', async () => { - simulator["timeIntervalLowerLimit"] = Date.now(); + // Wait a little to ensure a time difference in the next call + await new Promise((resolve) => setTimeout(resolve, 10)); - // Wait a little to ensure a time difference in the next call - await new Promise(resolve => setTimeout(resolve, 10)); + // Spy on `checkForChanges` + let timeIntervals: { lower: number; upper: number }[] = []; - // Spy on `checkForChanges` - let timeIntervals: { lower: number; upper: number }[] = []; - - (jest.spyOn(simulator as any, 'checkForChanges') as jest.Mock) - .mockImplementation((id: string, dataPoints: Set, upperLimit: number) => { + ( + jest.spyOn(simulator as any, "checkForChanges") as jest.Mock + ).mockImplementation((subscription: any, upperLimit: number) => { timeIntervals.push({ - lower: simulator["timeIntervalLowerLimit"]!, // Current lower limit - upper: upperLimit, // Passed upper limit + lower: simulator["timeIntervalLowerLimit"]!, // Current lower limit + upper: upperLimit, // Passed upper limit }); return Promise.resolve(undefined); // Simulate checkForChanges returning no updates }); - // First call - await (simulator as any).notifyDatabaseChanges(); + // First call + await (simulator as any).notifyDatabaseChanges(); - // Wait a little to ensure a time difference in the next call - await new Promise(resolve => setTimeout(resolve, 10)); + // Wait a little to ensure a time difference in the next call + await new Promise((resolve) => setTimeout(resolve, 10)); - // Second call - await (simulator as any).notifyDatabaseChanges(); + // Second call + await (simulator as any).notifyDatabaseChanges(); - // Expect two interval records (one for each call) - expect(timeIntervals.length).toBe(2); + // Assert that checkForChanges was called twice and time intervals are correct + expect((simulator as any).checkForChanges).toHaveBeenCalledTimes(2); - const {lower: lower1, upper: upper1} = timeIntervals[0]; - const {lower: lower2, upper: upper2} = timeIntervals[1]; + // Expect two interval records (one for each call) + expect(timeIntervals.length).toBe(2); - // Assertions - expect(upper1).toBeGreaterThan(lower1); // First interval: upper > lower - expect(upper2).toBeGreaterThan(lower2); // Second interval: upper > lower - expect(lower2).toBe(upper1); // Second interval's lower is the first interval's upper - }); + const { lower: lower1, upper: upper1 } = timeIntervals[0]; + const { lower: lower2, upper: upper2 } = timeIntervals[1]; - test('calls sendMessageToClient if there is a matching subscription and update', async () => { - simulator["timeIntervalLowerLimit"] = Date.now(); - - // Mock checkForChanges to return an update message - jest.spyOn(simulator as any, 'checkForChanges').mockResolvedValueOnce({ - type: "data", - instance: 'VIN_1', - schema: 'Vehicle', - data: [{"CurrentLocation.Latitude": 55, "CurrentLocation.Longitude": 66}] - } as DataContentMessage); // Return type MessageWithNodes - - // Call notifyDatabaseChanges - await (simulator as any).notifyDatabaseChanges(); - - // Assert that sendMessageToClient was called with the matching websocket and update message - expect(sendMessageToClientMock).toHaveBeenCalledWith( - {id: 'ws1'}, // WebSocket in the subscription - expect.objectContaining( - { + // Assertions + expect(upper1).toBeGreaterThan(lower1); // First interval: upper > lower + expect(upper2).toBeGreaterThan(lower2); // Second interval: upper > lower + expect(lower2).toBe(upper1); // Second interval's lower is the first interval's upper + }); + + test("calls sendMessageToClient if there is a matching subscription and update", async () => { + simulator["timeIntervalLowerLimit"] = Date.now(); + + // Mock checkForChanges to return an update message + jest.spyOn(simulator as any, "checkForChanges").mockResolvedValueOnce({ + type: "data", + instance: "VIN_1", + schema: "Vehicle", + data: [ + { "CurrentLocation.Latitude": 55, "CurrentLocation.Longitude": 66 }, + ], + } as DataContentMessage); + + // Call notifyDatabaseChanges + await (simulator as any).notifyDatabaseChanges(); + + // Assert that sendMessageToClient was called with the matching websocket and update message + expect(sendMessageToClientMock).toHaveBeenCalledWith( + { id: "ws1" }, // WebSocket in the subscription + expect.objectContaining({ instance: "VIN_1", - schema: 'Vehicle', - type: 'data', - data: [{ - "CurrentLocation.Latitude": 55, - "CurrentLocation.Longitude": 66 - }], - }) - ); + schema: "Vehicle", + type: "data", + data: [ + { + "CurrentLocation.Latitude": 55, + "CurrentLocation.Longitude": 66, + }, + ], + }), + ); + }); }); }); diff --git a/cdsp/information-layer/handlers/test/HandlerBase.format.test.ts b/cdsp/information-layer/handlers/test/HandlerBase.format.test.ts new file mode 100644 index 00000000..a2a11bc8 --- /dev/null +++ b/cdsp/information-layer/handlers/test/HandlerBase.format.test.ts @@ -0,0 +1,425 @@ +import { HandlerBase, QueryResult } from "../src/HandlerBase"; +import { + GetMessageType, + NewMessageType, + DataContentMessage, +} from "../../router/utils/NewMessage"; +import { WebSocketWithId } from "../../utils/database-params"; + +/** + * Test HandlerBase formatting and root options + */ + +// Mock implementation of HandlerBase for testing +class TestHandler extends HandlerBase { + constructor(sendMessageFn: any) { + super(sendMessageFn); + } + + getKnownDatapointsByPrefix(prefix: string): string[] { + // Mock implementation - return expected datapoints based on prefix + if (prefix.includes("CurrentLocation")) { + return [ + "Vehicle_CurrentLocation_Latitude", + "Vehicle_CurrentLocation_Longitude", + ]; + } else if (prefix.includes("Speed")) { + return ["Vehicle_Speed"]; + } else if (prefix === "Vehicle" || prefix === "") { + return [ + "Vehicle_CurrentLocation_Latitude", + "Vehicle_CurrentLocation_Longitude", + "Vehicle_Speed", + ]; + } + return []; + } + + async getDataPointsFromDB( + dataPoints: string[], + vin: string, + ): Promise { + // Mock DB response with test data + const dataPointsArray = dataPoints.map((dp) => { + if (dp === "Vehicle_CurrentLocation_Latitude") { + return { name: "Vehicle.CurrentLocation.Latitude", value: 60 }; + } else if (dp === "Vehicle_CurrentLocation_Longitude") { + return { name: "Vehicle.CurrentLocation.Longitude", value: 50 }; + } else if (dp === "Vehicle_Speed") { + return { name: "Vehicle.Speed", value: 100 }; + } + return { name: dp, value: null }; + }); + + const metadataArray = dataPoints.map((dp) => { + if (dp === "Vehicle_CurrentLocation_Latitude") { + return { + name: "Vehicle.CurrentLocation.Latitude", + value: { + timestamps: { + received: { seconds: 1770028431, nanos: 250057547 }, + }, + }, + }; + } else if (dp === "Vehicle_CurrentLocation_Longitude") { + return { + name: "Vehicle.CurrentLocation.Longitude", + value: { + timestamps: { + received: { seconds: 1770028431, nanos: 250057547 }, + }, + }, + }; + } else if (dp === "Vehicle_Speed") { + return { + name: "Vehicle.Speed", + value: { + timestamps: { + received: { seconds: 1770110638, nanos: 660818568 }, + }, + }, + }; + } + return { name: dp, value: {} }; + }); + + return { + success: true, + dataPoints: dataPointsArray, + metadata: metadataArray, + }; + } +} + +describe("HandlerBase - Format and Root Options", () => { + let handler: TestHandler; + let capturedMessage: DataContentMessage | null; + let mockWebSocket: WebSocketWithId; + + beforeEach(() => { + capturedMessage = null; + handler = new TestHandler((ws: WebSocketWithId, message: any) => { + if (message.type === "data") { + capturedMessage = message as DataContentMessage; + } + }); + mockWebSocket = { id: "test-ws" } as WebSocketWithId; + }); + + describe("path contains only root Vehicle", () => { + it("should format flat + relative", async () => { + const message: GetMessageType = { + type: NewMessageType.Get, + instance: "SMT123JN12J123456", + path: "Vehicle", + requestId: "2", + root: "relative", + format: "flat", + }; + + await handler.get(message, mockWebSocket); + + expect(capturedMessage).not.toBeNull(); + expect(capturedMessage?.data).toEqual({ + "CurrentLocation.Latitude": 60, + "CurrentLocation.Longitude": 50, + Speed: 100, + }); + expect(capturedMessage?.metadata).toEqual({ + "CurrentLocation.Latitude": { + timestamps: { + received: { seconds: 1770028431, nanos: 250057547 }, + }, + }, + "CurrentLocation.Longitude": { + timestamps: { + received: { seconds: 1770028431, nanos: 250057547 }, + }, + }, + Speed: { + timestamps: { + received: { seconds: 1770110638, nanos: 660818568 }, + }, + }, + }); + }); + + it("should format flat + absolute", async () => { + const message: GetMessageType = { + type: NewMessageType.Get, + instance: "SMT123JN12J123456", + path: "Vehicle", + requestId: "2", + root: "absolute", + format: "flat", + }; + + await handler.get(message, mockWebSocket); + + expect(capturedMessage).not.toBeNull(); + expect(capturedMessage?.data).toEqual({ + "CurrentLocation.Latitude": 60, + "CurrentLocation.Longitude": 50, + Speed: 100, + }); + }); + + it("should format nested + relative", async () => { + const message: GetMessageType = { + type: NewMessageType.Get, + instance: "SMT123JN12J123456", + path: "Vehicle", + requestId: "2", + root: "relative", + format: "nested", + }; + + await handler.get(message, mockWebSocket); + + expect(capturedMessage).not.toBeNull(); + expect(capturedMessage?.data).toEqual({ + CurrentLocation: { + Latitude: 60, + Longitude: 50, + }, + Speed: 100, + }); + }); + + it("should format nested + absolute", async () => { + const message: GetMessageType = { + type: NewMessageType.Get, + instance: "SMT123JN12J123456", + path: "Vehicle", + requestId: "2", + root: "absolute", + format: "nested", + }; + + await handler.get(message, mockWebSocket); + + expect(capturedMessage).not.toBeNull(); + expect(capturedMessage?.data).toEqual({ + CurrentLocation: { + Latitude: 60, + Longitude: 50, + }, + Speed: 100, + }); + }); + }); + + describe("Path contains Vehicle_CurrentLocation", () => { + it("should format flat + relative - removes CurrentLocation from keys", async () => { + const message: GetMessageType = { + type: NewMessageType.Get, + instance: "SMT123JN12J123456", + path: "Vehicle_CurrentLocation", + requestId: "2", + root: "relative", + format: "flat", + }; + + await handler.get(message, mockWebSocket); + + expect(capturedMessage).not.toBeNull(); + expect(capturedMessage?.data).toEqual({ + Latitude: 60, + Longitude: 50, + }); + expect(capturedMessage?.metadata).toEqual({ + Latitude: { + timestamps: { + received: { seconds: 1770028431, nanos: 250057547 }, + }, + }, + Longitude: { + timestamps: { + received: { seconds: 1770028431, nanos: 250057547 }, + }, + }, + }); + }); + + it("should format flat + absolute - keeps full path", async () => { + const message: GetMessageType = { + type: NewMessageType.Get, + instance: "SMT123JN12J123456", + path: "Vehicle_CurrentLocation", + requestId: "2", + root: "absolute", + format: "flat", + }; + + await handler.get(message, mockWebSocket); + + expect(capturedMessage).not.toBeNull(); + expect(capturedMessage?.data).toEqual({ + "CurrentLocation.Latitude": 60, + "CurrentLocation.Longitude": 50, + }); + expect(capturedMessage?.metadata).toEqual({ + "CurrentLocation.Latitude": { + timestamps: { + received: { seconds: 1770028431, nanos: 250057547 }, + }, + }, + "CurrentLocation.Longitude": { + timestamps: { + received: { seconds: 1770028431, nanos: 250057547 }, + }, + }, + }); + }); + + it("should format nested + relative - direct object without CurrentLocation", async () => { + const message: GetMessageType = { + type: NewMessageType.Get, + instance: "SMT123JN12J123456", + path: "Vehicle_CurrentLocation", + requestId: "2", + root: "relative", + format: "nested", + }; + + await handler.get(message, mockWebSocket); + + expect(capturedMessage).not.toBeNull(); + expect(capturedMessage?.data).toEqual({ + Latitude: 60, + Longitude: 50, + }); + }); + + it("should format nested + absolute - includes CurrentLocation hierarchy", async () => { + const message: GetMessageType = { + type: NewMessageType.Get, + instance: "SMT123JN12J123456", + path: "Vehicle_CurrentLocation", + requestId: "2", + root: "absolute", + format: "nested", + }; + + await handler.get(message, mockWebSocket); + + expect(capturedMessage).not.toBeNull(); + expect(capturedMessage?.data).toEqual({ + CurrentLocation: { + Latitude: 60, + Longitude: 50, + }, + }); + }); + }); + + describe("Path contains CurrentLocation.Latitude (leaf node)", () => { + beforeEach(() => { + // Override mock for single datapoint + handler.getKnownDatapointsByPrefix = jest + .fn() + .mockReturnValue(["Vehicle_CurrentLocation_Latitude"]); + }); + + it("should format flat + relative - empty string key for exact match", async () => { + const message: GetMessageType = { + type: NewMessageType.Get, + instance: "SMT123JN12J123456", + path: "Vehicle_CurrentLocation_Latitude", + requestId: "2", + root: "relative", + format: "flat", + }; + + await handler.get(message, mockWebSocket); + + expect(capturedMessage).not.toBeNull(); + expect(capturedMessage?.data).toEqual({ + "": 60, + }); + expect(capturedMessage?.metadata).toEqual({ + "": { + timestamps: { + received: { seconds: 1770028431, nanos: 250057547 }, + }, + }, + }); + }); + + it("should format flat + absolute - full path as key", async () => { + const message: GetMessageType = { + type: NewMessageType.Get, + instance: "SMT123JN12J123456", + path: "Vehicle_CurrentLocation_Latitude", + requestId: "2", + root: "absolute", + format: "flat", + }; + + await handler.get(message, mockWebSocket); + + expect(capturedMessage).not.toBeNull(); + expect(capturedMessage?.data).toEqual({ + "CurrentLocation.Latitude": 60, + }); + expect(capturedMessage?.metadata).toEqual({ + "CurrentLocation.Latitude": { + timestamps: { + received: { seconds: 1770028431, nanos: 250057547 }, + }, + }, + }); + }); + + it("should format nested + relative - direct value (no object wrapper)", async () => { + const message: GetMessageType = { + type: NewMessageType.Get, + instance: "SMT123JN12J123456", + path: "Vehicle_CurrentLocation_Latitude", + requestId: "2", + root: "relative", + format: "nested", + }; + + await handler.get(message, mockWebSocket); + + expect(capturedMessage).not.toBeNull(); + // Special case: nested + relative for exact match returns value directly + expect(capturedMessage?.data).toBe(60); + expect(capturedMessage?.metadata).toEqual({ + "": { + timestamps: { + received: { seconds: 1770028431, nanos: 250057547 }, + }, + }, + }); + }); + + it("should format nested + absolute - builds full hierarchy", async () => { + const message: GetMessageType = { + type: NewMessageType.Get, + instance: "SMT123JN12J123456", + path: "Vehicle_CurrentLocation_Latitude", + requestId: "2", + root: "absolute", + format: "nested", + }; + + await handler.get(message, mockWebSocket); + + expect(capturedMessage).not.toBeNull(); + expect(capturedMessage?.data).toEqual({ + CurrentLocation: { + Latitude: 60, + }, + }); + expect(capturedMessage?.metadata).toEqual({ + "CurrentLocation.Latitude": { + timestamps: { + received: { seconds: 1770028431, nanos: 250057547 }, + }, + }, + }); + }); + }); +}); diff --git a/cdsp/information-layer/router/src/websocket-server.ts b/cdsp/information-layer/router/src/websocket-server.ts index 1f5fba52..86e1f0cb 100644 --- a/cdsp/information-layer/router/src/websocket-server.ts +++ b/cdsp/information-layer/router/src/websocket-server.ts @@ -2,21 +2,17 @@ import {v4 as uuidv4} from 'uuid'; import WebSocket, {RawData} from "ws"; import {createHandler} from "../../handlers/src/HandlerCreator"; import {RequestValidator} from '../utils/RequestValidator'; -import { - logMessage, - logWithColor, - LogMessageType, - COLORS, - logErrorStr, -} from "../../utils/logger"; +import {COLORS, logErrorStr, logMessage, LogMessageType, logWithColor,} from "../../utils/logger"; import {WebSocketWithId} from "../../utils/database-params"; -import {STATUS_ERRORS} from "../utils/ErrorMessage"; import {HandlerBase} from "../../handlers/src/HandlerBase"; +import {MessageMapper} from "../utils/MessageMapper"; +import {DataContentMessage, ErrorMessage, STATUS_ERRORS, StatusMessage} from "../utils/NewMessage"; // WebSocket server creation const server = new WebSocket.Server({port: 8080}); // Define clients Map globally to store connected clients const clients = new Map(); +const messageMapper = new MessageMapper(); // Handle new client connections server.on("connection", (ws: WebSocket) => { @@ -28,10 +24,10 @@ server.on("connection", (ws: WebSocket) => { const wsWithId = addIdToWebsocket(ws); const connectionId = wsWithId.id; clients.set(connectionId, ws); // Add client to the Map - const handler = createHandler(); + const handler = createHandler(sendMessage); // Handle messages from the client - ws.on("message", handleMessage(connectionId, handler, wsWithId, ws)); + ws.on("message", handleMessage(connectionId, handler, wsWithId)); // Handle client disconnection ws.on("close", handleCloseConnection(handler, wsWithId, connectionId)); @@ -40,18 +36,20 @@ server.on("connection", (ws: WebSocket) => { logWithColor(`Client connected and authenticated with id ${connectionId}`, COLORS.YELLOW); }); -function handleMessage(connectionId: string, handler: HandlerBase, wsWithId: WebSocketWithId, ws: WebSocket) { +function handleMessage(connectionId: string, handler: HandlerBase, wsWithId: WebSocketWithId) { return (message: WebSocket.RawData) => { let messageString = rawDataToString(message); - logMessage(JSON.stringify(messageString), LogMessageType.RECEIVED, connectionId); + logMessage(messageString, LogMessageType.RECEIVED, connectionId); // validation const validator = new RequestValidator(); const result = validator.validate(messageString); if (result.valid) { + // Map DTO to internal business object + const messageBO = messageMapper.toDomain(result.messageDTO!); // Handle valid Message and exit - handler.handleMessage(result.message!, wsWithId); + handler.handleMessage(messageBO, wsWithId); return; } @@ -59,12 +57,14 @@ function handleMessage(connectionId: string, handler: HandlerBase, wsWithId: Web logErrorStr("Processing failed:"); result.errors?.forEach((error) => { logErrorStr(error); - const statusMessage = { - type: "status", + const errorMessage: ErrorMessage = { code: STATUS_ERRORS.BAD_REQUEST, + type: "error", message: `Processing Error: ${error}`, + reason:`Processing Error: ${error}`, + requestId: result.messageDTO?.id } - ws.send(JSON.stringify(statusMessage)); + sendMessage(wsWithId, errorMessage); }); }; } @@ -84,11 +84,25 @@ function addIdToWebsocket(ws: WebSocket): WebSocketWithId { return Object.assign(ws, {id: uuidv4()}); } +/** + * This must be the only function that sends messages via websocket to the connected clients to guarantee the right message structure. + * Don't use WebSocket.send(...) anywhere else. + * @param ws websocket object + * @param message message to send + */ +export function sendMessage( + ws: WebSocketWithId, + message: StatusMessage | DataContentMessage | ErrorMessage +): void { + + let dataResponseDTO = messageMapper.toDTO(message); + ws.send(JSON.stringify(dataResponseDTO)); + + logMessage(JSON.stringify(dataResponseDTO, null, 2), LogMessageType.SENT, ws.id); +} function rawDataToString(message: RawData): string { - if (typeof message === "string") { - return message; - } else if (Buffer.isBuffer(message)) { + if (Buffer.isBuffer(message)) { return message.toString(); } else if (message instanceof ArrayBuffer) { return new TextDecoder().decode(message); diff --git a/cdsp/information-layer/router/test/RequestValidator.test.ts b/cdsp/information-layer/router/test/RequestValidator.test.ts index 7bfe505b..1cfb7977 100644 --- a/cdsp/information-layer/router/test/RequestValidator.test.ts +++ b/cdsp/information-layer/router/test/RequestValidator.test.ts @@ -11,166 +11,165 @@ describe("RequestValidator", () => { { name: "Valid get request", json: `{ - "type": "get", - "schema": "Vehicle", - "instance": "SMT905JN26J262542" + "jsonrpc": "2.0", + "method": "get", + "id": "123", + "params": { + "schema": "Vehicle", + "instance": "SMT905JN26J262542" + } }`, expectedPath: "Vehicle", }, { name: "Valid set request", json: `{ - "type": "set", - "schema": "Vehicle", - "path": "Speed", - "instance": "SMT905JN26J262542", - "data": 100 + "jsonrpc": "2.0", + "method": "set", + "id": "123", + "params": { + "schema": "Vehicle", + "path": "Speed", + "instance": "SMT905JN26J262542", + "data": 100 + } }`, expectedPath: "Vehicle_Speed", }, { name: "Valid subscribe request", json: `{ - "type": "subscribe", - "schema": "Vehicle", - "instance": "SMT905JN26J262542" + "jsonrpc": "2.0", + "method": "subscribe", + "id": "123", + "params": { + "schema": "Vehicle", + "instance": "SMT905JN26J262542" + } }`, expectedPath: "Vehicle", }, { name: "Valid unsubscribe request", json: `{ - "type": "unsubscribe", - "schema": "Vehicle", - "instance": "SMT905JN26J262542" - }`, - expectedPath: "Vehicle", - }, - { - name: "Valid getTimeseries request", - json: `{ - "type": "timeseries/get", - "path": "Person.Vitals.Cardiovascular.HeartRate", - "instance": "P123", - "query": { - "gte": { "seconds": 1733389000, "nanos": 814343000 }, - "lte": { "seconds": 1733389389, "nanos": 814343000 } + "jsonrpc": "2.0", + "method": "unsubscribe", + "id": "123", + "params": { + "schema": "Vehicle", + "instance": "SMT905JN26J262542" } }`, - expectedPath: "Person_Vitals_Cardiovascular_HeartRate", - } + expectedPath: "Vehicle", + } ]; const invalidRequests = [ { name: "Additional not specified property in get", json: `{ - "type": "get", - "path": "Speed", - "schema": "Vehicle", - "instance": "SMT905JN26J262542", - "extraProperty": "should not be here" + "jsonrpc": "2.0", + "method": "get", + "id": "123", + "params": { + "path": "Speed", + "schema": "Vehicle", + "instance": "SMT905JN26J262542", + "extraProperty": "should not be here" + } }`, - error: "Unexpected property found", + error: "Unexpected property found in params", }, { name: "Missing required property in get", json: `{ - "type": "get", - "path": "Speed", - "schema": "Vehicle" + "jsonrpc": "2.0", + "method": "get", + "id": "123", + "params": { + "path": "Speed", + "schema": "Vehicle" + } }`, error: "The 'instance' field is required", }, { name: "Schema is blank", json: `{ - "type": "get", - "path": "Speed", - "schema": "", - "instance": "SMT905JN26J262542" + "jsonrpc": "2.0", + "method": "get", + "id": "123", + "params": { + "path": "Speed", + "schema": "", + "instance": "SMT905JN26J262542" + } }`, error: "The 'schema' field must contain at least one non-whitespace character", }, { name: "Schema contains only whitespace characters", json: `{ - "type": "get", - "path": "Speed", - "schema": " ", - "instance": "SMT905JN26J262542" + "jsonrpc": "2.0", + "method": "get", + "id": "123", + "params": { + "path": "Speed", + "schema": " ", + "instance": "SMT905JN26J262542" + } }`, error: "The 'schema' field must contain at least one non-whitespace character", }, { name: "Instance is blank", json: `{ - "type": "get", - "path": "Speed", - "instance": "", - "schema": "SMT905JN26J262542" + "jsonrpc": "2.0", + "method": "get", + "id": "123", + "params": { + "path": "Speed", + "instance": "", + "schema": "SMT905JN26J262542" + } }`, error: "The 'instance' field must contain at least one non-whitespace character", }, { name: "Instance contains only whitespace characters", json: `{ - "type": "get", - "path": "Speed", - "instance": " ", - "schema": "Vehicle" + "jsonrpc": "2.0", + "method": "get", + "id": "123", + "params": { + "path": "Speed", + "instance": " ", + "schema": "Vehicle" + } }`, error: "The 'instance' field must contain at least one non-whitespace character", }, { - name: "Unknown request 'type'", - json: `{ - "type": "unknownType", - "path": "Speed", - "schema": "Vehicle", - "instance": "Vehicle" - }`, - error: "Unknown request type: unknownType", - }, - { - name: "Missing instance in getTimeseries", - json: `{ - "type": "timeseries/get", - "path": "Person.Vitals.Cardiovascular.HeartRate" - }`, - error: "The 'instance' field is required", - }, - { - name: "Additional property in getTimeseries", - json: `{ - "type": "timeseries/get", - "path": "Person.Vitals.Cardiovascular.HeartRate", - "instance": "P123", - "extraProperty": "unexpected" - }`, - error: "Unexpected property found", - }, - { - name: "Invalid query in getTimeseries", + name: "Unknown request 'method'", json: `{ - "type": "timeseries/get", - "path": "Person.Vitals.Cardiovascular.HeartRate", - "instance": "P123", - "query": { - "gte": { "seconds": "invalid", "nanos": 814343000 } + "jsonrpc": "2.0", + "method": "unknownType", + "id": "123", + "params": { + "path": "Speed", + "schema": "Vehicle", + "instance": "Vehicle" } }`, - error: "must be number", + error: "Unknown request method: unknownType", } ]; describe("Valid Requests", () => { - validRequests.forEach(({ name, json, expectedPath }) => { + validRequests.forEach(({ name, json }) => { test(name, () => { const result = validator.validate(json); expect(result.valid).toBe(true); - expect(result).toHaveProperty("message"); - expect(result.message).toHaveProperty("path", expectedPath) }); }); @@ -182,7 +181,7 @@ describe("RequestValidator", () => { const result = validator.validate(json); // Ensure the result is invalid before accessing errors - if (result.valid === false) { + if (!result.valid) { expect(result.errors).toEqual(expect.arrayContaining([error])); } else { // Fail the test if the result is valid when it shouldn't be diff --git a/cdsp/information-layer/router/utils/ErrorMessage.ts b/cdsp/information-layer/router/utils/ErrorMessage.ts index 3b1a1be4..139597f9 100644 --- a/cdsp/information-layer/router/utils/ErrorMessage.ts +++ b/cdsp/information-layer/router/utils/ErrorMessage.ts @@ -1,19 +1,2 @@ -export type ErrorMessage = { - category: string; - statusCode: number; - message: string; -}; -export const STATUS_ERRORS = { - BAD_REQUEST: 400, - UNAUTHORIZED: 401, - FORBIDDEN: 403, - NOT_FOUND: 404, - INTERNAL_SERVER_ERROR: 500, - NOT_IMPLEMENTED: 501, - SERVICE_UNAVAILABLE: 503, -} as const; -export const STATUS_SUCCESS = { - OK: 200 -} diff --git a/cdsp/information-layer/router/utils/MessageDTO.ts b/cdsp/information-layer/router/utils/MessageDTO.ts new file mode 100644 index 00000000..07498fad --- /dev/null +++ b/cdsp/information-layer/router/utils/MessageDTO.ts @@ -0,0 +1,76 @@ +import { NewMessageType } from "./NewMessage"; + + +export type NewMessageDTO = + | GetMessageDTO + | SetMessageDTO + | SubscribeMessageDTO + | UnsubscribeMessageDTO; + +type Params = { + instance: string; + schema: string; + path: string; + format?: "nested" | "flat"; + root?: "absolute" | "relative"; +}; + +export type GetMessageDTO = { + jsonrpc: string; + method: NewMessageType.Get; + id: string; + params: Params; +}; + +export type SetMessageDTO = { + jsonrpc: string; + method: NewMessageType.Set; + id: string; + params: Params & { + data: any; + metadata?: Record; + timeseries?: boolean; + }; +}; + +export type SubscribeMessageDTO = { + jsonrpc: string; + method: NewMessageType.Subscribe; + id: string; + params: Params; +}; + +export type UnsubscribeMessageDTO = { + jsonrpc: string; + method: NewMessageType.Unsubscribe; + id: string; + params: Params; +}; + +export type DataResponseDTO = { + jsonrpc: string; + id: string | null; + result: { + instance: string; + data: any; + metadata?: any; + } +}; + +export type OkResponseDTO = { + jsonrpc: string; + id: string; + result: {} +}; + +export type ErrorResponseDTO = { + jsonrpc: string; + id: string | null; + error: { + code: number; + message: string; + data: { + reason: string; + }; + }; +}; diff --git a/cdsp/information-layer/router/utils/MessageMapper.ts b/cdsp/information-layer/router/utils/MessageMapper.ts new file mode 100644 index 00000000..36fe1e06 --- /dev/null +++ b/cdsp/information-layer/router/utils/MessageMapper.ts @@ -0,0 +1,155 @@ +import { + DataResponseDTO, + ErrorResponseDTO, + GetMessageDTO, + NewMessageDTO, + OkResponseDTO, + SetMessageDTO, + SubscribeMessageDTO, + UnsubscribeMessageDTO, +} from "./MessageDTO"; +import { + GetMessageType, + NewMessageType, + NewMessage, + SetMessageType, + SubscribeMessageType, + UnsubscribeMessageType, + DataContentMessage, + StatusMessage, + ErrorMessage, +} from "./NewMessage"; +import { replaceDotsWithUnderscore } from "../../handlers/utils/transformations"; + +export class MessageMapper { + toDomain(dto: NewMessageDTO): NewMessage { + this.modifyRequest(dto); + switch (dto.method) { + case NewMessageType.Get: + return this.toGetBO(dto as GetMessageDTO); + case NewMessageType.Set: + return this.toSetBO(dto as SetMessageDTO); + case NewMessageType.Subscribe: + return this.toSubscribeBO(dto as SubscribeMessageDTO); + case NewMessageType.Unsubscribe: + return this.toUnsubscribeBO(dto as UnsubscribeMessageDTO); + default: + throw new Error(`Unknown method received in ${dto}`); + } + } + + toDTO( + bo: StatusMessage | DataContentMessage | ErrorMessage, + ): DataResponseDTO | OkResponseDTO | ErrorResponseDTO { + switch (bo.type) { + case "status": + return this.toOkResponseDTO(bo); + + case "data": + return this.toDataResponseDTO(bo); + + case "error": + return this.toErrorResponseDTO(bo); + + default: + throw new Error(`Unknown message type to convert to DTO ${bo}`); + } + } + + private toGetBO(dto: GetMessageDTO): GetMessageType { + return { + type: dto.method, + path: dto.params.path, + instance: dto.params.instance, + requestId: dto.id, + root: dto.params.root ?? "relative", + format: dto.params.format ?? "nested", + }; + } + + private toSetBO(dto: SetMessageDTO): SetMessageType { + return { + type: dto.method, + path: dto.params.path, + instance: dto.params.instance, + data: dto.params.data, + metadata: dto.params.metadata, + timeseries: dto.params.timeseries, + requestId: dto.id, + }; + } + + private toSubscribeBO(dto: SubscribeMessageDTO): SubscribeMessageType { + return { + type: dto.method, + path: dto.params.path, + instance: dto.params.instance, + requestId: dto.id, + root: dto.params.root ?? "absolute", + format: dto.params.format ?? "nested", + }; + } + + private toUnsubscribeBO(dto: UnsubscribeMessageDTO): UnsubscribeMessageType { + return { + type: dto.method, + path: dto.params.path, + instance: dto.params.instance, + requestId: dto.id, + }; + } + + private toDataResponseDTO(bo: DataContentMessage): DataResponseDTO { + return { + jsonrpc: "2.0", + id: bo.requestId ?? null, + result: { + instance: bo.instance, + data: bo.data, + metadata: bo.metadata, + }, + }; + } + + private toOkResponseDTO(bo: StatusMessage): OkResponseDTO { + return { + jsonrpc: "2.0", + id: bo.requestId, + result: {}, + }; + } + + private toErrorResponseDTO(bo: ErrorMessage): ErrorResponseDTO { + return { + jsonrpc: "2.0", + id: bo.requestId ?? null, + error: { + code: bo.code, + message: bo.message, + data: { + reason: bo.reason, + }, + }, + }; + } + + /** + * Apply modifications to the request structure and transformations to request values. + */ + private modifyRequest(request: NewMessageDTO) { + this.moveSchemaToPathField(request.params); + request.params.path = replaceDotsWithUnderscore(request.params.path); + } + + /** + * Merges `schema` into `path` and removes `schema`. + */ + private moveSchemaToPathField(params: any) { + if (params.schema && params.path) { + params.path = `${params.schema}_${params.path}`; + } else if (params.schema && !params.path) { + params.path = params.schema; + } + delete params.schema; + } +} diff --git a/cdsp/information-layer/router/utils/NewMessage.ts b/cdsp/information-layer/router/utils/NewMessage.ts index 58c2789d..c7edf0a5 100644 --- a/cdsp/information-layer/router/utils/NewMessage.ts +++ b/cdsp/information-layer/router/utils/NewMessage.ts @@ -13,7 +13,9 @@ export type GetMessageType = { type: NewMessageType.Get; path: string; instance: string; - requestId?: string; + requestId: string; + format: "nested" | "flat"; + root: "absolute" | "relative"; }; export type SetMessageType = { @@ -21,7 +23,7 @@ export type SetMessageType = { path: string; instance: string; data: any; - requestId?: string; + requestId: string; metadata?: Record; timeseries?: boolean; }; @@ -30,14 +32,16 @@ export type SubscribeMessageType = { type: NewMessageType.Subscribe; path: string; instance: string; - requestId?: string; + requestId: string; + format: "nested" | "flat"; + root: "absolute" | "relative"; }; export type UnsubscribeMessageType = { type: NewMessageType.Unsubscribe; path: string; instance: string; - requestId?: string; + requestId: string; }; export type PermissionsEditMessageType = { @@ -46,14 +50,14 @@ export type PermissionsEditMessageType = { allow?: string[]; deny?: string[]; delete?: string[]; - requestId?: string; + requestId: string; }; export type TimeseriesGetMessageType = { type: NewMessageType.TimeseriesGet; path: string; instance: string; - requestId?: string; + requestId: string; query?: { gte?: Timestamp; lte?: Timestamp }; }; @@ -85,7 +89,7 @@ export type StatusMessage = { type: "status"; code: number; message: string; - requestId?: string; + requestId: string; timestamp: { seconds: number; nanos: number; @@ -96,10 +100,32 @@ export type StatusMessage = { export function ensureMessageType( message: NewMessage, - expectedType: NewMessageType + expectedType: NewMessageType, ): T { if (message.type !== expectedType) { - throw new Error(`Expected message type '${expectedType}', but received: '${message.type}'`); + throw new Error( + `Expected message type '${expectedType}', but received: '${message.type}'`, + ); } return message as T; // TypeScript safely casts here due to the check } + +export type ErrorMessage = { + type: "error"; + code: number; + message: string; + reason: string; + requestId?: string; +}; +export const STATUS_ERRORS = { + BAD_REQUEST: 400, + UNAUTHORIZED: 401, + FORBIDDEN: 403, + NOT_FOUND: 404, + INTERNAL_SERVER_ERROR: 500, + NOT_IMPLEMENTED: 501, + SERVICE_UNAVAILABLE: 503, +} as const; +export const STATUS_SUCCESS = { + OK: 200, +}; diff --git a/cdsp/information-layer/router/utils/RequestValidator.ts b/cdsp/information-layer/router/utils/RequestValidator.ts index 57e4b652..824956f1 100644 --- a/cdsp/information-layer/router/utils/RequestValidator.ts +++ b/cdsp/information-layer/router/utils/RequestValidator.ts @@ -1,13 +1,13 @@ import Ajv, {ValidateFunction, Schema} from "ajv"; import addFormats from "ajv-formats"; import addErrors from "ajv-errors"; -import {NewMessageType, NewMessage} from "./NewMessage"; -import {replaceDotsWithUnderscore} from "../../handlers/utils/transformations"; +import {NewMessageType} from "./NewMessage"; +import {NewMessageDTO} from "./MessageDTO"; export interface ValidationResult { valid: boolean; errors?: string[]; - message?: NewMessage; + messageDTO?: NewMessageDTO; } export class RequestValidator { @@ -22,112 +22,119 @@ export class RequestValidator { addFormats(this.ajv); addErrors(this.ajv); + // Base JSON-RPC envelope scheme + const baseRpcSchema = { + type: "object", + required: ["jsonrpc", "method", "id", "params"], + additionalProperties: false, + properties: { + jsonrpc: {type: "string", const: "2.0"}, + method: {type: "string"}, // overridden in each message + id: {...stringOrIntegerRule}, + params: {type: "object"}, // overridden in each message + }, + errorMessage: { + required: { + jsonrpc: "The 'jsonrpc' field (must be '2.0') is required", + method: "The 'method' field is required", + id: "The 'id' field is required", + params: "The 'params' field is required", + }, + additionalProperties: "Unexpected property found at top level", + properties: { + jsonrpc: "The 'jsonrpc' version must be the string '2.0'", + ...stringOrIntegerErrorMessage("id") + } + }, + }; + // Base params schema for all messages + const baseParamsSchema = { + type: "object", + required: ["instance", "schema"], + additionalProperties: false, + properties: { + path: {type: "string"}, + instance: {...stringValidation}, + schema: {...stringValidation}, + }, + errorMessage: { + required: { + instance: "The 'instance' field is required", + schema: "The 'schema' field is required", + }, + additionalProperties: "Unexpected property found in params", + properties: { + ...notEmptyStringErrorMessage("instance"), + ...notEmptyStringErrorMessage("schema"), + }, + }, + }; // Add schemas for each request type this.schemas = { [NewMessageType.Get]: this.compileSchema({ - type: "object", - required: ["type", "instance", "schema"], - additionalProperties: false, + ...baseRpcSchema, properties: { - type: {type: "string", const: "get"}, - path: {type: "string"}, - instance: {...notEmptyStringRule}, - schema: {...notEmptyStringRule}, - requestId: {type: "string"}, - format: {type: "string", enum: ["nested", "flat"]}, - root: {type: "string", enum: ["absolute", "relative"]}, - }, - errorMessage: { - required: { - type: "The 'type' field is required", - path: "The 'path' field is required", - instance: "The 'instance' field is required", + ...baseRpcSchema.properties, + method: {type: "string", const: "get"}, + params: { + ...baseParamsSchema, + properties: { + ...baseParamsSchema.properties, + format: {type: "string", enum: ["nested", "flat"]}, + root: {type: "string", enum: ["absolute", "relative"]}, + }, }, - additionalProperties: "Unexpected property found", - properties: { - ...notEmptyStringErrorMessage("schema"), - ...notEmptyStringErrorMessage("instance") - } }, }), [NewMessageType.Set]: this.compileSchema({ - type: "object", - required: ["type", "schema", "instance", "data"], - additionalProperties: false, + ...baseRpcSchema, properties: { - type: {type: "string", const: "set"}, - path: {type: "string"}, - instance: {...notEmptyStringRule}, - schema: {...notEmptyStringRule}, - data: {}, - requestId: {type: "string"}, - metadata: {type: "object"}, - sync: {type: "string", enum: ["off", "local", "remote"]}, - timeseries: {type: "boolean"}, - }, - errorMessage: { - required: { - type: "The 'type' field is required", - path: "The 'path' field is required", - instance: "The 'instance' field is required", - data: "The 'data' field is required", + ...baseRpcSchema.properties, + method: {type: "string", const: "set"}, + params: { + ...baseParamsSchema, + required: [...baseParamsSchema.required, "data"], + properties: { + ...baseParamsSchema.properties, + data: {}, + metadata: {type: "object"}, + sync: {type: "string", enum: ["off", "local", "remote"]}, + timeseries: {type: "boolean"}, + }, + errorMessage: { + ...baseParamsSchema.errorMessage, + required: { + ...baseParamsSchema.errorMessage.required, + data: "The 'data' field is required", + }, + }, }, - additionalProperties: "Unexpected property found", - properties: { - ...notEmptyStringErrorMessage("schema"), - ...notEmptyStringErrorMessage("instance") - } }, }), [NewMessageType.Subscribe]: this.compileSchema({ - type: "object", - required: ["type", "instance", "schema"], - additionalProperties: false, + ...baseRpcSchema, properties: { - type: {type: "string", enum: ["subscribe"]}, - path: {type: "string"}, - instance: {...notEmptyStringRule}, - schema: {...notEmptyStringRule}, - requestId: {type: "string"}, - format: {type: "string", enum: ["nested", "flat"]}, - root: {type: "string", enum: ["absolute", "relative"]}, - }, - errorMessage: { - required: { - type: "The 'type' field is required", - instance: "The 'instance' field is required", - schema: "The 'schema' field is required", + ...baseRpcSchema.properties, + method: {type: "string", const: "subscribe"}, + params: { + ...baseParamsSchema, + properties: { + ...baseParamsSchema.properties, + format: {type: "string", enum: ["nested", "flat"]}, + root: {type: "string", enum: ["absolute", "relative"]}, + }, }, - additionalProperties: "Unexpected property found", - properties: { - ...notEmptyStringErrorMessage("schema"), - ...notEmptyStringErrorMessage("instance") - } }, }), [NewMessageType.Unsubscribe]: this.compileSchema({ - type: "object", - required: ["type", "instance", "schema"], - additionalProperties: false, + ...baseRpcSchema, properties: { - type: {type: "string", enum: ["unsubscribe"]}, - path: {type: "string"}, - instance: {...notEmptyStringRule}, - schema: {...notEmptyStringRule}, - requestId: {type: "string"}, - }, - errorMessage: { - required: { - type: "The 'type' field is required", - instance: "The 'instance' field is required", - schema: "The 'schema' field is required", + ...baseRpcSchema.properties, + method: {type: "string", const: "unsubscribe"}, + params: { + ...baseParamsSchema, }, - additionalProperties: "Unexpected property found", - properties: { - ...notEmptyStringErrorMessage("schema"), - ...notEmptyStringErrorMessage("instance") - } }, }), [NewMessageType.PermissionsEdit]: this.compileSchema({ @@ -231,64 +238,54 @@ export class RequestValidator { } // Use the mapping to find the schema key - const schemaKey = this.typeToSchemaKey[request.type]; + const schemaKey = this.typeToSchemaKey[request.method]; if (!schemaKey) { - return {valid: false, errors: [`Unknown request type: ${request.type}`]}; + return {valid: false, errors: [`Unknown request method: ${request.method}`], messageDTO: request as NewMessageDTO}; } const validator = this.schemas[schemaKey]; if (!validator) { - return {valid: false, errors: [`Unknown request type: ${request.type}`]}; + return {valid: false, errors: [`Unknown request method: ${request.method}`], messageDTO: request as NewMessageDTO}; } // Validate the parsed object const valid = validator(request); if (valid) { - this.modifyRequest(request); - return {valid: true, message: request as NewMessage}; // Cast to NewMessage + return {valid: true, messageDTO: request as NewMessageDTO}; } else { return { valid: false, errors: validator.errors?.map((error) => error.message || "") || [], + messageDTO: request as NewMessageDTO, }; } } - - /** - * Apply modifications to the request structure and transformations to request values. - * - * @param request - The request object to modify. - */ - private modifyRequest(request: any) { - this.moveSchemaToPathField(request); - request.path = replaceDotsWithUnderscore(request.path); - } - - /** - * Merges `schema` into `path` and removes `schema`. - * If `path` is missing, sets it to `schema`. - * - * @param request - The request object to modify. - */ - private moveSchemaToPathField(request: any) { - if (request.schema && request.path) { - request.path = `${request.schema}_${request.path}`; - } else if (request.schema && !request.path) { - request.path = `${request.schema}`; - } - delete request.schema; - } } // Generic validation rule: Ensure a string is not empty or blank -const notEmptyStringRule = { +const stringValidation = { type: "string", - minLength: 1, // Prevents empty strings ("" ) + minLength: 1, // Prevents empty strings ("") pattern: "^(?!\\s*$).+" // Ensures the string is not just whitespace (" ") }; +// Generic validation rule: Ensure a string is not empty/blank, OR accept integers +const stringOrIntegerRule = { + anyOf: [ + stringValidation, + { + type: "integer", + }, + ], +}; + +// Generic error message: Used for any field requiring a non-empty string or integer +const stringOrIntegerErrorMessage = (fieldName: string) => ({ + [fieldName]: `The '${fieldName}' field must be a non-empty string or an integer`, +}); + // Generic error message: Used for any field requiring a non-empty string const notEmptyStringErrorMessage = (fieldName: string) => ({ [fieldName]: `The '${fieldName}' field must contain at least one non-whitespace character` diff --git a/cdsp/information-layer/utils/error-message-helper.ts b/cdsp/information-layer/utils/error-message-helper.ts deleted file mode 100644 index 41864b73..00000000 --- a/cdsp/information-layer/utils/error-message-helper.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { ErrorMessage } from "../router/utils/ErrorMessage"; - -/** - * Creates an error message object with the specified category, status code, and message. - * - * @param category - The category of the error. - * @param statusCode - The HTTP status code associated with the error. - * @param message - A descriptive message explaining the error. - * @returns An ErrorMessage object containing the category, status code, and message. - */ -export const createErrorMessage = ( - category: string, - statusCode: number, - message: string, -): ErrorMessage => ({ - category, - statusCode, - message, -}); diff --git a/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/ontologies/test_ontology.ttl b/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/ontologies/test_ontology.ttl index e52f3ec0..65ba33bc 100644 --- a/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/ontologies/test_ontology.ttl +++ b/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/ontologies/test_ontology.ttl @@ -1,4 +1,4 @@ -@prefix bmw: . +@prefix car: . @prefix rdfs: . @prefix sosa: . @prefix xsd: . @@ -7,19 +7,19 @@ # syntax issue # semantic issue -bmw:SlidingWindow1Min a bmw:1minSlideWindow ; - bmw:hasWindowSize "PT1M"^^xsd:duration ; +car:SlidingWindow1Min a car:1minSlideWindow ; + car:hasWindowSize "PT1M"^^xsd:duration ; rdfs:label "60". -bmw:SlidingWindow3Sec a bmw:3SecSlideWindow ; - bmw:hasWindowSize "PT3S"^^xsd:duration; +car:SlidingWindow3Sec a car:3SecSlideWindow ; + car:hasWindowSize "PT3S"^^xsd:duration; rdfs:label "3". -bmw:SlidingWindow6Sec a bmw:6SecSlideWindow ; - bmw:hasWindowSize "PT6S"^^xsd:duration; +car:SlidingWindow6Sec a car:6SecSlideWindow ; + car:hasWindowSize "PT6S"^^xsd:duration; rdfs:label "6". -bmw:SlidingWindow5Min a bmw:5MinSlideWindow ; - bmw:hasWindowSize "PT5M"^^xsd:duration ; +car:SlidingWindow5Min a car:5MinSlideWindow ; + car:hasWindowSize "PT5M"^^xsd:duration ; rdfs:label "300". \ No newline at end of file diff --git a/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/queries/output/test_query.rq b/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/queries/output/test_query.rq index 7baf29a3..fd925891 100644 --- a/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/queries/output/test_query.rq +++ b/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/queries/output/test_query.rq @@ -1,18 +1,18 @@ -PREFIX bmw: +PREFIX car: . PREFIX sosa: PREFIX xsd: SELECT ?Vehicle_Speed ?Vehicle_Chassis_SteeringWheel_Angle WHERE { ?speedObs a sosa:Observation ; - sosa:observedProperty bmw:speed ; + sosa:observedProperty car:speed ; sosa:hasSimpleResult ?Vehicle_Speed ; sosa:hasFeatureOfInterest ?vehicle . ?angleObs a sosa:Observation ; - sosa:observedProperty bmw:angle ; + sosa:observedProperty car:angle ; sosa:hasSimpleResult ?Vehicle_Chassis_SteeringWheel_Angle ; sosa:hasFeatureOfInterest ?steeringWheel . - ?vehicle bmw:hasPart ?chassis . - ?chassis bmw:hasPart ?steeringWheel . + ?vehicle car:hasPart ?chassis . + ?chassis car:hasPart ?steeringWheel . FILTER(?Vehicle_Speed > 80 && ABS(?Vehicle_Chassis_SteeringWheel_Angle) > 45) } \ No newline at end of file diff --git a/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/rules/test_rule.dlog b/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/rules/test_rule.dlog index 049fb917..f967a9e3 100644 --- a/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/rules/test_rule.dlog +++ b/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/rules/test_rule.dlog @@ -1,30 +1,29 @@ -@prefix bmw: . +PREFIX car: . @prefix sosa: . @prefix xsd: . # --- Detect sharp turns at high speed --- # Angle > 45°, Speed > 80 km/h -bmw:SharpTurnEvent[?event], -bmw:hasVehicle[?event, ?vehicle], -bmw:hasSpeed[?event, ?speed_value], -bmw:hasAngle[?event, ?angle_value], -bmw:hasTimestamp[?event, ?ts_trimmed] :- - +car:SharpTurnEvent[?event], +car:hasVehicle[?event, ?vehicle], +car:hasSpeed[?event, ?speed_value], +car:hasAngle[?event, ?angle_value], +car:hasTimestamp[?event, ?ts_trimmed] :- # Observation of vehicle speed sosa:Observation[?obs_speed], - sosa:observedProperty[?obs_speed, bmw:speed], + sosa:observedProperty[?obs_speed, car:speed], sosa:hasSimpleResult[?obs_speed, ?speed_value], sosa:phenomenonTime[?obs_speed, ?ts_speed], # Observation of steering angle sosa:Observation[?obs_angle], - sosa:observedProperty[?obs_angle, bmw:angle], + sosa:observedProperty[?obs_angle, car:angle], sosa:hasSimpleResult[?obs_angle, ?angle_value], sosa:phenomenonTime[?obs_angle, ?ts_angle], - # Optional: same vehicle if data linked by bmw:Vehicle - bmw:Vehicle[?vehicle], + # Optional: same vehicle if data linked by car:Vehicle + car:Vehicle[?vehicle], sosa:madeBySensor[?obs_speed, ?vehicle], sosa:madeBySensor[?obs_angle, ?vehicle], diff --git a/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/shacl/vehicle_shacl.ttl b/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/shacl/vehicle_shacl.ttl index f6553cf1..7696dfeb 100644 --- a/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/shacl/vehicle_shacl.ttl +++ b/cdsp/knowledge-layer/common-test-resources/common_fixtures/use_case_fixture/model/shacl/vehicle_shacl.ttl @@ -2,24 +2,24 @@ @prefix rdfs: . @prefix sh: . @prefix xsd: . -@prefix bmw: . +PREFIX car: . @prefix val: . val:VehicleShape a sh:NodeShape ; sh:name "Vehicle" ; - sh:targetClass bmw:Vehicle ; + sh:targetClass car:Vehicle ; sh:property [ sh:name "VIN" ; - sh:path bmw:vin ; + sh:path car:vin ; sh:datatype xsd:string ; sh:minCount 1 ; sh:maxCount 1 ; ] ; sh:property [ sh:name "CurrentLocation" ; - sh:path bmw:hasSignal ; - sh:class bmw:CurrentLocation ; + sh:path car:hasSignal ; + sh:class car:CurrentLocation ; sh:node val:CurrentLocationShape ; sh:nodeKind sh:IRI ; sh:minCount 0 ; @@ -27,22 +27,22 @@ val:VehicleShape a sh:NodeShape ; ] ; sh:property [ sh:name "Speed" ; - sh:path bmw:speed ; + sh:path car:speed ; sh:datatype xsd:float ; sh:minCount 0 ; sh:maxCount 1 ; ] ; sh:property [ sh:name "PWFStatus" ; - sh:path bmw:PWFStatus ; + sh:path car:PWFStatus ; sh:datatype xsd:string ; sh:minCount 0 ; sh:maxCount 1 ; ] ; sh:property [ sh:name "Cabin" ; - sh:path bmw:hasPart ; - sh:class bmw:Cabin ; + sh:path car:hasPart ; + sh:class car:Cabin ; sh:node val:CabinShape ; sh:nodeKind sh:IRI ; sh:minCount 0 ; @@ -50,8 +50,8 @@ val:VehicleShape a sh:NodeShape ; ] ; sh:property [ sh:name "Powertrain" ; - sh:path bmw:hasPart ; - sh:class bmw:Powertrain ; + sh:path car:hasPart ; + sh:class car:Powertrain ; sh:node val:PowertrainShape ; sh:nodeKind sh:IRI ; sh:minCount 0 ; @@ -59,8 +59,8 @@ val:VehicleShape a sh:NodeShape ; ] ; sh:property [ sh:name "Chassis" ; - sh:path bmw:hasPart ; - sh:class bmw:Chassis ; + sh:path car:hasPart ; + sh:class car:Chassis ; sh:node val:ChassisShape ; sh:nodeKind sh:IRI ; sh:minCount 0 ; @@ -69,11 +69,11 @@ val:VehicleShape a sh:NodeShape ; val:ChassisShape a sh:NodeShape; sh:name "Chassis"; - sh:targetClass bmw:Chassis; + sh:targetClass car:Chassis; sh:property [ sh:name "SteeringWheel" ; - sh:path bmw:hasPart ; - sh:class bmw:SteeringWheel ; + sh:path car:hasPart ; + sh:class car:SteeringWheel ; sh:node val:SteeringWheelShape ; sh:nodeKind sh:IRI ; sh:minCount 1 ; @@ -81,10 +81,10 @@ val:ChassisShape a sh:NodeShape; val:SteeringWheelShape a sh:NodeShape; sh:name "SteeringWheel"; - sh:targetClass bmw:SteeringWheel; + sh:targetClass car:SteeringWheel; sh:property [ sh:name "Angle" ; - sh:path bmw:angle ; + sh:path car:angle ; sh:datatype xsd:float ; sh:minCount 1 ; ] . @@ -92,10 +92,10 @@ val:SteeringWheelShape a sh:NodeShape; val:CurrentLocationShape a sh:NodeShape ; sh:name "CurrentLocation" ; - sh:targetClass bmw:CurrentLocation ; + sh:targetClass car:CurrentLocation ; sh:property [ sh:name "Latitude" ; - sh:path bmw:latitude ; + sh:path car:latitude ; sh:datatype xsd:double ; sh:minInclusive -90 ; sh:maxInclusive 90 ; @@ -106,7 +106,7 @@ val:CurrentLocationShape a sh:NodeShape ; ] ; sh:property [ sh:name "Longitude" ; - sh:path bmw:longitude ; + sh:path car:longitude ; sh:datatype xsd:double ; sh:minInclusive -180 ; sh:maxInclusive 180 ; @@ -117,7 +117,7 @@ val:CurrentLocationShape a sh:NodeShape ; ] ; sh:property [ sh:name "Timestamp" ; - sh:path bmw:timestamp ; + sh:path car:timestamp ; sh:datatype xsd:long ; val:unit "iso8601" ; sh:description "Timestamp from GNSS system for current location, formatted according to ISO 8601 with UTC time zone." ; @@ -126,7 +126,7 @@ val:CurrentLocationShape a sh:NodeShape ; ] ; sh:property [ sh:name "Heading" ; - sh:path bmw:heading ; + sh:path car:heading ; sh:datatype xsd:double ; sh:minInclusive 0 ; sh:maxInclusive 360 ; @@ -138,11 +138,11 @@ val:CurrentLocationShape a sh:NodeShape ; val:PowertrainShape a sh:NodeShape; sh:name "Powertrain"; - sh:targetClass bmw:Powertrain; + sh:targetClass car:Powertrain; sh:property [ sh:name "TractionBattery" ; - sh:path bmw:hasPart ; - sh:class bmw:TractionBattery ; + sh:path car:hasPart ; + sh:class car:TractionBattery ; sh:node val:TractionBatteryShape ; sh:nodeKind sh:IRI ; sh:minCount 0 ; @@ -150,8 +150,8 @@ val:PowertrainShape a sh:NodeShape; ] ; sh:property [ sh:name "Transmission" ; - sh:path bmw:hasPart ; - sh:class bmw:Transmission ; + sh:path car:hasPart ; + sh:class car:Transmission ; sh:node val:TransmissionShape ; sh:nodeKind sh:IRI ; sh:minCount 0 ; @@ -160,10 +160,10 @@ val:PowertrainShape a sh:NodeShape; val:TractionBatteryShape a sh:NodeShape; sh:name "TractionBattery"; - sh:targetClass bmw:TractionBattery ; + sh:targetClass car:TractionBattery ; sh:property [ sh:name "NominalVoltage" ; - sh:path bmw:nominalVoltage ; + sh:path car:nominalVoltage ; sh:datatype xsd:int ; sh:description "Nominal voltage typically refers to voltage of fully charged battery when delivering rated capacity." ; sh:minCount 0 ; @@ -171,8 +171,8 @@ val:TractionBatteryShape a sh:NodeShape; ] ; sh:property [ sh:name "StateOfCharge" ; - sh:path bmw:hasSignal ; - sh:class bmw:StateOfCharge; + sh:path car:hasSignal ; + sh:class car:StateOfCharge; sh:node val:StateOfChargeNodeShape; sh:nodeKind sh:IRI ; sh:description "Information on the state of charge of the vehicle's high voltage battery." ; @@ -182,10 +182,10 @@ val:TractionBatteryShape a sh:NodeShape; val:StateOfChargeNodeShape a sh:NodeShape ; sh:name "StateOfCharge" ; - sh:targetClass bmw:StateOfCharge ; + sh:targetClass car:StateOfCharge ; sh:property [ sh:name "CurrentEnergy" ; - sh:path bmw:currentEnergy ; + sh:path car:currentEnergy ; sh:datatype xsd:float ; sh:description "Physical state of charge of high voltage battery expressed in kWh." ; sh:minCount 0 ; @@ -195,10 +195,10 @@ val:StateOfChargeNodeShape a sh:NodeShape ; val:TransmissionShape a sh:NodeShape ; sh:name "Transmission" ; - sh:targetClass bmw:Transmission ; + sh:targetClass car:Transmission ; sh:property [ sh:name "CurrentGear" ; - sh:path bmw:currentGear ; + sh:path car:currentGear ; sh:datatype xsd:int ; sh:description "Is item open or closed? True = Fully or partially open. False = Fully closed." ; sh:minCount 0 ; @@ -208,11 +208,11 @@ val:TransmissionShape a sh:NodeShape ; val:CabinShape a sh:NodeShape ; sh:name "Cabin" ; - sh:targetClass bmw:Cabin ; + sh:targetClass car:Cabin ; sh:property [ sh:name "Light" ; - sh:path bmw:hasPart ; - sh:class bmw:CabinLight ; + sh:path car:hasPart ; + sh:class car:CabinLight ; sh:node val:CabinLightShape ; sh:nodeKind sh:IRI ; sh:minCount 0 ; @@ -220,8 +220,8 @@ val:CabinShape a sh:NodeShape ; ] ; sh:property [ sh:name "Door" ; - sh:path bmw:hasPart ; - sh:class bmw:CabinDoor ; + sh:path car:hasPart ; + sh:class car:CabinDoor ; sh:node val:CabinDoorShape ; sh:minCount 0 ; sh:maxCount 1 ; @@ -229,9 +229,9 @@ val:CabinShape a sh:NodeShape ; val:CabinDoorShape a sh:NodeShape ; sh:name "Door" ; - sh:targetClass bmw:CabinDoor ; + sh:targetClass car:CabinDoor ; sh:property [ - sh:path bmw:position ; + sh:path car:position ; sh:name "Row" ; sh:in ("Row1" "Row2") ; sh:datatype xsd:string ; @@ -239,7 +239,7 @@ val:CabinDoorShape a sh:NodeShape ; sh:maxCount 1 ; ] ; sh:property [ - sh:path bmw:position ; + sh:path car:position ; sh:name "Driver" ; sh:in ("Driver") ; sh:datatype xsd:string ; @@ -248,7 +248,7 @@ val:CabinDoorShape a sh:NodeShape ; ] ; sh:property [ sh:name "Passenger" ; - sh:path bmw:position ; + sh:path car:position ; sh:in ("Passenger") ; sh:datatype xsd:string ; sh:minCount 0 ; @@ -256,8 +256,8 @@ val:CabinDoorShape a sh:NodeShape ; ] ; sh:property [ sh:name "Window" ; - sh:path bmw:hasPart ; - sh:class bmw:Window ; + sh:path car:hasPart ; + sh:class car:Window ; sh:node val:WindowShape ; sh:nodeKind sh:IRI ; sh:minCount 0 ; @@ -266,10 +266,10 @@ val:CabinDoorShape a sh:NodeShape ; val:WindowShape a sh:NodeShape ; sh:name "Window" ; - sh:targetClass bmw:Window ; + sh:targetClass car:Window ; sh:property [ sh:name "IsOpen" ; - sh:path bmw:isOpen ; + sh:path car:isOpen ; sh:datatype xsd:boolean ; sh:description "Is item open or closed? True = Fully or partially open. False = Fully closed." ; sh:minCount 0 ; @@ -277,7 +277,7 @@ val:WindowShape a sh:NodeShape ; ] ; sh:property [ sh:name "Position" ; - sh:path bmw:windowPosition ; + sh:path car:windowPosition ; sh:datatype xsd:int ; sh:minInclusive 0 ; sh:maxInclusive 100 ; @@ -288,7 +288,7 @@ val:WindowShape a sh:NodeShape ; ] ; sh:property [ sh:name "Switch" ; - sh:path bmw:switch ; + sh:path car:switch ; sh:datatype xsd:string ; sh:in ("INACTIVE" "CLOSE" "OPEN" "ONE_SHOT_CLOSE" "ONE_SHOT_OPEN") ; sh:description "Switch controlling sliding action such as window, sunroof, or blind." ; @@ -298,11 +298,11 @@ val:WindowShape a sh:NodeShape ; val:CabinLightShape a sh:NodeShape ; sh:name "Light" ; - sh:targetClass bmw:CabinLight ; + sh:targetClass car:CabinLight ; sh:property [ sh:name "AmbientLight" ; - sh:path bmw:hasPart ; - sh:class bmw:AmbientLight ; + sh:path car:hasPart ; + sh:class car:AmbientLight ; sh:node val:AmbientLightShape ; sh:nodeKind sh:IRI ; sh:minCount 0 ; @@ -310,8 +310,8 @@ val:CabinLightShape a sh:NodeShape ; ] ; sh:property [ sh:name "SpotLight" ; - sh:path bmw:hasPart ; - sh:class bmw:SpotLight ; + sh:path car:hasPart ; + sh:class car:SpotLight ; sh:node val:SpotLightShape ; sh:nodeKind sh:IRI ; sh:minCount 0 ; @@ -320,9 +320,9 @@ val:CabinLightShape a sh:NodeShape ; val:AmbientLightShape a sh:NodeShape ; sh:name "AmbientLight" ; - sh:targetClass bmw:AmbientLight ; + sh:targetClass car:AmbientLight ; sh:property [ - sh:path bmw:position ; + sh:path car:position ; sh:name "Row" ; sh:in ("Row1" "Row2") ; sh:datatype xsd:string ; @@ -330,7 +330,7 @@ val:AmbientLightShape a sh:NodeShape ; sh:maxCount 1 ; ] ; sh:property [ - sh:path bmw:position ; + sh:path car:position ; sh:name "Driver" ; sh:in ("Driver") ; sh:datatype xsd:string ; @@ -338,7 +338,7 @@ val:AmbientLightShape a sh:NodeShape ; sh:maxCount 1 ; ] ; sh:property [ - sh:path bmw:position ; + sh:path car:position ; sh:name "Passenger" ; sh:in ("Passenger") ; sh:datatype xsd:string ; @@ -347,7 +347,7 @@ val:AmbientLightShape a sh:NodeShape ; ] ; sh:property [ sh:name "Colour" ; - sh:path bmw:lightColor ; + sh:path car:lightColor ; sh:datatype xsd:string ; sh:description "Hexadecimal color code represented as a 3-byte RGB (i.e. Red, Green, and Blue) value preceded by a hash symbol '#'."; sh:minCount 0 ; @@ -355,7 +355,7 @@ val:AmbientLightShape a sh:NodeShape ; ] ; sh:property [ sh:name "Intensity" ; - sh:path bmw:lightIntensity ; + sh:path car:lightIntensity ; sh:datatype xsd:integer ; sh:minInclusive 1 ; sh:maxInclusive 100 ; @@ -366,7 +366,7 @@ val:AmbientLightShape a sh:NodeShape ; ] ; sh:property [ sh:name "IsLightOn" ; - sh:path bmw:lightIsLightOn ; + sh:path car:lightIsLightOn ; sh:datatype xsd:boolean ; sh:description "Indicates whether the light is turned on. True = On, False = Off." ; sh:minCount 0 ; diff --git a/cdsp/knowledge-layer/connector/data-objects/dto/get_message_dto.h b/cdsp/knowledge-layer/connector/data-objects/dto/get_message_dto.h index 07159118..1825c822 100644 --- a/cdsp/knowledge-layer/connector/data-objects/dto/get_message_dto.h +++ b/cdsp/knowledge-layer/connector/data-objects/dto/get_message_dto.h @@ -40,7 +40,9 @@ inline void to_json(nlohmann::json &json_obj, const GetMessageDTO &dto) { params_obj["root"] = *dto.root; } - json_obj = - nlohmann::json{{"jsonrpc", getJsonRpcVersion()}, {"id", dto.id}, {"params", params_obj}}; + json_obj = nlohmann::json{{"jsonrpc", getJsonRpcVersion()}, + {"method", "get"}, + {"id", dto.id}, + {"params", params_obj}}; } -#endif // GET_MESSAGE_DTO_H \ No newline at end of file +#endif // GET_MESSAGE_DTO_H diff --git a/cdsp/knowledge-layer/connector/data-objects/dto/subscribe_message_dto.h b/cdsp/knowledge-layer/connector/data-objects/dto/subscribe_message_dto.h index b153383f..e8416f40 100644 --- a/cdsp/knowledge-layer/connector/data-objects/dto/subscribe_message_dto.h +++ b/cdsp/knowledge-layer/connector/data-objects/dto/subscribe_message_dto.h @@ -41,8 +41,10 @@ inline void to_json(nlohmann::json &json_obj, const SubscribeMessageDTO &dto) { if (dto.root) { params_obj["root"] = *dto.root; } - json_obj = - nlohmann::json{{"jsonrpc", getJsonRpcVersion()}, {"id", dto.id}, {"params", params_obj}}; + json_obj = nlohmann::json{{"jsonrpc", getJsonRpcVersion()}, + {"method", "subscribe"}, + {"id", dto.id}, + {"params", params_obj}}; } -#endif // SUBSCRIBE_MESSAGE_DTO_H \ No newline at end of file +#endif // SUBSCRIBE_MESSAGE_DTO_H diff --git a/cdsp/knowledge-layer/connector/json-rdf-convertor/services/tests/reasoning_query_service_integration_test.cpp b/cdsp/knowledge-layer/connector/json-rdf-convertor/services/tests/reasoning_query_service_integration_test.cpp index 318ae056..db9701a1 100644 --- a/cdsp/knowledge-layer/connector/json-rdf-convertor/services/tests/reasoning_query_service_integration_test.cpp +++ b/cdsp/knowledge-layer/connector/json-rdf-convertor/services/tests/reasoning_query_service_integration_test.cpp @@ -74,37 +74,37 @@ TEST_F(ReasoningQueryServiceTest, ProcessRegularReasoningQuery_Success) { )"_json); const std::string triple_data = R"( - @prefix bmw: . + PREFIX car: . @prefix sosa: . @prefix xsd: . - bmw:VehicleObservation_speed_20251021114517921301000 + car:VehicleObservation_speed_20251021114517921301000 a sosa:Observation ; - sosa:hasFeatureOfInterest bmw:VehicleVINABC_Vehicle ; + sosa:hasFeatureOfInterest car:VehicleVINABC_Vehicle ; sosa:hasSimpleResult "100"^^xsd:float ; - sosa:observedProperty bmw:speed ; + sosa:observedProperty car:speed ; sosa:phenomenonTime "2025-10-21T11:45:17.921301000Z"^^xsd:dateTime . - bmw:VehicleVINABC_Vehicle - a bmw:Vehicle . + car:VehicleVINABC_Vehicle + a car:Vehicle . - bmw:ChassisVINABC_Vehicle - a bmw:Chassis ; - bmw:hasPart bmw:SteeringWheelVINABC_Vehicle . + car:ChassisVINABC_Vehicle + a car:Chassis ; + car:hasPart car:SteeringWheelVINABC_Vehicle . - bmw:SteeringWheelVINABC_Vehicle - a bmw:SteeringWheel . + car:SteeringWheelVINABC_Vehicle + a car:SteeringWheel . - bmw:VehicleObservation_angle_20251021114518921301000 + car:VehicleObservation_angle_20251021114518921301000 a sosa:Observation ; - sosa:hasFeatureOfInterest bmw:SteeringWheelVINABC_Vehicle ; + sosa:hasFeatureOfInterest car:SteeringWheelVINABC_Vehicle ; sosa:hasSimpleResult "55"^^xsd:float ; - sosa:observedProperty bmw:angle ; + sosa:observedProperty car:angle ; sosa:phenomenonTime "2025-10-21T11:45:18.921301000Z"^^xsd:dateTime . - bmw:VehicleVINABC_Vehicle - a bmw:Vehicle ; - bmw:hasPart bmw:ChassisVINABC_Vehicle . + car:VehicleVINABC_Vehicle + a car:Vehicle ; + car:hasPart car:ChassisVINABC_Vehicle . )"; reasoner_service_->loadData(triple_data, model_config->getReasonerSettings().getOutputFormat()); diff --git a/cdsp/knowledge-layer/connector/websocket-client/services/converters/model_config_converter.cpp b/cdsp/knowledge-layer/connector/websocket-client/services/converters/model_config_converter.cpp index 0d190ccc..ad07eb46 100644 --- a/cdsp/knowledge-layer/connector/websocket-client/services/converters/model_config_converter.cpp +++ b/cdsp/knowledge-layer/connector/websocket-client/services/converters/model_config_converter.cpp @@ -3,7 +3,9 @@ #include #include +#include "data_types.h" #include "globals.h" +#include "helper.h" #include "model_config.h" #include "triple_assembler_helper.h" @@ -207,7 +209,7 @@ std::map ModelConfigConverter::getInputsFromDto( for (const auto& [collection, data] : inputs) { auto collection_name = collection.substr(0, collection.find(INPUT_SUFFIX)); auto schema_type = stringToSchemaType(collection_name); - auto data_points = getSupportedDataPoints(data); + auto data_points = getSupportedDataPoints(data, schema_type); inputs_map[schema_type] = data_points; } return inputs_map; @@ -251,7 +253,8 @@ ModelConfigConverter::getReasonerSyntaxTypeAndContent(const std::vectorreadFile(root); @@ -262,6 +265,15 @@ SchemaInputList ModelConfigConverter::getSupportedDataPoints(std::string file_na std::istringstream file(file_content); std::string line; while (std::getline(file, line)) { + // Attempt to detect schema type prefix in the line + auto str_schema_type = schemaTypeToString(schema_type); + auto get_chars_number = str_schema_type.length() + 1; // +1 for the dot separator + auto line_prefix = line.substr(0, get_chars_number); + if (Helper::toLowerCase(line_prefix) == Helper::toLowerCase(str_schema_type) + ".") { + schema_input_list.subscribe.push_back( + line.substr(get_chars_number, line.length() - get_chars_number)); + continue; + } schema_input_list.subscribe.push_back(line); } return schema_input_list; diff --git a/cdsp/knowledge-layer/connector/websocket-client/services/converters/model_config_converter.h b/cdsp/knowledge-layer/connector/websocket-client/services/converters/model_config_converter.h index c19cf62f..e347d7be 100644 --- a/cdsp/knowledge-layer/connector/websocket-client/services/converters/model_config_converter.h +++ b/cdsp/knowledge-layer/connector/websocket-client/services/converters/model_config_converter.h @@ -23,7 +23,7 @@ class ModelConfigConverter { const std::string& reasoning_output_queries_path); std::map getInputsFromDto( const std::map& inputs); - SchemaInputList getSupportedDataPoints(std::string file_name); + SchemaInputList getSupportedDataPoints(std::string file_name, SchemaType schema_type); std::string getFullModelConfigPath(const std::string& model_config_file); std::vector> getReasonerSyntaxTypeAndContent( const std::vector& file_list); diff --git a/cdsp/knowledge-layer/connector/websocket-client/services/message_service.cpp b/cdsp/knowledge-layer/connector/websocket-client/services/message_service.cpp index d80ef90f..bca5060c 100644 --- a/cdsp/knowledge-layer/connector/websocket-client/services/message_service.cpp +++ b/cdsp/knowledge-layer/connector/websocket-client/services/message_service.cpp @@ -250,7 +250,7 @@ std::variant MessageService::displayAndParseMe if (json_message.contains("error") || (json_message.contains("result") && json_message["result"].empty())) { std::cout << "(" << Helper::getFormattedTimestampNow("%Y-%m-%dT%H:%M:%S", true, true) - << ") Websocket-Server: Error status message received\n" + << ") Websocket-Server: Status message received correctly\n" << "Message Content: " << json_message.dump() << "\n"; return DtoService::parseStatusJsonToDto(json_message); } diff --git a/cdsp/knowledge-layer/connector/websocket-client/src/websocket_client.cpp b/cdsp/knowledge-layer/connector/websocket-client/src/websocket_client.cpp index 4dd57d65..6ab65a31 100644 --- a/cdsp/knowledge-layer/connector/websocket-client/src/websocket_client.cpp +++ b/cdsp/knowledge-layer/connector/websocket-client/src/websocket_client.cpp @@ -39,6 +39,7 @@ WebSocketClient::WebSocketClient(SystemConfig system_config, model_config_(std::move(model_config)), connection_(std::move(connection)), triple_assembler_(model_config_, *reasoner_service_, file_handler_, triple_writer_), + request_registry_(std::make_shared()), reasoner_query_service_(std::make_shared(reasoner_service_)) { triple_assembler_.initialize(); } @@ -52,7 +53,7 @@ WebSocketClient::WebSocketClient(SystemConfig system_config, */ void WebSocketClient::initializeConnection() { if (!connection_) { - auto self = shared_from_this(); + auto self = std::enable_shared_from_this::shared_from_this(); connection_ = std::make_shared(io_context_, self); } } diff --git a/examples/remotivelabs-feeder/rl-bridge.py b/examples/remotivelabs-feeder/rl-bridge.py index 6923ecf4..ad9aea2b 100644 --- a/examples/remotivelabs-feeder/rl-bridge.py +++ b/examples/remotivelabs-feeder/rl-bridge.py @@ -106,37 +106,43 @@ async def websocket_handler(element_id: str): websocket = await websockets.connect(information_layer_url) print("Connected to information-layer websocket.") + message_id = 1 + while not stopped: async with signal_map_async_lock: # Use asyncio.Lock here if not signal_map: await asyncio.sleep(0.1) continue - # Prepare WebSocket data format + # Prepare WebSocket data format using JSON-RPC 2.0 websocket_data = { - "type": "set", - "instance": element_id, + "jsonrpc": "2.0", + "method": "set", + "id": str(message_id), + "params": { + "instance": element_id, + } } if len(signal_map) == 1: signal_name, signal_value_timestamp = next(iter(signal_map.items())) schema, _, path = signal_name.partition('.') - websocket_data["schema"] = schema - websocket_data["path"] = path - websocket_data["data"] = signal_value_timestamp[0] - websocket_data["metadata"] = timestamp_to_metadata_format(signal_value_timestamp[1]) + websocket_data["params"]["schema"] = schema + websocket_data["params"]["path"] = path + websocket_data["params"]["data"] = signal_value_timestamp[0] + websocket_data["params"]["metadata"] = timestamp_to_metadata_format(signal_value_timestamp[1]) else: # take any signal to extract the root path, that should be identical for all cases common_prefix, _ = next(iter(signal_map)).split('.', 1) - websocket_data["schema"] = common_prefix.rstrip('.') - websocket_data["data"] = {} + websocket_data["params"]["schema"] = common_prefix.rstrip('.') + websocket_data["params"]["data"] = {} metadata_accumulation = {} for name, value_timestamp in signal_map.items(): parts = name.split('.') - current_level = websocket_data["data"] + current_level = websocket_data["params"]["data"] # Traverse through the parts except the first and last one, creating nested dictionaries as needed. - # First part can be removed, because it is used in the websocket_data["path"] field. + # First part can be removed, because it is used in the websocket_data["params"]["schema"] field. # Last part is stored not as dictionary, but as a value. for part in parts[1:-1]: current_level = current_level.setdefault(part, {}) @@ -147,7 +153,7 @@ async def websocket_handler(element_id: str): metadata_accumulation.update(timestamp_to_metadata_format(value_timestamp[1], data_point_path)) # add accumulated metadata - websocket_data["metadata"] = metadata_accumulation + websocket_data["params"]["metadata"] = metadata_accumulation signal_map.clear() try: @@ -157,11 +163,13 @@ async def websocket_handler(element_id: str): response = await websocket.recv() response_data = json.loads(response) - if response_data.get("code") != 200: + if "error" in response_data: safe_exit(f"Unexpected response from information-layer: {response}") timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] print(f"{timestamp} Received response from information-layer: {response}") + message_id += 1 + except Exception as e: if not stopped: safe_exit(f"Error communicating with information-layer: {e}") From b6812a2fa2ebe1180ca87a08bcc10d9958a15f82 Mon Sep 17 00:00:00 2001 From: Christian Muehlbauer Date: Tue, 17 Feb 2026 08:01:54 +0100 Subject: [PATCH 5/7] Change IotDB healthcheck to use the REST /ping endpoint, so it is more efficient and stable. Signed-off-by: Christian Muehlbauer --- docker/docker-compose-cdsp.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/docker-compose-cdsp.yml b/docker/docker-compose-cdsp.yml index 484f48d8..7e02c36f 100644 --- a/docker/docker-compose-cdsp.yml +++ b/docker/docker-compose-cdsp.yml @@ -29,8 +29,8 @@ services: volumes: - ./iotdb-data:/iotdb/data - ./iotdb-logs:/iotdb/logs - healthcheck: - test: [ "CMD", "./start-cli.sh", "-h", "iotdb-service", "-p", "6667", "-u", "root", "-pw", "root", "-e", "SHOW DATABASES;" ] + healthcheck: + test: [ "CMD", "curl", "-f", "http://iotdb-service:18080/ping" ] interval: 10s timeout: 10s retries: 5 From efe221375ee76b0d40552b60326546cd1365f916 Mon Sep 17 00:00:00 2001 From: Christian Muehlbauer Date: Tue, 17 Feb 2026 08:18:59 +0100 Subject: [PATCH 6/7] Remove RealmDB from documentation Add documentation for adding certificate to knowledge layer docker file. Update documentation for using the services from the docker folder Signed-off-by: Christian Muehlbauer --- cdsp/information-layer/.dockerignore | 4 +- cdsp/information-layer/.gitignore | 5 +- cdsp/information-layer/README.md | 43 +---- cdsp/information-layer/handlers/src/README.md | 31 ++-- .../handlers/src/iotdb/README.md | 4 +- cdsp/information-layer/router/README.md | 2 +- cdsp/knowledge-layer/README.md | 8 +- docker/.gitignore | 4 +- docker/README.md | 152 +++++++++++++----- docker/docker-compose-cdsp.yml | 7 + .../content/overview/cdsp-logical-concept.md | 4 +- docs/example-guideline.md | 4 +- examples/knowledgelayer-hello-world/README.md | 4 +- examples/remotivelabs-feeder/README.md | 4 +- 14 files changed, 157 insertions(+), 119 deletions(-) diff --git a/cdsp/information-layer/.dockerignore b/cdsp/information-layer/.dockerignore index 1c412eb8..843500cc 100644 --- a/cdsp/information-layer/.dockerignore +++ b/cdsp/information-layer/.dockerignore @@ -1,6 +1,4 @@ -# ignore mongodb generated files -mongodb-realm # ignore all node generated file node_modules -package-lock.json \ No newline at end of file +package-lock.json diff --git a/cdsp/information-layer/.gitignore b/cdsp/information-layer/.gitignore index 060b1233..567ef892 100644 --- a/cdsp/information-layer/.gitignore +++ b/cdsp/information-layer/.gitignore @@ -1,8 +1,5 @@ -# MongoDB Realm -mongodb-realm/ - # Node.js node_modules/ # typescript migration -dist \ No newline at end of file +dist diff --git a/cdsp/information-layer/README.md b/cdsp/information-layer/README.md index a14fc1e9..8c9bd880 100644 --- a/cdsp/information-layer/README.md +++ b/cdsp/information-layer/README.md @@ -1,6 +1,6 @@ # Information Layer Server (IL) -The information layer Server (IL) is responsible for providing a raw data access [API](#api) (read, write, subscribe). +The information layer Server (IL) is responsible for providing a raw data access [API](#access-information-layer-server-api) (read, write, subscribe). Its intention is to abstract the underlying data storage database technology. It is written in Typescript. Clients can interact with it using websockets and JSON payload. @@ -14,9 +14,7 @@ The IL consists of two logical components: flowchart LR client <--> router router <--> handlers - handlers <--> realm_handler handlers <--> iotdb_handler - realm_handler <--> realmdb iotdb_handler <--> iotdb ``` @@ -48,7 +46,7 @@ or npx jest --verbose ``` -## Run IL with backend option 1: Timeseries Data Server IoTDB +## Run IL with IotDB: Timeseries Data Server IoTDB ### Start the database @@ -102,43 +100,6 @@ npm install HANDLER_TYPE=iotdb IOTDB_HOST=localhost npm start ``` -## Run IL with backend option 2: Document Data Service RealmDB - -### Prepare cloud - -- Ensure that in your [ATLAS cloud](https://cloud.mongodb.com/) app there is a vehicle _document_ with an - `Vehicle_VehicleIdentification_VIN` in a collection named _`Vehicles`_. -- Ensure that this document as well contains VSS data. Here you can see the data supported in this repository for a vehicle document within - _Vehicles_ that should be reflected in ATLAS: - -``` -_id: "" (String) -Vehicle_Speed: (Double) -Vehicle_TraveledDistance: "" (Double) -``` - -### Start Information Layer Server - -Build the Information Layer image - -```bash -docker build -t information-layer . -``` - -Run the Information Layer - -```bash -# Docker -docker run -d --rm --name information-layer --network cdsp-net -p 8080:8080 -e HANDLER_TYPE=realmdb -e VERSION_REALMDB_SCHEMA=0 -e REALMDB_APP_ID= -e REALMDB_API_KEY= information-layer -# OR natively -npm install -HANDLER_TYPE=realmdb VERSION_REALMDB_SCHEMA=0 REALM_APP_ID= REALM_API_KEY= npm start -``` - -## Usage - -See [api](#api) how to interact with the router. - # Access Information Layer Server API Connect your own websocket client by connecting to `ws://localhost:8080`. diff --git a/cdsp/information-layer/handlers/src/README.md b/cdsp/information-layer/handlers/src/README.md index 8fa8f3f7..5ee10b12 100644 --- a/cdsp/information-layer/handlers/src/README.md +++ b/cdsp/information-layer/handlers/src/README.md @@ -1,10 +1,10 @@ # Database handlers -This project already contains handlers configured to be used with RealmDB and IoTDB. +This project contains one handler configured to be used with IoTDB. ## Adding a New Database Handler -This project uses a handler interface to dynamically integrate new database backends such as RealmDB or IoTDB. Each handler must implement the core functionality to handle WebSocket messages (get, set, subscribe, unsubscribe). +This project uses a handler interface to dynamically integrate new database backends such as IoTDB, RealmDB and other DB solutions. Each handler must implement necessary methods and core functionalities to handle WebSocket messages (get, set, subscribe, unsubscribe). ### How to Add a New Database Handler @@ -13,23 +13,30 @@ This project uses a handler interface to dynamically integrate new database back 2. **Implement the handler methods**: You must implement the following methods in your new handler: - `authenticateAndConnect()`: Establish a connection with the database and authenticate. - - `get(message, ws)`: Retrieve data from the database based on the incoming WebSocket message. + - `getKnownDatapointsByPrefix(prefix)`: Return a list of all known data point names that begin with the specified prefix. + - `getDataPointsFromDB(dataPoints, vin)`: Return the dataPoints from DB based on the provided list of dataPoints and VIN. - `set(message, ws)`: Write data to the database. - `subscribe(message, ws)`: Subscribe to changes in the database, and automatically send updates over WebSocket. - `unsubscribe(message, ws)`: Unsubscribe from database updates. + - `unsubscribe_client(ws)`: Unsubscribing the client itself on a closed connection. 3. **Example Handler Implementation**: Here’s a basic template you can follow: ```js const Handler = require('../../handler'); -class MyDBHandler extends Handler { +export class MyDBHandler extends Handler { + async authenticateAndConnect() { // Connect to your database here } -async get(message, ws) { - // Implement the logic to read data from the database +getKnownDatapointsByPrefix(datapointPrefix) { + // Return a list of all known data point names that begin with the specified prefix +} + +async getDataPointsFromDB(dataPoints, vin) { + // Return the dataPoints from DB based on the provided list of dataPoints and VIN } async set(message, ws) { @@ -44,12 +51,10 @@ async unsubscribe(message, ws) { // Implement the logic to unsubscribe from updates } } - -module.exports = MyDBHandler; ``` 4. **Create configuration files**: - Create the the configuration files into `./mydb/config` to include parameters for your new database (e.g., database names, data schemas, etc.). + Create the configuration files into `./mydb/config` to include parameters for your new database (e.g., database names, data schemas, etc.). > [!IMPORTANT] > Ensure to create the necessary files to support the necessary data points that will be store in the DB and required for your clients. See [how](../config/README.md). @@ -81,9 +86,6 @@ In order to work with your custom database handler, it is required to create it ```ts switch (handlerType) { - case "realmdb": - handler = new RealmDBHandler(); - break; case "iotdb": handler = new IoTDBHandler(); break; @@ -101,7 +103,6 @@ Run the WebSocket server, and connect with your handler by sending WebSocket mes ### Existing Handlers You can check the following examples to understand how to structure your new handler: -- **RealmDB Handler**: [RealmHandler](./realmdb/src/RealmDBHandler.ts) provides an example of how to interact with RealmDB. -- **IoTDB Handler**: A similar implementation can be followed for [IoTDBHandler](./iotdb/src/IoTDBHandler.ts). +- **IoTDB Handler**: provides an example implementation with IotDB. [IoTDBHandler](./iotdb/src/IoTDBHandler.ts). -For additional logging, you can utilize the `logMessage` function from [logger.ts](../../utils/logger.ts). \ No newline at end of file +For additional logging, you can utilize the `logMessage` function from [logger.ts](../../utils/logger.ts). diff --git a/cdsp/information-layer/handlers/src/iotdb/README.md b/cdsp/information-layer/handlers/src/iotdb/README.md index 51311abf..758942b4 100644 --- a/cdsp/information-layer/handlers/src/iotdb/README.md +++ b/cdsp/information-layer/handlers/src/iotdb/README.md @@ -37,8 +37,8 @@ Create (if it does not exist) `/docker/.env` and add the following environment v IOTDB_USER="your-iotdb-user" # Default "root" IOTDB_PASSWORD="your-iotdb-password" # Default "root" IOTDB_TIMEZONE="your-time-zone" # Default your local configured time zone - FETCH_SIZE=10000 #number of rows that will be fetched from the database at a time when executing a query, default 10000 - IOTDB_POLL_INTERVAL_LEN_IN_SEC=5 #number of seconds of interval to poll for changes on the IoTDB + IOTDB_FETCH_SIZE=10000 #number of rows that will be fetched from the database at a time when executing a query, default 10000 + IOTDB_POLL_INTERVAL_LEN_IN_SEC=5 #number of seconds of interval to poll for changes on the IoTDB, default "0.2" ``` > [!WARNING] diff --git a/cdsp/information-layer/router/README.md b/cdsp/information-layer/router/README.md index 4cf5859c..584738af 100644 --- a/cdsp/information-layer/router/README.md +++ b/cdsp/information-layer/router/README.md @@ -2,4 +2,4 @@ Provides information layer data in tree-like format. Abstracts data store details. -This project is a [WebSocket server](src/websocket-server.ts) that dynamically integrates different database handlers (e.g., RealmDB, IoTDB) based on the [configuration](../handlers/src/README.md). It listens for incoming WebSocket connections, processes messages according to the specified handler, and broadcasts messages to connected clients. +This project is a [WebSocket server](src/websocket-server.ts) that dynamically integrates different database handlers (e.g. IoTDB) based on the [configuration](../handlers/src/README.md). It listens for incoming WebSocket connections, processes messages according to the specified handler, and broadcasts messages to connected clients. diff --git a/cdsp/knowledge-layer/README.md b/cdsp/knowledge-layer/README.md index 9f5b409a..0f710fa5 100644 --- a/cdsp/knowledge-layer/README.md +++ b/cdsp/knowledge-layer/README.md @@ -140,10 +140,10 @@ VEHICLE_OBJECT_ID="vin_to_subscribe" # RDFox CONFIGURATION # ################################## -HOST_RDFOX_SERVER="your_custom_rdfox_server_host" -PORT_RDFOX_SERVER="your_custom_rdfox_server_port" -AUTH_RDFOX_SERVER_BASE64="your_custom_rdfox_server_authentication" -RDFOX_DATASTORE="your_custom_rdfox_server_data_store" +HOST_REASONER_SERVER="your_custom_rdfox_server_host" +PORT_REASONER_SERVER="your_custom_rdfox_server_port" +AUTH_REASONER_SERVER_BASE64="your_custom_rdfox_server_authentication" +REASONER_DATASTORE_NAME="your_custom_rdfox_server_data_store" ``` ### Start the Reasoner Websocket Client diff --git a/docker/.gitignore b/docker/.gitignore index 3003327a..69a986c3 100644 --- a/docker/.gitignore +++ b/docker/.gitignore @@ -1,9 +1,11 @@ # Docker generated files data/ logs/ +iotdb-data/ +iotdb-logs/ test-model/ rdfox/ scripts/ # Enviroment variables containing secrets -.env \ No newline at end of file +.env diff --git a/docker/README.md b/docker/README.md index e13c1bb2..e4128735 100644 --- a/docker/README.md +++ b/docker/README.md @@ -23,6 +23,14 @@ Comprehensive Docker CE [installation instructions](https://docs.docker.com/engi If you have a recent version of Docker installed but without the compose plugin, please add the plugin or upgrade. +### Prepare Environment Variables for Docker +To use the provided [docker-compose-cdsp.yml](docker-compose-cdsp.yml) there must be a `.env` file in the same folder. +Create it and add a placeholder for the RDFox license file for now, like this: +``` +RDFOX_LIC_PATH="." # set path to RDFox.lic file +``` + + ## VISSR docker image build setup The [VISSR VISS Data Server](https://github.com/COVESA/vissr) has no pre-built image in a docker image repository and must therefore be built. Whilst the upstream documentation for VISSR is considered the reference documentation for build environment setup this section collects information that we have needed for a successful build of master branch commit 27d6de0. @@ -104,28 +112,22 @@ $ sudo docker compose -f docker-compose-cdsp.yml up -d iotdb-service ``` ## Websocket-Server (CDSP - information layer) docker image build setup -This guide provides instructions for deploying the Information Layer Server using Docker Compose. You can choose to deploy the server with either RealmDB or IoTDB as the backend service. +This guide provides instructions for deploying the Information Layer Server using Docker Compose. You can deploy the server only with IoTDB as the backend service for now. ### Prerequisites -- Correct configuration of the `.env` file based on the database you are using (RealmDB or IoTDB) +- Create a `.env` file in this folder (docker/.env) if it is missing and add for now the variable `RDFOX_LIC_PATH="." # set path to RDFox.lic file`. this environment variable needs to exist for the [docker compose file](docker-compose-cdsp.yml) to work in general. +How to set the correct value of this variable is described later at [Knowledge Layer Prerequisites](#prerequisites-1) +- Not only information layer service but also others services will expect the `.env` file to exist and some environment variables to be set. More details can be found in other service readme sections. ### Configuring the `.env` File -The `.env` file contains environment variables that need to be configured depending on the database you are using. Detailed instructions on how to configure the `.env` file can be found in the [database handlers README](../cdsp/information-layer/handlers/README.md). +The `.env` file contains environment variables that need to be configured depending on the database you are using. Detailed instructions on how to configure the `.env` file for the included database can be found in the [IotDB handlers README](../cdsp/information-layer/handlers/src/iotdb/README.md). +A custom database handler can be implemented following [database handlers README](../cdsp/information-layer/handlers/src/README.md). > [!IMPORTANT] > Before proceeding with the deployment, ensure that the `.env` file is correctly configured for the database you plan to use. -### Using RealmDB - -To deploy the information-layer with RealmDB, use the following command: - -```shell -$ sudo docker compose -f docker-compose-cdsp.yml up -d information-layer -# [+] Running 1/1 -# ✔ Container information-layer Started 0.4s -``` ### Using IoTDB When deploying with IoTDB, ensure that the iotdb-service is up and running before starting the Information Layer Server. @@ -140,16 +142,16 @@ To start both the iotdb-service and information-layer, use the following command ```shell $ sudo docker compose -f docker-compose-cdsp.yml up -d iotdb-service # [+] Running 1/1 -# ✔ Container iotdb-service Started 0.4s 0.4s +# ✔ Container iotdb-service Started $ sudo docker compose -f docker-compose-cdsp.yml up -d information-layer -# [+] Running 1/1 -# ✔ Container information-layer Started 0.4s +# [+] Running 1/1 +# ✔ Container information-layer Started ``` ### Expected result -Listing should show three running containers as shown below: +Listing should show two running containers as shown below: ```shell $ sudo docker ps ``` @@ -157,7 +159,7 @@ $ sudo docker ps ``` CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 025b5dd05c56 cdsp-information-layer "docker-entrypoint.s…" 16 minutes ago Up 16 minutes 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp information-layer -e16c8ed4ed42 apache/iotdb:1.2.2-standalone "/usr/bin/dumb-init …" 23 minutes ago Up 19 minutes 0.0.0.0:6667->6667/tcp, :::6667->6667/tcp iotdb-service +e16c8ed4ed42 cdsp-iotdb-service "/usr/bin/dumb-init …" 23 minutes ago Up 19 minutes 0.0.0.0:6667->6667/tcp, :::6667->6667/tcp iotdb-service ``` ## Websocket client (CDSP - knowledge layer) docker image build setup @@ -169,7 +171,9 @@ In order to get access to `RDFox RESTfull API` it is required to build two Docke ### Prerequisites - **Running Information Layer Server:** See how to start the information layer [here](#websocket-server-cdsp---information-layer-docker-image-build-setup). -- **RDFox.lic:** this is the license file required by RDFox, containing information that authorizes the use of RDFox, expiry time and usage. **The license file must be provided when running RDFox images to activate the software.**. The path of the file should be provided using the environment variable `RDFOX_LIC_PATH="/RDFox.lic"` in the `.env` file in this folder. The file is generally provided when you acquire a license from [Oxford Semantic Technologies](https://www.oxfordsemantic.tech/). +- **RDFox.lic:** this is the license file required by RDFox, containing information that authorizes the use of RDFox, expiry time and usage. **The license file must be provided when running RDFox images to activate the software.**. The relative path of the file should be provided using the environment variable `RDFOX_LIC_PATH="/RDFox.lic"` in the `.env` file in this folder. The file is generally provided when you acquire a license from [Oxford Semantic Technologies](https://www.oxfordsemantic.tech/). +- For example, you could create here a new folder `rdfox` and place `RDFox.lic` in it. +Then set the environment variable in `.env` to `RDFOX_LIC_PATH="./rdfox/RDFox.lic"` #### 1. **Initialization (`rdfox-init` Image)**: - The `rdfox-init` image is used to set up the RDFox server directory, define roles, and configure persistence. @@ -191,41 +195,107 @@ In order to get access to `RDFox RESTfull API` it is required to build two Docke Use the following commands to start both images: ```shell -$ docker compose -f docker-compose-cdsp.yml up -d rdfox-init +$ sudo docker compose -f docker-compose-cdsp.yml up -d rdfox-init # ... # [+] Running 2/2 -# ✔ Volume "cdsp_rdfox-server-directory" created 0.0s -# ✔ Container rdfox-init Started 0.4s +# ✔ Volume "cdsp_rdfox-server-directory" created +# ✔ Container rdfox-init Started + +$ sudo docker compose -f docker-compose-cdsp.yml up -d rdfox-service +# ... +# [+] Running 2/2 +# ✔ Container rdfox-init Started +# ✔ Container rdfox-service Started +``` -$ docker compose -f docker-compose-cdsp.yml up -d rdfox-service +#### 3. **Websocket client (CDSP - knowledge layer)**: +- Knowledge Layer configuration needs to be added to the `.env` file, like described at [Knowledge Layer Readme](../cdsp/knowledge-layer/README.md#websocket-server-and-rdfox-configuration) +- Keep in mind that the default values for **HOST_WEBSOCKET_SERVER** and **HOST_REASONER_SERVER** are designed for accessing all services running natively on the same machine. But this guide focuses on running all services in separate docker containers, so the host variables needs to reflect the container names. In this case the environment variables should look like this: +```text +# schema REQUIRED_VSS_DATA_POINTS_FILE is the TXT file containing the required data points +REQUIRED_VSS_DATA_POINTS_FILE=vss_data_required.txt + +HOST_WEBSOCKET_SERVER="information-layer" +PORT_WEBSOCKET_SERVER="8080" +HOST_REASONER_SERVER="rdfox-service" +PORT_REASONER_SERVER="12110" +# The following are the default Base64-encoded string of the RDFox credentials configured in the /docker/docker-compose-cdsp.yml file +# The credentials are: root:admin +AUTH_REASONER_SERVER_BASE64="cm9vdDphZG1pbg==" +REASONER_DATASTORE_NAME="ds-test" +VEHICLE_OBJECT_ID="VINABCD1234567890" +``` + +Use the following commands to start knowledge layer: + +```shell +$ sudo docker compose -f docker-compose-cdsp.yml up -d knowledge-layer # ... # [+] Running 2/2 -# ✔ Container rdfox-init Started 0.4s -# ✔ Container rdfox-service Started 0.9s +# ✔ knowledge-layer Created +# ✔ Container knowledge-layer Started ``` -## Deploy with Docker Compose +#### Tip: Corporate CA security (download error "tls: failed to verify certificate:") +If you are working behind a corporate security system that places a _man-in-the-middle_ between your host and the internet you may see security errors when artifacts are downloaded as part of the build process. + +For example: + +``` +3.254 client/client-1.0/simple_client.go:27:2: github.com/akamensky/argparse@v1.4.0: Get "https://proxy.golang.org/github.com/akamensky/argparse/@v/v1.4.0.zip": tls: failed to verify certificate: x509: certificate signed by unknown authority +``` +This is an attribute of docker, rather than the playground and results when the CA root certificate for the security system is not known. + +A solution is to add the CA root certificate to the [Knowledge Layer Dockerfile](../cdsp/knowledge-layer/Dockerfile) which will update the CA store as part of the build and allow the downloads to proceed successfully. + +The VISSR Dockerfile already includes an example of this for a Cisco system which uses the CA root certificate `cisco.crt`, +[see VISSR Tip: Corporate CA security](#tip-corporate-ca-security-download-error-tls-failed-to-verify-certificate) + +To add your own certificate: +1. Ask your IT for the CA root certificate for the security system they use. +2. Copy the `.crt` file into `cdsp/knowledge-layer` +3. Add your own line to the Dockerfile to copy the certificate so that it is included when `update-ca-certificates` is run. As shown below. + +```dockerfile +COPY /usr/local/share/ca-certificates/ +RUN update-ca-certificates +``` + + +## Deploy All Services with Docker Compose ### Start/stop containers Start the containers: ```shell $ sudo docker compose -f docker-compose-cdsp.yml up -d # [+] Running 4/5 -# ⠴ Network cdsp_default Created 1.5s -# ✔ Container vissr_container_volumes Started 0.5s -# ✔ Container iotdb-service Started 0.7s -# ✔ Container app_redis Started 1.0s -# ✔ Container vissv2server Started 1.3s +# ⠴ Network cdsp_default Created +# ✔ Container vissr_container_volumes Started +# ✔ Container iotdb-service Started +# ✔ Container app_redis Started +# ✔ Container vissv2server Started +# ✔ Container rdfox-service Started +# ✔ Container rdfox-init Exited +# ✔ Container information-layer Started +# ✔ Container knowledge-layer Started +# ✔ Container rdfox-service Started ``` +Sometimes a service might not start up successfully when all are started with a single docker compose command. The reason for this are racing conditions between service startups, that docker has no control over. +An easy fix can be to run the same startup command again. Stop and remove the containers: ```shell $ sudo docker compose -f docker-compose-cdsp.yml down # [+] Running 5/5 -# ✔ Container vissv2server Removed 0.0s -# ✔ Container app_redis Removed 0.3s -# ✔ Container iotdb-service Removed 2.2s -# ✔ Container vissr_container_volumes Removed 0.0s -# ✔ Network cdsp_default Removed 0.1s +# ✔ Container vissv2server Removed +# ✔ Container app_redis Removed +# ✔ Container iotdb-service Removed +# ✔ Container vissr_container_volumes Removed +# ✔ Container rdfox-service Removed +# ✔ Container rdfox-init Removed +# ✔ Container information-layer Removed +# ✔ Container knowledge-layer Removed +# ✔ Container rdfox-service Removed +# ✔ Network cdsp_default Removed ``` ### Expected Result Listing should show three running containers as shown below: @@ -233,10 +303,14 @@ Listing should show three running containers as shown below: $ sudo docker ps -CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES -f43ed0c6ba0a cdsp-iotdb-service "/usr/bin/dumb-init …" About a minute ago Up About a minute 0.0.0.0:6667->6667/tcp, :::6667->6667/tcp iotdb-service -7829813bdcb8 cdsp-vissv2server "/app/vissv2server -…" 5 days ago Up 8 seconds 0.0.0.0:8081->8081/tcp, 0.0.0.0:8600->8600/tcp, 0.0.0.0:8887->8887/tcp, 127.0.0.1:8888->8888/tcp vissv2server -8e21a556e398 redis "docker-entrypoint.s…" 5 days ago Up 5 days 6379/tcp app_redis +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +f43ed0c6ba0a cdsp-iotdb-service "/usr/bin/dumb-init …" 11 minutes ago Up 10 minutes 0.0.0.0:6667->6667/tcp, :::6667->6667/tcp iotdb-service +7829813bdcb8 cdsp-vissv2server "/app/vissv2server -…" 11 minutes ago Up 11 minutes 0.0.0.0:8081->8081/tcp, 0.0.0.0:8600->8600/tcp, 0.0.0.0:8887->8887/tcp, 127.0.0.1:8888->8888/tcp vissv2server +8e21a556e398 redis "docker-entrypoint.s…" 11 minutes ago Up 11 minutes 6379/tcp app_redis +37b461fcdc8f cdsp-knowledge-layer "/src/build/bin/reas…" 11 minutes ago Up 11 minutes knowledge-layer +92c4eef4df32 oxfordsemantic/rdfox:latest "/opt/RDFox/RDFox da…" 11 minutes ago Up 11 minutes 0.0.0.0:12110->12110/tcp, [::]:12110->12110/tcp rdfox-service +6391e41171a5 cdsp-iotdb-service "/usr/bin/dumb-init …" 11 minutes ago Up 11 minutes (healthy) 0.0.0.0:6667->6667/tcp, [::]:6667->6667/tcp iotdb-service +3af122904fef cdsp-information-layer "docker-entrypoint.s…" 11 minutes ago Up 11 minutes 0.0.0.0:8080->8080/tcp, [::]:8080->8080/tcp information-layer ``` #### Apache IoTDB diff --git a/docker/docker-compose-cdsp.yml b/docker/docker-compose-cdsp.yml index 7e02c36f..1560ad6d 100644 --- a/docker/docker-compose-cdsp.yml +++ b/docker/docker-compose-cdsp.yml @@ -141,6 +141,13 @@ services: dockerfile: Dockerfile container_name: knowledge-layer entrypoint: ["/src/build/bin/reasoner_client"] + depends_on: + rdfox-service: + condition: service_started + information-layer: + condition: service_started + env_file: + - .env volumes: rdfox-server-directory: diff --git a/docs/docs-gen/content/overview/cdsp-logical-concept.md b/docs/docs-gen/content/overview/cdsp-logical-concept.md index 924baec0..dd4a1c8b 100644 --- a/docs/docs-gen/content/overview/cdsp-logical-concept.md +++ b/docs/docs-gen/content/overview/cdsp-logical-concept.md @@ -78,7 +78,7 @@ At its core the service has requirements in three key areas: 3. Application logic / APIs: standardized APIs for accessing the data such as the [Vehicle Information Service Specification (VISS)](https://github.com/COVESA/vehicle-information-service-specification) or GraphQL. -_Additional requirements:_ It is recognized that additional features, such as synchronization are absolutely desirable and have been a part of discussions in the Data Architecture team. Such features may also already be part of the feature set of the playground components. For example, both the Apache IoTDB and MongoDB Realm databases have sync capabilities. A base feature set is described as a starting point to help readers quickly grasp the concept. Additional features will be created or illustrated collectively based on interest and participation. +_Additional requirements:_ It is recognized that additional features, such as synchronization are absolutely desirable and have been a part of discussions in the Data Architecture team. Such features may also already be part of the feature set of the playground components. For example, the Apache IoTDB databases have sync capabilities. A base feature set is described as a starting point to help readers quickly grasp the concept. Additional features will be created or illustrated collectively based on interest and participation. ### Components @@ -144,4 +144,4 @@ Source: [vss-otaku](https://github.com/slawr/vss-otaku) + Follow the OSS mantra of adopt where you can, extend if needed, create where necessary. -+ Promote flexible reconfiguration of components by favouring loose coupling over tight coupling. \ No newline at end of file ++ Promote flexible reconfiguration of components by favouring loose coupling over tight coupling. diff --git a/docs/example-guideline.md b/docs/example-guideline.md index d729813c..1393bb4b 100644 --- a/docs/example-guideline.md +++ b/docs/example-guideline.md @@ -56,7 +56,7 @@ The examples/ directory contains specific real-world test scenarios, each demons Each example directory contains: -+ **docker-compose.yml**: Extends the main docker/docker-compose-cdsp.yml file without duplicating default configurations to keep things modular and maintainable. Naming of the service can reflect the use-case and configuration. ex. *Information Layer Server IotDB* vs. *Information Layer Server RealmDB* ++ **docker-compose.yml**: Extends the main docker/docker-compose-cdsp.yml file without duplicating default configurations to keep things modular and maintainable. Naming of the service can reflect the use-case and configuration. ex. *Information Layer Server IotDB* vs. *Information Layer Server otherDB* + **KL-config/**: Folder with scenario specific rules, validations, queries, etc. and a JSON config file for finding them and configuration. Will be mounted into the Knowledge-Layer on service startup. @@ -71,4 +71,4 @@ Each example directory contains: 3. The **results** folder in each example is mounted to the **knowledge layer**, allowing persistent storage and easy access to generated outputs. -4. After execution, the results can be reviewed directly within the respective **results** folder. \ No newline at end of file +4. After execution, the results can be reviewed directly within the respective **results** folder. diff --git a/examples/knowledgelayer-hello-world/README.md b/examples/knowledgelayer-hello-world/README.md index 9d2d0e0d..cbd7ce62 100644 --- a/examples/knowledgelayer-hello-world/README.md +++ b/examples/knowledgelayer-hello-world/README.md @@ -2,7 +2,7 @@ ## Why? -This repository demonstrates a simple "Hello World" application of the Knowledge Layer, whose joint development was [proposed](https://wiki.covesa.global/pages/viewpage.action?pageId=71074417) in the COVESA Data Architecture working group. Given the generic nature of the underlying architecture, this use case can serve as a starting point for implementing more complex scenarios across various domains. One major advantage is that the use case is built on logical [playground components](../../cdsp/README.md). These components can often be swapped out (based on availability of alternatives) without significantly impacting other components (sometimes even not at all, e.g., whether using [RealmDB](../../cdsp/information-layer/handlers/src/realmdb/README.md) or [IoTDB](../../cdsp/information-layer/handlers/src/iotdb/README.md), it does not affect the Knowledge Layer). +This repository demonstrates a simple "Hello World" application of the Knowledge Layer, whose joint development was [proposed](https://wiki.covesa.global/pages/viewpage.action?pageId=71074417) in the COVESA Data Architecture working group. Given the generic nature of the underlying architecture, this use case can serve as a starting point for implementing more complex scenarios across various domains. One major advantage is that the use case is built on logical [playground components](../../cdsp/README.md). These components can often be swapped out (based on availability of alternatives) without significantly impacting other components (sometimes even not at all, e.g., whether using [IoTDB](../../cdsp/information-layer/handlers/src/iotdb/README.md) or other database component, it does not affect the Knowledge Layer). ### Key Aspects Illustrated: - **Standardized Data Models:** Using models like [VSS](https://github.com/COVESA/vehicle_signal_specification/) and associated tools enables scalable, generic transformation of data into other formats, like graph data formats. @@ -28,7 +28,7 @@ Live VSS data from the current drive made accessible in the [Information Layer S #### Logical Components and Implementation Decisions: - **Data Model:** [VSS](https://github.com/COVESA/vehicle_signal_specification/) - Describes vehicle data in a standardized format - **Simulator:** [Remotive Labs](../../examples/remotivelabs-feeder/README.md) - Provides raw data for a test drive -- **DB Handler:** [RealmDB](../../cdsp/information-layer/handlers/src/realmdb/README.md), [IoTDB](../../cdsp/information-layer/handlers/src/iotdb/README.md) - Abstracts data store details +- **DB Handler:** [IoTDB](../../cdsp/information-layer/handlers/src/iotdb/README.md) - Abstracts data store details - **DB Router:** [WebSocket Server](../../cdsp/information-layer/router/src/websocket-server.ts) - Provides read, write and subscribe capabilities to VSS data via a websocket server - **Connector:** [Knowledge Layer Connector](../../cdsp/knowledge-layer/connector/README.md) - Connects Knowledge Layer to Information Layer via websocket client and manages data tasks within Knowledge Layer diff --git a/examples/remotivelabs-feeder/README.md b/examples/remotivelabs-feeder/README.md index 12d516e3..8c61101a 100644 --- a/examples/remotivelabs-feeder/README.md +++ b/examples/remotivelabs-feeder/README.md @@ -4,7 +4,7 @@ This a simple example of using the [RemotiveLabs virtual signal platform](https: It is implemented as a bridge between an RemotiveLabs Broker as the data source and the IoTDB server or the Information Layer Server. You specify the signals you wish to subscribe too. The bridge waits for the subscribed signals to be sent from the RemotiveLabs broker, formats them and then writes them * to IoTDB using the specified IoTDB path or -* the websocket API on the Information Layer Server. Depending on the server configuration the data is written to IoTDB or RealmDB. +* the websocket API on the Information Layer Server. Depending on the server configuration the data is written to IoTDB or other database solution. For IoTDB the code assumes the timeseries into which the data will be written already exists in IoTDB. See the setup section for an example. @@ -23,8 +23,6 @@ CREATE DATABASE root.test2 ``` CREATE ALIGNED TIMESERIES root.test2.dev1(`Vehicle.Speed` FLOAT, `Vehicle.Chassis.Accelerator.PedalPosition` INT32, `Vehicle.Powertrain.Transmission.CurrentGear` INT32, `Vehicle.Powertrain.TractionBattery.NominalVoltage` INT32, `Vehicle.Powertrain.TractionBattery.StateOfCharge.CurrentEnergy` FLOAT, `Vehicle.Chassis.SteeringWheel.Angle` INT32, `Vehicle.CurrentLocation.Longitude` DOUBLE, `Vehicle.CurrentLocation.Latitude` DOUBLE, `Vehicle.CurrentLocation.HorizontalAccuracy` DOUBLE) ``` -### RealmDB schema -Is to be clarified ### Bridge install The bridge requires Python3. A list of required packages is provided that can be installed with the pip package manager. From a3ac5de8725216ceaba3568e2218c227d8991a7f Mon Sep 17 00:00:00 2001 From: Haonan Qiu Date: Tue, 17 Mar 2026 13:47:36 +0100 Subject: [PATCH 7/7] Fix the Build Issue install cmake >= 3.30 required for CMP0167 policy replace ubuntu:24.10 with ubuntu:24.04 LTS Signed-off-by: Haonan Qiu --- cdsp/knowledge-layer/CMakeLists.txt | 6 ++++-- cdsp/knowledge-layer/Dockerfile | 29 ++++++++++++++++++++++------- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/cdsp/knowledge-layer/CMakeLists.txt b/cdsp/knowledge-layer/CMakeLists.txt index 51b0c537..216b8226 100644 --- a/cdsp/knowledge-layer/CMakeLists.txt +++ b/cdsp/knowledge-layer/CMakeLists.txt @@ -7,8 +7,10 @@ project(knowledge-layer VERSION 1.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) -# Set the policy related to changes in handling the FindBoost module -cmake_policy(SET CMP0167 NEW) +# Set the safety policy related to changes in handling the FindBoost module +if(POLICY CMP0167) + cmake_policy(SET CMP0167 NEW) +endif() # Find Boost libraries find_package(Boost 1.87 REQUIRED COMPONENTS system filesystem thread) diff --git a/cdsp/knowledge-layer/Dockerfile b/cdsp/knowledge-layer/Dockerfile index ce821b24..1647bc0b 100644 --- a/cdsp/knowledge-layer/Dockerfile +++ b/cdsp/knowledge-layer/Dockerfile @@ -1,13 +1,29 @@ -FROM ubuntu:24.10 AS builder +FROM ubuntu:24.04 AS builder -RUN apt-get -y update && apt-get install -y --no-install-recommends \ +# Install dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential \ - cmake \ g++ \ ca-certificates \ git \ - wget - + wget \ + && rm -rf /var/lib/apt/lists/* + +# Install CMake +ARG CMAKE_VERSION=3.30.3 +ARG TARGETARCH + +RUN case "${TARGETARCH}" in \ + amd64) CMAKE_ARCH=linux-x86_64 ;; \ + arm64) CMAKE_ARCH=linux-aarch64 ;; \ + *) echo "Unsupported architecture: ${TARGETARCH}" && exit 1 ;; \ + esac \ + && wget -q https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERSION}/cmake-${CMAKE_VERSION}-${CMAKE_ARCH}.sh \ + && chmod +x cmake-${CMAKE_VERSION}-${CMAKE_ARCH}.sh \ + && ./cmake-${CMAKE_VERSION}-${CMAKE_ARCH}.sh --skip-license --prefix=/usr/local \ + && rm cmake-${CMAKE_VERSION}-${CMAKE_ARCH}.sh + +# Install Boost RUN wget https://archives.boost.io/release/1.87.0/source/boost_1_87_0.tar.gz \ && tar xzf boost_1_87_0.tar.gz \ && cd boost_1_87_0 \ @@ -27,5 +43,4 @@ WORKDIR /src/build RUN cmake -DCMAKE_BUILD_TYPE=Release .. && make reasoner_client -j8 -ENTRYPOINT ["/src/build/bin/reasoner_client"] - +ENTRYPOINT ["/src/build/bin/reasoner_client"] \ No newline at end of file