From 9254730291238a372df28c2a0115bf611fa84764 Mon Sep 17 00:00:00 2001 From: dev Date: Mon, 20 Oct 2025 02:43:23 +0200 Subject: [PATCH] cidstructurable con los tests --- package-lock.json | 543 +++++++++++++++++++++++++++++- package.json | 11 +- src/lib/config/cidentifiable.ts | 25 ++ src/lib/config/cidstructural.ts | 162 +++++++++ src/lib/config/cmapids.ts | 116 +++++++ src/lib/config/cvalidator.ts | 502 +++++++++++++++++++++++++++ src/lib/config/cvalue.ts | 3 +- src/routes/test/+page.svelte | 18 + src/tests/cidstructurable.test.ts | 143 ++++++++ tsconfig.json | 11 +- vite.config.ts | 7 +- vitest.config.ts | 16 + 12 files changed, 1531 insertions(+), 26 deletions(-) create mode 100644 src/lib/config/cidentifiable.ts create mode 100644 src/lib/config/cidstructural.ts create mode 100644 src/lib/config/cmapids.ts create mode 100644 src/lib/config/cvalidator.ts create mode 100644 src/routes/test/+page.svelte create mode 100644 src/tests/cidstructurable.test.ts create mode 100644 vitest.config.ts diff --git a/package-lock.json b/package-lock.json index f28e0bd..6678117 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,9 @@ "": { "name": "app-selector-acabados", "version": "0.0.1", + "dependencies": { + "vite-tsconfig-paths": "^5.1.4" + }, "devDependencies": { "@sveltejs/adapter-static": "^3.0.9", "@sveltejs/kit": "^2.43.2", @@ -24,7 +27,9 @@ "svelte-check": "^4.3.2", "tailwindcss": "^4.1.14", "typescript": "^5.9.2", - "vite": "^7.1.7" + "vite": "^7.1.7", + "vite-plugin-devtools-json": "^1.0.0", + "vitest": "^3.2.4" } }, "node_modules/@alloc/quick-lru": { @@ -1256,6 +1261,16 @@ "vite": "^5.2.0 || ^6 || ^7" } }, + "node_modules/@types/chai": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", + "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*" + } + }, "node_modules/@types/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", @@ -1263,11 +1278,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/node": { @@ -1280,6 +1302,121 @@ "undici-types": "~7.14.0" } }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -1303,6 +1440,16 @@ "node": ">= 0.4" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/autoprefixer": { "version": "10.4.21", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", @@ -1395,6 +1542,16 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001750", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001750.tgz", @@ -1416,6 +1573,33 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -1466,7 +1650,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1480,6 +1663,16 @@ } } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -1528,11 +1721,18 @@ "node": ">=10.13.0" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.25.11", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz", "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -1597,11 +1797,31 @@ "@jridgewell/sourcemap-codec": "^1.4.15" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -1654,6 +1874,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "license": "MIT" + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -1710,6 +1936,13 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -1966,6 +2199,13 @@ "dev": true, "license": "MIT" }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/magic-string": { "version": "0.30.19", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", @@ -2023,14 +2263,13 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -2069,18 +2308,35 @@ "dev": true, "license": "MIT" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/picomatch": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=12" @@ -2103,7 +2359,7 @@ "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "dev": true, + "devOptional": true, "funding": [ { "type": "opencollective", @@ -2351,7 +2607,7 @@ "version": "4.52.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz", "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.8" @@ -2409,6 +2665,13 @@ "dev": true, "license": "MIT" }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/sirv": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", @@ -2438,12 +2701,39 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -2545,11 +2835,25 @@ "node": ">=18" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -2562,6 +2866,36 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -2572,11 +2906,31 @@ "node": ">=6" } }, + "node_modules/tsconfck": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", + "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", + "license": "MIT", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -2624,11 +2978,25 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uuid": { + "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==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/vite": { "version": "7.1.10", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.10.tgz", "integrity": "sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", @@ -2699,6 +3067,61 @@ } } }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-plugin-devtools-json": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/vite-plugin-devtools-json/-/vite-plugin-devtools-json-1.0.0.tgz", + "integrity": "sha512-MobvwqX76Vqt/O4AbnNMNWoXWGrKUqZbphCUle/J2KXH82yKQiunOeKnz/nqEPosPsoWWPP9FtNuPBSYpiiwkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "uuid": "^11.1.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/vite-tsconfig-paths": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", + "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + }, + "peerDependencies": { + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, "node_modules/vitefu": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz", @@ -2719,6 +3142,96 @@ } } }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", diff --git a/package.json b/package.json index bba40c4..53a3050 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,9 @@ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "format": "prettier --write .", - "lint": "prettier --check ." + "lint": "prettier --check .", + "test": "vitest run", + "test:watch": "vitest" }, "devDependencies": { "@sveltejs/adapter-static": "^3.0.9", @@ -30,6 +32,11 @@ "svelte-check": "^4.3.2", "tailwindcss": "^4.1.14", "typescript": "^5.9.2", - "vite": "^7.1.7" + "vite": "^7.1.7", + "vite-plugin-devtools-json": "^1.0.0", + "vitest": "^3.2.4" + }, + "dependencies": { + "vite-tsconfig-paths": "^5.1.4" } } diff --git a/src/lib/config/cidentifiable.ts b/src/lib/config/cidentifiable.ts new file mode 100644 index 0000000..f97dae5 --- /dev/null +++ b/src/lib/config/cidentifiable.ts @@ -0,0 +1,25 @@ +// cidentificable.ts + + + +/** + * Representa un objeto que puede ser identificado de forma única + * mediante una propiedad `id` de tipo genérico `ID`. + * + * @template ID - Tipo del identificador (por defecto `string`) + * + * @example + * interface User extends Identifiable { + * name: string; + * } + * + * const user: User = { id: 123, name: 'Alice' }; + */ +export interface CIdentifiable { + id: TID; +} + + + + + diff --git a/src/lib/config/cidstructural.ts b/src/lib/config/cidstructural.ts new file mode 100644 index 0000000..ead8138 --- /dev/null +++ b/src/lib/config/cidstructural.ts @@ -0,0 +1,162 @@ +// cidstructural.ts + +import type { CIdentifiable } from './cidentifiable'; +import { CError } from '$lib/config/cerror'; + +const maxLevel: number = 3; +const levelSeparator: string = '|'; +const subLevelSeparator: string = '/'; + +// Escapar caracteres especiales de regex +const escapedSep = subLevelSeparator.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + +/** + * Expresión regular que valida un nivel del identificador estructurado. + * Reglas: + * - Debe empezar con un carácter alfanumérico. + * - Puede contener separadores entre subniveles (`/`). + * - No puede terminar con el separador. + * - No puede tener separadores consecutivos. + * - Solo se permiten caracteres alfanuméricos, guiones (`-`) y guiones bajos (`_`). + */ +const subLevelRegex = new RegExp(`^[a-zA-Z0-9_-]+(?:${escapedSep}[a-zA-Z0-9_-]+)*$`); + +/** + * Representa un identificador estructurado jerárquico, dividido en niveles y subniveles. + * + * Ejemplo de estructura: + * ``` + * Entidad|Grupo/Subgrupo|Opcion + * ``` + * + * Donde: + * - `|` separa los niveles jerárquicos. + * - `/` separa subniveles dentro de un nivel. + * + * Reglas: + * - El identificador no puede estar vacío. + * - No se permiten niveles intermedios vacíos. + * - Solo se permiten vacíos al principio si todos los anteriores lo son. + * - El número máximo de niveles (`maxLevel`) es 3. + * - Si se alcanza el nivel máximo, el último nivel solo puede tener un único valor. + */ +export class CIdStructural implements CIdentifiable { + /** Identificador completo */ + readonly id: string; + + /** Cache interno con las partes procesadas */ + private parts: string[][] | null = null; + + /** + * Crea una nueva instancia de `CIdStructural`. + * + * @param {string} id - Identificador estructurado. Ejemplo: `"Entidad|Grupo/Subgrupo|Opcion"`. + * @param {boolean} [lazy=true] - Si es `true`, no valida ni analiza el ID hasta que se llame a `getParts()`. + * Si es `false`, se valida inmediatamente. + * @throws {CError} Si el identificador está vacío o no cumple las reglas de formato. + */ + constructor(id: string, lazy: boolean = true) { + if (!id) { + throw new CError('invalid_id_format', 'id cannot be empty'); + } + this.id = id; + + // Validación temprana opcional + if (!lazy) { + this.getParts(); + } + } + + /** + * Devuelve las partes jerárquicas del identificador. + * + * Ejemplo: + * ```ts + * const id = new CIdStructural('Entidad|Grupo/Subgrupo|Opcion'); + * id.getParts(); + * // Resultado: + * // [ + * // ['Entidad'], + * // ['Grupo', 'Subgrupo'], + * // ['Opcion'] + * // ] + * ``` + * + * @returns {string[][]} Matriz de partes separadas por niveles y subniveles. + * @throws {CError} Si el formato del ID es inválido, tiene niveles vacíos no permitidos + * o excede la profundidad máxima. + */ + getParts(): string[][] { + if (this.parts) return this.parts; + + this.parts = []; + let canBeEmpty = true; + const sparts = this.id.split(levelSeparator); + const sl = sparts.length; + + if (sl > maxLevel) { + throw new CError('invalid_id', `levels ${sl} is out of range max levels ${maxLevel}`); + } + + for (let i = 0; i < sl; i++) { + const s = sparts[i]; + if (s.length === 0) { + if (canBeEmpty) { + this.parts.push([]); + continue; + } + throw new CError('invalid_id', 'can be empty middle parts'); + } + if (!subLevelRegex.test(s)) { + throw new CError('invalid_id', `Invalid format in level ${i}: "${s}"`); + } + this.parts.push(s.split(subLevelSeparator)); + canBeEmpty = false; + } + + // Validar longitud del último nivel + if (sl === maxLevel && this.parts[sl - 1].length !== 1) { + throw new CError('invalid_id', 'last part length must be 1'); + } + + return this.parts; + } + + /** + * Crea un nuevo identificador hijo (sub-id) a partir de la instancia actual. + * + * @param {string} id - Identificador hijo que se agregará. + * @param {boolean} [same=false] - Si es `true`, se usa el separador de subnivel (`/`), + * de lo contrario, se usa el separador de nivel (`|`). + * @returns {CIdStructural} Nueva instancia combinada. + */ + createSubId(id: string, same: boolean = false): CIdStructural { + return CIdStructural.createFromParent(this, id, same); + } + + /** + * Crea un nuevo identificador estructural combinando un padre y un identificador propio. + * + * @param {CIdentifiable} parent - Identificador padre. + * @param {string} ownId - Parte propia que se añadirá al identificador. + * @param {boolean} [same=false] - Si es `true`, se añade como subnivel (`/`), + * de lo contrario como nuevo nivel (`|`). + * @returns {CIdStructural} Nueva instancia con el ID combinado. + * @throws {CError} Si `parent.id` o `ownId` no son válidos. + */ + static createFromParent( + parent: CIdentifiable, + ownId: string, + same?: boolean + ): CIdStructural { + if (!parent.id || !ownId) { + throw new CError( + 'invalid_param', + `${parent.id ? 'ownId' : 'parent.id'} must be provided or be valid` + ); + } + return new CIdStructural( + `${parent.id}${same ? subLevelSeparator : levelSeparator}${ownId}` + ); + } +} diff --git a/src/lib/config/cmapids.ts b/src/lib/config/cmapids.ts new file mode 100644 index 0000000..94e0f6a --- /dev/null +++ b/src/lib/config/cmapids.ts @@ -0,0 +1,116 @@ +// cmapids.ts + +import { type CIdentifiable } from './cidentifiable'; + +/** + * Estructura de datos genérica basada en `Map` que asocia objetos con un `id` único . + * + * ✅ Mantiene el orden de inserción (gracias a `Map`). + * ✅ Permite acceso rápido por `id`. + * ✅ Soporta operaciones de listado, mapeo e iteración ordenada. + * + * @typeParam T - Tipo de los elementos, que deben tener un campo `id: string`. + */ +export class CMapIds, ID = string > { + + /** + * Mapa interno de elementos, donde la clave es el `id` + * y el valor es el objeto completo. + */ + private map = new Map(); + + + /** + * Obtiene un elemento por su `id`. + * + * @param id - Identificador único del elemento. + * @returns El elemento asociado o `undefined` si no existe. + */ + get(id: ID): T | undefined { return this.map.get(id); } + + /** + * Añade o reemplaza un elemento en el mapa. + * + * Si el `id` ya existe, sobrescribe el valor anterior. + * + * @param item - Elemento a insertar o actualizar. + */ + add(item: T): void { this.map.set(item.id, item); } + + + /** + * Reemplaza completamente el contenido del mapa con una nueva lista de elementos. + * + * @param items - Array de elementos a registrar. + */ + set(items: T[]): void { + this.map.clear(); + for (const item of items) this.map.set(item.id, item); + } + + + /** + * Elimina un elemento del mapa según su `id`. + * + * @param id - Identificador del elemento a eliminar. + */ + del(id: ID): void { this.map.delete(id); } + + + /** + * Devuelve el número total de elementos en el mapa. + */ + size(): number { return this.map.size; } + + + /** + * Devuelve todos los elementos en un array, manteniendo + * el orden en que fueron insertados. + */ + all(): T[] { return Array.from(this.map.values()); } + + + /** + * Devuelve un array con todas las claves (`id`) registradas, + * manteniendo el orden de inserción. + * + * @returns Array de claves (`string[]`). + */ + keys(): ID[] { return Array.from(this.map.keys()); } + + + /** + * Itera sobre todos los elementos del mapa en orden de inserción, + * ejecutando un callback por cada uno. + * + * @param callback - Función llamada con `(item, index)` por cada elemento. + */ + forEach(callback: (item: T, index: number) => void): void { + let i = 0; + for (const item of this.map.values()) callback(item, i++); + } + + + /** + * Aplica una función de transformación a cada elemento del mapa + * y devuelve un array con los resultados. + * + * @param callback - Función que recibe `(item, index)` y devuelve un nuevo valor. + * @returns Array de resultados. + */ + mapItems(callback: (item: T, index: number) => U): U[] { + const result: U[] = []; + let i = 0; + for (const item of this.map.values()) result.push(callback(item, i++)); + return result; + } + + + /** + * Comprueba si existe un elemento con el `id` indicado. + * + * @param id - Identificador a buscar. + * @returns `true` si existe, `false` en caso contrario. + */ + has(id: ID): boolean { return this.map.has(id); } +} diff --git a/src/lib/config/cvalidator.ts b/src/lib/config/cvalidator.ts new file mode 100644 index 0000000..544fe29 --- /dev/null +++ b/src/lib/config/cvalidator.ts @@ -0,0 +1,502 @@ +// cvalidator.ts +import type { CValue } from './cvalue'; +import { CError } from '$lib/config/cerror'; + +/** + * Función de validación que verifica si un CValue cumple una condición. + * + * @param {CValue} v - Valor a validar + * @returns {boolean} `true` si el valor es válido, `false` en caso contrario + * + * @description + * Función pura que implementa la lógica de validación específica. + * Debe retornar boolean y no lanzar errores (la gestión de errores la hace CValidatorRegistry). + * + * @example + * // Validador de email simple + * const isEmail: CValidatorFn = (v) => { + * if (typeof v !== 'string') return false; + * return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v); + * }; + * + * @example + * // Validador de rango numérico + * const isPositive: CValidatorFn = (v) => { + * return typeof v === 'number' && v > 0; + * }; + * + * @example + * // Validador de longitud mínima + * const minLength = (min: number): CValidatorFn => (v) => { + * return typeof v === 'string' && v.length >= min; + * }; + */ +export type CValidatorFn = (v: CValue) => boolean; + +/** + * Definición de un validador con su función de validación y metadatos. + * + * @interface CValidator + * + * @property {string} name - Identificador único del validador + * @property {CValidatorFn} exec - Función que ejecuta la validación + * @property {string} [class] - Clase CSS opcional para estilizar mensajes de error en UI + * @property {string} [msg] - Mensaje de error descriptivo cuando la validación falla + * + * @description + * Representa un validador completo que puede ser registrado y ejecutado. + * Encapsula la lógica de validación junto con metadatos útiles para UI. + * + * @example + * // Validador básico + * const emailValidator: CValidator = { + * name: 'email', + * exec: (v) => typeof v === 'string' && /\S+@\S+\.\S+/.test(v), + * msg: 'Must be a valid email address' + * }; + * + * @example + * // Validador con clase CSS para UI + * const requiredValidator: CValidator = { + * name: 'required', + * exec: (v) => { + * if (typeof v === 'string') return v.trim().length > 0; + * if (typeof v === 'number') return !isNaN(v); + * return v !== undefined && v !== null; + * }, + * class: 'error-required', + * msg: 'This field is required' + * }; + * + * @example + * // Factory function para validadores parametrizados + * function createMinLengthValidator(min: number): CValidator { + * return { + * name: `minLength_${min}`, + * exec: (v) => typeof v === 'string' && v.length >= min, + * msg: `Must be at least ${min} characters` + * }; + * } + */ +export interface CValidator { + /** Identificador único del validador */ + name: string; + /** Función que ejecuta la validación */ + exec: CValidatorFn; + /** Clase de validador */ + class?: string; + /** Mensaje de error cuando la validación falla */ + msg?: string; +} + +/** + * Resultado de ejecutar un validador sobre un valor. + * + * @interface CValidatorResult + * + * @property {string} name - Nombre del validador ejecutado + * @property {boolean} [isValid] - `true` si pasó la validación, `false` si falló, `undefined` si hubo error + * @property {string} [msg] - Mensaje de error del validador (presente si isValid es false) + * @property {Error} [error] - Error capturado durante la ejecución (validador no registrado o excepción) + * + * @description + * Representa el resultado de una validación individual. + * Puede tener tres estados: + * - **Éxito**: isValid = true, sin msg ni error + * - **Fallo**: isValid = false, con msg opcional + * - **Error**: error presente (validador no existe o lanzó excepción) + * + * @example + * // Validación exitosa + * { name: 'email', isValid: true } + * + * @example + * // Validación fallida + * { name: 'minLength', isValid: false, msg: 'Must be at least 5 characters' } + * + * @example + * // Error: validador no registrado + * { + * name: 'unknown', + * error: CError { code: 'validator_not_reg', message: '...' } + * } + * + * @example + * // Error: excepción durante ejecución + * { + * name: 'custom', + * error: TypeError { message: 'Cannot read property...' } + * } + */ +export interface CValidatorResult { + /** Nombre del validador ejecutado */ + name: string; + /** true si pasó la validación, false si falló */ + isValid?: boolean; + /** Mensaje de error (presente si isValid es false) */ + msg?: string; + /** Error capturado (validador no registrado o excepción) */ + error?: Error; +} + +/** + * Registro centralizado de validadores reutilizables. + * + * @class CValidatorRegistry + * + * @description + * Gestiona un conjunto de validadores que pueden ser ejecutados por nombre. + * Permite: + * - Registrar validadores reutilizables + * - Ejecutar validaciones individuales o múltiples + * - Manejar errores de forma consistente + * - Compartir validadores entre componentes + * + * **Patrón Singleton recomendado**: Crear una única instancia global + * para compartir validadores en toda la aplicación. + * + * @example + * // Crear registro global + * export const validators = new CValidatorRegistry(); + * + * // Registrar validadores comunes + * validators.reg({ + * name: 'email', + * exec: (v) => typeof v === 'string' && /\S+@\S+\.\S+/.test(v), + * msg: 'Invalid email format' + * }); + * + * validators.reg({ + * name: 'required', + * exec: (v) => v !== '' && v !== null && v !== undefined, + * msg: 'This field is required' + * }); + * + * @example + * // Validar valor único + * try { + * const isValid = validators.exec('email', 'user@example.com'); // true + * } catch (error) { + * console.error('Validator not found'); + * } + * + * @example + * // Validar con múltiples validadores + * const results = validators.execAll( + * ['required', 'email', 'minLength'], + * userInput + * ); + * + * const hasErrors = results.some(r => r.error || r.isValid === false); + * if (hasErrors) { + * console.error('Validation failed:', results); + * } + * + * @example + * // Uso en formularios + * const formValidators = new CValidatorRegistry(); + * + * formValidators.reg({ + * name: 'password', + * exec: (v) => typeof v === 'string' && v.length >= 8 && /\d/.test(v), + * msg: 'Password must be 8+ chars with at least one number', + * class: 'error-password' + * }); + * + * const passwordResults = formValidators.execAll(['required', 'password'], password); + */ +export class CValidatorRegistry { + /** + * Almacén interno de validadores registrados + * @private + */ + private repo: Map = new Map(); + + /** + * Registra un nuevo validador en el registry. + * + * @param {CValidator} v - Validador a registrar + * @throws {CError} Con código 'validator_already_reg' si ya existe un validador con ese nombre + * + * @description + * Añade un validador al registro para que pueda ser ejecutado posteriormente por nombre. + * No permite sobrescribir validadores existentes para evitar conflictos. + * + * @example + * const registry = new CValidatorRegistry(); + * + * registry.reg({ + * name: 'positive', + * exec: (v) => typeof v === 'number' && v > 0, + * msg: 'Must be a positive number' + * }); + * + * @example + * // Error: registrar el mismo nombre dos veces + * registry.reg({ name: 'email', exec: (v) => true }); + * registry.reg({ name: 'email', exec: (v) => false }); // ✗ CError + * + * @example + * // Registrar validadores parametrizados + * for (let min of [5, 10, 20]) { + * registry.reg({ + * name: `minLength_${min}`, + * exec: (v) => typeof v === 'string' && v.length >= min, + * msg: `Minimum ${min} characters required` + * }); + * } + */ + reg(v: CValidator) { + if (this.repo.has(v.name)) { + throw new CError('validator_already_reg', `validator already reg(${v.name})`); + } + this.repo.set(v.name, v); + } + + /** + * Obtiene un validador registrado por su nombre. + * + * @param {string} name - Nombre del validador + * @returns {CValidator | undefined} El validador si existe, undefined si no está registrado + * + * @description + * Permite acceder a la definición completa de un validador. + * Útil para inspeccionar metadatos (msg, class) antes de ejecutar. + * + * @example + * const emailValidator = registry.get('email'); + * if (emailValidator) { + * console.log('Message:', emailValidator.msg); + * console.log('CSS class:', emailValidator.class); + * } + * + * @example + * // Verificar si existe antes de usar + * if (!registry.get('customValidator')) { + * console.warn('Validator not registered'); + * } + */ + get(name: string): CValidator | undefined { + return this.repo.get(name); + } + + /** + * Elimina un validador del registro. + * + * @param {string} name - Nombre del validador a eliminar + * @returns {boolean} `true` si se eliminó, `false` si no existía + * + * @description + * Remueve un validador del registro. Útil para testing o para + * reemplazar validadores en tiempo de ejecución. + * + * @example + * registry.del('obsoleteValidator'); // true si existía + * + * @example + * // Reemplazar un validador + * registry.del('email'); + * registry.reg({ + * name: 'email', + * exec: (v) => improvedEmailValidation(v), + * msg: 'Better email validation' + * }); + * + * @example + * // Limpiar validadores de test + * afterEach(() => { + * registry.del('testValidator1'); + * registry.del('testValidator2'); + * }); + */ + del(name: string): boolean { + return this.repo.delete(name); + } + + /** + * Ejecuta un validador sobre un valor. + * + * @param {string} name - Nombre del validador a ejecutar + * @param {CValue} value - Valor a validar + * @returns {boolean} `true` si la validación pasa, `false` si falla + * @throws {CError} Con código 'validator_not_reg' si el validador no existe + * + * @description + * Ejecuta la función de validación de un validador registrado. + * Lanza error si el validador no existe (usar get() primero para verificar). + * No captura excepciones de la función exec - deben manejarse externamente. + * + * @example + * // Validación simple + * try { + * const isValid = registry.exec('email', 'test@example.com'); + * if (!isValid) { + * console.error('Invalid email'); + * } + * } catch (error) { + * console.error('Validator not found:', error); + * } + * + * @example + * // Validación condicional + * if (requiresValidation) { + * try { + * const result = registry.exec('complex', userInput); + * setFieldValid(result); + * } catch (err) { + * if (err instanceof CError && err.code === 'validator_not_reg') { + * console.warn('Validator not available, skipping validation'); + * } + * } + * } + */ + exec(name: string, value: CValue): boolean { + const v = this.repo.get(name); + if (!v) throw new CError('validator_not_reg', `validator not registered(${name})`); + return v.exec(value); + } + + /** + * Ejecuta múltiples validadores sobre un mismo valor. + * + * @param {string[]} names - Array de nombres de validadores a ejecutar + * @param {CValue} value - Valor a validar + * @returns {CValidatorResult[]} Array de resultados, uno por cada validador + * + * @description + * Ejecuta todos los validadores especificados y retorna sus resultados. + * **No lanza errores** - los captura y los incluye en los resultados. + * Útil para validar formularios con múltiples reglas. + * + * **Comportamiento**: + * - Ejecuta todos los validadores (no se detiene en el primer error) + * - Validador no registrado: result.error con CError + * - Validador lanza excepción: result.error con la excepción + * - Validación falla: result.isValid = false, result.msg del validador + * - Validación pasa: result.isValid = true + * + * @example + * // Validación de formulario + * const results = registry.execAll( + * ['required', 'email', 'maxLength'], + * emailInput.value + * ); + * + * // Procesar resultados + * const errors = results + * .filter(r => r.error || r.isValid === false) + * .map(r => r.error?.message || r.msg); + * + * if (errors.length > 0) { + * displayErrors(errors); + * } + * + * @example + * // Renderizar errores en UI + * const results = registry.execAll(['required', 'minLength'], password); + * + * results.forEach(result => { + * if (result.isValid === false) { + * const validator = registry.get(result.name); + * showError(result.msg, validator?.class); + * } + * }); + * + * @example + * // Validación compleja con reporte detallado + * const fieldValidations = { + * email: ['required', 'email'], + * password: ['required', 'minLength_8', 'hasNumber'], + * age: ['required', 'positive', 'maxValue_120'] + * }; + * + * const report: Record = {}; + * for (const [field, validators] of Object.entries(fieldValidations)) { + * report[field] = registry.execAll(validators, formData[field]); + * } + * + * // Verificar si hay errores + * const hasErrors = Object.values(report) + * .flat() + * .some(r => r.error || r.isValid === false); + * + * @example + * // Early exit si hay error crítico + * const results = registry.execAll(['required', 'email'], input); + * const criticalError = results.find(r => + * r.name === 'required' && r.isValid === false + * ); + * + * if (criticalError) { + * return; // No procesar validaciones adicionales + * } + */ + execAll(names: string[], value: CValue): CValidatorResult[] { + const results: CValidatorResult[] = []; + for (let i = 0; i < names.length; i++) { + const name = names[i]; + const v = this.repo.get(name); + const result: CValidatorResult = { name }; + if (!v) { + result.error = new CError('validator_not_reg', `Validator not registered (${name})`); + } else { + try { + const valid: boolean = v.exec(value); + result.isValid = valid; + if (!valid) result.msg = v.msg; + } catch (error: any) { + result.error = error; + } + } + results.push(result); + } + return results; + } +} + +/** + * @example + * // Pattern: Validadores parametrizados con factory + * function createRangeValidator(min: number, max: number): CValidator { + * return { + * name: `range_${min}_${max}`, + * exec: (v) => typeof v === 'number' && v >= min && v <= max, + * msg: `Must be between ${min} and ${max}` + * }; + * } + * + * registry.reg(createRangeValidator(18, 100)); + * + * @example + * // Pattern: Validadores compuestos + * registry.reg({ + * name: 'strongPassword', + * exec: (v) => { + * if (typeof v !== 'string') return false; + * return v.length >= 8 && + * /[A-Z]/.test(v) && + * /[a-z]/.test(v) && + * /\d/.test(v) && + * /[!@#$%^&*]/.test(v); + * }, + * msg: 'Password must have 8+ chars, uppercase, lowercase, number and symbol' + * }); + * + * @example + * // Pattern: Validadores asincrónicos (wrapper) + * // Nota: CValidatorFn es síncrono, pero puedes crear un wrapper + * const asyncValidators = new Map Promise>(); + * + * asyncValidators.set('uniqueEmail', async (email) => { + * const response = await fetch(`/api/check-email?email=${email}`); + * return response.ok; + * }); + * + * // Ejecutar validaciones async separadamente + * const syncResults = registry.execAll(['required', 'email'], email); + * if (syncResults.every(r => r.isValid)) { + * const isUnique = await asyncValidators.get('uniqueEmail')?.(email); + * } + */ + + diff --git a/src/lib/config/cvalue.ts b/src/lib/config/cvalue.ts index eb7cf7d..4a5b4ac 100644 --- a/src/lib/config/cvalue.ts +++ b/src/lib/config/cvalue.ts @@ -63,7 +63,8 @@ export type CValue = string | number | boolean; * @see {@link checkTypeCValue} para validación con type assertion */ export function isCValue(val: any): boolean { - return typeof val === 'string' || typeof val === 'boolean' || typeof val === 'number'; + const t = typeof val; + return t === 'string' || t === 'boolean' || t === 'number'; } /** diff --git a/src/routes/test/+page.svelte b/src/routes/test/+page.svelte new file mode 100644 index 0000000..7f81fcd --- /dev/null +++ b/src/routes/test/+page.svelte @@ -0,0 +1,18 @@ + \ No newline at end of file diff --git a/src/tests/cidstructurable.test.ts b/src/tests/cidstructurable.test.ts new file mode 100644 index 0000000..d3ad23b --- /dev/null +++ b/src/tests/cidstructurable.test.ts @@ -0,0 +1,143 @@ +import { describe, it, expect } from 'vitest'; +import { CIdStructural } from '$lib/config/cidstructural'; +import { CError } from '$lib/config/cerror'; + + +describe('CIdStructural (extended)', () => { + + // ------------------------------ + // ✅ Casos válidos + // ------------------------------ + + it('permite id simple sin niveles', () => { + const id = new CIdStructural('root'); + expect(id.getParts()).toEqual([['root']]); + }); + + it('permite id con guiones y guiones bajos', () => { + const id = new CIdStructural('empresa-1|departamento_2|producto-xyz'); + expect(id.getParts()).toEqual([ + ['empresa-1'], + ['departamento_2'], + ['producto-xyz'] + ]); + }); + + it('permite subniveles en niveles intermedios', () => { + const id = new CIdStructural('empresa|departamento/subdep|producto'); + expect(id.getParts()).toEqual([ + ['empresa'], + ['departamento', 'subdep'], + ['producto'] + ]); + }); + + it('permite niveles vacíos iniciales', () => { + const id = new CIdStructural('||producto'); + expect(id.getParts()).toEqual([[], [], ['producto']]); + }); + + it('permite construir subIds encadenados', () => { + const root = new CIdStructural('empresa'); + const dep = root.createSubId('ventas'); + const subdep = dep.createSubId('online', true); + const prod = subdep.createSubId('producto'); + expect(prod.id).toBe('empresa|ventas/online|producto'); + }); + + // ------------------------------ + // ❌ Casos inválidos + // ------------------------------ + + it('lanza error si se supera el maxLevel', () => { + expect(() => new CIdStructural('a|b|c|d').getParts()).toThrow(CError); + }); + + it('lanza error si hay separadores consecutivos en medio', () => { + expect(() => new CIdStructural('empresa||departamento|producto').getParts()).toThrow(CError); + }); + + it('lanza error si el último nivel tiene más de un subnivel', () => { + expect(() => new CIdStructural('empresa|departamento|producto/a').getParts()).toThrow(CError); + }); + + it('lanza error si un nivel es vacío y no puede serlo', () => { + expect(() => new CIdStructural('empresa||').getParts()).toThrow(CError); + }); + + it('lanza error si contiene caracteres no permitidos', () => { + expect(() => new CIdStructural('empresa|departamento|prod@cto').getParts()).toThrow(CError); + }); + + it('lanza error si el formato del nivel es inválido (doble /)', () => { + expect(() => new CIdStructural('empresa|dep//subdep|producto').getParts()).toThrow(CError); + }); + + // ------------------------------ + // 🧠 Lazy loading + // ------------------------------ + + it('no valida inmediatamente si lazy=true', () => { + const instance = new CIdStructural('empresa|dep//subdep|producto', true); + // No explota aquí, pero sí al pedir las partes: + expect(() => instance.getParts()).toThrow(CError); + }); + + it('valida inmediatamente si lazy=false', () => { + expect(() => new CIdStructural('empresa|dep/subdep|producto', false)).not.toThrow(); + }); + + // ------------------------------ + // ⚙️ Comportamiento de createFromParent + // ------------------------------ + + it('createFromParent concatena con | cuando same=false', () => { + const parent = new CIdStructural('empresa|ventas'); + const child = CIdStructural.createFromParent(parent, 'online', false); + expect(child.id).toBe('empresa|ventas|online'); + }); + + it('createFromParent concatena con / cuando same=true', () => { + const parent = new CIdStructural('empresa|ventas'); + const child = CIdStructural.createFromParent(parent, 'online', true); + expect(child.id).toBe('empresa|ventas/online'); + }); + + it('createFromParent lanza error si ownId vacío', () => { + const parent = new CIdStructural('empresa'); + expect(() => CIdStructural.createFromParent(parent, '')).toThrow(CError); + }); + + it('createFromParent lanza error si parent.id vacío', () => { + const fakeParent = { id: '' }; + expect(() => CIdStructural.createFromParent(fakeParent, 'algo')).toThrow(CError); + }); + + // ------------------------------ + // 🧩 Casos límite y mixtos + // ------------------------------ + + it('permite id con subniveles complejos en niveles intermedios', () => { + const id = new CIdStructural('raiz|a/b/c|final'); + expect(id.getParts()).toEqual([ + ['raiz'], + ['a', 'b', 'c'], + ['final'] + ]); + }); + + it('lanza error si id termina con separador', () => { + expect(() => new CIdStructural('empresa|departamento|producto/').getParts()).toThrow(CError); + }); + + it('lanza error si un nivel contiene espacio', () => { + expect(() => new CIdStructural('empresa|depar tamento|producto').getParts()).toThrow(CError); + }); + + it('mantiene coherencia entre llamadas repetidas a getParts()', () => { + const id = new CIdStructural('empresa|departamento|producto'); + const first = id.getParts(); + const second = id.getParts(); + expect(first).toBe(second); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index a5567ee..05dbb55 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,11 +9,8 @@ "skipLibCheck": true, "sourceMap": true, "strict": true, - "moduleResolution": "bundler" - } - // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias - // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files - // - // To make changes to top-level options such as include and exclude, we recommend extending - // the generated config; see https://svelte.dev/docs/kit/configuration#typescript + "moduleResolution": "bundler", + + } + } diff --git a/vite.config.ts b/vite.config.ts index 2d35c4f..324a054 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,12 @@ import tailwindcss from '@tailwindcss/vite'; import { sveltekit } from '@sveltejs/kit/vite'; +import devtoolsJson from 'vite-plugin-devtools-json'; import { defineConfig } from 'vite'; export default defineConfig({ - plugins: [tailwindcss(), sveltekit()] + plugins: [tailwindcss(), sveltekit(),devtoolsJson()], + + // Tell Vitest to use the `browser` entry points in `package.json` files, even though it's running in Node + // resolve: process.env.VITEST ? { conditions: ['browser'] } : undefined + }); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..f71c948 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'vitest/config'; +import viteConfig from './vite.config.js'; + +export default defineConfig({ + ...viteConfig, + test: { + // Carpeta donde están los tests + include: ['src/tests/**/*.test.ts'], + + // Opcional: ignorar ciertos directorios + exclude: ['node_modules', 'build'], + + // Opcional: correr en modo watch automáticamente + watch: true, + }, +}); \ No newline at end of file