From 63c842bdb3b69cd8d878bed34e16d35571026a2f Mon Sep 17 00:00:00 2001 From: ShareVB Date: Sat, 8 Mar 2025 21:48:19 +0100 Subject: [PATCH] feat(JSON to C#): update to add options from json2csharp.com rootTypeName (choose root object name) pascalCase (type and member names) useFields (instead of properties) useNullable (types) addJsonProperty (Newtonsoft) and nullValueHandlingIgnore addJsonPropertyName (System.Text.Json) generateImmutableClasses useRecordTypes useReadonlyLists --- components.d.ts | 1 + package.json | 1 - pnpm-lock.yaml | 200 ++++-- src/composable/queryParams.ts | 33 +- src/tools/json-to-csharp/json-to-csharp.vue | 100 ++- src/tools/json-to-csharp/json2csharp.d.ts | 3 - src/tools/json-to-csharp/json2csharp.test.ts | 617 +++++++++++++++++++ src/tools/json-to-csharp/json2csharp.ts | 215 +++++++ 8 files changed, 1106 insertions(+), 64 deletions(-) delete mode 100644 src/tools/json-to-csharp/json2csharp.d.ts create mode 100644 src/tools/json-to-csharp/json2csharp.test.ts create mode 100644 src/tools/json-to-csharp/json2csharp.ts diff --git a/components.d.ts b/components.d.ts index 9c82cf1b..8ab21fd4 100644 --- a/components.d.ts +++ b/components.d.ts @@ -140,6 +140,7 @@ declare module '@vue/runtime-core' { NLayoutSider: typeof import('naive-ui')['NLayoutSider'] NMenu: typeof import('naive-ui')['NMenu'] NScrollbar: typeof import('naive-ui')['NScrollbar'] + NSpace: typeof import('naive-ui')['NSpace'] NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default'] OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default'] PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default'] diff --git a/package.json b/package.json index c6887614..9f39ff1d 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,6 @@ "highlight.js": "^11.7.0", "iarna-toml-esm": "^3.0.5", "ibantools": "^4.3.3", - "json2csharp": "^1.0.3", "json5": "^2.2.3", "jwt-decode": "^3.1.2", "libphonenumber-js": "^1.10.28", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a07cca67..8515535e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,7 +37,7 @@ dependencies: version: 10.3.0(vue@3.3.4) '@vueuse/head': specifier: ^1.0.0 - version: 1.0.0(vue@3.3.4) + version: 1.0.0(typescript@5.2.2)(vue@3.3.4) '@vueuse/router': specifier: ^10.0.0 version: 10.0.0(vue-router@4.1.6)(vue@3.3.4) @@ -92,9 +92,6 @@ dependencies: ibantools: specifier: ^4.3.3 version: 4.3.3 - json2csharp: - specifier: ^1.0.3 - version: 1.0.3 json5: specifier: ^2.2.3 version: 2.2.3 @@ -811,6 +808,11 @@ packages: resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==} engines: {node: '>=6.9.0'} + /@babel/helper-string-parser@7.25.9: + resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} + engines: {node: '>=6.9.0'} + dev: false + /@babel/helper-validator-identifier@7.22.20: resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} engines: {node: '>=6.9.0'} @@ -819,6 +821,11 @@ packages: resolution: {integrity: sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==} engines: {node: '>=6.9.0'} + /@babel/helper-validator-identifier@7.25.9: + resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} + engines: {node: '>=6.9.0'} + dev: false + /@babel/helper-validator-option@7.22.15: resolution: {integrity: sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==} engines: {node: '>=6.9.0'} @@ -911,6 +918,14 @@ packages: dependencies: '@babel/types': 7.23.0 + /@babel/parser@7.26.9: + resolution: {integrity: sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.26.9 + dev: false + /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.22.15(@babel/core@7.23.2): resolution: {integrity: sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg==} engines: {node: '>=6.9.0'} @@ -1898,6 +1913,14 @@ packages: '@babel/helper-validator-identifier': 7.22.20 to-fast-properties: 2.0.0 + /@babel/types@7.26.9: + resolution: {integrity: sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + dev: false + /@css-render/plugin-bem@0.15.12(css-render@0.15.12): resolution: {integrity: sha512-Lq2jSOZn+wYQtsyaFj6QRz2EzAnd3iW5fZeHO1WSXQdVYwvwGX0ZiH3X2JQgtgYLT1yeGtrwrqJdNdMEUD2xTw==} peerDependencies: @@ -2336,6 +2359,10 @@ packages: /@jridgewell/sourcemap-codec@1.4.15: resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + /@jridgewell/sourcemap-codec@1.5.0: + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + dev: false + /@jridgewell/trace-mapping@0.3.18: resolution: {integrity: sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==} dependencies: @@ -3347,18 +3374,18 @@ packages: '@unhead/schema': 0.5.1 dev: false - /@unhead/vue@0.5.1(vue@3.3.4): + /@unhead/vue@0.5.1(typescript@5.2.2)(vue@3.3.4): resolution: {integrity: sha512-s4y4uj3NMqaUs0K+WQXbWGj/2+Glk/DEJ9yeJOcJIiro/+IhUMByD71jyCM43Xn8YBPy14VY/ZYb9ZFU2WCZgw==} peerDependencies: vue: '>=2.7 || >=3' dependencies: '@unhead/dom': 0.5.1 '@unhead/schema': 0.5.1 - '@vueuse/shared': 10.9.0(vue@3.3.4) + '@vueuse/shared': 12.8.2(typescript@5.2.2) unhead: 0.5.1 vue: 3.3.4 transitivePeerDependencies: - - '@vue/composition-api' + - typescript dev: false /@unocss/astro@0.57.1(rollup@2.79.1)(vite@4.4.9): @@ -3747,6 +3774,16 @@ packages: dev: true optional: true + /@vue/compiler-core@3.5.13: + resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==} + dependencies: + '@babel/parser': 7.26.9 + '@vue/shared': 3.5.13 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + dev: false + /@vue/compiler-dom@3.2.47: resolution: {integrity: sha512-dBBnEHEPoftUiS03a4ggEig74J2YBZ2UIeyfpcRM2tavgMWo4bsEfgCGsu+uJIL/vax9S+JztH8NmQerUo7shQ==} dependencies: @@ -3768,6 +3805,13 @@ packages: dev: true optional: true + /@vue/compiler-dom@3.5.13: + resolution: {integrity: sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==} + dependencies: + '@vue/compiler-core': 3.5.13 + '@vue/shared': 3.5.13 + dev: false + /@vue/compiler-sfc@3.2.47: resolution: {integrity: sha512-rog05W+2IFfxjMcFw10tM9+f7i/+FFpZJJ5XHX72NP9eC2uRD+42M3pYcQqDXVYoj74kHMSEdQ/WmCjt8JFksQ==} dependencies: @@ -3797,6 +3841,20 @@ packages: postcss: 8.4.28 source-map-js: 1.0.2 + /@vue/compiler-sfc@3.5.13: + resolution: {integrity: sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==} + dependencies: + '@babel/parser': 7.26.9 + '@vue/compiler-core': 3.5.13 + '@vue/compiler-dom': 3.5.13 + '@vue/compiler-ssr': 3.5.13 + '@vue/shared': 3.5.13 + estree-walker: 2.0.2 + magic-string: 0.30.17 + postcss: 8.5.3 + source-map-js: 1.2.1 + dev: false + /@vue/compiler-ssr@3.2.47: resolution: {integrity: sha512-wVXC+gszhulcMD8wpxMsqSOpvDZ6xKXSVWkf50Guf/S+28hTAXPDYRTbLQ3EDkOP5Xz/+SY37YiwDquKbJOgZw==} dependencies: @@ -3819,6 +3877,13 @@ packages: dev: true optional: true + /@vue/compiler-ssr@3.5.13: + resolution: {integrity: sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==} + dependencies: + '@vue/compiler-dom': 3.5.13 + '@vue/shared': 3.5.13 + dev: false + /@vue/devtools-api@6.5.0: resolution: {integrity: sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q==} @@ -3865,12 +3930,25 @@ packages: dependencies: '@vue/shared': 3.3.4 + /@vue/reactivity@3.5.13: + resolution: {integrity: sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==} + dependencies: + '@vue/shared': 3.5.13 + dev: false + /@vue/runtime-core@3.3.4: resolution: {integrity: sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==} dependencies: '@vue/reactivity': 3.3.4 '@vue/shared': 3.3.4 + /@vue/runtime-core@3.5.13: + resolution: {integrity: sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==} + dependencies: + '@vue/reactivity': 3.5.13 + '@vue/shared': 3.5.13 + dev: false + /@vue/runtime-dom@3.3.4: resolution: {integrity: sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==} dependencies: @@ -3878,6 +3956,15 @@ packages: '@vue/shared': 3.3.4 csstype: 3.1.2 + /@vue/runtime-dom@3.5.13: + resolution: {integrity: sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==} + dependencies: + '@vue/reactivity': 3.5.13 + '@vue/runtime-core': 3.5.13 + '@vue/shared': 3.5.13 + csstype: 3.1.3 + dev: false + /@vue/server-renderer@3.3.4(vue@3.3.4): resolution: {integrity: sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==} peerDependencies: @@ -3898,6 +3985,16 @@ packages: dev: true optional: true + /@vue/server-renderer@3.5.13(vue@3.5.13): + resolution: {integrity: sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==} + peerDependencies: + vue: 3.5.13 + dependencies: + '@vue/compiler-ssr': 3.5.13 + '@vue/shared': 3.5.13 + vue: 3.5.13(typescript@5.2.2) + dev: false + /@vue/shared@3.2.47: resolution: {integrity: sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ==} dev: true @@ -3911,6 +4008,10 @@ packages: dev: true optional: true + /@vue/shared@3.5.13: + resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==} + dev: false + /@vue/test-utils@2.3.2(vue@3.3.4): resolution: {integrity: sha512-hJnVaYhbrIm0yBS0+e1Y0Sj85cMyAi+PAbK4JHqMRUZ6S622Goa+G7QzkRSyvCteG8wop7tipuEbHoZo26wsSA==} peerDependencies: @@ -3948,17 +4049,17 @@ packages: - vue dev: false - /@vueuse/head@1.0.0(vue@3.3.4): + /@vueuse/head@1.0.0(typescript@5.2.2)(vue@3.3.4): resolution: {integrity: sha512-wighjD6iLxEitpg6EDeS5dGDB9tcOSMhpblrAOKR6qBP93U3cjG72n0LhlBUD9miu41lNxXFVGHgSc6BVJ9BMg==} peerDependencies: vue: '>=2.7 || >=3' dependencies: '@unhead/schema': 0.5.1 '@unhead/ssr': 0.5.1 - '@unhead/vue': 0.5.1(vue@3.3.4) + '@unhead/vue': 0.5.1(typescript@5.2.2)(vue@3.3.4) vue: 3.3.4 transitivePeerDependencies: - - '@vue/composition-api' + - typescript dev: false /@vueuse/metadata@10.3.0: @@ -3996,13 +4097,12 @@ packages: - vue dev: false - /@vueuse/shared@10.9.0(vue@3.3.4): - resolution: {integrity: sha512-Uud2IWncmAfJvRaFYzv5OHDli+FbOzxiVEQdLCKQKLyhz94PIyFC3CHcH7EDMwIn8NPtD06+PNbC/PiO0LGLtw==} + /@vueuse/shared@12.8.2(typescript@5.2.2): + resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==} dependencies: - vue-demi: 0.14.7(vue@3.3.4) + vue: 3.5.13(typescript@5.2.2) transitivePeerDependencies: - - '@vue/composition-api' - - vue + - typescript dev: false /@zhead/schema@1.0.0-beta.13: @@ -4715,6 +4815,10 @@ packages: /csstype@3.1.2: resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} + /csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + dev: false + /dash-get@1.0.2: resolution: {integrity: sha512-4FbVrHDwfOASx7uQVxeiCTo7ggSdYZbqs8lH+WU6ViypPlDbe9y6IP5VVUDQBv9DcnyaiPT5XT0UWHgJ64zLeQ==} dev: false @@ -5004,7 +5108,6 @@ packages: /entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} - dev: true /errno@0.1.8: resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==} @@ -6584,10 +6687,6 @@ packages: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} dev: true - /json2csharp@1.0.3: - resolution: {integrity: sha512-hbe5csNMtvYf4neEDvYRWkJf8fgxeheuMAfhG80f2HIpeUMunPvv+4F2fS1PLpXbdzBp6EMQjZiocRvKhtahgg==} - dev: false - /json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -6772,6 +6871,12 @@ packages: dependencies: '@jridgewell/sourcemap-codec': 1.4.15 + /magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + dev: false + /magic-string@0.30.2: resolution: {integrity: sha512-lNZdu7pewtq/ZvWUp9Wpf/x7WzMTsR26TWV03BRZrXFsv+BI6dy8RAiKgm1uM/kyR0rCfUcqvOlXKG66KhIGug==} engines: {node: '>=12'} @@ -7005,6 +7110,12 @@ packages: resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + /nanoid@3.3.9: + resolution: {integrity: sha512-SppoicMGpZvbF1l3z4x7No3OlIjP7QJvC9XR7AhZr1kL133KHnKPztkKDc+Ir4aJ/1VhTySrtKhrsycmrMQfvg==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: false + /natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true @@ -7380,6 +7491,10 @@ packages: /picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + /picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + dev: false + /picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} @@ -7470,6 +7585,15 @@ packages: source-map-js: 1.0.2 dev: true + /postcss@8.5.3: + resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.9 + picocolors: 1.1.1 + source-map-js: 1.2.1 + dev: false + /prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -8132,6 +8256,11 @@ packages: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} + /source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + dev: false + /source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} dependencies: @@ -9158,21 +9287,6 @@ packages: vue: 3.3.4 dev: false - /vue-demi@0.14.7(vue@3.3.4): - resolution: {integrity: sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==} - engines: {node: '>=12'} - hasBin: true - requiresBuild: true - peerDependencies: - '@vue/composition-api': ^1.0.0-rc.1 - vue: ^3.0.0-0 || ^2.6.0 - peerDependenciesMeta: - '@vue/composition-api': - optional: true - dependencies: - vue: 3.3.4 - dev: false - /vue-eslint-parser@9.3.1(eslint@8.47.0): resolution: {integrity: sha512-Clr85iD2XFZ3lJ52/ppmUDG/spxQu6+MAeHXjjyI4I1NUYZ9xmenQp4N0oaHJhrA8OOxltCVxMRfANGa70vU0g==} engines: {node: ^14.17.0 || >=16.0.0} @@ -9239,6 +9353,22 @@ packages: '@vue/server-renderer': 3.3.4(vue@3.3.4) '@vue/shared': 3.3.4 + /vue@3.5.13(typescript@5.2.2): + resolution: {integrity: sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@vue/compiler-dom': 3.5.13 + '@vue/compiler-sfc': 3.5.13 + '@vue/runtime-dom': 3.5.13 + '@vue/server-renderer': 3.5.13(vue@3.5.13) + '@vue/shared': 3.5.13 + typescript: 5.2.2 + dev: false + /vueuc@0.4.51(vue@3.3.4): resolution: {integrity: sha512-pLiMChM4f+W8czlIClGvGBYo656lc2Y0/mXFSCydcSmnCR1izlKPGMgiYBGjbY9FDkFG8a2HEVz7t0DNzBWbDw==} peerDependencies: diff --git a/src/composable/queryParams.ts b/src/composable/queryParams.ts index 9699abbc..7cc8cc0d 100644 --- a/src/composable/queryParams.ts +++ b/src/composable/queryParams.ts @@ -1,7 +1,8 @@ import { useRouteQuery } from '@vueuse/router'; import { computed } from 'vue'; +import { useStorage } from '@vueuse/core'; -export { useQueryParam }; +export { useQueryParam, useQueryParamOrStorage }; const transformers = { number: { @@ -16,6 +17,12 @@ const transformers = { fromQuery: (value: string) => value.toLowerCase() === 'true', toQuery: (value: boolean) => (value ? 'true' : 'false'), }, + object: { + fromQuery: (value: string) => { + return JSON.parse(value); + }, + toQuery: (value: object) => JSON.stringify(value), + }, }; function useQueryParam({ name, defaultValue }: { name: string; defaultValue: T }) { @@ -33,3 +40,27 @@ function useQueryParam({ name, defaultValue }: { name: string; defaultValue: }, }); } + +function useQueryParamOrStorage({ name, storageName, defaultValue }: { name: string; storageName: string; defaultValue: T }) { + const type = typeof defaultValue; + const transformer = transformers[type as keyof typeof transformers] ?? transformers.string; + + const storageRef = useStorage(storageName, defaultValue); + const proxyDefaultValue = transformer.toQuery(defaultValue as never); + const proxy = useRouteQuery(name, proxyDefaultValue); + + const r = ref(defaultValue); + + watch(r, + (value) => { + proxy.value = transformer.toQuery(value as never); + storageRef.value = value as never; + }, + { deep: true }); + + r.value = (proxy.value && proxy.value !== proxyDefaultValue + ? transformer.fromQuery(proxy.value) as unknown as T + : storageRef.value as T) as never; + + return r; +} diff --git a/src/tools/json-to-csharp/json-to-csharp.vue b/src/tools/json-to-csharp/json-to-csharp.vue index 8068cb3f..3e109850 100644 --- a/src/tools/json-to-csharp/json-to-csharp.vue +++ b/src/tools/json-to-csharp/json-to-csharp.vue @@ -1,9 +1,10 @@ diff --git a/src/tools/json-to-csharp/json2csharp.d.ts b/src/tools/json-to-csharp/json2csharp.d.ts deleted file mode 100644 index 990ecc1f..00000000 --- a/src/tools/json-to-csharp/json2csharp.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -declare module 'json2csharp'{ - export default function(json: string, useNewtonsoftAnnotations?: boolean): string; -} \ No newline at end of file diff --git a/src/tools/json-to-csharp/json2csharp.test.ts b/src/tools/json-to-csharp/json2csharp.test.ts new file mode 100644 index 00000000..52750f75 --- /dev/null +++ b/src/tools/json-to-csharp/json2csharp.test.ts @@ -0,0 +1,617 @@ +import { describe, expect, it } from 'vitest'; +import { getPrimitiveProp, isDate, json2classes, json2csharp } from './json2csharp'; + +// Test isDate function +describe('isDate', () => { + it('should return true for valid date strings', () => { + expect(isDate('2024-03-08')).toBe(true); + expect(isDate('2024-03-08T12:34:56Z')).toBe(true); + }); + + it('should return false for invalid date strings', () => { + expect(isDate('hello')).toBe(false); + expect(isDate('2024-13-08')).toBe(false); // Invalid month + expect(isDate('9999-99-99')).toBe(false); // Invalid format + }); +}); + +// Test getPrimitiveProp function +describe('getPrimitiveProp', () => { + it('should return \'string\' type for non-date strings', () => { + expect(getPrimitiveProp('hello', 'testKey')).toEqual({ + type: 'string', + name: 'testKey', + canBeNullable: false, + }); + }); + + it('should return \'DateTime\' type for date strings', () => { + expect(getPrimitiveProp('2024-03-08', 'dateKey')).toEqual({ + type: 'DateTime', + name: 'dateKey', + canBeNullable: true, + }); + }); + + it('should return \'int\' for integers', () => { + expect(getPrimitiveProp(42, 'numberKey')).toEqual({ + type: 'int', + name: 'numberKey', + canBeNullable: true, + }); + }); + + it('should return \'double\' for floating point numbers', () => { + expect(getPrimitiveProp(3.14, 'floatKey')).toEqual({ + type: 'double', + name: 'floatKey', + canBeNullable: true, + }); + }); + + it('should return \'bool\' for boolean values', () => { + expect(getPrimitiveProp(true, 'boolKey')).toEqual({ + type: 'bool', + name: 'boolKey', + canBeNullable: true, + }); + }); + + it('should throw an error for unsupported types', () => { + expect(() => getPrimitiveProp({}, 'objKey')).toThrow('Unexpected key \'objKey\' of type object'); + }); +}); + +// Test json2classes function +describe('json2classes', () => { + it('should convert JSON to class definitions with custom root', () => { + const json = { name: 'John', age: 30 }; + const result = json2classes(json, 'Main'); + expect(result).to.deep.eq([ + { + key: 'Main', + props: [ + { type: 'string', name: 'name', canBeNullable: false }, + { type: 'int', name: 'age', canBeNullable: true }, + ], + }, + ]); + }); + + it('should handle nested objects', () => { + const json = { user: { name: 'John' } }; + const result = json2classes(json); + expect(result).to.deep.eq([ + { key: 'Root', props: [{ type: 'User', name: 'user' }] }, + { key: 'User', props: [{ type: 'string', name: 'name', canBeNullable: false }] }, + ]); + }); + + it('should handle arrays of primitives', () => { + const json = { numbers: [1, 2, 3] }; + const result = json2classes(json); + expect(result).to.deep.eq([ + { key: 'Root', props: [{ type: 'int', name: 'numbers', isArray: true, canBeNullable: true }] }, + ]); + }); + + it('should handle arrays of objects', () => { + const json = { users: [{ name: 'John' }, { name: 'Jane' }] }; + const result = json2classes(json); + expect(result).to.deep.eq([ + { key: 'Root', props: [{ type: 'UsersItem', name: 'users', isArray: true }] }, + { key: 'UsersItem', props: [{ type: 'string', name: 'name', canBeNullable: false }] }, + ]); + }); +}); + +// Test json2csharp function +describe('json2csharp', () => { + const cleanTabs = (s: string) => s.replace(/\t/g, ' '); + + it('should convert JSON to C# class with default options', () => { + const json = JSON.stringify({ name: 'John', age: 30 }); + const result = cleanTabs(json2csharp({ src: json })); + + expect(result).toBe(`using System; +using System.Text.Json; + +public class Root +{ + [JsonPropertyName(\"name\")] + public string Name { get; set; } + [JsonPropertyName(\"age\")] + public int Age { get; set; } +}`); + }); + + it('should not change case if pascalCase option is false', () => { + const json = JSON.stringify({ test: { user_name: 'John' } }); + const result = cleanTabs(json2csharp({ src: json, pascalCase: false })); + + expect(result).toBe(`using System; +using System.Text.Json; + +public class Root +{ + [JsonPropertyName(\"test\")] + public Test test { get; set; } +} + +public class Test +{ + [JsonPropertyName(\"user_name\")] + public string user_name { get; set; } +}`); + }); + + it('should handle when root is an array', () => { + const json = JSON.stringify([{ age: 30 }]); + const result = cleanTabs(json2csharp({ src: json, useNullable: true })); + + expect(result).toBe(`using System; +using System.Text.Json; + +public class RootItem +{ + [JsonPropertyName(\"age\")] + public int? Age { get; set; } +}`); + }); + + it('should use nullable types if useNullable option is true', () => { + const json = JSON.stringify({ age: 30 }); + const result = cleanTabs(json2csharp({ src: json, useNullable: true })); + + expect(result).toBe(`using System; +using System.Text.Json; + +public class Root +{ + [JsonPropertyName(\"age\")] + public int? Age { get; set; } +}`); + }); + + it('should add JsonProperty attributes if addJsonProperty is true', () => { + const json = JSON.stringify({ name: 'John' }); + const result = cleanTabs(json2csharp({ src: json, addJsonProperty: true })); + + expect(result).toBe(`using System; +using Newtonsoft.Json; +using System.Text.Json; + +public class Root +{ + [JsonProperty(\"name\", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName(\"name\")] + public string Name { get; set; } +}`); + }); + + it('should add JsonPropertyName attributes if addJsonPropertyName is true', () => { + const json = JSON.stringify({ name: 'John' }); + const result = cleanTabs(json2csharp({ src: json, addJsonPropertyName: true })); + + expect(result).toBe(`using System; +using System.Text.Json; + +public class Root +{ + [JsonPropertyName(\"name\")] + public string Name { get; set; } +}`); + }); + + it('should generate immutable classes if generateImmutableClasses is true', () => { + const json = JSON.stringify({ name: 'John' }); + const result = cleanTabs(json2csharp({ src: json, generateImmutableClasses: true })); + + expect(result).toBe(`using System; +using System.Text.Json; + +public class Root +{ + public Root( + string name + ){ + this.Name = name; + } + + [JsonPropertyName(\"name\")] + public string Name { get; } +}`); + }); + + it('should convert JSON with arrays into C# classes', () => { + const json = { + users: [{ name: 'John', age: 30 }, { name: 'Jane', age: 25 }], + }; + + expect(cleanTabs(json2csharp({ src: json }))).toBe(`using System; +using System.Text.Json; + +public class Root +{ + [JsonPropertyName(\"users\")] + public UsersItem[] Users { get; set; } +} + +public class UsersItem +{ + [JsonPropertyName(\"name\")] + public string Name { get; set; } + [JsonPropertyName(\"age\")] + public int Age { get; set; } +}`); + expect(cleanTabs(json2csharp({ src: json, useRecordTypes: true }))).toBe(`using System; +using System.Text.Json; + +public record Root( + [property: JsonPropertyName(\"users\")] + UsersItem[] Users +); + +public record UsersItem( + [property: JsonPropertyName(\"name\")] + string Name, + [property: JsonPropertyName(\"age\")] + int Age +);`); + expect(cleanTabs(json2csharp({ src: json, useRecordTypes: true, pascalCase: false, useReadonlyLists: true }))).toBe(`using System; +using System.Text.Json; + +public record Root( + [property: JsonPropertyName(\"users\")] + IReadonlyList users +); + +public record UsersItem( + [property: JsonPropertyName(\"name\")] + string name, + [property: JsonPropertyName(\"age\")] + int age +);`); + }); + + it('should generate C# record types when useRecordTypes is true', () => { + const json = JSON.stringify({ name: 'John', age: 30 }); + const result = cleanTabs(json2csharp({ src: json, useRecordTypes: true })); + + expect(result).toBe(`using System; +using System.Text.Json; + +public record Root( + [property: JsonPropertyName(\"name\")] + string Name, + [property: JsonPropertyName(\"age\")] + int Age +);`); + }); + + it('should generate C# record types with attributes when useRecordTypes/addJsonProperty/addJsonPropertyName are true', () => { + const json = JSON.stringify({ name: 'John', age: 30 }); + const result = cleanTabs(json2csharp({ + src: json, + useRecordTypes: true, + addJsonProperty: true, + addJsonPropertyName: true, + })); + + expect(result).toBe(`using System; +using Newtonsoft.Json; +using System.Text.Json; + +public record Root( + [property: JsonProperty(\"name\", NullValueHandling = NullValueHandling.Ignore)] + [property: JsonPropertyName(\"name\")] + string Name, + [property: JsonProperty(\"age\", NullValueHandling = NullValueHandling.Ignore)] + [property: JsonPropertyName(\"age\")] + int Age +);`); + }); + + it('should handle arrays of primitives', () => { + const json = JSON.stringify({ numbers: [1, 2, 3] }); + const result = cleanTabs(json2csharp({ src: json })); + + expect(result).toBe(`using System; +using System.Text.Json; + +public class Root +{ + [JsonPropertyName(\"numbers\")] + public int[] Numbers { get; set; } +}`); + }); + + it('should handle arrays of objects with PascalCase enabled', () => { + const json = JSON.stringify({ + items: [{ item_name: 'A', price: 10.5 }], + }); + const result = cleanTabs(json2csharp({ + src: json, + pascalCase: true, + addJsonProperty: true, + addJsonPropertyName: true, + })); + + expect(result).toBe(`using System; +using Newtonsoft.Json; +using System.Text.Json; + +public class Root +{ + [JsonProperty(\"items\", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName(\"items\")] + public ItemsItem[] Items { get; set; } +} + +public class ItemsItem +{ + [JsonProperty(\"item_name\", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName(\"item_name\")] + public string ItemName { get; set; } + [JsonProperty(\"price\", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName(\"price\")] + public double Price { get; set; } +}`); + }); + + it('should use nullable types for optional fields when useNullable is true', () => { + const json = JSON.stringify({ age: null, price: 10.5 }); + const result = cleanTabs(json2csharp({ src: json, useNullable: true })); + + expect(result).toBe(`using System; +using System.Text.Json; + +public class Root +{ + [JsonPropertyName(\"age\")] + public Age Age { get; set; } + [JsonPropertyName(\"price\")] + public double? Price { get; set; } +} + +public class Age +{ +}`); + }); + + it('should use fields when useFields is true', () => { + const json = JSON.stringify({ age: 1, price: 10.5 }); + const result = cleanTabs(json2csharp({ src: json, useFields: true })); + + expect(result).toBe(`using System; +using System.Text.Json; + +public class Root +{ + [JsonPropertyName(\"age\")] + public int Age; + [JsonPropertyName(\"price\")] + public double Price; +}`); + }); + + it('should add JsonProperty attributes when addJsonProperty is true', () => { + const json = JSON.stringify({ firstName: 'John', lastName: 'Doe' }); + const result = cleanTabs(json2csharp({ src: json, addJsonProperty: true, nullValueHandlingIgnore: false })); + + expect(result).toBe(`using System; +using Newtonsoft.Json; +using System.Text.Json; + +public class Root +{ + [JsonProperty(\"firstName\")] + [JsonPropertyName(\"firstName\")] + public string FirstName { get; set; } + [JsonProperty(\"lastName\")] + [JsonPropertyName(\"lastName\")] + public string LastName { get; set; } +}`); + }); + + it('should add JsonProperty attributes when addJsonProperty and nullValueHandlingIgnore are true', () => { + const json = JSON.stringify({ firstName: 'John', lastName: 'Doe' }); + const result = cleanTabs(json2csharp({ src: json, addJsonProperty: true, nullValueHandlingIgnore: true })); + + expect(result).toBe(`using System; +using Newtonsoft.Json; +using System.Text.Json; + +public class Root +{ + [JsonProperty(\"firstName\", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName(\"firstName\")] + public string FirstName { get; set; } + [JsonProperty(\"lastName\", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName(\"lastName\")] + public string LastName { get; set; } +}`); + }); + + it('should handle all types', () => { + const json = JSON.stringify({ + id: 1, date: '2025-06-09', amount: 1.4, group: null, active: true, email: 'test@example.com', + }); + const result = cleanTabs(json2csharp({ src: json })); + + expect(result).toBe(`using System; +using System.Text.Json; + +public class Root +{ + [JsonPropertyName(\"id\")] + public int Id { get; set; } + [JsonPropertyName(\"date\")] + public DateTime Date { get; set; } + [JsonPropertyName(\"amount\")] + public double Amount { get; set; } + [JsonPropertyName(\"group\")] + public Group Group { get; set; } + [JsonPropertyName(\"active\")] + public bool Active { get; set; } + [JsonPropertyName(\"email\")] + public string Email { get; set; } +} + +public class Group +{ +}`); + }); + + it('should use readonly lists when useReadonlyLists is true', () => { + const json = JSON.stringify({ + items: [{ name: 'Item1' }, { name: 'Item2' }], + }); + const result = cleanTabs(json2csharp({ src: json, useReadonlyLists: true })); + + expect(result).toBe(`using System; +using System.Text.Json; + +public class Root +{ + [JsonPropertyName(\"items\")] + public List Items { get; } = new List; +} + +public class ItemsItem +{ + [JsonPropertyName(\"name\")] + public string Name { get; set; } +}`); + }); + + it('should use immutable class properties when generateImmutableClasses is true', () => { + const json = JSON.stringify({ title: 'Book', pages: 300 }); + const result = cleanTabs(json2csharp({ src: json, generateImmutableClasses: true })); + + expect(result).toBe(`using System; +using System.Text.Json; + +public class Root +{ + public Root( + string title, + int pages + ){ + this.Title = title; + this.Pages = pages; + } + + [JsonPropertyName(\"title\")] + public string Title { get; } + [JsonPropertyName(\"pages\")] + public int Pages { get; } +}`); + }); + + it('should generate a combination of options correctly', () => { + const json = JSON.stringify({ + people: [ + { first_name: 'John', age: 30 }, + { first_name: 'Jane', age: 25 }, + ], + }); + + expect(cleanTabs(json2csharp({ + src: json, + pascalCase: true, + useNullable: true, + addJsonProperty: true, + generateImmutableClasses: true, + useRecordTypes: false, + useReadonlyLists: true, + }))).toBe(`using System; +using Newtonsoft.Json; +using System.Text.Json; + +public class Root +{ + public Root( + List people + ){ + this.People = people; + } + + [JsonProperty(\"people\", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName(\"people\")] + public List People { get; } = new List; +} + +public class PeopleItem +{ + public PeopleItem( + string firstName, + int? age + ){ + this.FirstName = firstName; + this.Age = age; + } + + [JsonProperty(\"first_name\", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName(\"first_name\")] + public string FirstName { get; } + [JsonProperty(\"age\", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName(\"age\")] + public int? Age { get; } +}`); + expect(cleanTabs(json2csharp({ + src: json, + pascalCase: true, + useNullable: true, + addJsonProperty: true, + addJsonPropertyName: true, + generateImmutableClasses: false, + useRecordTypes: false, + useReadonlyLists: true, + }))).toBe(`using System; +using Newtonsoft.Json; +using System.Text.Json; + +public class Root +{ + [JsonProperty(\"people\", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName(\"people\")] + public List People { get; } = new List; +} + +public class PeopleItem +{ + [JsonProperty(\"first_name\", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName(\"first_name\")] + public string FirstName { get; set; } + [JsonProperty(\"age\", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName(\"age\")] + public int? Age { get; set; } +}`); + expect(cleanTabs(json2csharp({ + src: json, + pascalCase: true, + useNullable: true, + addJsonProperty: true, + useRecordTypes: true, + useReadonlyLists: true, + }))).toBe(`using System; +using Newtonsoft.Json; +using System.Text.Json; + +public record Root( + [property: JsonProperty(\"people\", NullValueHandling = NullValueHandling.Ignore)] + [property: JsonPropertyName(\"people\")] + IReadonlyList People +); + +public record PeopleItem( + [property: JsonProperty(\"first_name\", NullValueHandling = NullValueHandling.Ignore)] + [property: JsonPropertyName(\"first_name\")] + string FirstName, + [property: JsonProperty(\"age\", NullValueHandling = NullValueHandling.Ignore)] + [property: JsonPropertyName(\"age\")] + int? Age +);`); + }); +}); diff --git a/src/tools/json-to-csharp/json2csharp.ts b/src/tools/json-to-csharp/json2csharp.ts new file mode 100644 index 00000000..d0b0b7d8 --- /dev/null +++ b/src/tools/json-to-csharp/json2csharp.ts @@ -0,0 +1,215 @@ +import { camelCase as convertToCamelCase, pascalCase as convertToPascalCase } from 'change-case'; +import JSON5 from 'json5'; + +function normalize(src: string, firstUpper = true) { + // return /^\d$/.test(src.charAt(0)) + // ? `N${pascalCase(src)}` + // : pascalCase(src); + return /^\d$/.test(src.charAt(0)) + ? `N${src}` + : (firstUpper ? src.charAt(0).toUpperCase() + src.slice(1) : src); +} + +export function isDate(src: string) { + return /^(\d{4})(-(0[1-9]|1[0-2])(-([12]\d|0[1-9]|3[01]))([T\s]((([01]\d|2[0-3])((:)[0-5]\d))([\:]\d+)?)?(:[0-5]\d([\.]\d+)?)?([zZ]|([\+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)$/.test( + src, + ); +} + +interface Prop { type: string; name: string; isArray?: boolean; canBeNullable?: boolean } + +export function getPrimitiveProp(obj: any, key: string): Prop { + const type = typeof obj; + switch (type) { + case 'string': + // eslint-disable-next-line no-case-declarations + const isObjDate = isDate(obj); + return { type: isObjDate ? 'DateTime' : 'string', name: key, canBeNullable: isObjDate }; + case 'number': + return { type: Number.isInteger(obj) ? 'int' : 'double', name: key, canBeNullable: true }; + case 'boolean': + return { type: 'bool', name: key, canBeNullable: true }; + default: + throw new Error(`Unexpected key '${key}' of type ${type}`); + } +} + +interface ClassType { key: string; props: Array } + +function handleObject( + classes: Array, + obj: any, + key: string, + skip: boolean = false) { + const normalizedKey = normalize(key); + let target: ClassType = classes.find(x => x.key === normalizedKey) as never; + if (!target) { + target = { key: normalizedKey, props: [] }; + if (!skip) { + classes.push(target); + } + } + + if (obj != null) { + Object.keys(obj).forEach((k) => { + const keyName = Number.isNaN(Number(k)) ? k : `${normalizedKey}Item`; + let prop: Prop | null = null; + if (typeof obj[k] == 'object') { + if (Array.isArray(obj[k])) { + if (Array.from(obj[k]).length > 0) { + if (typeof obj[k][0] == 'object') { + handleObject(classes, obj[k], keyName, true); + prop = { + type: normalize(`${keyName}Item`), + name: keyName, + isArray: true, + }; + } + else { + prop = getPrimitiveProp(obj[k][0], keyName); + prop.isArray = true; + } + } + } + else { + handleObject(classes, obj[k], keyName); + prop = { type: normalize(keyName), name: keyName }; + } + } + else { + prop = getPrimitiveProp(obj[k], keyName); + } + + if (prop && !target.props.some(x => x.name === prop?.name)) { + target.props.push(prop); + } + }); + } +} + +export function json2classes(src: object, rootTypeName: string = 'Root') { + const classes: Array = []; + handleObject(classes, src, rootTypeName); + return classes; +} + +export function json2csharp( + { + src, + rootTypeName = 'Root', + pascalCase = true, + nullValueHandlingIgnore = true, + addJsonPropertyName = true, + useReadonlyLists, + useRecordTypes, + addJsonProperty, + generateImmutableClasses, + useFields, + useNullable, + }: { + src: any + rootTypeName?: string + pascalCase?: boolean + useFields?: boolean + useNullable?: boolean + addJsonProperty?: boolean + nullValueHandlingIgnore?: boolean + addJsonPropertyName?: boolean + generateImmutableClasses?: boolean + useRecordTypes?: boolean + useReadonlyLists?: boolean + }) { + const normalizeCase = (s: string) => { + const normalized = normalize(s, pascalCase); + return pascalCase ? convertToPascalCase(normalized) : normalized; + }; + + const getPropType = (p: Prop) => { + let type = p.type; + if (p.isArray) { + if (useReadonlyLists) { + type = useRecordTypes ? `IReadonlyList<${type}>` : `List<${type}>`; + } + else { + type = `${type}[]`; + } + } + const nullable = useNullable && p.canBeNullable ? '?' : ''; + return { type, nullable }; + }; + + const srcObj = typeof src === 'string' ? JSON5.parse(src) : src; + const classes = json2classes( + Array.isArray(srcObj) ? srcObj[0] : srcObj, + Array.isArray(srcObj) ? `${rootTypeName}Item` : rootTypeName); + let result = ''; + result += 'using System;\n'; + result += addJsonProperty ? 'using Newtonsoft.Json;\n' : ''; + result += addJsonPropertyName ? 'using System.Text.Json;\n' : ''; + result += '\n'; + classes.forEach((c) => { + if (useRecordTypes) { + result += `public record ${normalizeCase(c.key)}(\n`; + } + else { + result += `public class ${normalizeCase(c.key)}\n`; + result += '{\n'; + } + if (!useRecordTypes && generateImmutableClasses) { + result += `\tpublic ${normalizeCase(c.key)}(\n`; + c.props.forEach((p) => { + const propType = getPropType(p); + result += `\t\t${propType.type}${propType.nullable} ${convertToCamelCase(p.name)},\n`; + }); + result = result.replace(/,\n$/, '\n'); + result += '\t){\n'; + c.props.forEach((p) => { + result += `\t\tthis.${normalizeCase(p.name)} = ${convertToCamelCase(p.name)};\n`; + }); + result += '\t}\n\n'; + } + c.props.forEach((p) => { + const scope = useRecordTypes ? 'property: ' : ''; + if (addJsonProperty) { + if (nullValueHandlingIgnore) { + result += `\t[${scope}JsonProperty("${p.name}", NullValueHandling = NullValueHandling.Ignore)]\n`; + } + else { + result += `\t[${scope}JsonProperty("${p.name}")]\n`; + } + } + if (addJsonPropertyName) { + result += `\t[${scope}JsonPropertyName("${p.name}")]\n`; + } + + const propType = getPropType(p); + if (useRecordTypes) { + result += `\t${propType.type}${propType.nullable} ${normalizeCase(p.name)},\n`; + } + else { + const newList = useReadonlyLists && p.isArray ? ` = new ${propType.type};` : ''; + if (useFields) { + result += `\tpublic ${propType.type}${propType.nullable} ${normalizeCase(p.name)}${(newList || ';')}`; + } + else if (generateImmutableClasses || (p.isArray && useReadonlyLists)) { + result += `\tpublic ${propType.type}${propType.nullable} ${normalizeCase(p.name)} { get; }${newList}`; + } + else { + result += `\tpublic ${propType.type}${propType.nullable} ${normalizeCase(p.name)} { get; set; }${newList}`; + } + result += '\n'; + } + }); + if (useRecordTypes) { + result = result.replace(/,\n$/, '\n'); + result += ');\n\n'; + } + else { + result += '}\n\n'; + } + }); + + return result.trim(); +} + +export default json2csharp;