From bd207066d5128b03e57297a3bf5d9b7a8425c4ed Mon Sep 17 00:00:00 2001 From: Sebastian Schleemilch Date: Fri, 25 Oct 2024 13:17:09 +0200 Subject: [PATCH] Added struct default validation Signed-off-by: Sebastian Schleemilch --- poetry.lock | 185 +++++++++++++++++- pyproject.toml | 1 + src/vss_tools/datatypes.py | 4 +- src/vss_tools/main.py | 11 +- src/vss_tools/model.py | 60 ++++-- src/vss_tools/tree.py | 60 +++++- .../struct_default_model_nok.vspec | 25 +++ .../struct_default_model_ok.vspec | 25 +++ .../test_structs/struct_default_types.vspec | 26 +++ .../test_structs/test_data_type_parsing.py | 2 +- tests/vspec/test_structs/test_default.py | 31 +++ 11 files changed, 406 insertions(+), 24 deletions(-) create mode 100644 tests/vspec/test_structs/struct_default_model_nok.vspec create mode 100644 tests/vspec/test_structs/struct_default_model_ok.vspec create mode 100644 tests/vspec/test_structs/struct_default_types.vspec create mode 100644 tests/vspec/test_structs/test_default.py diff --git a/poetry.lock b/poetry.lock index ab92299a..e86ca2e1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. [[package]] name = "annotated-types" @@ -25,6 +25,25 @@ files = [ [package.dependencies] six = "*" +[[package]] +name = "attrs" +version = "24.2.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, + {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, +] + +[package.extras] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] + [[package]] name = "cfgv" version = "3.4.0" @@ -278,6 +297,41 @@ files = [ [package.dependencies] six = "*" +[[package]] +name = "jsonschema" +version = "4.23.0" +description = "An implementation of JSON Schema validation for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566"}, + {file = "jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +jsonschema-specifications = ">=2023.03.6" +referencing = ">=0.28.4" +rpds-py = ">=0.7.1" + +[package.extras] +format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=24.6.0)"] + +[[package]] +name = "jsonschema-specifications" +version = "2024.10.1" +description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" +optional = false +python-versions = ">=3.9" +files = [ + {file = "jsonschema_specifications-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf"}, + {file = "jsonschema_specifications-2024.10.1.tar.gz", hash = "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272"}, +] + +[package.dependencies] +referencing = ">=0.31.0" + [[package]] name = "markdown-it-py" version = "3.0.0" @@ -750,6 +804,21 @@ html = ["html5lib (>=1.0,<2.0)"] lxml = ["lxml (>=4.3.0,<5.0.0)"] networkx = ["networkx (>=2.0.0,<3.0.0)"] +[[package]] +name = "referencing" +version = "0.35.1" +description = "JSON Referencing + Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "referencing-0.35.1-py3-none-any.whl", hash = "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de"}, + {file = "referencing-0.35.1.tar.gz", hash = "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c"}, +] + +[package.dependencies] +attrs = ">=22.2.0" +rpds-py = ">=0.7.0" + [[package]] name = "rich" version = "13.9.2" @@ -789,6 +858,118 @@ typing-extensions = "*" dev = ["mypy", "packaging", "pre-commit", "pytest", "pytest-cov", "rich-codex", "ruff", "types-setuptools"] docs = ["markdown-include", "mkdocs", "mkdocs-glightbox", "mkdocs-material-extensions", "mkdocs-material[imaging] (>=9.5.18,<9.6.0)", "mkdocs-rss-plugin", "mkdocstrings[python]", "rich-codex"] +[[package]] +name = "rpds-py" +version = "0.20.0" +description = "Python bindings to Rust's persistent data structures (rpds)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "rpds_py-0.20.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3ad0fda1635f8439cde85c700f964b23ed5fc2d28016b32b9ee5fe30da5c84e2"}, + {file = "rpds_py-0.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9bb4a0d90fdb03437c109a17eade42dfbf6190408f29b2744114d11586611d6f"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6377e647bbfd0a0b159fe557f2c6c602c159fc752fa316572f012fc0bf67150"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb851b7df9dda52dc1415ebee12362047ce771fc36914586b2e9fcbd7d293b3e"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e0f80b739e5a8f54837be5d5c924483996b603d5502bfff79bf33da06164ee2"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a8c94dad2e45324fc74dce25e1645d4d14df9a4e54a30fa0ae8bad9a63928e3"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8e604fe73ba048c06085beaf51147eaec7df856824bfe7b98657cf436623daf"}, + {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:df3de6b7726b52966edf29663e57306b23ef775faf0ac01a3e9f4012a24a4140"}, + {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf258ede5bc22a45c8e726b29835b9303c285ab46fc7c3a4cc770736b5304c9f"}, + {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:55fea87029cded5df854ca7e192ec7bdb7ecd1d9a3f63d5c4eb09148acf4a7ce"}, + {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ae94bd0b2f02c28e199e9bc51485d0c5601f58780636185660f86bf80c89af94"}, + {file = "rpds_py-0.20.0-cp310-none-win32.whl", hash = "sha256:28527c685f237c05445efec62426d285e47a58fb05ba0090a4340b73ecda6dee"}, + {file = "rpds_py-0.20.0-cp310-none-win_amd64.whl", hash = "sha256:238a2d5b1cad28cdc6ed15faf93a998336eb041c4e440dd7f902528b8891b399"}, + {file = "rpds_py-0.20.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac2f4f7a98934c2ed6505aead07b979e6f999389f16b714448fb39bbaa86a489"}, + {file = "rpds_py-0.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:220002c1b846db9afd83371d08d239fdc865e8f8c5795bbaec20916a76db3318"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d7919548df3f25374a1f5d01fbcd38dacab338ef5f33e044744b5c36729c8db"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:758406267907b3781beee0f0edfe4a179fbd97c0be2e9b1154d7f0a1279cf8e5"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3d61339e9f84a3f0767b1995adfb171a0d00a1185192718a17af6e124728e0f5"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1259c7b3705ac0a0bd38197565a5d603218591d3f6cee6e614e380b6ba61c6f6"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c1dc0f53856b9cc9a0ccca0a7cc61d3d20a7088201c0937f3f4048c1718a209"}, + {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7e60cb630f674a31f0368ed32b2a6b4331b8350d67de53c0359992444b116dd3"}, + {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dbe982f38565bb50cb7fb061ebf762c2f254ca3d8c20d4006878766e84266272"}, + {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:514b3293b64187172bc77c8fb0cdae26981618021053b30d8371c3a902d4d5ad"}, + {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d0a26ffe9d4dd35e4dfdd1e71f46401cff0181c75ac174711ccff0459135fa58"}, + {file = "rpds_py-0.20.0-cp311-none-win32.whl", hash = "sha256:89c19a494bf3ad08c1da49445cc5d13d8fefc265f48ee7e7556839acdacf69d0"}, + {file = "rpds_py-0.20.0-cp311-none-win_amd64.whl", hash = "sha256:c638144ce971df84650d3ed0096e2ae7af8e62ecbbb7b201c8935c370df00a2c"}, + {file = "rpds_py-0.20.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a84ab91cbe7aab97f7446652d0ed37d35b68a465aeef8fc41932a9d7eee2c1a6"}, + {file = "rpds_py-0.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:56e27147a5a4c2c21633ff8475d185734c0e4befd1c989b5b95a5d0db699b21b"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2580b0c34583b85efec8c5c5ec9edf2dfe817330cc882ee972ae650e7b5ef739"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b80d4a7900cf6b66bb9cee5c352b2d708e29e5a37fe9bf784fa97fc11504bf6c"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50eccbf054e62a7b2209b28dc7a22d6254860209d6753e6b78cfaeb0075d7bee"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:49a8063ea4296b3a7e81a5dfb8f7b2d73f0b1c20c2af401fb0cdf22e14711a96"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea438162a9fcbee3ecf36c23e6c68237479f89f962f82dae83dc15feeceb37e4"}, + {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:18d7585c463087bddcfa74c2ba267339f14f2515158ac4db30b1f9cbdb62c8ef"}, + {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d4c7d1a051eeb39f5c9547e82ea27cbcc28338482242e3e0b7768033cb083821"}, + {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4df1e3b3bec320790f699890d41c59d250f6beda159ea3c44c3f5bac1976940"}, + {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2cf126d33a91ee6eedc7f3197b53e87a2acdac63602c0f03a02dd69e4b138174"}, + {file = "rpds_py-0.20.0-cp312-none-win32.whl", hash = "sha256:8bc7690f7caee50b04a79bf017a8d020c1f48c2a1077ffe172abec59870f1139"}, + {file = "rpds_py-0.20.0-cp312-none-win_amd64.whl", hash = "sha256:0e13e6952ef264c40587d510ad676a988df19adea20444c2b295e536457bc585"}, + {file = "rpds_py-0.20.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:aa9a0521aeca7d4941499a73ad7d4f8ffa3d1affc50b9ea11d992cd7eff18a29"}, + {file = "rpds_py-0.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1f1d51eccb7e6c32ae89243cb352389228ea62f89cd80823ea7dd1b98e0b91"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a86a9b96070674fc88b6f9f71a97d2c1d3e5165574615d1f9168ecba4cecb24"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c8ef2ebf76df43f5750b46851ed1cdf8f109d7787ca40035fe19fbdc1acc5a7"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b74b25f024b421d5859d156750ea9a65651793d51b76a2e9238c05c9d5f203a9"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57eb94a8c16ab08fef6404301c38318e2c5a32216bf5de453e2714c964c125c8"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1940dae14e715e2e02dfd5b0f64a52e8374a517a1e531ad9412319dc3ac7879"}, + {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d20277fd62e1b992a50c43f13fbe13277a31f8c9f70d59759c88f644d66c619f"}, + {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:06db23d43f26478303e954c34c75182356ca9aa7797d22c5345b16871ab9c45c"}, + {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b2a5db5397d82fa847e4c624b0c98fe59d2d9b7cf0ce6de09e4d2e80f8f5b3f2"}, + {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a35df9f5548fd79cb2f52d27182108c3e6641a4feb0f39067911bf2adaa3e57"}, + {file = "rpds_py-0.20.0-cp313-none-win32.whl", hash = "sha256:fd2d84f40633bc475ef2d5490b9c19543fbf18596dcb1b291e3a12ea5d722f7a"}, + {file = "rpds_py-0.20.0-cp313-none-win_amd64.whl", hash = "sha256:9bc2d153989e3216b0559251b0c260cfd168ec78b1fac33dd485750a228db5a2"}, + {file = "rpds_py-0.20.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:f2fbf7db2012d4876fb0d66b5b9ba6591197b0f165db8d99371d976546472a24"}, + {file = "rpds_py-0.20.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1e5f3cd7397c8f86c8cc72d5a791071431c108edd79872cdd96e00abd8497d29"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce9845054c13696f7af7f2b353e6b4f676dab1b4b215d7fe5e05c6f8bb06f965"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c3e130fd0ec56cb76eb49ef52faead8ff09d13f4527e9b0c400307ff72b408e1"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b16aa0107ecb512b568244ef461f27697164d9a68d8b35090e9b0c1c8b27752"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa7f429242aae2947246587d2964fad750b79e8c233a2367f71b554e9447949c"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af0fc424a5842a11e28956e69395fbbeab2c97c42253169d87e90aac2886d751"}, + {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b8c00a3b1e70c1d3891f0db1b05292747f0dbcfb49c43f9244d04c70fbc40eb8"}, + {file = "rpds_py-0.20.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:40ce74fc86ee4645d0a225498d091d8bc61f39b709ebef8204cb8b5a464d3c0e"}, + {file = "rpds_py-0.20.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:4fe84294c7019456e56d93e8ababdad5a329cd25975be749c3f5f558abb48253"}, + {file = "rpds_py-0.20.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:338ca4539aad4ce70a656e5187a3a31c5204f261aef9f6ab50e50bcdffaf050a"}, + {file = "rpds_py-0.20.0-cp38-none-win32.whl", hash = "sha256:54b43a2b07db18314669092bb2de584524d1ef414588780261e31e85846c26a5"}, + {file = "rpds_py-0.20.0-cp38-none-win_amd64.whl", hash = "sha256:a1862d2d7ce1674cffa6d186d53ca95c6e17ed2b06b3f4c476173565c862d232"}, + {file = "rpds_py-0.20.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:3fde368e9140312b6e8b6c09fb9f8c8c2f00999d1823403ae90cc00480221b22"}, + {file = "rpds_py-0.20.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9824fb430c9cf9af743cf7aaf6707bf14323fb51ee74425c380f4c846ea70789"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11ef6ce74616342888b69878d45e9f779b95d4bd48b382a229fe624a409b72c5"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c52d3f2f82b763a24ef52f5d24358553e8403ce05f893b5347098014f2d9eff2"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d35cef91e59ebbeaa45214861874bc6f19eb35de96db73e467a8358d701a96c"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d72278a30111e5b5525c1dd96120d9e958464316f55adb030433ea905866f4de"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4c29cbbba378759ac5786730d1c3cb4ec6f8ababf5c42a9ce303dc4b3d08cda"}, + {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6632f2d04f15d1bd6fe0eedd3b86d9061b836ddca4c03d5cf5c7e9e6b7c14580"}, + {file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d0b67d87bb45ed1cd020e8fbf2307d449b68abc45402fe1a4ac9e46c3c8b192b"}, + {file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ec31a99ca63bf3cd7f1a5ac9fe95c5e2d060d3c768a09bc1d16e235840861420"}, + {file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:22e6c9976e38f4d8c4a63bd8a8edac5307dffd3ee7e6026d97f3cc3a2dc02a0b"}, + {file = "rpds_py-0.20.0-cp39-none-win32.whl", hash = "sha256:569b3ea770c2717b730b61998b6c54996adee3cef69fc28d444f3e7920313cf7"}, + {file = "rpds_py-0.20.0-cp39-none-win_amd64.whl", hash = "sha256:e6900ecdd50ce0facf703f7a00df12374b74bbc8ad9fe0f6559947fb20f82364"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:617c7357272c67696fd052811e352ac54ed1d9b49ab370261a80d3b6ce385045"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9426133526f69fcaba6e42146b4e12d6bc6c839b8b555097020e2b78ce908dcc"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deb62214c42a261cb3eb04d474f7155279c1a8a8c30ac89b7dcb1721d92c3c02"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcaeb7b57f1a1e071ebd748984359fef83ecb026325b9d4ca847c95bc7311c92"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d454b8749b4bd70dd0a79f428731ee263fa6995f83ccb8bada706e8d1d3ff89d"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d807dc2051abe041b6649681dce568f8e10668e3c1c6543ebae58f2d7e617855"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3c20f0ddeb6e29126d45f89206b8291352b8c5b44384e78a6499d68b52ae511"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b7f19250ceef892adf27f0399b9e5afad019288e9be756d6919cb58892129f51"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:4f1ed4749a08379555cebf4650453f14452eaa9c43d0a95c49db50c18b7da075"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:dcedf0b42bcb4cfff4101d7771a10532415a6106062f005ab97d1d0ab5681c60"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:39ed0d010457a78f54090fafb5d108501b5aa5604cc22408fc1c0c77eac14344"}, + {file = "rpds_py-0.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:bb273176be34a746bdac0b0d7e4e2c467323d13640b736c4c477881a3220a989"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f918a1a130a6dfe1d7fe0f105064141342e7dd1611f2e6a21cd2f5c8cb1cfb3e"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:f60012a73aa396be721558caa3a6fd49b3dd0033d1675c6d59c4502e870fcf0c"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d2b1ad682a3dfda2a4e8ad8572f3100f95fad98cb99faf37ff0ddfe9cbf9d03"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:614fdafe9f5f19c63ea02817fa4861c606a59a604a77c8cdef5aa01d28b97921"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa518bcd7600c584bf42e6617ee8132869e877db2f76bcdc281ec6a4113a53ab"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0475242f447cc6cb8a9dd486d68b2ef7fbee84427124c232bff5f63b1fe11e5"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f90a4cd061914a60bd51c68bcb4357086991bd0bb93d8aa66a6da7701370708f"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:def7400461c3a3f26e49078302e1c1b38f6752342c77e3cf72ce91ca69fb1bc1"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:65794e4048ee837494aea3c21a28ad5fc080994dfba5b036cf84de37f7ad5074"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:faefcc78f53a88f3076b7f8be0a8f8d35133a3ecf7f3770895c25f8813460f08"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:5b4f105deeffa28bbcdff6c49b34e74903139afa690e35d2d9e3c2c2fba18cec"}, + {file = "rpds_py-0.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fdfc3a892927458d98f3d55428ae46b921d1f7543b89382fdb483f5640daaec8"}, + {file = "rpds_py-0.20.0.tar.gz", hash = "sha256:d72a210824facfdaf8768cf2d7ca25a042c30320b3020de2fa04640920d4e121"}, +] + [[package]] name = "six" version = "1.16.0" @@ -886,4 +1067,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "c93cfbe20f6f7d404dcf9a8a6d7cd9fb630838e7d4f5f718e32c87856106c30c" +content-hash = "2a00b92d8fc1ec5f72288bafbfa098045f6ce754a8db3f7b212a1c6b6ca9ec1f" diff --git a/pyproject.toml b/pyproject.toml index 392f4537..178d83b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ importlib-metadata = "^7.0" click = "^8.1.7" rich-click = "^1.8.3" pydantic = "^2.8.2" +jsonschema = "^4.23.0" [tool.poetry.group.dev.dependencies] mypy = "*" diff --git a/src/vss_tools/datatypes.py b/src/vss_tools/datatypes.py index e3ed5e94..22eb1cd7 100644 --- a/src/vss_tools/datatypes.py +++ b/src/vss_tools/datatypes.py @@ -11,9 +11,9 @@ # Global objects to be extended by other code parts dynamic_datatypes: Set[str] = set() +dynamic_struct_schemas: dict[str, dict[str, Any]] = {} dynamic_quantities: list[str] = [] -# This one contains the unit name as well as the list of allowed-datatypes -dynamic_units: dict[str, list] = {} +dynamic_units: dict[str, list] = {} # unit name -> allowed datatypes class DatatypesException(Exception): diff --git a/src/vss_tools/main.py b/src/vss_tools/main.py index d8ccb5ab..355f4960 100644 --- a/src/vss_tools/main.py +++ b/src/vss_tools/main.py @@ -22,7 +22,7 @@ VSSDataStruct, get_all_model_fields, ) -from vss_tools.tree import ModelValidationException, VSSNode, build_tree +from vss_tools.tree import ModelValidationException, VSSNode, add_struct_schemas, build_tree from vss_tools.units_quantities import load_quantities, load_units from vss_tools.vspec import InvalidSpecDuplicatedEntryException, InvalidSpecException, load_vspec @@ -126,10 +126,6 @@ def get_types_root(types: tuple[Path, ...], include_dirs: list[Path]) -> VSSNode else: types_root = root - if dynamic_datatypes: - log.info(f"Dynamic datatypes added={len(dynamic_datatypes)}") - log.debug(f"Dynamic datatypes:\n{dynamic_datatypes}") - # Checking whether user defined root types e.g 'MyType' # instead of 'Types.MyType' if not all(["." in t for t in dynamic_datatypes]): @@ -142,6 +138,11 @@ def get_types_root(types: tuple[Path, ...], include_dirs: list[Path]) -> VSSNode log.critical(e) exit(1) + if dynamic_datatypes: + log.info(f"Dynamic datatypes added={len(dynamic_datatypes)}") + log.debug(f"Dynamic datatypes:\n{dynamic_datatypes}") + add_struct_schemas(types_root) + return types_root diff --git a/src/vss_tools/model.py b/src/vss_tools/model.py index b25bd886..52f25dd4 100644 --- a/src/vss_tools/model.py +++ b/src/vss_tools/model.py @@ -9,6 +9,7 @@ from enum import Enum from typing import Any +import jsonschema from pydantic import ( BaseModel, ConfigDict, @@ -23,7 +24,9 @@ from vss_tools import log from vss_tools.datatypes import ( Datatypes, + DatatypesException, dynamic_quantities, + dynamic_struct_schemas, dynamic_units, get_all_datatypes, is_array, @@ -48,7 +51,7 @@ def __init__(self, element: str | None, ve: ValidationError): self.ve = ve def __str__(self) -> str: - errors = self.ve.errors(include_url=False) + errors = self.ve.errors(include_url=False, include_context=False) return f"'{self.element}' has {len(errors)} model error(s):\n{pretty_repr(errors)}" @@ -140,7 +143,7 @@ def fill_instances(cls, v: Any) -> list[str]: if v is None: return [] if not (isinstance(v, str) or isinstance(v, list)): - assert False, f"'{v}' is not a valid 'instances' content" + raise ValueError(f"'{v}' is not a valid 'instances' content") if isinstance(v, str): return [v] return v @@ -180,7 +183,7 @@ class VSSDataDatatype(VSSData): max: int | float | None = None unit: str | None = None allowed: list[str | int | float | bool] | None = None - default: list[str | int | float | bool] | str | int | float | bool | None = None + default: Any = None @model_validator(mode="after") def check_type_arraysize_consistency(self) -> Self: @@ -192,30 +195,57 @@ def check_type_arraysize_consistency(self) -> Self: assert is_array(self.datatype), f"'arraysize' set on a non array datatype: '{self.datatype}'" return self + def check_min_max_valid_datatype(self) -> Self: + if self.min or self.max: + try: + Datatypes.is_subtype_of(self.datatype, Datatypes.NUMERIC[0]) + except DatatypesException: + raise ValueError(f"Cannot define min/max for datatype '{self.datatype}'") + if is_array(self.datatype): + raise ValueError("Cannot define min/max for array datatypes") + return self + + def check_default_min_max(self) -> Self: + if self.default: + if self.min and self.default < self.min: + raise ValueError(f"'default' smaller than 'min': {self.default}<{self.min}") + if self.max and self.default > self.max: + raise ValueError(f"'default' greater than 'max': {self.default}>{self.min}") + return self + def check_type_default_consistency(self) -> Self: """ Checks that the default value is consistent with the given datatype """ if self.default is not None: - if is_array(self.datatype): + array = is_array(self.datatype) + if array: assert isinstance( self.default, list ), f"'default' with type '{type(self.default)}' does not match datatype '{self.datatype}'" if self.arraysize: assert len(self.default) == self.arraysize, "'default' array size does not match 'arraysize'" - for v in self.default: - assert Datatypes.is_datatype(v, self.datatype), f"'{v}' is not of type '{self.datatype}'" else: assert not isinstance( self.default, list ), f"'default' with type '{type(self.default)}' does not match datatype '{self.datatype}'" - assert Datatypes.is_datatype( - self.default, self.datatype - ), f"'{self.default}' is not of type '{self.datatype}'" + + check_values = [self.default] + if array: + check_values = self.default + + if Datatypes.get_type(self.datatype) is None: + for check_value in check_values: + try: + jsonschema.validate(check_value, dynamic_struct_schemas[self.datatype.strip("[]")]) + except jsonschema.ValidationError as e: + raise ValueError(f"invalid 'default' format for datatype '{self.datatype}': {e.message}") + else: + for v in check_values: + assert Datatypes.is_datatype(v, self.datatype), f"'{v}' is not of type '{self.datatype}'" return self - @model_validator(mode="after") def check_default_values_allowed(self) -> Self: """ Checks that the given default values @@ -235,6 +265,7 @@ def check_allowed_datatype_consistency(self) -> Self: datatypes """ if self.allowed: + assert Datatypes.get_type(self.datatype), "'allowed' cannot be used with struct datatype" for v in self.allowed: assert Datatypes.is_datatype(v, self.datatype), f"'{v}' is not of type '{self.datatype}'" return self @@ -252,8 +283,11 @@ def check_allowed_min_max(self) -> Self: def check_datatype(self) -> Self: assert self.datatype in get_all_datatypes(self.fqn), f"'{self.datatype}' is not a valid datatype" self.datatype = resolve_datatype(self.datatype, self.fqn) - self.check_type_default_consistency() - self.check_allowed_datatype_consistency() + self = self.check_type_default_consistency() + self = self.check_allowed_datatype_consistency() + self = self.check_default_values_allowed() + self = self.check_min_max_valid_datatype() + self = self.check_default_min_max() return self @field_validator("unit") @@ -271,7 +305,7 @@ def check_datatype_matching_allowed_unit_datatypes(self) -> Self: referenced in the unit if given """ if self.unit: - assert Datatypes.get_type(self.datatype), f"Cannot use 'unit' with complex datatype: '{self.datatype}'" + assert Datatypes.get_type(self.datatype), f"Cannot use 'unit' with struct datatype: '{self.datatype}'" assert any( Datatypes.is_subtype_of(self.datatype.rstrip("[]"), a) for a in dynamic_units[self.unit] ), f"'{self.datatype}' is not allowed for unit '{self.unit}'" diff --git a/src/vss_tools/tree.py b/src/vss_tools/tree.py index 02786fb6..01889403 100644 --- a/src/vss_tools/tree.py +++ b/src/vss_tools/tree.py @@ -15,12 +15,13 @@ from pydantic import ValidationError from vss_tools import log -from vss_tools.datatypes import Datatypes, dynamic_datatypes +from vss_tools.datatypes import Datatypes, dynamic_datatypes, dynamic_struct_schemas, is_array from vss_tools.model import ( ModelValidationException, VSSData, VSSDataBranch, VSSDataDatatype, + VSSDataProperty, VSSDataStruct, VSSRaw, get_vss_raw, @@ -530,3 +531,60 @@ def expand_string(s: str) -> list[str]: for i in range(int(match.group(2)), int(match.group(3)) + 1): expanded.append(s.replace(match.group(1), str(i))) return expanded + + +def add_struct_schemas(types_root: VSSNode): + for node in PreOrderIter(types_root, filter_=lambda n: isinstance(n.data, VSSDataStruct)): + log.info(node) + schema = { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + } + add_node_schema(types_root, node.get_fqn(), schema) + dynamic_struct_schemas[node.get_fqn()] = schema + + +def add_node_schema(root: VSSNode, fqn: str, schema: dict[str, Any]) -> None: + datatype_map = { + Datatypes.UINT8[0]: "number", + Datatypes.INT8[0]: "number", + Datatypes.UINT16[0]: "number", + Datatypes.INT16[0]: "number", + Datatypes.UINT32[0]: "number", + Datatypes.INT32[0]: "number", + Datatypes.UINT64[0]: "number", + Datatypes.INT64[0]: "number", + Datatypes.FLOAT[0]: "number", + Datatypes.DOUBLE[0]: "number", + Datatypes.NUMERIC[0]: "number", + Datatypes.BOOLEAN[0]: "boolean", + } + + node = root.get_node_with_fqn(fqn) + if node: + properties: dict[str, Any] = {} + child: VSSNode + for child in node.children: + if isinstance(child.data, VSSDataProperty): + array = is_array(child.data.datatype) + input_datatype = child.data.datatype.strip("[]") + datatype: str | None = None + if input_datatype in datatype_map: + datatype = datatype_map[input_datatype] + else: + d = Datatypes.get_type(input_datatype) + if d: + datatype = d[0] + if datatype: + log.debug(f"Datatype: {datatype}") + if array: + properties[child.name] = {"type": "array", "items": {"type": datatype}} + else: + properties[child.name] = {"type": datatype} + # A referenced struct + else: + properties[child.name] = {"type": "object"} + add_node_schema(root, input_datatype, properties[child.name]) + + schema["required"] = list(properties.keys()) + schema["properties"] = properties diff --git a/tests/vspec/test_structs/struct_default_model_nok.vspec b/tests/vspec/test_structs/struct_default_model_nok.vspec new file mode 100644 index 00000000..cbcad3fd --- /dev/null +++ b/tests/vspec/test_structs/struct_default_model_nok.vspec @@ -0,0 +1,25 @@ +A: + type: branch + description: A + +A.B: + type: sensor + description: B + datatype: Types.T + default: + m: definitely not an uint8 + complex: + m: + - true + - false + +A.C: + type: sensor + description: C + datatype: Types.T[] + default: + - m: definitely not an uint8 + complex: + m: + - true + - false diff --git a/tests/vspec/test_structs/struct_default_model_ok.vspec b/tests/vspec/test_structs/struct_default_model_ok.vspec new file mode 100644 index 00000000..baf2ed25 --- /dev/null +++ b/tests/vspec/test_structs/struct_default_model_ok.vspec @@ -0,0 +1,25 @@ +A: + type: branch + description: A + +A.B: + type: sensor + description: B + datatype: Types.T + default: + m: 10 + complex: + m: + - true + - false + +A.C: + type: sensor + description: C + datatype: Types.T[] + default: + - m: 10 + complex: + m: + - true + - false diff --git a/tests/vspec/test_structs/struct_default_types.vspec b/tests/vspec/test_structs/struct_default_types.vspec new file mode 100644 index 00000000..b1685188 --- /dev/null +++ b/tests/vspec/test_structs/struct_default_types.vspec @@ -0,0 +1,26 @@ +Types: + type: branch + description: Types + +Types.T: + type: struct + description: T + +Types.T.m: + type: property + description: Tx + datatype: uint8 + +Types.T.complex: + type: property + description: TComplex + datatype: Z + +Types.Z: + type: struct + description: Z + +Types.Z.m: + type: property + description: Zm + datatype: boolean[] diff --git a/tests/vspec/test_structs/test_data_type_parsing.py b/tests/vspec/test_structs/test_data_type_parsing.py index 8106f7de..8e22795b 100644 --- a/tests/vspec/test_structs/test_data_type_parsing.py +++ b/tests/vspec/test_structs/test_data_type_parsing.py @@ -316,7 +316,7 @@ def test_error_when_no_user_defined_data_types_are_provided(tmp_path): ( "test_with_unit_on_struct_signal.vspec", "VehicleDataTypes.vspec", - "Cannot use 'unit' with complex datatype: 'VehicleDataTypes.TestBranch1.ParentStruct'", + "Cannot use 'unit' with struct datatype: 'VehicleDataTypes.TestBranch1.ParentStruct'", ), ], ) diff --git a/tests/vspec/test_structs/test_default.py b/tests/vspec/test_structs/test_default.py new file mode 100644 index 00000000..419a7a2a --- /dev/null +++ b/tests/vspec/test_structs/test_default.py @@ -0,0 +1,31 @@ +# Copyright (c) 2024 Contributors to COVESA +# +# This program and the accompanying materials are made available under the +# terms of the Mozilla Public License 2.0 which is available at +# https://www.mozilla.org/en-US/MPL/2.0/ +# +# SPDX-License-Identifier: MPL-2.0 + +import subprocess +from pathlib import Path + +HERE = Path(__file__).resolve().parent + + +def test_struct_default(tmp_path): + vspec = HERE / "struct_default_model_ok.vspec" + types = HERE / "struct_default_types.vspec" + cmd = f"vspec export tree -s {vspec} -t {types}" + + # ok + p = subprocess.run(cmd.split()) + assert p.returncode == 0 + + # nok + log = tmp_path / "log.txt" + vspec = HERE / "struct_default_model_nok.vspec" + cmd = f"vspec --log-file {log} export tree -s {vspec} -t {types}" + p = subprocess.run(cmd.split(), capture_output=True, text=True) + assert p.returncode != 0 + print(log.read_text()) + assert "invalid 'default' format for datatype 'Types.T'" in log.read_text()