├── .gitignore ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── elm-test ├── README.md ├── elm.json └── src │ └── Main.elm ├── elm_rs ├── Cargo.toml ├── README.md ├── examples │ └── example.rs └── src │ ├── elm.rs │ ├── elm_decode.rs │ ├── elm_encode.rs │ ├── elm_query.rs │ ├── lib.rs │ └── test │ ├── complex.rs │ ├── enums_adjacent.rs │ ├── enums_external.rs │ ├── enums_internal.rs │ ├── enums_untagged.rs │ ├── etc_serde.rs │ ├── hygiene.rs │ ├── mod.rs │ ├── nested.rs │ ├── query.rs │ ├── regression.rs │ ├── structs.rs │ ├── structs_serde.rs │ └── types.rs ├── elm_rs_derive ├── Cargo.toml ├── README.md └── src │ ├── attributes.rs │ ├── elm.rs │ ├── elm_decode.rs │ ├── elm_encode.rs │ ├── elm_query.rs │ ├── elm_query_field.rs │ └── lib.rs └── rustfmt.toml /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | 3 | /elm-test/elm-stuff 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### Formatting 2 | The project uses unstable `rustfmt` settings, so the formatting has to be done with nightly. 3 | ```console 4 | cargo +nightly fmt 5 | ``` 6 | 7 | ### Testing 8 | The tests use optional dependencies, so `--all-features` is needed. The tests use `elm repl` so Elm has to be installed. 9 | 10 | ```console 11 | cargo test --all-features 12 | ``` 13 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "android-tzdata" 7 | version = "0.1.1" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 10 | 11 | [[package]] 12 | name = "android_system_properties" 13 | version = "0.1.5" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 16 | dependencies = [ 17 | "libc", 18 | ] 19 | 20 | [[package]] 21 | name = "autocfg" 22 | version = "1.4.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 25 | 26 | [[package]] 27 | name = "bumpalo" 28 | version = "3.17.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" 31 | 32 | [[package]] 33 | name = "cc" 34 | version = "1.2.16" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" 37 | dependencies = [ 38 | "shlex", 39 | ] 40 | 41 | [[package]] 42 | name = "cfg-if" 43 | version = "1.0.0" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 46 | 47 | [[package]] 48 | name = "chrono" 49 | version = "0.4.40" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" 52 | dependencies = [ 53 | "android-tzdata", 54 | "iana-time-zone", 55 | "js-sys", 56 | "num-traits", 57 | "serde", 58 | "wasm-bindgen", 59 | "windows-link", 60 | ] 61 | 62 | [[package]] 63 | name = "core-foundation-sys" 64 | version = "0.8.7" 65 | source = "registry+https://github.com/rust-lang/crates.io-index" 66 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 67 | 68 | [[package]] 69 | name = "deranged" 70 | version = "0.3.11" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 73 | dependencies = [ 74 | "powerfmt", 75 | ] 76 | 77 | [[package]] 78 | name = "elm_rs" 79 | version = "0.2.3" 80 | dependencies = [ 81 | "chrono", 82 | "elm_rs_derive", 83 | "serde", 84 | "serde_json", 85 | "time", 86 | "unescape", 87 | "uuid", 88 | ] 89 | 90 | [[package]] 91 | name = "elm_rs_derive" 92 | version = "0.2.3" 93 | dependencies = [ 94 | "heck", 95 | "proc-macro2", 96 | "quote", 97 | "syn", 98 | ] 99 | 100 | [[package]] 101 | name = "heck" 102 | version = "0.5.0" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 105 | 106 | [[package]] 107 | name = "iana-time-zone" 108 | version = "0.1.61" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" 111 | dependencies = [ 112 | "android_system_properties", 113 | "core-foundation-sys", 114 | "iana-time-zone-haiku", 115 | "js-sys", 116 | "wasm-bindgen", 117 | "windows-core", 118 | ] 119 | 120 | [[package]] 121 | name = "iana-time-zone-haiku" 122 | version = "0.1.2" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 125 | dependencies = [ 126 | "cc", 127 | ] 128 | 129 | [[package]] 130 | name = "itoa" 131 | version = "1.0.14" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 134 | 135 | [[package]] 136 | name = "js-sys" 137 | version = "0.3.77" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 140 | dependencies = [ 141 | "once_cell", 142 | "wasm-bindgen", 143 | ] 144 | 145 | [[package]] 146 | name = "libc" 147 | version = "0.2.170" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" 150 | 151 | [[package]] 152 | name = "log" 153 | version = "0.4.26" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" 156 | 157 | [[package]] 158 | name = "memchr" 159 | version = "2.7.4" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 162 | 163 | [[package]] 164 | name = "num-conv" 165 | version = "0.1.0" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 168 | 169 | [[package]] 170 | name = "num-traits" 171 | version = "0.2.19" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 174 | dependencies = [ 175 | "autocfg", 176 | ] 177 | 178 | [[package]] 179 | name = "once_cell" 180 | version = "1.20.3" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" 183 | 184 | [[package]] 185 | name = "powerfmt" 186 | version = "0.2.0" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 189 | 190 | [[package]] 191 | name = "proc-macro2" 192 | version = "1.0.93" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" 195 | dependencies = [ 196 | "unicode-ident", 197 | ] 198 | 199 | [[package]] 200 | name = "quote" 201 | version = "1.0.38" 202 | source = "registry+https://github.com/rust-lang/crates.io-index" 203 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 204 | dependencies = [ 205 | "proc-macro2", 206 | ] 207 | 208 | [[package]] 209 | name = "rustversion" 210 | version = "1.0.19" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" 213 | 214 | [[package]] 215 | name = "ryu" 216 | version = "1.0.19" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" 219 | 220 | [[package]] 221 | name = "serde" 222 | version = "1.0.218" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" 225 | dependencies = [ 226 | "serde_derive", 227 | ] 228 | 229 | [[package]] 230 | name = "serde_derive" 231 | version = "1.0.218" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" 234 | dependencies = [ 235 | "proc-macro2", 236 | "quote", 237 | "syn", 238 | ] 239 | 240 | [[package]] 241 | name = "serde_json" 242 | version = "1.0.139" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6" 245 | dependencies = [ 246 | "itoa", 247 | "memchr", 248 | "ryu", 249 | "serde", 250 | ] 251 | 252 | [[package]] 253 | name = "shlex" 254 | version = "1.3.0" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 257 | 258 | [[package]] 259 | name = "syn" 260 | version = "2.0.98" 261 | source = "registry+https://github.com/rust-lang/crates.io-index" 262 | checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" 263 | dependencies = [ 264 | "proc-macro2", 265 | "quote", 266 | "unicode-ident", 267 | ] 268 | 269 | [[package]] 270 | name = "time" 271 | version = "0.3.37" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" 274 | dependencies = [ 275 | "deranged", 276 | "num-conv", 277 | "powerfmt", 278 | "serde", 279 | "time-core", 280 | ] 281 | 282 | [[package]] 283 | name = "time-core" 284 | version = "0.1.2" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 287 | 288 | [[package]] 289 | name = "unescape" 290 | version = "0.1.0" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "ccb97dac3243214f8d8507998906ca3e2e0b900bf9bf4870477f125b82e68f6e" 293 | 294 | [[package]] 295 | name = "unicode-ident" 296 | version = "1.0.17" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" 299 | 300 | [[package]] 301 | name = "uuid" 302 | version = "1.15.1" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "e0f540e3240398cce6128b64ba83fdbdd86129c16a3aa1a3a252efd66eb3d587" 305 | dependencies = [ 306 | "serde", 307 | ] 308 | 309 | [[package]] 310 | name = "wasm-bindgen" 311 | version = "0.2.100" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 314 | dependencies = [ 315 | "cfg-if", 316 | "once_cell", 317 | "rustversion", 318 | "wasm-bindgen-macro", 319 | ] 320 | 321 | [[package]] 322 | name = "wasm-bindgen-backend" 323 | version = "0.2.100" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 326 | dependencies = [ 327 | "bumpalo", 328 | "log", 329 | "proc-macro2", 330 | "quote", 331 | "syn", 332 | "wasm-bindgen-shared", 333 | ] 334 | 335 | [[package]] 336 | name = "wasm-bindgen-macro" 337 | version = "0.2.100" 338 | source = "registry+https://github.com/rust-lang/crates.io-index" 339 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 340 | dependencies = [ 341 | "quote", 342 | "wasm-bindgen-macro-support", 343 | ] 344 | 345 | [[package]] 346 | name = "wasm-bindgen-macro-support" 347 | version = "0.2.100" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 350 | dependencies = [ 351 | "proc-macro2", 352 | "quote", 353 | "syn", 354 | "wasm-bindgen-backend", 355 | "wasm-bindgen-shared", 356 | ] 357 | 358 | [[package]] 359 | name = "wasm-bindgen-shared" 360 | version = "0.2.100" 361 | source = "registry+https://github.com/rust-lang/crates.io-index" 362 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 363 | dependencies = [ 364 | "unicode-ident", 365 | ] 366 | 367 | [[package]] 368 | name = "windows-core" 369 | version = "0.52.0" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 372 | dependencies = [ 373 | "windows-targets", 374 | ] 375 | 376 | [[package]] 377 | name = "windows-link" 378 | version = "0.1.0" 379 | source = "registry+https://github.com/rust-lang/crates.io-index" 380 | checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" 381 | 382 | [[package]] 383 | name = "windows-targets" 384 | version = "0.52.6" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 387 | dependencies = [ 388 | "windows_aarch64_gnullvm", 389 | "windows_aarch64_msvc", 390 | "windows_i686_gnu", 391 | "windows_i686_gnullvm", 392 | "windows_i686_msvc", 393 | "windows_x86_64_gnu", 394 | "windows_x86_64_gnullvm", 395 | "windows_x86_64_msvc", 396 | ] 397 | 398 | [[package]] 399 | name = "windows_aarch64_gnullvm" 400 | version = "0.52.6" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 403 | 404 | [[package]] 405 | name = "windows_aarch64_msvc" 406 | version = "0.52.6" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 409 | 410 | [[package]] 411 | name = "windows_i686_gnu" 412 | version = "0.52.6" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 415 | 416 | [[package]] 417 | name = "windows_i686_gnullvm" 418 | version = "0.52.6" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 421 | 422 | [[package]] 423 | name = "windows_i686_msvc" 424 | version = "0.52.6" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 427 | 428 | [[package]] 429 | name = "windows_x86_64_gnu" 430 | version = "0.52.6" 431 | source = "registry+https://github.com/rust-lang/crates.io-index" 432 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 433 | 434 | [[package]] 435 | name = "windows_x86_64_gnullvm" 436 | version = "0.52.6" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 439 | 440 | [[package]] 441 | name = "windows_x86_64_msvc" 442 | version = "0.52.6" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 445 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "elm_rs", 4 | "elm_rs_derive" 5 | ] 6 | resolver = "2" 7 | 8 | 9 | [patch.crates-io] 10 | elm_rs = { path = "elm_rs" } 11 | elm_rs_derive = { path = "elm_rs_derive" } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at https://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # elm_rs 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/elm_rs)](https://crates.io/crates/elm_rs) 4 | [![docs.rs](https://img.shields.io/badge/docs.rs-elm__rs-success)](https://docs.rs/elm_rs) 5 | [![Crates.io](https://img.shields.io/crates/l/elm_rs)](https://choosealicense.com/licenses/mpl-2.0/) 6 | [![GitHub](https://img.shields.io/badge/GitHub-Heliozoa-24292f)](https://github.com/Heliozoa/elm_rs) 7 | 8 | Automatically generate type definitions and functions for your Elm frontend from your Rust backend types, making it easy to keep the two in sync. Currently supports generating 9 | - Elm types with the `Elm` trait and derive macro 10 | - JSON encoders with the `ElmEncode` trait and derive macro, compatible with `serde_json` 11 | - JSON decoders with the `ElmDecode` trait and derive macro, compatible with `serde_json` 12 | - URL query encoders with the `ElmQuery` and `ElmQueryField` traits and derive macros 13 | 14 | ## Usage 15 | For example, the following code 16 | ```rust 17 | use elm_rs::{Elm, ElmEncode, ElmDecode, ElmQuery, ElmQueryField}; 18 | 19 | #[derive(Elm, ElmEncode, ElmDecode)] 20 | enum Filetype { 21 | Jpeg, 22 | Png, 23 | } 24 | 25 | #[derive(Elm, ElmEncode, ElmDecode)] 26 | struct Drawing { 27 | title: String, 28 | authors: Vec, 29 | filename: String, 30 | filetype: Filetype, 31 | } 32 | 33 | #[derive(Elm, ElmQuery)] 34 | struct Query { 35 | page: usize, 36 | thumbnail_size: Size, 37 | } 38 | 39 | #[derive(Elm, ElmQueryField)] 40 | enum Size { 41 | Small, 42 | Large, 43 | } 44 | 45 | fn main() { 46 | // the target would typically be a file 47 | let mut target = vec![]; 48 | // elm_rs provides a macro for conveniently creating an Elm module with everything needed 49 | elm_rs::export!("Bindings", &mut target, { 50 | // generates types and encoders for types implementing ElmEncoder 51 | encoders: [Filetype, Drawing], 52 | // generates types and decoders for types implementing ElmDecoder 53 | decoders: [Filetype, Drawing], 54 | // generates types and functions for forming queries for types implementing ElmQuery 55 | queries: [Query], 56 | // generates types and functions for forming queries for types implementing ElmQueryField 57 | query_fields: [Size], 58 | }).unwrap(); 59 | let output = String::from_utf8(target).unwrap(); 60 | println!("{}", output); 61 | } 62 | ``` 63 | prints out 64 | ```elm 65 | 66 | -- generated by elm_rs 67 | 68 | 69 | module Bindings exposing (..) 70 | 71 | import Dict exposing (Dict) 72 | import Http 73 | import Json.Decode 74 | import Json.Encode 75 | import Url.Builder 76 | 77 | 78 | resultEncoder : (e -> Json.Encode.Value) -> (t -> Json.Encode.Value) -> (Result e t -> Json.Encode.Value) 79 | resultEncoder errEncoder okEncoder enum = 80 | case enum of 81 | Ok inner -> 82 | Json.Encode.object [ ( "Ok", okEncoder inner ) ] 83 | Err inner -> 84 | Json.Encode.object [ ( "Err", errEncoder inner ) ] 85 | 86 | 87 | resultDecoder : Json.Decode.Decoder e -> Json.Decode.Decoder t -> Json.Decode.Decoder (Result e t) 88 | resultDecoder errDecoder okDecoder = 89 | Json.Decode.oneOf 90 | [ Json.Decode.map Ok (Json.Decode.field "Ok" okDecoder) 91 | , Json.Decode.map Err (Json.Decode.field "Err" errDecoder) 92 | ] 93 | 94 | 95 | type Filetype 96 | = Jpeg 97 | | Png 98 | 99 | 100 | filetypeEncoder : Filetype -> Json.Encode.Value 101 | filetypeEncoder enum = 102 | case enum of 103 | Jpeg -> 104 | Json.Encode.string "Jpeg" 105 | Png -> 106 | Json.Encode.string "Png" 107 | 108 | type alias Drawing = 109 | { title : String 110 | , authors : List (String) 111 | , filename : String 112 | , filetype : Filetype 113 | } 114 | 115 | 116 | drawingEncoder : Drawing -> Json.Encode.Value 117 | drawingEncoder struct = 118 | Json.Encode.object 119 | [ ( "title", (Json.Encode.string) struct.title ) 120 | , ( "authors", (Json.Encode.list (Json.Encode.string)) struct.authors ) 121 | , ( "filename", (Json.Encode.string) struct.filename ) 122 | , ( "filetype", (filetypeEncoder) struct.filetype ) 123 | ] 124 | 125 | 126 | filetypeDecoder : Json.Decode.Decoder Filetype 127 | filetypeDecoder = 128 | Json.Decode.oneOf 129 | [ Json.Decode.string 130 | |> Json.Decode.andThen 131 | (\x -> 132 | case x of 133 | "Jpeg" -> 134 | Json.Decode.succeed Jpeg 135 | unexpected -> 136 | Json.Decode.fail <| "Unexpected variant " ++ unexpected 137 | ) 138 | , Json.Decode.string 139 | |> Json.Decode.andThen 140 | (\x -> 141 | case x of 142 | "Png" -> 143 | Json.Decode.succeed Png 144 | unexpected -> 145 | Json.Decode.fail <| "Unexpected variant " ++ unexpected 146 | ) 147 | ] 148 | 149 | drawingDecoder : Json.Decode.Decoder Drawing 150 | drawingDecoder = 151 | Json.Decode.succeed Drawing 152 | |> Json.Decode.andThen (\x -> Json.Decode.map x (Json.Decode.field "title" (Json.Decode.string))) 153 | |> Json.Decode.andThen (\x -> Json.Decode.map x (Json.Decode.field "authors" (Json.Decode.list (Json.Decode.string)))) 154 | |> Json.Decode.andThen (\x -> Json.Decode.map x (Json.Decode.field "filename" (Json.Decode.string))) 155 | |> Json.Decode.andThen (\x -> Json.Decode.map x (Json.Decode.field "filetype" (filetypeDecoder))) 156 | 157 | 158 | type alias Query = 159 | { page : Int 160 | , thumbnailSize : Size 161 | } 162 | 163 | 164 | urlEncodeQuery : Query -> List Url.Builder.QueryParameter 165 | urlEncodeQuery struct = 166 | [ Url.Builder.int "page" (identity struct.page), Url.Builder.string "thumbnail_size" (queryFieldEncoderSize struct.thumbnailSize) ] 167 | 168 | 169 | type Size 170 | = Small 171 | | Large 172 | 173 | 174 | queryFieldEncoderSize : Size -> String 175 | queryFieldEncoderSize var = 176 | case var of 177 | Small -> "Small" 178 | Large -> "Large" 179 | 180 | ``` 181 | 182 | ## Functionality 183 | 184 | ### Cargo features 185 | - `derive`: Activated by default. Enables deriving the `Elm` and `ElmEncode` traits. 186 | - `serde`: Enables compatibility with many of serde's attributes. (`serde v1`) 187 | - `chrono`: Trait implementations for chrono types. (`chrono v0.4`) 188 | - `time`: Trait implementations for time types. (`time v0.3`) 189 | - `uuid`: Trait implementations for uuid types. (`uuid v1`) 190 | 191 | ### Serde compatibility 192 | The `serde` feature enables compatibility with serde attributes. Currently the following attributes are supported: 193 | 194 | #### Container attributes 195 | - rename_all 196 | - tag 197 | - tag & content 198 | - untagged 199 | - transparent 200 | 201 | #### Variant attributes 202 | - rename 203 | - rename_all 204 | - skip 205 | - other 206 | 207 | #### Field attributes 208 | - rename 209 | - skip 210 | 211 | ### 0.2.0 212 | - [x] Generate Elm types with the `Elm` trait and derive macro 213 | - [x] Generate JSON encoders and decoders with the `ElmEncode` and `ElmDecode` traits and derive macros 214 | - [x] Basic generic support 215 | - [x] Compatibility with most serde attributes 216 | - [x] Support for simple queries 217 | 218 | ### Planned 219 | - [ ] Support for forms and complex queries 220 | - [ ] Compatibility with more serde attributes 221 | - [ ] flatten 222 | - [ ] alias 223 | - [ ] skip_(de)serializing 224 | - [ ] Optionally include definitions for the dependencies of exported types 225 | - [ ] Implement support for more `serde::{Deserialize, Serialize}` std types 226 | - [ ] IpAddr, Ipv4Addr, Ipv6Addr 227 | - [ ] SocketAddr, SocketAddrV4, SocketAddrV6 228 | - [ ] PhantomData 229 | - [ ] Handle recursive types 230 | - [ ] Attributes for controlling the name of the Elm type etc. 231 | 232 | ### Known limitations 233 | Generic types are not well supported when they are used with more than one set of concrete types. For example, for 234 | ```rust 235 | struct Generic(T); 236 | ``` 237 | `Generic::::elm_definition()` and `Generic::::elm_definition()` will both use the name `Generic` for the Elm definition, causing an error in Elm. Accidentally using different generic types for the generated JSON and the Elm definition can also result in some confusing error messages. 238 | 239 | Reusing enum variant names is allowed in Rust but not in Elm. Therefore generating definitions for the two enums 240 | ```rust 241 | enum Enum1 { 242 | Variant 243 | } 244 | enum Enum2 { 245 | Variant 246 | } 247 | ``` 248 | will cause an error in Elm due to `Variant` being ambiguous. 249 | 250 | ## Alternatives 251 | 252 | - Generate an OpenAPI spec from Rust with something like https://crates.io/crates/okapi and generate Elm code from the spec with something like https://openapi-generator.tech/. 253 | 254 | ## License 255 | Licensed under the Mozilla Public License Version 2.0. 256 | -------------------------------------------------------------------------------- /elm-test/README.md: -------------------------------------------------------------------------------- 1 | This project is used to provide Elm dependencies during `elm_rs` tests. 2 | -------------------------------------------------------------------------------- /elm-test/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "elm/browser": "1.0.2", 10 | "elm/core": "1.0.5", 11 | "elm/html": "1.0.0", 12 | "elm/json": "1.1.3", 13 | "elm/url": "1.0.0" 14 | }, 15 | "indirect": { 16 | "elm/time": "1.0.0", 17 | "elm/virtual-dom": "1.0.2" 18 | } 19 | }, 20 | "test-dependencies": { 21 | "direct": {}, 22 | "indirect": {} 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /elm-test/src/Main.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (..) 2 | -------------------------------------------------------------------------------- /elm_rs/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "elm_rs" 3 | version = "0.2.3" 4 | authors = ["Heliozoa "] 5 | edition = "2021" 6 | rust-version = "1.56" 7 | description = "Generate Elm bindings for your Rust types" 8 | readme = "README.md" 9 | repository = "https://github.com/Heliozoa/elm_rs" 10 | license = "MPL-2.0" 11 | keywords = ["elm", "bindings"] 12 | categories = ["development-tools::ffi", "web-programming"] 13 | resolver = "2" 14 | 15 | [features] 16 | default = ["derive", "serde"] 17 | derive = ["elm_rs_derive", "elm_rs_derive/json", "elm_rs_derive/query"] 18 | serde = ["elm_rs_derive/serde"] 19 | 20 | [dependencies] 21 | elm_rs_derive = { version = "0.2.2", optional = true } 22 | 23 | # optional 24 | chrono = { version = "0.4.19", optional = true } 25 | time = { version = "0.3.13", optional = true } 26 | uuid = { version = "1.1.2", optional = true } 27 | 28 | [dev-dependencies] 29 | chrono = { version = "0.4.19", features = ["serde"] } 30 | serde = { version = "1.0.136", features = ["derive", "rc"] } 31 | serde_json = { version = "1.0.78" } 32 | unescape = "0.1.0" 33 | uuid = { version = "1.1.2", features = ["serde"] } 34 | 35 | [package.metadata.docs.rs] 36 | all-features = true 37 | -------------------------------------------------------------------------------- /elm_rs/README.md: -------------------------------------------------------------------------------- 1 | # elm_rs 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/elm_rs)](https://crates.io/crates/elm_rs) 4 | [![docs.rs](https://img.shields.io/badge/docs.rs-elm_rs-success)](https://docs.rs/elm_rs) 5 | [![Crates.io](https://img.shields.io/crates/l/elm_rs)](https://choosealicense.com/licenses/mpl-2.0/) 6 | [![GitHub](https://img.shields.io/badge/GitHub-Heliozoa-24292f)](https://github.com/Heliozoa/elm_rs) 7 | 8 | Automatically generate type definitions and functions for your Elm frontend from your Rust backend types, making it easy to keep the two in sync. Currently supports generating 9 | - Elm types with the `Elm` trait and derive macro 10 | - JSON encoders with the `ElmEncode` trait and derive macro, compatible with `serde_json` 11 | - JSON decoders with the `ElmDecode` trait and derive macro, compatible with `serde_json` 12 | - URL query encoders with the `ElmQuery` and `ElmQueryField` traits and derive macros 13 | 14 | ## Usage 15 | For example, the following code 16 | ```rust 17 | use elm_rs::{Elm, ElmEncode, ElmDecode, ElmQuery, ElmQueryField}; 18 | 19 | #[derive(Elm, ElmEncode, ElmDecode)] 20 | enum Filetype { 21 | Jpeg, 22 | Png, 23 | } 24 | 25 | #[derive(Elm, ElmEncode, ElmDecode)] 26 | struct Drawing { 27 | title: String, 28 | authors: Vec, 29 | filename: String, 30 | filetype: Filetype, 31 | } 32 | 33 | #[derive(Elm, ElmQuery)] 34 | struct Query { 35 | page: usize, 36 | thumbnail_size: Size, 37 | } 38 | 39 | #[derive(Elm, ElmQueryField)] 40 | enum Size { 41 | Small, 42 | Large, 43 | } 44 | 45 | fn main() { 46 | // the target would typically be a file 47 | let mut target = vec![]; 48 | // elm_rs provides a macro for conveniently creating an Elm module with everything needed 49 | elm_rs::export!("Bindings", &mut target, { 50 | // generates types and encoders for types implementing ElmEncoder 51 | encoders: [Filetype, Drawing], 52 | // generates types and decoders for types implementing ElmDecoder 53 | decoders: [Filetype, Drawing], 54 | // generates types and functions for forming queries for types implementing ElmQuery 55 | queries: [Query], 56 | // generates types and functions for forming queries for types implementing ElmQueryField 57 | query_fields: [Size], 58 | }).unwrap(); 59 | let output = String::from_utf8(target).unwrap(); 60 | println!("{}", output); 61 | } 62 | ``` 63 | prints out 64 | ```elm 65 | 66 | -- generated by elm_rs 67 | 68 | 69 | module Bindings exposing (..) 70 | 71 | import Dict exposing (Dict) 72 | import Http 73 | import Json.Decode 74 | import Json.Encode 75 | import Url.Builder 76 | 77 | 78 | resultEncoder : (e -> Json.Encode.Value) -> (t -> Json.Encode.Value) -> (Result e t -> Json.Encode.Value) 79 | resultEncoder errEncoder okEncoder enum = 80 | case enum of 81 | Ok inner -> 82 | Json.Encode.object [ ( "Ok", okEncoder inner ) ] 83 | Err inner -> 84 | Json.Encode.object [ ( "Err", errEncoder inner ) ] 85 | 86 | 87 | resultDecoder : Json.Decode.Decoder e -> Json.Decode.Decoder t -> Json.Decode.Decoder (Result e t) 88 | resultDecoder errDecoder okDecoder = 89 | Json.Decode.oneOf 90 | [ Json.Decode.map Ok (Json.Decode.field "Ok" okDecoder) 91 | , Json.Decode.map Err (Json.Decode.field "Err" errDecoder) 92 | ] 93 | 94 | 95 | type Filetype 96 | = Jpeg 97 | | Png 98 | 99 | 100 | filetypeEncoder : Filetype -> Json.Encode.Value 101 | filetypeEncoder enum = 102 | case enum of 103 | Jpeg -> 104 | Json.Encode.string "Jpeg" 105 | Png -> 106 | Json.Encode.string "Png" 107 | 108 | type alias Drawing = 109 | { title : String 110 | , authors : List (String) 111 | , filename : String 112 | , filetype : Filetype 113 | } 114 | 115 | 116 | drawingEncoder : Drawing -> Json.Encode.Value 117 | drawingEncoder struct = 118 | Json.Encode.object 119 | [ ( "title", (Json.Encode.string) struct.title ) 120 | , ( "authors", (Json.Encode.list (Json.Encode.string)) struct.authors ) 121 | , ( "filename", (Json.Encode.string) struct.filename ) 122 | , ( "filetype", (filetypeEncoder) struct.filetype ) 123 | ] 124 | 125 | 126 | filetypeDecoder : Json.Decode.Decoder Filetype 127 | filetypeDecoder = 128 | Json.Decode.oneOf 129 | [ Json.Decode.string 130 | |> Json.Decode.andThen 131 | (\x -> 132 | case x of 133 | "Jpeg" -> 134 | Json.Decode.succeed Jpeg 135 | unexpected -> 136 | Json.Decode.fail <| "Unexpected variant " ++ unexpected 137 | ) 138 | , Json.Decode.string 139 | |> Json.Decode.andThen 140 | (\x -> 141 | case x of 142 | "Png" -> 143 | Json.Decode.succeed Png 144 | unexpected -> 145 | Json.Decode.fail <| "Unexpected variant " ++ unexpected 146 | ) 147 | ] 148 | 149 | drawingDecoder : Json.Decode.Decoder Drawing 150 | drawingDecoder = 151 | Json.Decode.succeed Drawing 152 | |> Json.Decode.andThen (\x -> Json.Decode.map x (Json.Decode.field "title" (Json.Decode.string))) 153 | |> Json.Decode.andThen (\x -> Json.Decode.map x (Json.Decode.field "authors" (Json.Decode.list (Json.Decode.string)))) 154 | |> Json.Decode.andThen (\x -> Json.Decode.map x (Json.Decode.field "filename" (Json.Decode.string))) 155 | |> Json.Decode.andThen (\x -> Json.Decode.map x (Json.Decode.field "filetype" (filetypeDecoder))) 156 | 157 | 158 | type alias Query = 159 | { page : Int 160 | , thumbnailSize : Size 161 | } 162 | 163 | 164 | urlEncodeQuery : Query -> List Url.Builder.QueryParameter 165 | urlEncodeQuery struct = 166 | [ Url.Builder.int "page" (identity struct.page), Url.Builder.string "thumbnail_size" (queryFieldEncoderSize struct.thumbnailSize) ] 167 | 168 | 169 | type Size 170 | = Small 171 | | Large 172 | 173 | 174 | queryFieldEncoderSize : Size -> String 175 | queryFieldEncoderSize var = 176 | case var of 177 | Small -> "Small" 178 | Large -> "Large" 179 | 180 | ``` 181 | 182 | ## Functionality 183 | 184 | ### Cargo features 185 | - `derive`: Activated by default. Enables deriving the `Elm` and `ElmEncode` traits. 186 | - `serde`: Enables compatibility with many of serde's attributes. (`serde v1`) 187 | - `chrono`: Trait implementations for chrono types. (`chrono v0.4`) 188 | - `time`: Trait implementations for time types. (`time v0.3`) 189 | - `uuid`: Trait implementations for uuid types. (`uuid v1`) 190 | 191 | ### Serde compatibility 192 | The `serde` feature enables compatibility with serde attributes. Currently the following attributes are supported: 193 | 194 | #### Container attributes 195 | - rename_all 196 | - tag 197 | - tag & content 198 | - untagged 199 | - transparent 200 | 201 | #### Variant attributes 202 | - rename 203 | - rename_all 204 | - skip 205 | - other 206 | 207 | #### Field attributes 208 | - rename 209 | - skip 210 | 211 | ### 0.2.0 212 | - [x] Generate Elm types with the `Elm` trait and derive macro 213 | - [x] Generate JSON encoders and decoders with the `ElmEncode` and `ElmDecode` traits and derive macros 214 | - [x] Basic generic support 215 | - [x] Compatibility with most serde attributes 216 | - [x] Support for simple queries 217 | 218 | ### Planned 219 | - [ ] Support for forms and complex queries 220 | - [ ] Compatibility with more serde attributes 221 | - [ ] flatten 222 | - [ ] alias 223 | - [ ] skip_(de)serializing 224 | - [ ] Optionally include definitions for the dependencies of exported types 225 | - [ ] Implement support for more `serde::{Deserialize, Serialize}` std types 226 | - [ ] IpAddr, Ipv4Addr, Ipv6Addr 227 | - [ ] SocketAddr, SocketAddrV4, SocketAddrV6 228 | - [ ] PhantomData 229 | - [ ] Handle recursive types 230 | - [ ] Attributes for controlling the name of the Elm type etc. 231 | 232 | ### Known limitations 233 | Generic types are not well supported when they are used with more than one set of concrete types. For example, for 234 | ```rust 235 | struct Generic(T); 236 | ``` 237 | `Generic::::elm_definition()` and `Generic::::elm_definition()` will both use the name `Generic` for the Elm definition, causing an error in Elm. Accidentally using different generic types for the generated JSON and the Elm definition can also result in some confusing error messages. 238 | 239 | Reusing enum variant names is allowed in Rust but not in Elm. Therefore generating definitions for the two enums 240 | ```rust 241 | enum Enum1 { 242 | Variant 243 | } 244 | enum Enum2 { 245 | Variant 246 | } 247 | ``` 248 | will cause an error in Elm due to `Variant` being ambiguous. 249 | 250 | ## Alternatives 251 | 252 | - Generate an OpenAPI spec from Rust with something like https://crates.io/crates/okapi and generate Elm code from the spec with something like https://openapi-generator.tech/. 253 | 254 | ## License 255 | Licensed under Mozilla Public License Version 2.0 256 | -------------------------------------------------------------------------------- /elm_rs/examples/example.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | 3 | use elm_rs::{Elm, ElmDecode, ElmEncode, ElmQuery, ElmQueryField}; 4 | 5 | #[derive(Elm, ElmEncode, ElmDecode)] 6 | enum Filetype { 7 | Jpeg, 8 | Png, 9 | } 10 | 11 | #[derive(Elm, ElmEncode, ElmDecode)] 12 | struct Drawing { 13 | title: String, 14 | authors: Vec, 15 | filename: String, 16 | filetype: Filetype, 17 | } 18 | 19 | #[derive(Elm, ElmQuery)] 20 | struct Query { 21 | page: usize, 22 | thumbnail_size: Size, 23 | } 24 | 25 | #[derive(Elm, ElmQueryField)] 26 | enum Size { 27 | Small, 28 | Large, 29 | } 30 | 31 | fn main() { 32 | // the target would typically be a file 33 | let mut target = vec![]; 34 | // elm_rs provides a macro for conveniently creating an Elm module with everything needed 35 | elm_rs::export!("Bindings", &mut target, { 36 | // generates types and encoders for types implementing ElmEncoder 37 | encoders: [Filetype, Drawing], 38 | // generates types and decoders for types implementing ElmDecoder 39 | decoders: [Filetype, Drawing], 40 | // generates types and functions for forming queries for types implementing ElmQuery 41 | queries: [Query], 42 | // generates types and functions for forming queries for types implementing ElmQueryField 43 | query_fields: [Size], 44 | }) 45 | .unwrap(); 46 | let output = String::from_utf8(target).unwrap(); 47 | println!("{}", output); 48 | } 49 | -------------------------------------------------------------------------------- /elm_rs/src/elm.rs: -------------------------------------------------------------------------------- 1 | //! Contains the `Elm` trait. 2 | 3 | #[cfg(feature = "derive")] 4 | pub use elm_rs_derive::Elm; 5 | 6 | /// Used to represent Rust types in Elm. 7 | pub trait Elm { 8 | /// The name of the type in Elm. 9 | fn elm_type() -> String; 10 | /// The definition of the type in Elm. None for types already defined in Elm. 11 | fn elm_definition() -> Option; 12 | } 13 | 14 | impl Elm for (T,) 15 | where 16 | T: Elm, 17 | { 18 | fn elm_type() -> String { 19 | T::elm_type() 20 | } 21 | 22 | fn elm_definition() -> Option { 23 | None 24 | } 25 | } 26 | 27 | impl Elm for (T, U) 28 | where 29 | T: Elm, 30 | U: Elm, 31 | { 32 | fn elm_type() -> String { 33 | ::std::format!("( {}, {} )", T::elm_type(), U::elm_type()) 34 | } 35 | 36 | fn elm_definition() -> Option { 37 | None 38 | } 39 | } 40 | 41 | impl Elm for (T, U, V) 42 | where 43 | T: Elm, 44 | U: Elm, 45 | V: Elm, 46 | { 47 | fn elm_type() -> String { 48 | ::std::format!( 49 | "( {}, {}, {} )", 50 | T::elm_type(), 51 | U::elm_type(), 52 | V::elm_type() 53 | ) 54 | } 55 | 56 | fn elm_definition() -> Option { 57 | None 58 | } 59 | } 60 | 61 | impl Elm for std::borrow::Cow<'_, T> { 62 | fn elm_type() -> String { 63 | T::elm_type() 64 | } 65 | 66 | fn elm_definition() -> Option { 67 | T::elm_definition() 68 | } 69 | } 70 | 71 | impl Elm for [T; U] 72 | where 73 | T: Elm, 74 | { 75 | fn elm_type() -> String { 76 | <[T]>::elm_type() 77 | } 78 | 79 | fn elm_definition() -> Option { 80 | <[T]>::elm_definition() 81 | } 82 | } 83 | 84 | impl Elm for std::time::Duration { 85 | fn elm_type() -> String { 86 | "Duration".to_string() 87 | } 88 | 89 | fn elm_definition() -> Option { 90 | Some( 91 | "\ 92 | type alias Duration = 93 | { secs : Int 94 | , nanos : Int 95 | } 96 | " 97 | .to_string(), 98 | ) 99 | } 100 | } 101 | 102 | impl Elm for Result { 103 | fn elm_type() -> String { 104 | ::std::format!("Result ({}) ({})", E::elm_type(), T::elm_type()) 105 | } 106 | 107 | fn elm_definition() -> Option { 108 | None 109 | } 110 | } 111 | 112 | impl Elm for std::time::SystemTime { 113 | fn elm_type() -> String { 114 | "SystemTime".to_string() 115 | } 116 | 117 | fn elm_definition() -> Option { 118 | Some( 119 | "\ 120 | type alias SystemTime = 121 | { secs_since_epoch : Int 122 | , nanos_since_epoch : Int 123 | } 124 | " 125 | .to_string(), 126 | ) 127 | } 128 | } 129 | 130 | macro_rules! impl_builtin { 131 | ($rust_type: ty, $elm_type: expr) => { 132 | impl Elm for $rust_type { 133 | fn elm_type() -> String { 134 | $elm_type.to_string() 135 | } 136 | 137 | fn elm_definition() -> Option { 138 | None 139 | } 140 | } 141 | }; 142 | } 143 | 144 | macro_rules! impl_builtin_container { 145 | ($rust_type: ty, $elm_name: expr) => { 146 | impl Elm for $rust_type { 147 | fn elm_type() -> String { 148 | ::std::format!("{} ({})", $elm_name, T::elm_type()) 149 | } 150 | 151 | fn elm_definition() -> Option { 152 | None 153 | } 154 | } 155 | }; 156 | } 157 | 158 | macro_rules! impl_builtin_map { 159 | ($rust_type: ty) => { 160 | impl Elm for $rust_type { 161 | fn elm_type() -> String { 162 | ::std::format!("Dict String ({})", T::elm_type()) 163 | } 164 | 165 | fn elm_definition() -> Option { 166 | None 167 | } 168 | } 169 | }; 170 | } 171 | 172 | macro_rules! impl_builtin_ptr { 173 | ($rust_type: ty) => { 174 | impl Elm for $rust_type { 175 | fn elm_type() -> String { 176 | ::std::format!("{}", T::elm_type()) 177 | } 178 | 179 | fn elm_definition() -> Option { 180 | T::elm_definition() 181 | } 182 | } 183 | }; 184 | } 185 | 186 | impl_builtin!((), "()"); 187 | impl_builtin_ptr!(&'_ T); 188 | impl_builtin_ptr!(&'_ mut T); 189 | impl_builtin_ptr!(std::sync::Arc); 190 | impl_builtin!(std::sync::atomic::AtomicBool, "Bool"); 191 | impl_builtin!(std::sync::atomic::AtomicU8, "Int"); 192 | impl_builtin!(std::sync::atomic::AtomicU16, "Int"); 193 | impl_builtin!(std::sync::atomic::AtomicU32, "Int"); 194 | impl_builtin!(std::sync::atomic::AtomicU64, "Int"); 195 | impl_builtin!(std::sync::atomic::AtomicUsize, "Int"); 196 | impl_builtin!(std::sync::atomic::AtomicI8, "Int"); 197 | impl_builtin!(std::sync::atomic::AtomicI16, "Int"); 198 | impl_builtin!(std::sync::atomic::AtomicI32, "Int"); 199 | impl_builtin!(std::sync::atomic::AtomicI64, "Int"); 200 | impl_builtin!(std::sync::atomic::AtomicIsize, "Int"); 201 | impl_builtin_map!(std::collections::BTreeMap); 202 | impl_builtin_container!(std::collections::BTreeSet, "List"); 203 | impl_builtin_ptr!(Box); 204 | impl_builtin_ptr!(std::cell::Cell); 205 | impl_builtin_map!(std::collections::HashMap); 206 | impl_builtin_container!(std::collections::HashSet, "List"); 207 | // todo ipaddrs 208 | impl_builtin_container!(std::collections::LinkedList, "List"); 209 | impl_builtin_ptr!(std::sync::Mutex); 210 | impl_builtin!(std::num::NonZeroU8, "Int"); 211 | impl_builtin!(std::num::NonZeroU16, "Int"); 212 | impl_builtin!(std::num::NonZeroU32, "Int"); 213 | impl_builtin!(std::num::NonZeroU64, "Int"); 214 | impl_builtin!(std::num::NonZeroU128, "Int"); 215 | impl_builtin!(std::num::NonZeroUsize, "Int"); 216 | impl_builtin!(std::num::NonZeroI8, "Int"); 217 | impl_builtin!(std::num::NonZeroI16, "Int"); 218 | impl_builtin!(std::num::NonZeroI32, "Int"); 219 | impl_builtin!(std::num::NonZeroI64, "Int"); 220 | impl_builtin!(std::num::NonZeroI128, "Int"); 221 | impl_builtin!(std::num::NonZeroIsize, "Int"); 222 | 223 | impl_builtin_container!(Option, "Maybe"); 224 | impl_builtin!(std::path::Path, "String"); 225 | impl_builtin!(std::path::PathBuf, "String"); 226 | // todo phantomdata 227 | impl_builtin_ptr!(std::rc::Rc); 228 | impl_builtin_ptr!(std::cell::RefCell); 229 | impl_builtin_ptr!(std::sync::RwLock); 230 | // todo socketaddrs 231 | impl_builtin!(String, "String"); 232 | impl_builtin_container!(Vec, "List"); 233 | impl_builtin_container!([T], "List"); 234 | impl_builtin!(bool, "Bool"); 235 | impl_builtin!(u8, "Int"); 236 | impl_builtin!(u16, "Int"); 237 | impl_builtin!(u32, "Int"); 238 | impl_builtin!(u64, "Int"); 239 | impl_builtin!(u128, "Int"); 240 | impl_builtin!(usize, "Int"); 241 | impl_builtin!(i8, "Int"); 242 | impl_builtin!(i16, "Int"); 243 | impl_builtin!(i32, "Int"); 244 | impl_builtin!(i64, "Int"); 245 | impl_builtin!(i128, "Int"); 246 | impl_builtin!(isize, "Int"); 247 | impl_builtin!(f32, "Float"); 248 | impl_builtin!(f64, "Float"); 249 | impl_builtin!(str, "String"); 250 | 251 | #[cfg(feature = "uuid")] 252 | impl_builtin!(uuid::Uuid, "String"); 253 | 254 | #[cfg(feature = "chrono")] 255 | impl_builtin!(chrono::NaiveTime, "String"); 256 | #[cfg(feature = "chrono")] 257 | impl_builtin!(chrono::NaiveDate, "String"); 258 | #[cfg(feature = "chrono")] 259 | impl_builtin!(chrono::NaiveDateTime, "String"); 260 | #[cfg(feature = "chrono")] 261 | impl Elm for chrono::DateTime { 262 | fn elm_type() -> String { 263 | String::elm_type() 264 | } 265 | 266 | fn elm_definition() -> Option { 267 | String::elm_definition() 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /elm_rs/src/elm_decode.rs: -------------------------------------------------------------------------------- 1 | //! Contains the `ElmDecode` trait. 2 | 3 | use crate::Elm; 4 | #[cfg(feature = "derive")] 5 | pub use elm_rs_derive::ElmDecode; 6 | 7 | /// Used to generate JSON decoders for our Rust types in Elm. 8 | pub trait ElmDecode { 9 | /// The name of the decoder in Elm. 10 | fn decoder_type() -> String; 11 | /// The decoder function in Elm. None for decoders in Json.Decode. 12 | fn decoder_definition() -> Option; 13 | } 14 | 15 | impl ElmDecode for () { 16 | fn decoder_type() -> String { 17 | "Json.Decode.null ()".to_string() 18 | } 19 | 20 | fn decoder_definition() -> Option { 21 | None 22 | } 23 | } 24 | 25 | impl ElmDecode for (T,) 26 | where 27 | T: Elm + ElmDecode, 28 | { 29 | fn decoder_type() -> String { 30 | ::std::format!("(Json.Decode.index 0 ({}))", T::decoder_type()) 31 | } 32 | 33 | fn decoder_definition() -> Option { 34 | None 35 | } 36 | } 37 | 38 | impl ElmDecode for (T, U) 39 | where 40 | T: Elm + ElmDecode, 41 | U: Elm + ElmDecode, 42 | { 43 | fn decoder_type() -> String { 44 | ::std::format!( 45 | "Json.Decode.map2 (\\a b -> ( a, b )) (Json.Decode.index 0 ({})) (Json.Decode.index 1 ({}))", 46 | T::decoder_type(), 47 | U::decoder_type() 48 | ) 49 | } 50 | 51 | fn decoder_definition() -> Option { 52 | None 53 | } 54 | } 55 | 56 | impl ElmDecode for (T, U, V) 57 | where 58 | T: Elm + ElmDecode, 59 | U: Elm + ElmDecode, 60 | V: Elm + ElmDecode, 61 | { 62 | fn decoder_type() -> String { 63 | ::std::format!( 64 | "Json.Decode.map3 (\\a b c -> ( a, b, c )) (Json.Decode.index 0 ({})) (Json.Decode.index 1 ({})) (Json.Decode.index 2 ({}))", 65 | T::decoder_type(), 66 | U::decoder_type(), 67 | V::decoder_type(), 68 | ) 69 | } 70 | 71 | fn decoder_definition() -> Option { 72 | None 73 | } 74 | } 75 | 76 | impl ElmDecode for std::borrow::Cow<'_, T> { 77 | fn decoder_type() -> String { 78 | T::decoder_type() 79 | } 80 | 81 | fn decoder_definition() -> Option { 82 | T::decoder_definition() 83 | } 84 | } 85 | 86 | impl ElmDecode for [T; U] 87 | where 88 | T: Elm + ElmDecode, 89 | { 90 | fn decoder_type() -> String { 91 | <[T]>::decoder_type() 92 | } 93 | 94 | fn decoder_definition() -> Option { 95 | <[T]>::decoder_definition() 96 | } 97 | } 98 | 99 | impl ElmDecode for std::time::Duration { 100 | fn decoder_type() -> String { 101 | "durationDecoder".to_string() 102 | } 103 | 104 | fn decoder_definition() -> Option { 105 | Some( 106 | r#"durationDecoder : Json.Decode.Decoder Duration 107 | durationDecoder = 108 | Json.Decode.succeed Duration 109 | |> Json.Decode.andThen (\x -> Json.Decode.map x (Json.Decode.field "secs" (Json.Decode.int))) 110 | |> Json.Decode.andThen (\x -> Json.Decode.map x (Json.Decode.field "nanos" (Json.Decode.int))) 111 | "# 112 | .to_string(), 113 | ) 114 | } 115 | } 116 | 117 | impl ElmDecode for Result { 118 | fn decoder_type() -> String { 119 | format!( 120 | "resultDecoder ({}) ({})", 121 | E::decoder_type(), 122 | T::decoder_type() 123 | ) 124 | } 125 | 126 | fn decoder_definition() -> Option { 127 | Some(r#"resultDecoder : Json.Decode.Decoder e -> Json.Decode.Decoder t -> Json.Decode.Decoder (Result e t) 128 | resultDecoder errDecoder okDecoder = 129 | Json.Decode.oneOf 130 | [ Json.Decode.map Ok (Json.Decode.field "Ok" okDecoder) 131 | , Json.Decode.map Err (Json.Decode.field "Err" errDecoder) 132 | ]"# 133 | .to_string()) 134 | } 135 | } 136 | 137 | impl ElmDecode for std::time::SystemTime { 138 | fn decoder_type() -> String { 139 | "systemTimeDecoder".to_string() 140 | } 141 | 142 | fn decoder_definition() -> Option { 143 | Some( 144 | r#"systemTimeDecoder : Json.Decode.Decoder SystemTime 145 | systemTimeDecoder = 146 | Json.Decode.succeed SystemTime 147 | |> Json.Decode.andThen (\x -> Json.Decode.map x (Json.Decode.field "secs_since_epoch" (Json.Decode.int))) 148 | |> Json.Decode.andThen (\x -> Json.Decode.map x (Json.Decode.field "nanos_since_epoch" (Json.Decode.int))) 149 | "# 150 | .to_string(), 151 | ) 152 | } 153 | } 154 | 155 | macro_rules! impl_builtin { 156 | ($rust_type: ty, $elm_type: expr, $elm_decoder: expr) => { 157 | impl ElmDecode for $rust_type { 158 | fn decoder_type() -> String { 159 | $elm_decoder.to_string() 160 | } 161 | 162 | fn decoder_definition() -> Option { 163 | None 164 | } 165 | } 166 | }; 167 | } 168 | 169 | macro_rules! impl_builtin_container { 170 | ($rust_type: ty, $elm_name: expr, $elm_decoder: expr) => { 171 | impl ElmDecode for $rust_type { 172 | fn decoder_type() -> String { 173 | ::std::format!("{} ({})", $elm_decoder, T::decoder_type()) 174 | } 175 | 176 | fn decoder_definition() -> Option { 177 | None 178 | } 179 | } 180 | }; 181 | } 182 | 183 | macro_rules! impl_builtin_map { 184 | ($rust_type: ty) => { 185 | impl ElmDecode for $rust_type { 186 | fn decoder_type() -> String { 187 | ::std::format!("Json.Decode.dict ({})", T::decoder_type()) 188 | } 189 | 190 | fn decoder_definition() -> Option { 191 | None 192 | } 193 | } 194 | }; 195 | } 196 | 197 | macro_rules! impl_builtin_ptr { 198 | ($rust_type: ty) => { 199 | impl ElmDecode for $rust_type { 200 | fn decoder_type() -> String { 201 | ::std::format!("{}", T::decoder_type()) 202 | } 203 | 204 | fn decoder_definition() -> Option { 205 | T::decoder_definition() 206 | } 207 | } 208 | }; 209 | } 210 | 211 | impl_builtin_ptr!(&'_ T); 212 | impl_builtin_ptr!(&'_ mut T); 213 | impl_builtin_ptr!(std::sync::Arc); 214 | impl_builtin!(std::sync::atomic::AtomicBool, "Bool", "Json.Decode.bool"); 215 | impl_builtin!(std::sync::atomic::AtomicU8, "Int", "Json.Decode.int"); 216 | impl_builtin!(std::sync::atomic::AtomicU16, "Int", "Json.Decode.int"); 217 | impl_builtin!(std::sync::atomic::AtomicU32, "Int", "Json.Decode.int"); 218 | impl_builtin!(std::sync::atomic::AtomicU64, "Int", "Json.Decode.int"); 219 | impl_builtin!(std::sync::atomic::AtomicUsize, "Int", "Json.Decode.int"); 220 | impl_builtin!(std::sync::atomic::AtomicI8, "Int", "Json.Decode.int"); 221 | impl_builtin!(std::sync::atomic::AtomicI16, "Int", "Json.Decode.int"); 222 | impl_builtin!(std::sync::atomic::AtomicI32, "Int", "Json.Decode.int"); 223 | impl_builtin!(std::sync::atomic::AtomicI64, "Int", "Json.Decode.int"); 224 | impl_builtin!(std::sync::atomic::AtomicIsize, "Int", "Json.Decode.int"); 225 | impl_builtin_map!(std::collections::BTreeMap); 226 | impl_builtin_container!(std::collections::BTreeSet, "List", "Json.Decode.list"); 227 | impl_builtin_ptr!(Box); 228 | impl_builtin_ptr!(std::cell::Cell); 229 | impl_builtin_map!(std::collections::HashMap); 230 | impl_builtin_container!(std::collections::HashSet, "List", "Json.Decode.list"); 231 | // todo ipaddrs 232 | impl_builtin_container!(std::collections::LinkedList, "List", "Json.Decode.list"); 233 | impl_builtin_ptr!(std::sync::Mutex); 234 | impl_builtin!(std::num::NonZeroU8, "Int", "Json.Decode.int"); 235 | impl_builtin!(std::num::NonZeroU16, "Int", "Json.Decode.int"); 236 | impl_builtin!(std::num::NonZeroU32, "Int", "Json.Decode.int"); 237 | impl_builtin!(std::num::NonZeroU64, "Int", "Json.Decode.int"); 238 | impl_builtin!(std::num::NonZeroU128, "Int", "Json.Decode.int"); 239 | impl_builtin!(std::num::NonZeroUsize, "Int", "Json.Decode.int"); 240 | impl_builtin!(std::num::NonZeroI8, "Int", "Json.Decode.int"); 241 | impl_builtin!(std::num::NonZeroI16, "Int", "Json.Decode.int"); 242 | impl_builtin!(std::num::NonZeroI32, "Int", "Json.Decode.int"); 243 | impl_builtin!(std::num::NonZeroI64, "Int", "Json.Decode.int"); 244 | impl_builtin!(std::num::NonZeroI128, "Int", "Json.Decode.int"); 245 | impl_builtin!(std::num::NonZeroIsize, "Int", "Json.Decode.int"); 246 | impl_builtin_container!(Option, "Maybe", "Json.Decode.nullable"); 247 | impl_builtin!(std::path::Path, "String", "Json.Decode.string"); 248 | impl_builtin!(std::path::PathBuf, "String", "Json.Decode.string"); 249 | // todo phantomdata 250 | impl_builtin_ptr!(std::rc::Rc); 251 | impl_builtin_ptr!(std::cell::RefCell); 252 | impl_builtin_ptr!(std::sync::RwLock); 253 | // todo socketaddrs 254 | impl_builtin!(String, "String", "Json.Decode.string"); 255 | impl_builtin_container!(Vec, "List", "Json.Decode.list"); 256 | impl_builtin_container!([T], "List", "Json.Decode.list"); 257 | impl_builtin!(bool, "Bool", "Json.Decode.bool"); 258 | impl_builtin!(u8, "Int", "Json.Decode.int"); 259 | impl_builtin!(u16, "Int", "Json.Decode.int"); 260 | impl_builtin!(u32, "Int", "Json.Decode.int"); 261 | impl_builtin!(u64, "Int", "Json.Decode.int"); 262 | impl_builtin!(u128, "Int", "Json.Decode.int"); 263 | impl_builtin!(usize, "Int", "Json.Decode.int"); 264 | impl_builtin!(i8, "Int", "Json.Decode.int"); 265 | impl_builtin!(i16, "Int", "Json.Decode.int"); 266 | impl_builtin!(i32, "Int", "Json.Decode.int"); 267 | impl_builtin!(i64, "Int", "Json.Decode.int"); 268 | impl_builtin!(i128, "Int", "Json.Decode.int"); 269 | impl_builtin!(isize, "Int", "Json.Decode.int"); 270 | impl_builtin!(f32, "Float", "Json.Decode.float"); 271 | impl_builtin!(f64, "Float", "Json.Decode.float"); 272 | impl_builtin!(str, "String", "Json.Decode.string"); 273 | 274 | #[cfg(feature = "uuid")] 275 | impl_builtin!(uuid::Uuid, "String", "Json.Decode.string"); 276 | 277 | #[cfg(feature = "chrono")] 278 | impl_builtin!(chrono::NaiveTime, "String", "Json.Decode.string"); 279 | #[cfg(feature = "chrono")] 280 | impl_builtin!(chrono::NaiveDate, "String", "Json.Decode.string"); 281 | #[cfg(feature = "chrono")] 282 | impl_builtin!(chrono::NaiveDateTime, "String", "Json.Decode.string"); 283 | #[cfg(feature = "chrono")] 284 | impl ElmDecode for chrono::DateTime { 285 | fn decoder_type() -> String { 286 | String::decoder_type() 287 | } 288 | 289 | fn decoder_definition() -> Option { 290 | String::decoder_definition() 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /elm_rs/src/elm_encode.rs: -------------------------------------------------------------------------------- 1 | //! Contains the `ElmEncode` trait. 2 | 3 | use crate::Elm; 4 | #[cfg(feature = "derive")] 5 | pub use elm_rs_derive::ElmEncode; 6 | 7 | /// Used to generate JSON encoders for our Rust types in Elm. 8 | pub trait ElmEncode { 9 | /// The name of the encoder in Elm. 10 | fn encoder_type() -> String; 11 | /// The encoder function in Elm. None for encoders in Json.Encode. 12 | fn encoder_definition() -> Option; 13 | } 14 | 15 | impl ElmEncode for () { 16 | fn encoder_type() -> String { 17 | r#"(\_ -> Json.Encode.null)"#.to_string() 18 | } 19 | 20 | fn encoder_definition() -> Option { 21 | None 22 | } 23 | } 24 | 25 | impl ElmEncode for (T,) 26 | where 27 | T: Elm + ElmEncode, 28 | { 29 | fn encoder_type() -> String { 30 | ::std::format!( 31 | "\\( a ) -> Json.Encode.list identity [ {} a ]", 32 | T::encoder_type(), 33 | ) 34 | } 35 | 36 | fn encoder_definition() -> Option { 37 | None 38 | } 39 | } 40 | 41 | impl ElmEncode for (T, U) 42 | where 43 | T: Elm + ElmEncode, 44 | U: Elm + ElmEncode, 45 | { 46 | fn encoder_type() -> String { 47 | ::std::format!( 48 | "\\( a, b) -> Json.Encode.list identity [ {} a, {} b ]", 49 | T::encoder_type(), 50 | U::encoder_type(), 51 | ) 52 | } 53 | 54 | fn encoder_definition() -> Option { 55 | None 56 | } 57 | } 58 | 59 | impl ElmEncode for (T, U, V) 60 | where 61 | T: Elm + ElmEncode, 62 | U: Elm + ElmEncode, 63 | V: Elm + ElmEncode, 64 | { 65 | fn encoder_type() -> String { 66 | ::std::format!( 67 | "\\( a, b, c ) -> Json.Encode.list identity [ {} a, {} b, {} c ]", 68 | T::encoder_type(), 69 | U::encoder_type(), 70 | V::encoder_type(), 71 | ) 72 | } 73 | 74 | fn encoder_definition() -> Option { 75 | None 76 | } 77 | } 78 | 79 | impl ElmEncode for std::borrow::Cow<'_, T> { 80 | fn encoder_type() -> String { 81 | T::encoder_type() 82 | } 83 | 84 | fn encoder_definition() -> Option { 85 | T::encoder_definition() 86 | } 87 | } 88 | 89 | impl ElmEncode for [T; U] 90 | where 91 | T: Elm + ElmEncode, 92 | { 93 | fn encoder_type() -> String { 94 | <[T]>::encoder_type() 95 | } 96 | 97 | fn encoder_definition() -> Option { 98 | <[T]>::encoder_definition() 99 | } 100 | } 101 | 102 | impl ElmEncode for std::time::Duration { 103 | fn encoder_type() -> String { 104 | "durationEncoder".to_string() 105 | } 106 | 107 | fn encoder_definition() -> Option { 108 | Some( 109 | r#"durationEncoder : Duration -> Json.Encode.Value 110 | durationEncoder duration = 111 | Json.Encode.object 112 | [ ( "secs", Json.Encode.int duration.secs ) 113 | , ( "nanos", Json.Encode.int duration.nanos ) 114 | ] 115 | "# 116 | .to_string(), 117 | ) 118 | } 119 | } 120 | 121 | impl ElmEncode for Result { 122 | fn encoder_type() -> String { 123 | format!( 124 | "resultEncoder ({}) ({})", 125 | E::encoder_type(), 126 | T::encoder_type() 127 | ) 128 | } 129 | 130 | fn encoder_definition() -> Option { 131 | Some(r#"resultEncoder : (e -> Json.Encode.Value) -> (t -> Json.Encode.Value) -> (Result e t -> Json.Encode.Value) 132 | resultEncoder errEncoder okEncoder enum = 133 | case enum of 134 | Ok inner -> 135 | Json.Encode.object [ ( "Ok", okEncoder inner ) ] 136 | Err inner -> 137 | Json.Encode.object [ ( "Err", errEncoder inner ) ]"# 138 | .to_string()) 139 | } 140 | } 141 | 142 | impl ElmEncode for std::time::SystemTime { 143 | fn encoder_type() -> String { 144 | "systemTimeEncoder".to_string() 145 | } 146 | 147 | fn encoder_definition() -> Option { 148 | Some( 149 | r#"systemTimeEncoder : SystemTime -> Json.Encode.Value 150 | systemTimeEncoder duration = 151 | Json.Encode.object 152 | [ ( "secs_since_epoch", Json.Encode.int duration.secs_since_epoch ) 153 | , ( "nanos_since_epoch", Json.Encode.int duration.nanos_since_epoch ) 154 | ] 155 | "# 156 | .to_string(), 157 | ) 158 | } 159 | } 160 | 161 | macro_rules! impl_builtin { 162 | ($rust_type: ty, $elm_type: expr, $elm_encoder: expr) => { 163 | impl ElmEncode for $rust_type { 164 | fn encoder_type() -> String { 165 | $elm_encoder.to_string() 166 | } 167 | 168 | fn encoder_definition() -> Option { 169 | None 170 | } 171 | } 172 | }; 173 | } 174 | 175 | macro_rules! impl_builtin_container { 176 | ($rust_type: ty, $elm_name: expr, $elm_encoder: expr) => { 177 | impl ElmEncode for $rust_type { 178 | fn encoder_type() -> String { 179 | ::std::format!("{} ({})", $elm_encoder, T::encoder_type()) 180 | } 181 | 182 | fn encoder_definition() -> Option { 183 | None 184 | } 185 | } 186 | }; 187 | } 188 | 189 | macro_rules! impl_builtin_map { 190 | ($rust_type: ty) => { 191 | impl ElmEncode for $rust_type { 192 | fn encoder_type() -> String { 193 | ::std::format!("Json.Encode.dict identity ({})", T::encoder_type()) 194 | } 195 | 196 | fn encoder_definition() -> Option { 197 | None 198 | } 199 | } 200 | }; 201 | } 202 | 203 | macro_rules! impl_builtin_ptr { 204 | ($rust_type: ty) => { 205 | impl ElmEncode for $rust_type { 206 | fn encoder_type() -> String { 207 | ::std::format!("{}", T::encoder_type()) 208 | } 209 | 210 | fn encoder_definition() -> Option { 211 | T::encoder_definition() 212 | } 213 | } 214 | }; 215 | } 216 | 217 | impl_builtin_ptr!(&'_ T); 218 | impl_builtin_ptr!(&'_ mut T); 219 | impl_builtin_ptr!(std::sync::Arc); 220 | impl_builtin!(std::sync::atomic::AtomicBool, "Bool", "Json.Encode.bool"); 221 | impl_builtin!(std::sync::atomic::AtomicU8, "Int", "Json.Encode.int"); 222 | impl_builtin!(std::sync::atomic::AtomicU16, "Int", "Json.Encode.int"); 223 | impl_builtin!(std::sync::atomic::AtomicU32, "Int", "Json.Encode.int"); 224 | impl_builtin!(std::sync::atomic::AtomicU64, "Int", "Json.Encode.int"); 225 | impl_builtin!(std::sync::atomic::AtomicUsize, "Int", "Json.Encode.int"); 226 | impl_builtin!(std::sync::atomic::AtomicI8, "Int", "Json.Encode.int"); 227 | impl_builtin!(std::sync::atomic::AtomicI16, "Int", "Json.Encode.int"); 228 | impl_builtin!(std::sync::atomic::AtomicI32, "Int", "Json.Encode.int"); 229 | impl_builtin!(std::sync::atomic::AtomicI64, "Int", "Json.Encode.int"); 230 | impl_builtin!(std::sync::atomic::AtomicIsize, "Int", "Json.Encode.int"); 231 | impl_builtin_map!(std::collections::BTreeMap); 232 | impl_builtin_container!(std::collections::BTreeSet, "List", "Json.Encode.list"); 233 | impl_builtin_ptr!(Box); 234 | impl_builtin_ptr!(std::cell::Cell); 235 | impl_builtin_map!(std::collections::HashMap); 236 | impl_builtin_container!(std::collections::HashSet, "List", "Json.Encode.list"); 237 | // todo ipaddrs 238 | impl_builtin_container!(std::collections::LinkedList, "List", "Json.Encode.list"); 239 | impl_builtin_ptr!(std::sync::Mutex); 240 | impl_builtin!(std::num::NonZeroU8, "Int", "Json.Encode.int"); 241 | impl_builtin!(std::num::NonZeroU16, "Int", "Json.Encode.int"); 242 | impl_builtin!(std::num::NonZeroU32, "Int", "Json.Encode.int"); 243 | impl_builtin!(std::num::NonZeroU64, "Int", "Json.Encode.int"); 244 | impl_builtin!(std::num::NonZeroU128, "Int", "Json.Encode.int"); 245 | impl_builtin!(std::num::NonZeroUsize, "Int", "Json.Encode.int"); 246 | impl_builtin!(std::num::NonZeroI8, "Int", "Json.Encode.int"); 247 | impl_builtin!(std::num::NonZeroI16, "Int", "Json.Encode.int"); 248 | impl_builtin!(std::num::NonZeroI32, "Int", "Json.Encode.int"); 249 | impl_builtin!(std::num::NonZeroI64, "Int", "Json.Encode.int"); 250 | impl_builtin!(std::num::NonZeroI128, "Int", "Json.Encode.int"); 251 | impl_builtin!(std::num::NonZeroIsize, "Int", "Json.Encode.int"); 252 | impl_builtin_container!( 253 | Option, 254 | "Maybe", 255 | "Maybe.withDefault Json.Encode.null << Maybe.map" 256 | ); 257 | impl_builtin!(std::path::Path, "String", "Json.Encode.string"); 258 | impl_builtin!(std::path::PathBuf, "String", "Json.Encode.string"); 259 | // todo phantomdata 260 | impl_builtin_ptr!(std::rc::Rc); 261 | impl_builtin_ptr!(std::cell::RefCell); 262 | impl_builtin_ptr!(std::sync::RwLock); 263 | // todo socketaddrs 264 | impl_builtin!(String, "String", "Json.Encode.string"); 265 | impl_builtin_container!(Vec, "List", "Json.Encode.list"); 266 | impl_builtin_container!([T], "List", "Json.Encode.list"); 267 | impl_builtin!(bool, "Bool", "Json.Encode.bool"); 268 | impl_builtin!(u8, "Int", "Json.Encode.int"); 269 | impl_builtin!(u16, "Int", "Json.Encode.int"); 270 | impl_builtin!(u32, "Int", "Json.Encode.int"); 271 | impl_builtin!(u64, "Int", "Json.Encode.int"); 272 | impl_builtin!(u128, "Int", "Json.Encode.int"); 273 | impl_builtin!(usize, "Int", "Json.Encode.int"); 274 | impl_builtin!(i8, "Int", "Json.Encode.int"); 275 | impl_builtin!(i16, "Int", "Json.Encode.int"); 276 | impl_builtin!(i32, "Int", "Json.Encode.int"); 277 | impl_builtin!(i64, "Int", "Json.Encode.int"); 278 | impl_builtin!(i128, "Int", "Json.Encode.int"); 279 | impl_builtin!(isize, "Int", "Json.Encode.int"); 280 | impl_builtin!(f32, "Float", "Json.Encode.float"); 281 | impl_builtin!(f64, "Float", "Json.Encode.float"); 282 | impl_builtin!(str, "String", "Json.Encode.string"); 283 | 284 | #[cfg(feature = "uuid")] 285 | impl_builtin!(uuid::Uuid, "String", "Json.Encode.string"); 286 | 287 | #[cfg(feature = "chrono")] 288 | impl_builtin!(chrono::NaiveTime, "String", "Json.Encode.string"); 289 | #[cfg(feature = "chrono")] 290 | impl_builtin!(chrono::NaiveDate, "String", "Json.Encode.string"); 291 | #[cfg(feature = "chrono")] 292 | impl_builtin!(chrono::NaiveDateTime, "String", "Json.Encode.string"); 293 | #[cfg(feature = "chrono")] 294 | impl ElmEncode for chrono::DateTime { 295 | fn encoder_type() -> String { 296 | String::encoder_type() 297 | } 298 | 299 | fn encoder_definition() -> Option { 300 | String::encoder_definition() 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /elm_rs/src/elm_query.rs: -------------------------------------------------------------------------------- 1 | //! Contains the `ElmQuery` trait. 2 | 3 | #[cfg(feature = "derive")] 4 | pub use elm_rs_derive::ElmQuery; 5 | #[cfg(feature = "derive")] 6 | pub use elm_rs_derive::ElmQueryField; 7 | 8 | /// Used to generate URL encoded key-value pairs in Elm. 9 | pub trait ElmQuery { 10 | /// Generates an Elm function that creates a `List Url.Builder.QueryParameter`. 11 | fn elm_query() -> String; 12 | } 13 | 14 | impl ElmQuery for &'_ T { 15 | fn elm_query() -> String { 16 | T::elm_query() 17 | } 18 | } 19 | 20 | impl ElmQuery for &'_ mut T { 21 | fn elm_query() -> String { 22 | T::elm_query() 23 | } 24 | } 25 | 26 | /// Used to generate the fields for `ElmQuery::elm_query`. 27 | pub trait ElmQueryField { 28 | /// The `Url.Builder` type of the field (either `Url.Builder.string` or `Url.Builder.int`). 29 | fn query_field_type() -> &'static str; 30 | /// The name of the Elm function used to transform it a String or Int for the `query_field_type` (usually `identity`). 31 | fn query_field_encoder_name() -> &'static str { 32 | "identity" 33 | } 34 | /// If the type needs a custom encoder, this function generates its definition. 35 | fn query_field_encoder_definition() -> Option { 36 | None 37 | } 38 | } 39 | 40 | impl ElmQueryField for &'_ T { 41 | fn query_field_type() -> &'static str { 42 | T::query_field_type() 43 | } 44 | } 45 | 46 | impl ElmQueryField for &'_ mut T { 47 | fn query_field_type() -> &'static str { 48 | T::query_field_type() 49 | } 50 | } 51 | 52 | macro_rules! impl_for { 53 | ($e:expr, $($t:ty),+) => { 54 | $( 55 | impl ElmQueryField for $t { 56 | fn query_field_type() -> &'static str { 57 | $e 58 | } 59 | } 60 | )* 61 | }; 62 | } 63 | 64 | impl_for!( 65 | "Url.Builder.string", 66 | String, 67 | str, 68 | std::path::Path, 69 | std::path::PathBuf 70 | ); 71 | #[cfg(feature = "uuid")] 72 | impl_for!("Url.Builder.string", uuid::Uuid); 73 | #[cfg(feature = "chrono")] 74 | impl_for!( 75 | "Url.Builder.string", 76 | chrono::NaiveTime, 77 | chrono::NaiveDate, 78 | chrono::NaiveDateTime 79 | ); 80 | #[cfg(feature = "chrono")] 81 | impl ElmQueryField for chrono::DateTime { 82 | fn query_field_type() -> &'static str { 83 | "Url.Builder.string" 84 | } 85 | } 86 | 87 | impl_for!( 88 | "Url.Builder.int", 89 | u8, 90 | u16, 91 | u32, 92 | u64, 93 | u128, 94 | usize, 95 | i8, 96 | i16, 97 | i32, 98 | i64, 99 | i128, 100 | isize, 101 | std::sync::atomic::AtomicU8, 102 | std::sync::atomic::AtomicU16, 103 | std::sync::atomic::AtomicU32, 104 | std::sync::atomic::AtomicU64, 105 | std::sync::atomic::AtomicUsize, 106 | std::sync::atomic::AtomicI8, 107 | std::sync::atomic::AtomicI16, 108 | std::sync::atomic::AtomicI32, 109 | std::sync::atomic::AtomicI64, 110 | std::sync::atomic::AtomicIsize, 111 | std::num::NonZeroU8, 112 | std::num::NonZeroU16, 113 | std::num::NonZeroU32, 114 | std::num::NonZeroU64, 115 | std::num::NonZeroU128, 116 | std::num::NonZeroUsize, 117 | std::num::NonZeroI8, 118 | std::num::NonZeroI16, 119 | std::num::NonZeroI32, 120 | std::num::NonZeroI64, 121 | std::num::NonZeroI128, 122 | std::num::NonZeroIsize 123 | ); 124 | -------------------------------------------------------------------------------- /elm_rs/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(doc, doc = include_str!("../README.md"))] 2 | 3 | mod elm; 4 | mod elm_decode; 5 | mod elm_encode; 6 | mod elm_query; 7 | #[cfg(test)] 8 | mod test; 9 | 10 | #[cfg(test)] 11 | extern crate self as elm_rs; 12 | 13 | pub use self::{ 14 | elm::Elm, 15 | elm_decode::ElmDecode, 16 | elm_encode::ElmEncode, 17 | elm_query::{ElmQuery, ElmQueryField}, 18 | }; 19 | 20 | #[macro_export] 21 | /// Writes an Elm module to the target. Assumes `elm/json` and `elm/http` are installed. 22 | /// 23 | /// # Example 24 | /// ```no_run 25 | #[doc = include_str!("../examples/example.rs")] 26 | /// ``` 27 | macro_rules! export { 28 | ($name: expr, $target: expr, { 29 | $( 30 | encoders: [ $($encode: ty),* $(,)? ] $(,)? 31 | )? 32 | $( 33 | decoders: [ $($decode: ty),* $(,)? ] $(,)? 34 | )? 35 | $( 36 | queries: [ $($query: ty),* $(,)? ] $(,)? 37 | )? 38 | $( 39 | query_fields: [ $($query_field: ty),* $(,)? ] $(,)? 40 | )? 41 | }) => { 42 | { 43 | fn _export(name: &::std::primitive::str, target: &mut impl ::std::io::Write) -> ::std::result::Result<(), ::std::io::Error> { 44 | ::std::writeln!(target, r#" 45 | -- generated by elm_rs 46 | 47 | 48 | module {} exposing (..) 49 | 50 | import Dict exposing (Dict) 51 | import Http 52 | import Json.Decode 53 | import Json.Encode 54 | import Url.Builder 55 | 56 | 57 | {} 58 | 59 | 60 | {} 61 | 62 | "#, 63 | name, 64 | <::std::result::Result::<(), ()> as $crate::ElmEncode>::encoder_definition().unwrap(), 65 | <::std::result::Result::<(), ()> as $crate::ElmDecode>::decoder_definition().unwrap(), 66 | )?; 67 | let mut generated_elm_definitions = ::std::collections::HashSet::<&str>::new(); 68 | $($( 69 | if !generated_elm_definitions.contains(stringify!($encode)) { 70 | generated_elm_definitions.insert(stringify!($encode)); 71 | if let ::std::option::Option::Some(elm_definition) = <$encode as $crate::Elm>::elm_definition() { 72 | ::std::writeln!(target, "{}\n", elm_definition)?; 73 | } 74 | } 75 | if let ::std::option::Option::Some(encoder_definition) = <$encode as $crate::ElmEncode>::encoder_definition() { 76 | ::std::writeln!(target, "{}\n", encoder_definition)?; 77 | } 78 | )*)? 79 | $($( 80 | if !generated_elm_definitions.contains(stringify!($decode)) { 81 | generated_elm_definitions.insert(stringify!($decode)); 82 | if let ::std::option::Option::Some(elm_definition) = <$decode as $crate::Elm>::elm_definition() { 83 | ::std::writeln!(target, "{}\n", elm_definition)?; 84 | } 85 | } 86 | if let ::std::option::Option::Some(decoder_definition) = <$decode as $crate::ElmDecode>::decoder_definition() { 87 | ::std::writeln!(target, "{}\n", decoder_definition)?; 88 | } 89 | )*)? 90 | $($( 91 | if !generated_elm_definitions.contains(stringify!($query)) { 92 | generated_elm_definitions.insert(stringify!($query)); 93 | if let ::std::option::Option::Some(elm_definition) = <$query as $crate::Elm>::elm_definition() { 94 | ::std::writeln!(target, "{}\n", elm_definition)?; 95 | } 96 | } 97 | let query_definition = <$query as $crate::ElmQuery>::elm_query(); 98 | ::std::writeln!(target, "{}\n", query_definition)?; 99 | )*)? 100 | $($( 101 | if !generated_elm_definitions.contains(stringify!($query_field)) { 102 | generated_elm_definitions.insert(stringify!($query_field)); 103 | if let ::std::option::Option::Some(elm_definition) = <$query_field as $crate::Elm>::elm_definition() { 104 | ::std::writeln!(target, "{}\n", elm_definition)?; 105 | } 106 | } 107 | if let Some(query_field_encoder_definition) = <$query_field as $crate::ElmQueryField>::query_field_encoder_definition() { 108 | ::std::writeln!(target, "{}\n", query_field_encoder_definition)?; 109 | } 110 | )*)? 111 | ::std::result::Result::Ok(()) 112 | } 113 | _export($name, $target) 114 | } 115 | }; 116 | } 117 | -------------------------------------------------------------------------------- /elm_rs/src/test/complex.rs: -------------------------------------------------------------------------------- 1 | use crate::{Elm, ElmDecode, ElmEncode}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Debug, Elm, ElmEncode, ElmDecode, Serialize, Deserialize, PartialEq)] 5 | enum Enum1 { 6 | Unit11, 7 | Unit12, 8 | Newtype11(T), 9 | Newtype12(T), 10 | Tuple11(T, T), 11 | Tuple12(T, T), 12 | Named11 { t: T }, 13 | Named12 { t: T }, 14 | } 15 | 16 | #[derive(Debug, Elm, ElmEncode, ElmDecode, Serialize, Deserialize, PartialEq)] 17 | enum Enum2 { 18 | Unit21, 19 | Unit22, 20 | Newtype21(T), 21 | Newtype22(T), 22 | Tuple21(T, T), 23 | Tuple22(T, T), 24 | Named21 { t: T }, 25 | Named22 { t: T }, 26 | } 27 | 28 | #[derive(Debug, Elm, ElmEncode, ElmDecode, Serialize, Deserialize, PartialEq)] 29 | struct Struct { 30 | unit: Enum1, 31 | newtype: Enum1, 32 | tuple: Enum1, 33 | named: Enum1, 34 | named_unit: Enum2>, 35 | named_newtype: Enum2>, 36 | named_tuple: Enum2>, 37 | named_named: Enum2>, 38 | } 39 | 40 | #[test] 41 | fn complex() { 42 | super::test_json_with_deps( 43 | Struct { 44 | unit: Enum1::Unit11, 45 | newtype: Enum1::Newtype11(vec![1, 2, 3, 4]), 46 | tuple: Enum1::Tuple11(vec![1, 2, 3, 4], vec![1, 2, 3, 4]), 47 | named: Enum1::Named11 { 48 | t: vec![1, 2, 3, 4], 49 | }, 50 | named_unit: Enum2::Named21 { t: Enum1::Unit11 }, 51 | named_newtype: Enum2::Named21 { 52 | t: Enum1::Newtype11(vec![1, 2, 3, 4]), 53 | }, 54 | named_tuple: Enum2::Named21 { 55 | t: Enum1::Tuple11(vec![1, 2, 3, 4], vec![1, 2, 3, 4]), 56 | }, 57 | named_named: Enum2::Named21 { 58 | t: Enum1::Named11 { 59 | t: vec![1, 2, 3, 4], 60 | }, 61 | }, 62 | }, 63 | &format!( 64 | "\ 65 | {} 66 | 67 | {} 68 | 69 | {} 70 | 71 | {} 72 | 73 | {} 74 | 75 | {} 76 | ", 77 | Enum1::>::elm_definition().unwrap(), 78 | Enum1::>::encoder_definition().unwrap(), 79 | Enum1::>::decoder_definition().unwrap(), 80 | Enum2::>>::elm_definition().unwrap(), 81 | Enum2::>>::encoder_definition().unwrap(), 82 | Enum2::>>::decoder_definition().unwrap(), 83 | ), 84 | ) 85 | } 86 | -------------------------------------------------------------------------------- /elm_rs/src/test/enums_adjacent.rs: -------------------------------------------------------------------------------- 1 | use crate::{Elm, ElmDecode, ElmEncode}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Debug, PartialEq, Deserialize, Serialize, Elm, ElmEncode, ElmDecode)] 5 | #[serde(tag = "t", content = "c")] 6 | enum Enum { 7 | Unit1, 8 | Unit2, 9 | Newtype1(i32), 10 | Newtype2(i32), 11 | Tuple1(i32, i32), 12 | Tuple2(i32, i32), 13 | Named1 { field: i32 }, 14 | Named2 { field: i32 }, 15 | } 16 | 17 | #[test] 18 | fn unit() { 19 | super::test_json(Enum::Unit1); 20 | } 21 | 22 | #[test] 23 | fn newtype() { 24 | super::test_json(Enum::Newtype1(123)); 25 | } 26 | 27 | #[test] 28 | fn tuple() { 29 | super::test_json(Enum::Tuple1(123, 234)); 30 | } 31 | 32 | #[test] 33 | fn named() { 34 | super::test_json(Enum::Named1 { field: 123 }); 35 | } 36 | -------------------------------------------------------------------------------- /elm_rs/src/test/enums_external.rs: -------------------------------------------------------------------------------- 1 | use crate::{Elm, ElmDecode, ElmEncode}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Debug, PartialEq, Deserialize, Serialize, Elm, ElmEncode, ElmDecode)] 5 | enum Enum { 6 | Unit1, 7 | Unit2, 8 | Newtype1(i32), 9 | Newtype2(i32), 10 | Tuple1(i32, i32), 11 | Tuple2(i32, i32), 12 | Named1 { field: i32 }, 13 | Named2 { field: i32 }, 14 | } 15 | 16 | #[test] 17 | fn unit() { 18 | super::test_json(Enum::Unit1); 19 | } 20 | 21 | #[test] 22 | fn newtype() { 23 | super::test_json(Enum::Newtype1(123)); 24 | } 25 | 26 | #[test] 27 | fn tuple() { 28 | super::test_json(Enum::Tuple1(123, 234)); 29 | } 30 | 31 | #[test] 32 | fn named() { 33 | super::test_json(Enum::Named1 { field: 123 }); 34 | } 35 | -------------------------------------------------------------------------------- /elm_rs/src/test/enums_internal.rs: -------------------------------------------------------------------------------- 1 | use crate::{Elm, ElmDecode, ElmEncode}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Debug, PartialEq, Deserialize, Serialize, Elm, ElmEncode, ElmDecode)] 5 | #[serde(tag = "t")] 6 | enum Enum { 7 | Unit1, 8 | Unit2, 9 | Named1 { field: i32 }, 10 | Named2 { field: i32 }, 11 | } 12 | 13 | #[test] 14 | fn unit() { 15 | super::test_json(Enum::Unit1); 16 | } 17 | 18 | #[test] 19 | fn named() { 20 | super::test_json(Enum::Named1 { field: 123 }); 21 | } 22 | -------------------------------------------------------------------------------- /elm_rs/src/test/enums_untagged.rs: -------------------------------------------------------------------------------- 1 | use crate::{Elm, ElmDecode, ElmEncode}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Debug, PartialEq, Deserialize, Serialize, Elm, ElmEncode, ElmDecode)] 5 | #[serde(untagged)] 6 | enum Enum { 7 | Unit1, 8 | Unit2, 9 | Newtype1(i32), 10 | Newtype2(i32), 11 | Tuple1(i32, i32), 12 | Tuple2(i32, i32), 13 | Named1 { field: i32 }, 14 | Named2 { field: i32 }, 15 | } 16 | 17 | #[test] 18 | fn unit() { 19 | super::test_json(Enum::Unit1); 20 | } 21 | 22 | #[test] 23 | fn newtype() { 24 | super::test_json(Enum::Newtype1(123)); 25 | } 26 | 27 | #[test] 28 | fn tuple() { 29 | super::test_json(Enum::Tuple1(123, 234)); 30 | } 31 | 32 | #[test] 33 | fn named() { 34 | super::test_json(Enum::Named1 { field: 123 }); 35 | } 36 | -------------------------------------------------------------------------------- /elm_rs/src/test/etc_serde.rs: -------------------------------------------------------------------------------- 1 | use crate::{Elm, ElmDecode, ElmEncode}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Debug, PartialEq, Deserialize, Serialize, Elm, ElmEncode, ElmDecode)] 5 | #[serde(transparent)] 6 | struct TransparentNamed { 7 | field: u8, 8 | } 9 | 10 | #[derive(Debug, PartialEq, Deserialize, Serialize, Elm, ElmEncode, ElmDecode)] 11 | #[serde(transparent)] 12 | struct TransparentNewtype(u8); 13 | 14 | #[derive(Debug, PartialEq, Deserialize, Serialize, Elm, ElmEncode, ElmDecode)] 15 | enum Other { 16 | A, 17 | #[serde(other)] 18 | B, 19 | } 20 | 21 | #[derive(Debug, PartialEq, Deserialize, Serialize, Elm, ElmEncode, ElmDecode)] 22 | #[serde(rename_all = "UPPERCASE")] 23 | struct RenameStruct { 24 | uppercase: u8, 25 | #[serde(rename = "another-field")] 26 | renamed: u8, 27 | #[serde(rename(serialize = "se"))] 28 | rename_for_serialization: u8, 29 | #[serde(rename(deserialize = "de"))] 30 | rename_for_deserialization: u8, 31 | } 32 | 33 | #[derive(Debug, PartialEq, Deserialize, Serialize, Elm, ElmEncode, ElmDecode)] 34 | #[serde(rename_all = "UPPERCASE")] 35 | enum RenameEnum { 36 | Uppercase, 37 | #[serde(rename = "another-variant")] 38 | Renamed, 39 | #[serde(rename(serialize = "se"))] 40 | RenameForSerialization, 41 | #[serde(rename(deserialize = "de"))] 42 | RenameForDeserialization, 43 | } 44 | 45 | #[derive(Debug, PartialEq, Deserialize, Serialize, Elm, ElmEncode, ElmDecode)] 46 | struct Skip { 47 | #[serde(skip)] 48 | skipped: u8, 49 | not_skipped: u8, 50 | } 51 | 52 | #[test] 53 | fn transparent_struct() { 54 | super::test_json(TransparentNamed { field: 0 }); 55 | } 56 | 57 | #[test] 58 | fn transparent_newtype() { 59 | super::test_json(TransparentNewtype(0)); 60 | } 61 | 62 | #[test] 63 | fn other() { 64 | let val: Other = super::test_with_json("\\\"other\\\"", ""); 65 | assert_eq!(val, Other::B); 66 | } 67 | 68 | #[test] 69 | fn rename_struct() { 70 | super::test_json(RenameStruct { 71 | uppercase: 0, 72 | renamed: 0, 73 | rename_for_serialization: 0, 74 | rename_for_deserialization: 0, 75 | }) 76 | } 77 | 78 | #[test] 79 | fn rename_enum() { 80 | super::test_json(RenameEnum::Uppercase); 81 | super::test_json(RenameEnum::Renamed); 82 | super::test_json(RenameEnum::RenameForSerialization); 83 | super::test_json(RenameEnum::RenameForDeserialization); 84 | } 85 | 86 | #[test] 87 | fn skip() { 88 | super::test_json(Skip { 89 | skipped: 0, 90 | not_skipped: 0, 91 | }); 92 | } 93 | -------------------------------------------------------------------------------- /elm_rs/src/test/hygiene.rs: -------------------------------------------------------------------------------- 1 | #![no_implicit_prelude] 2 | #![allow(dead_code)] 3 | 4 | #[derive(crate::elm_rs::Elm, crate::elm_rs::ElmEncode, crate::elm_rs::ElmDecode)] 5 | enum Filetype { 6 | Jpeg, 7 | Png, 8 | } 9 | 10 | #[derive(crate::elm_rs::Elm, crate::elm_rs::ElmEncode, crate::elm_rs::ElmDecode)] 11 | struct Drawing { 12 | filename: ::std::string::String, 13 | filetype: Filetype, 14 | } 15 | 16 | #[test] 17 | fn hygiene() { 18 | let mut target = ::std::vec![]; 19 | crate::elm_rs::export!("Bindings", &mut target, { 20 | encoders: [Drawing, Filetype], 21 | decoders: [Drawing, Filetype], 22 | }) 23 | .unwrap(); 24 | } 25 | -------------------------------------------------------------------------------- /elm_rs/src/test/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{Elm, ElmDecode, ElmEncode, ElmQuery, ElmQueryField}; 2 | use serde::{de::DeserializeOwned, Serialize}; 3 | use std::{ 4 | fmt::Debug, 5 | io::Write, 6 | process::{Command, Stdio}, 7 | }; 8 | 9 | mod complex; 10 | mod enums_adjacent; 11 | mod enums_external; 12 | mod enums_internal; 13 | mod enums_untagged; 14 | mod etc_serde; 15 | mod hygiene; 16 | mod nested; 17 | mod query; 18 | mod regression; 19 | mod structs; 20 | mod structs_serde; 21 | mod types; 22 | 23 | fn test_json( 24 | t: T, 25 | ) { 26 | let t_2 = test_json_without_eq(&t, ""); 27 | assert_eq!(t, t_2); 28 | return; 29 | } 30 | 31 | fn test_json_with_deps< 32 | T: Elm + ElmEncode + ElmDecode + Serialize + DeserializeOwned + PartialEq + Debug, 33 | >( 34 | t: T, 35 | deps: &str, 36 | ) { 37 | let t_2 = test_json_without_eq(&t, deps); 38 | assert_eq!(t, t_2); 39 | return; 40 | } 41 | 42 | fn test_json_without_eq< 43 | T: Elm + ElmEncode + ElmDecode + ElmDecode + Serialize + DeserializeOwned + Debug, 44 | >( 45 | t: &T, 46 | deps: &str, 47 | ) -> T { 48 | let json = serde_json::to_string(&t).unwrap().replace("\"", "\\\""); 49 | test_with_json(&json, deps) 50 | } 51 | 52 | fn test_with_json( 53 | json: &str, 54 | deps: &str, 55 | ) -> T { 56 | let encoder_type = T::encoder_type(); 57 | let decoder_type = T::decoder_type(); 58 | let elm_type = T::elm_definition().unwrap(); 59 | let encoder = T::encoder_definition().unwrap(); 60 | let decoder = T::decoder_definition().unwrap(); 61 | 62 | let input = format!( 63 | r#" 64 | import Json.Decode 65 | import Json.Encode 66 | import Dict exposing (Dict) 67 | 68 | 69 | {} 70 | 71 | {} 72 | 73 | {} 74 | 75 | {} 76 | 77 | {} 78 | 79 | {} 80 | 81 | decoded = Json.Decode.decodeString {} "{}" 82 | 83 | reEncoded = Result.map {} decoded 84 | 85 | s = case reEncoded of 86 | Ok value -> 87 | Json.Encode.encode 0 value 88 | Err err -> 89 | Json.Decode.errorToString err 90 | 91 | "START" 92 | s 93 | "END" 94 | 95 | :exit 96 | "#, 97 | deps, 98 | Result::<(), ()>::decoder_definition().unwrap(), 99 | Result::<(), ()>::encoder_definition().unwrap(), 100 | elm_type, 101 | encoder, 102 | decoder, 103 | decoder_type, 104 | json, 105 | encoder_type 106 | ); 107 | 108 | let json = run_repl(&input); 109 | let unescaped = unescape::unescape(&json).unwrap(); 110 | println!("{}", unescaped); 111 | return serde_json::from_str(&unescaped).unwrap(); 112 | } 113 | 114 | fn test_query< 115 | T: Elm + ElmEncode + ElmDecode + ElmQuery + Serialize, 116 | U: Elm + ElmEncode + ElmDecode + ElmQueryField + Serialize, 117 | >( 118 | val: T, 119 | expected: &str, 120 | ) { 121 | let json = serde_json::to_string(&val).unwrap().replace("\"", "\\\""); 122 | 123 | let decoder_type = T::decoder_type(); 124 | let elm_type = T::elm_definition().unwrap(); 125 | let query = T::elm_query(); 126 | let query_function = format!("urlEncode{}", T::elm_type()); 127 | let decoder = T::decoder_definition().unwrap(); 128 | 129 | let u_elm_type = U::elm_definition().unwrap(); 130 | let u_decoder = U::decoder_definition().unwrap(); 131 | let u_query = U::query_field_encoder_definition().unwrap(); 132 | 133 | let input = format!( 134 | r#" 135 | import Json.Decode 136 | import Url.Builder 137 | 138 | {u_elm_type} 139 | 140 | {elm_type} 141 | 142 | {u_decoder} 143 | 144 | {decoder} 145 | 146 | {u_query} 147 | 148 | {query} 149 | 150 | decoded = Json.Decode.decodeString {decoder_type} "{json}" 151 | 152 | 153 | s = case decoded of 154 | Ok value -> 155 | Url.Builder.toQuery ({query_function} value) 156 | Err err -> 157 | Json.Decode.errorToString err 158 | 159 | "START" 160 | s 161 | "END" 162 | 163 | :exit 164 | "#, 165 | ); 166 | let output = run_repl(&input); 167 | assert_eq!(output, expected); 168 | } 169 | 170 | fn run_repl(input: &str) -> String { 171 | println!("{}", input); 172 | let mut cmd = Command::new("elm") 173 | .arg("repl") 174 | .current_dir("../elm-test") 175 | .stdin(Stdio::piped()) 176 | .stdout(Stdio::piped()) 177 | .stderr(Stdio::piped()) 178 | .spawn() 179 | .unwrap(); 180 | 181 | let mut stdin = cmd.stdin.take().unwrap(); 182 | stdin.write_all(input.as_bytes()).unwrap(); 183 | 184 | let output = cmd.wait_with_output().unwrap(); 185 | 186 | let stdout = String::from_utf8(output.stdout).unwrap(); 187 | let stderr = String::from_utf8(output.stderr).unwrap(); 188 | println!("stdout: {}", stdout); 189 | println!("stderr: {}", stderr); 190 | 191 | assert!(output.status.success()); 192 | 193 | let mut reading = false; 194 | for line in stdout.lines() { 195 | if line.contains("END") { 196 | break; 197 | } 198 | if reading { 199 | let first_quote = line.find('"').unwrap(); 200 | let last_quote = line.rfind('"').unwrap(); 201 | return line[first_quote + 1..last_quote].to_string(); 202 | } 203 | if line.contains("START") { 204 | reading = true; 205 | } 206 | } 207 | panic!("not found"); 208 | } 209 | 210 | #[cfg(any(not(feature = "derive"), not(feature = "serde")))] 211 | compile_error!("The tests require the `derive` and `serde` features to be activated."); 212 | -------------------------------------------------------------------------------- /elm_rs/src/test/nested.rs: -------------------------------------------------------------------------------- 1 | use crate::{Elm, ElmDecode, ElmEncode}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Debug, Serialize, Deserialize, Elm, ElmDecode, ElmEncode, Clone, Eq, PartialEq)] 5 | pub struct NestedTypes { 6 | pub result: Result, u32>, 7 | pub option: Option>, 8 | pub vec: Vec>, 9 | } 10 | 11 | #[test] 12 | fn nestedtypes() { 13 | super::test_json(NestedTypes { 14 | result: Ok(vec![]), 15 | option: Some(vec![]), 16 | vec: vec![vec![]], 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /elm_rs/src/test/query.rs: -------------------------------------------------------------------------------- 1 | use crate::{Elm, ElmDecode, ElmEncode, ElmQuery, ElmQueryField}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Debug, PartialEq, Deserialize, Serialize, Elm, ElmDecode, ElmEncode, ElmQuery)] 5 | struct Named { 6 | first: i32, 7 | second: String, 8 | e: Enum, 9 | } 10 | 11 | #[test] 12 | fn query_struct() { 13 | super::test_query::<_, Enum>( 14 | Named { 15 | first: 123, 16 | second: "234".to_string(), 17 | e: Enum::First, 18 | }, 19 | "?first=123&second=234&e=First", 20 | ); 21 | } 22 | 23 | #[derive(Debug, PartialEq, Deserialize, Serialize, Elm, ElmDecode, ElmEncode, ElmQuery)] 24 | struct ContainsEnum { 25 | e: Enum, 26 | } 27 | 28 | #[derive(Debug, PartialEq, Deserialize, Serialize, Elm, ElmDecode, ElmEncode, ElmQueryField)] 29 | enum Enum { 30 | First, 31 | Second, 32 | } 33 | 34 | #[test] 35 | fn query_enum() { 36 | super::test_query::<_, Enum>(ContainsEnum { e: Enum::First }, "?e=First"); 37 | } 38 | -------------------------------------------------------------------------------- /elm_rs/src/test/regression.rs: -------------------------------------------------------------------------------- 1 | use crate::{Elm, ElmDecode, ElmEncode}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Debug, Serialize, Deserialize, Elm, ElmEncode, ElmDecode, PartialEq)] 5 | #[serde(tag = "type")] 6 | enum Msg { 7 | Unit1, 8 | Unit2, 9 | Fields1 { field: i32 }, 10 | Fields2 { field: i32 }, 11 | } 12 | 13 | #[test] 14 | fn regression_2() { 15 | super::test_json(Msg::Unit1); 16 | } 17 | 18 | #[derive(Debug, Elm, ElmEncode, ElmDecode, Serialize, Deserialize, PartialEq)] 19 | 20 | enum E { 21 | V { a: Option }, 22 | } 23 | 24 | #[test] 25 | fn regression_4() { 26 | super::test_json_with_deps( 27 | E::V { a: Some(1234) }, 28 | &format!( 29 | "\ 30 | {} 31 | 32 | {} 33 | 34 | {} 35 | ", 36 | E::elm_definition().unwrap(), 37 | E::encoder_definition().unwrap(), 38 | E::decoder_definition().unwrap(), 39 | ), 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /elm_rs/src/test/structs.rs: -------------------------------------------------------------------------------- 1 | use crate::{Elm, ElmDecode, ElmEncode}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Debug, PartialEq, Deserialize, Serialize, Elm, ElmEncode, ElmDecode)] 5 | struct Unit; 6 | 7 | #[derive(Debug, PartialEq, Deserialize, Serialize, Elm, ElmEncode, ElmDecode)] 8 | struct Newtype(i32); 9 | 10 | #[derive(Debug, PartialEq, Deserialize, Serialize, Elm, ElmEncode, ElmDecode)] 11 | struct Tuple(i32, i32); 12 | 13 | #[derive(Debug, PartialEq, Deserialize, Serialize, Elm, ElmEncode, ElmDecode)] 14 | struct Named { 15 | first: i32, 16 | second: String, 17 | } 18 | 19 | #[test] 20 | fn unit() { 21 | super::test_json(Unit); 22 | } 23 | 24 | #[test] 25 | fn newtype() { 26 | super::test_json(Newtype(123)); 27 | } 28 | 29 | #[test] 30 | fn tuple() { 31 | super::test_json(Tuple(123, 234)); 32 | } 33 | 34 | #[test] 35 | fn named() { 36 | super::test_json(Named { 37 | first: 123, 38 | second: "234".to_string(), 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /elm_rs/src/test/structs_serde.rs: -------------------------------------------------------------------------------- 1 | use crate::{Elm, ElmDecode, ElmEncode}; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Debug, PartialEq, Deserialize, Serialize, Elm, ElmEncode, ElmDecode)] 5 | #[serde(rename = "renamed")] 6 | struct Unit; 7 | 8 | #[derive(Debug, PartialEq, Deserialize, Serialize, Elm, ElmEncode, ElmDecode)] 9 | #[serde(rename = "renamed")] 10 | struct Newtype(i32); 11 | 12 | #[derive(Debug, PartialEq, Deserialize, Serialize, Elm, ElmEncode, ElmDecode)] 13 | #[serde(rename = "renamed")] 14 | struct Tuple(i32, i32); 15 | 16 | #[derive(Debug, PartialEq, Deserialize, Serialize, Elm, ElmEncode, ElmDecode)] 17 | #[serde(rename = "renamed")] 18 | #[serde(rename_all = "UPPERCASE")] 19 | struct Named { 20 | field: i32, 21 | #[serde(rename = "another-field")] 22 | renamed: i32, 23 | } 24 | 25 | #[test] 26 | fn unit() { 27 | super::test_json(Unit); 28 | } 29 | 30 | #[test] 31 | fn newtype() { 32 | super::test_json(Newtype(123)); 33 | } 34 | 35 | #[test] 36 | fn tuple() { 37 | super::test_json(Tuple(123, 234)); 38 | } 39 | 40 | #[test] 41 | fn named() { 42 | super::test_json(Named { 43 | field: 123, 44 | renamed: 0, 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /elm_rs/src/test/types.rs: -------------------------------------------------------------------------------- 1 | use crate::{Elm, ElmDecode, ElmEncode}; 2 | use serde::{Deserialize, Serialize}; 3 | use std::{ 4 | borrow::Cow, 5 | cell::{Cell, RefCell}, 6 | collections::*, 7 | hash::Hash, 8 | num::*, 9 | path::PathBuf, 10 | rc::Rc, 11 | sync::{atomic::*, Arc, Mutex, RwLock}, 12 | time::{Duration, SystemTime}, 13 | }; 14 | 15 | #[derive(Debug, Deserialize, Serialize, Elm, ElmEncode, ElmDecode)] 16 | struct Types { 17 | t: T, 18 | unit: (), 19 | one: (u8,), 20 | two: (u8, u8), 21 | three: (u8, u8, u8), 22 | arc: Arc, 23 | abool: AtomicBool, 24 | ai8: AtomicI8, 25 | ai16: AtomicI16, 26 | ai32: AtomicI32, 27 | ai64: AtomicI64, 28 | aisize: AtomicIsize, 29 | au8: AtomicU8, 30 | au16: AtomicU16, 31 | au32: AtomicU32, 32 | au64: AtomicU64, 33 | ausize: AtomicUsize, 34 | btreemap: BTreeMap, 35 | btreeset: BTreeSet, 36 | b: Box, 37 | cell: Cell, 38 | cow: Cow<'static, u8>, 39 | duration: Duration, 40 | hashmap: HashMap, 41 | hashset: HashSet, 42 | linkedlist: LinkedList, 43 | mutex: Mutex, 44 | nu8: NonZeroU8, 45 | nu16: NonZeroU16, 46 | nu32: NonZeroU32, 47 | nu64: NonZeroU64, 48 | nusize: NonZeroUsize, 49 | ni8: NonZeroI8, 50 | ni16: NonZeroI16, 51 | ni32: NonZeroI32, 52 | ni64: NonZeroI64, 53 | nisize: NonZeroIsize, 54 | option_some: Option, 55 | option_none: Option, 56 | pathbuf: PathBuf, 57 | rc: Rc, 58 | refcell: RefCell, 59 | result: Result, 60 | rwlock: RwLock, 61 | string: String, 62 | systemtime: SystemTime, 63 | vec: Vec, 64 | array: [T; 2], 65 | bool: bool, 66 | f32: f32, 67 | f64: f64, 68 | u8: u8, 69 | u16: u16, 70 | u32: u32, 71 | u64: u64, 72 | usize: usize, 73 | i8: i8, 74 | i16: i16, 75 | i32: i32, 76 | i64: i64, 77 | isize: isize, 78 | } 79 | 80 | #[test] 81 | fn types() { 82 | super::test_json_without_eq( 83 | &Types { 84 | t: 0u8, 85 | unit: (), 86 | one: (0,), 87 | two: (0, 0), 88 | three: (0, 0, 0), 89 | arc: Arc::new(0), 90 | abool: AtomicBool::default(), 91 | ai8: AtomicI8::default(), 92 | ai16: AtomicI16::default(), 93 | ai32: AtomicI32::default(), 94 | ai64: AtomicI64::default(), 95 | aisize: AtomicIsize::default(), 96 | au8: AtomicU8::default(), 97 | au16: AtomicU16::default(), 98 | au32: AtomicU32::default(), 99 | au64: AtomicU64::default(), 100 | ausize: AtomicUsize::default(), 101 | btreemap: BTreeMap::default(), 102 | btreeset: BTreeSet::default(), 103 | b: Box::new(0), 104 | cell: Cell::new(0), 105 | cow: Cow::Owned(0), 106 | duration: Duration::from_secs(0), 107 | hashmap: HashMap::default(), 108 | hashset: HashSet::default(), 109 | linkedlist: LinkedList::default(), 110 | mutex: Mutex::new(0), 111 | nu8: NonZeroU8::new(1).unwrap(), 112 | nu16: NonZeroU16::new(1).unwrap(), 113 | nu32: NonZeroU32::new(1).unwrap(), 114 | nu64: NonZeroU64::new(1).unwrap(), 115 | nusize: NonZeroUsize::new(1).unwrap(), 116 | ni8: NonZeroI8::new(1).unwrap(), 117 | ni16: NonZeroI16::new(1).unwrap(), 118 | ni32: NonZeroI32::new(1).unwrap(), 119 | ni64: NonZeroI64::new(1).unwrap(), 120 | nisize: NonZeroIsize::new(1).unwrap(), 121 | option_some: Some(0), 122 | option_none: None, 123 | pathbuf: PathBuf::default(), 124 | rc: Rc::new(0), 125 | refcell: RefCell::new(0), 126 | result: Err(0), 127 | rwlock: RwLock::new(0), 128 | string: "0".to_string(), 129 | systemtime: SystemTime::UNIX_EPOCH, 130 | vec: vec![0, 0], 131 | array: [0, 0], 132 | bool: false, 133 | f32: 0.0, 134 | f64: 0.0, 135 | u8: 0, 136 | u16: 0, 137 | u32: 0, 138 | u64: 0, 139 | usize: 0, 140 | i8: 0, 141 | i16: 0, 142 | i32: 0, 143 | i64: 0, 144 | isize: 0, 145 | }, 146 | &::std::format!( 147 | "\ 148 | {} 149 | 150 | {} 151 | 152 | {} 153 | 154 | {} 155 | 156 | {} 157 | 158 | {} 159 | 160 | {} 161 | 162 | {} 163 | 164 | ", 165 | std::time::Duration::elm_definition().unwrap(), 166 | std::time::Duration::encoder_definition().unwrap(), 167 | std::time::Duration::decoder_definition().unwrap(), 168 | std::time::SystemTime::elm_definition().unwrap(), 169 | std::time::SystemTime::encoder_definition().unwrap(), 170 | std::time::SystemTime::decoder_definition().unwrap(), 171 | Result::<(), ()>::encoder_definition().unwrap(), 172 | Result::<(), ()>::decoder_definition().unwrap(), 173 | ), 174 | ); 175 | } 176 | -------------------------------------------------------------------------------- /elm_rs_derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "elm_rs_derive" 3 | version = "0.2.3" 4 | authors = ["Heliozoa "] 5 | edition = "2021" 6 | rust-version = "1.56" 7 | description = "Derive macros for elm_rs" 8 | readme = "README.md" 9 | repository = "https://github.com/Heliozoa/elm_rs" 10 | license = "MPL-2.0" 11 | keywords = [] 12 | categories = [] 13 | resolver = "2" 14 | 15 | [lib] 16 | proc-macro = true 17 | 18 | [features] 19 | default = [] 20 | json = [] 21 | query = [] 22 | serde = [] 23 | 24 | [dependencies] 25 | heck = "0.5.0" 26 | proc-macro2 = "1.0.36" 27 | quote = "1.0.15" 28 | syn = "2.0.98" 29 | -------------------------------------------------------------------------------- /elm_rs_derive/README.md: -------------------------------------------------------------------------------- 1 | # elm_rs_derive 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/elm_rs_derive)](https://crates.io/crates/elm_rs_derive) 4 | [![docs.rs](https://img.shields.io/badge/docs.rs-elm_rs_derive-success)](https://docs.rs/elm_rs_derive) 5 | [![Crates.io](https://img.shields.io/crates/l/elm_rs_derive)](https://choosealicense.com/licenses/mpl-2.0/) 6 | [![GitHub](https://img.shields.io/badge/GitHub-Heliozoa-24292f)](https://github.com/Heliozoa/elm_rs_derive) 7 | 8 | Derive macros for `elm_rs::{Elm, ElmEncode, ElmDecode, ElmQuery, ElmQueryField}`. 9 | 10 | ## Features 11 | - `default`: None. 12 | - `json`: Enables the derive macros for `ElmEncode` and `ElmDecode`. 13 | - `query`: Enables the derive macro for `ElmQuery` and `ElmQueryField`. 14 | - `serde`: Enables compatibility with serde attributes like `#[serde(rename_all = "camelCase")]`. 15 | -------------------------------------------------------------------------------- /elm_rs_derive/src/attributes.rs: -------------------------------------------------------------------------------- 1 | //! Parsing macro attributes. 2 | 3 | use syn::Attribute; 4 | 5 | #[derive(Default)] 6 | pub struct ContainerAttributes { 7 | #[cfg(feature = "serde")] 8 | pub serde: serde::ContainerAttributes, 9 | } 10 | 11 | impl ContainerAttributes { 12 | pub fn parse(attrs: &[Attribute]) -> syn::Result { 13 | let mut attributes = Self::default(); 14 | for attr in attrs { 15 | #[cfg(feature = "serde")] 16 | if attr.path().is_ident("serde") { 17 | attributes.serde.parse(attr)?; 18 | } 19 | } 20 | Ok(attributes) 21 | } 22 | } 23 | 24 | #[derive(Default)] 25 | pub struct VariantAttributes { 26 | #[cfg(feature = "serde")] 27 | pub serde: serde::VariantAttributes, 28 | } 29 | 30 | impl VariantAttributes { 31 | pub fn parse(attrs: &[Attribute]) -> syn::Result { 32 | let mut attributes = Self::default(); 33 | 34 | for attr in attrs { 35 | #[cfg(feature = "serde")] 36 | if attr.path().is_ident("serde") { 37 | attributes.serde.parse(attr)?; 38 | } 39 | } 40 | 41 | Ok(attributes) 42 | } 43 | } 44 | 45 | #[derive(Default)] 46 | pub struct FieldAttributes { 47 | #[cfg(feature = "serde")] 48 | pub serde: serde::FieldAttributes, 49 | } 50 | 51 | impl FieldAttributes { 52 | pub fn parse(attrs: &[Attribute]) -> syn::Result { 53 | let mut attributes = Self::default(); 54 | 55 | for attr in attrs { 56 | #[cfg(feature = "serde")] 57 | if attr.path().is_ident("serde") { 58 | attributes.serde.parse(attr)?; 59 | } 60 | } 61 | 62 | Ok(attributes) 63 | } 64 | } 65 | 66 | #[cfg(feature = "serde")] 67 | pub mod serde { 68 | use heck::{ 69 | ToKebabCase, ToLowerCamelCase, ToPascalCase, ToShoutyKebabCase, ToShoutySnakeCase, 70 | ToSnakeCase, 71 | }; 72 | use proc_macro2::Ident; 73 | use syn::{token, Attribute, LitStr, Token}; 74 | 75 | #[derive(Clone, Copy)] 76 | pub enum RenameAll { 77 | Lowercase, 78 | Uppercase, 79 | PascalCase, 80 | CamelCase, 81 | SnakeCase, 82 | ScreamingSnakeCase, 83 | KebabCase, 84 | ScreamingKebabCase, 85 | } 86 | 87 | impl RenameAll { 88 | pub fn from(s: &str) -> Option { 89 | match s { 90 | "lowercase" => Some(RenameAll::Lowercase), 91 | "UPPERCASE" => Some(RenameAll::Uppercase), 92 | "PascalCase" => Some(RenameAll::PascalCase), 93 | "camelCase" => Some(RenameAll::CamelCase), 94 | "snake_case" => Some(RenameAll::SnakeCase), 95 | "SCREAMING_SNAKE_CASE" => Some(RenameAll::ScreamingSnakeCase), 96 | "kebab-case" => Some(RenameAll::KebabCase), 97 | "SCREAMING-KEBAB-CASE" => Some(RenameAll::ScreamingKebabCase), 98 | _ => None, 99 | } 100 | } 101 | 102 | pub fn rename_ident(self, ident: &Ident) -> String { 103 | match self { 104 | RenameAll::Lowercase => ident.to_string().to_lowercase(), 105 | RenameAll::Uppercase => ident.to_string().to_uppercase(), 106 | RenameAll::PascalCase => ident.to_string().to_pascal_case(), 107 | RenameAll::CamelCase => ident.to_string().to_lower_camel_case(), 108 | RenameAll::SnakeCase => ident.to_string().to_snake_case(), 109 | RenameAll::ScreamingSnakeCase => ident.to_string().to_shouty_snake_case(), 110 | RenameAll::KebabCase => ident.to_string().to_kebab_case(), 111 | RenameAll::ScreamingKebabCase => ident.to_string().to_shouty_kebab_case(), 112 | } 113 | } 114 | } 115 | 116 | #[derive(Default)] 117 | pub struct ContainerAttributes { 118 | pub rename: Option, 119 | pub rename_deserialize: Option, 120 | pub rename_serialize: Option, 121 | pub rename_all: Option, 122 | pub rename_all_deserialize: Option, 123 | pub rename_all_serialize: Option, 124 | pub rename_all_fields: Option, 125 | pub rename_all_fields_deserialize: Option, 126 | pub rename_all_fields_serialize: Option, 127 | pub enum_representation: EnumRepresentation, 128 | pub transparent: bool, 129 | } 130 | 131 | impl ContainerAttributes { 132 | pub fn parse(&mut self, attr: &Attribute) -> syn::Result<()> { 133 | let mut tag_attr = None; 134 | let mut content_attr = None; 135 | 136 | attr.parse_nested_meta(|meta| { 137 | if meta.path.is_ident("rename") { 138 | // rename(..) or rename = ".." 139 | if meta.input.peek(token::Paren) { 140 | // rename(..) 141 | meta.parse_nested_meta(|meta| { 142 | if meta.input.parse::().is_ok() { 143 | if meta.path.is_ident("deserialize") { 144 | let content = meta.input.parse::()?; 145 | self.rename_deserialize = Some(content.value()); 146 | } 147 | 148 | if meta.path.is_ident("serialize") { 149 | let content = meta.input.parse::()?; 150 | self.rename_serialize = Some(content.value()); 151 | } 152 | } 153 | 154 | Ok(()) 155 | })?; 156 | } else if meta.input.parse::().is_ok() { 157 | // rename = ".." 158 | let content = meta.input.parse::()?; 159 | self.rename = Some(content.value()); 160 | } 161 | } 162 | 163 | if meta.path.is_ident("rename_all") { 164 | // rename_all(..) or rename_all = ".." 165 | if meta.input.peek(token::Paren) { 166 | // rename_all(..) 167 | meta.parse_nested_meta(|meta| { 168 | if meta.input.parse::().is_ok() { 169 | if meta.path.is_ident("deserialize") { 170 | let content = meta.input.parse::()?; 171 | self.rename_all_deserialize = RenameAll::from(&content.value()); 172 | } 173 | 174 | if meta.path.is_ident("serialize") { 175 | let content = meta.input.parse::()?; 176 | self.rename_all_serialize = RenameAll::from(&content.value()); 177 | } 178 | } 179 | 180 | Ok(()) 181 | })?; 182 | } else if meta.input.parse::().is_ok() { 183 | // rename_all = ".." 184 | let content = meta.input.parse::()?; 185 | self.rename_all = RenameAll::from(&content.value()); 186 | } 187 | } 188 | 189 | if meta.path.is_ident("rename_all_fields") { 190 | // rename_all_fields(..) or rename_all_fields = ".." 191 | if meta.input.peek(token::Paren) { 192 | // rename_all_fields(..) 193 | meta.parse_nested_meta(|meta| { 194 | if meta.input.parse::().is_ok() { 195 | if meta.path.is_ident("deserialize") { 196 | let content = meta.input.parse::()?; 197 | self.rename_all_fields_deserialize = 198 | RenameAll::from(&content.value()); 199 | } 200 | 201 | if meta.path.is_ident("serialize") { 202 | let content = meta.input.parse::()?; 203 | self.rename_all_fields_serialize = 204 | RenameAll::from(&content.value()); 205 | } 206 | } 207 | 208 | Ok(()) 209 | })?; 210 | } else if meta.input.parse::().is_ok() { 211 | // rename_all_fields = ".." 212 | let content = meta.input.parse::()?; 213 | self.rename_all_fields = RenameAll::from(&content.value()); 214 | } 215 | } 216 | 217 | if meta.path.is_ident("tag") { 218 | // tag = ".." 219 | if meta.input.parse::().is_ok() { 220 | let content = meta.input.parse::()?; 221 | tag_attr = Some(content.value()); 222 | } 223 | } 224 | 225 | if meta.path.is_ident("content") { 226 | // content = ".." 227 | if meta.input.parse::().is_ok() { 228 | let content = meta.input.parse::()?; 229 | content_attr = Some(content.value()); 230 | } 231 | } 232 | 233 | if meta.path.is_ident("untagged") { 234 | // untagged 235 | self.enum_representation = EnumRepresentation::Untagged; 236 | } 237 | 238 | if meta.path.is_ident("transparent") { 239 | self.transparent = true; 240 | } 241 | 242 | // we don't need to handle all serde attributes 243 | Ok(()) 244 | })?; 245 | 246 | if let Some(tag) = tag_attr { 247 | if let Some(content) = content_attr { 248 | self.enum_representation = EnumRepresentation::Adjacent { tag, content }; 249 | } else { 250 | self.enum_representation = EnumRepresentation::Internal { tag }; 251 | } 252 | } 253 | 254 | Ok(()) 255 | } 256 | } 257 | 258 | #[derive(Default)] 259 | pub struct VariantAttributes { 260 | pub rename: Option, 261 | pub rename_deserialize: Option, 262 | pub rename_serialize: Option, 263 | pub rename_all: Option, 264 | pub rename_all_deserialize: Option, 265 | pub rename_all_serialize: Option, 266 | pub aliases: Vec, 267 | pub skip: bool, 268 | pub other: bool, 269 | } 270 | 271 | impl VariantAttributes { 272 | pub fn parse(&mut self, attr: &Attribute) -> syn::Result<()> { 273 | attr.parse_nested_meta(|meta| { 274 | if meta.path.is_ident("rename") { 275 | // rename(..) or rename = ".." 276 | if meta.input.peek(token::Paren) { 277 | // rename(..) 278 | meta.parse_nested_meta(|meta| { 279 | if meta.input.parse::().is_ok() { 280 | if meta.path.is_ident("deserialize") { 281 | let content = meta.input.parse::()?; 282 | self.rename_deserialize = Some(content.value()); 283 | } 284 | 285 | if meta.path.is_ident("serialize") { 286 | let content = meta.input.parse::()?; 287 | self.rename_serialize = Some(content.value()); 288 | } 289 | } 290 | 291 | Ok(()) 292 | })?; 293 | } else if meta.input.parse::().is_ok() { 294 | // rename = ".." 295 | let content = meta.input.parse::()?; 296 | self.rename = Some(content.value()); 297 | } 298 | } 299 | 300 | if meta.path.is_ident("alias") && meta.input.parse::().is_ok() { 301 | // alias = ".." 302 | let content = meta.input.parse::()?; 303 | self.aliases.push(content.value()); 304 | } 305 | 306 | if meta.path.is_ident("rename_all") { 307 | // rename_all(..) or rename_all = ".." 308 | if meta.input.peek(token::Paren) { 309 | // rename_all(..) 310 | meta.parse_nested_meta(|meta| { 311 | if meta.input.parse::().is_ok() { 312 | if meta.path.is_ident("deserialize") { 313 | let content = meta.input.parse::()?; 314 | self.rename_all_deserialize = RenameAll::from(&content.value()); 315 | } 316 | 317 | if meta.path.is_ident("serialize") { 318 | let content = meta.input.parse::()?; 319 | self.rename_all_serialize = RenameAll::from(&content.value()); 320 | } 321 | } 322 | 323 | Ok(()) 324 | })?; 325 | } else if meta.input.parse::().is_ok() { 326 | // rename_all = ".." 327 | let content = meta.input.parse::()?; 328 | self.rename_all = RenameAll::from(&content.value()); 329 | } 330 | } 331 | 332 | if meta.path.is_ident("skip") { 333 | self.skip = true; 334 | } 335 | 336 | if meta.path.is_ident("other") { 337 | self.other = true; 338 | } 339 | 340 | Ok(()) 341 | })?; 342 | 343 | Ok(()) 344 | } 345 | } 346 | 347 | #[derive(Default)] 348 | pub struct FieldAttributes { 349 | pub rename: Option, 350 | pub rename_deserialize: Option, 351 | pub rename_serialize: Option, 352 | pub aliases: Vec, 353 | pub flatten: bool, 354 | pub skip: bool, 355 | } 356 | 357 | impl FieldAttributes { 358 | pub fn parse(&mut self, attr: &Attribute) -> syn::Result<()> { 359 | attr.parse_nested_meta(|meta| { 360 | if meta.path.is_ident("rename") { 361 | // rename(..) or rename = ".." 362 | if meta.input.peek(token::Paren) { 363 | // rename(..) 364 | meta.parse_nested_meta(|meta| { 365 | if meta.input.parse::().is_ok() { 366 | if meta.path.is_ident("deserialize") { 367 | let content = meta.input.parse::()?; 368 | self.rename_deserialize = Some(content.value()); 369 | } 370 | 371 | if meta.path.is_ident("serialize") { 372 | let content = meta.input.parse::()?; 373 | self.rename_serialize = Some(content.value()); 374 | } 375 | } 376 | 377 | Ok(()) 378 | })?; 379 | } else if meta.input.parse::().is_ok() { 380 | // rename = ".." 381 | let content = meta.input.parse::()?; 382 | self.rename = Some(content.value()); 383 | } 384 | } 385 | 386 | if meta.path.is_ident("alias") && meta.input.parse::().is_ok() { 387 | // alias = ".." 388 | let content = meta.input.parse::()?; 389 | self.aliases.push(content.value()); 390 | } 391 | 392 | if meta.path.is_ident("flatten") { 393 | self.flatten = true; 394 | } 395 | 396 | if meta.path.is_ident("skip") { 397 | self.skip = true; 398 | } 399 | 400 | Ok(()) 401 | })?; 402 | 403 | Ok(()) 404 | } 405 | } 406 | 407 | #[derive(Default, Clone)] 408 | pub enum EnumRepresentation { 409 | #[default] 410 | External, 411 | Internal { 412 | tag: String, 413 | }, 414 | Adjacent { 415 | tag: String, 416 | content: String, 417 | }, 418 | Untagged, 419 | } 420 | 421 | #[cfg(test)] 422 | mod test { 423 | use super::*; 424 | 425 | #[test] 426 | fn parses_container_rename() { 427 | let mut ca = ContainerAttributes::default(); 428 | 429 | ca.parse(&syn::parse_quote!(#[serde(rename = "re")])) 430 | .unwrap(); 431 | assert_eq!(ca.rename, Some("re".to_string())); 432 | 433 | ca.parse(&syn::parse_quote!(#[serde(rename(deserialize = "de"))])) 434 | .unwrap(); 435 | assert_eq!(ca.rename_deserialize, Some("de".to_string())); 436 | 437 | ca.parse(&syn::parse_quote!(#[serde(rename(serialize = "se"))])) 438 | .unwrap(); 439 | assert_eq!(ca.rename_serialize, Some("se".to_string())); 440 | 441 | ca.parse(&syn::parse_quote!(#[serde(rename(deserialize = "de2", serialize = "se2"))])) 442 | .unwrap(); 443 | assert_eq!(ca.rename_deserialize, Some("de2".to_string())); 444 | assert_eq!(ca.rename_serialize, Some("se2".to_string())); 445 | } 446 | 447 | #[test] 448 | fn parses_container_rename_all() { 449 | let mut ca = ContainerAttributes::default(); 450 | 451 | ca.parse(&syn::parse_quote!(#[serde(rename_all = "UPPERCASE")])) 452 | .unwrap(); 453 | assert!(matches!(ca.rename_all, Some(RenameAll::Uppercase))); 454 | 455 | ca.parse(&syn::parse_quote!(#[serde(rename_all(deserialize = "UPPERCASE"))])) 456 | .unwrap(); 457 | assert!(matches!( 458 | ca.rename_all_deserialize, 459 | Some(RenameAll::Uppercase) 460 | )); 461 | 462 | ca.parse(&syn::parse_quote!(#[serde(rename_all(serialize = "UPPERCASE"))])) 463 | .unwrap(); 464 | assert!(matches!( 465 | ca.rename_all_serialize, 466 | Some(RenameAll::Uppercase) 467 | )); 468 | 469 | ca.parse(&syn::parse_quote!(#[serde(rename_all(deserialize = "lowercase", serialize = "lowercase"))])) 470 | .unwrap(); 471 | assert!(matches!( 472 | ca.rename_all_deserialize, 473 | Some(RenameAll::Lowercase) 474 | )); 475 | assert!(matches!( 476 | ca.rename_all_serialize, 477 | Some(RenameAll::Lowercase) 478 | )); 479 | } 480 | 481 | #[test] 482 | fn parses_container_rename_all_fields() { 483 | let mut ca = ContainerAttributes::default(); 484 | 485 | ca.parse(&syn::parse_quote!(#[serde(rename_all_fields = "UPPERCASE")])) 486 | .unwrap(); 487 | assert!(matches!(ca.rename_all_fields, Some(RenameAll::Uppercase))); 488 | 489 | ca.parse(&syn::parse_quote!(#[serde(rename_all_fields(deserialize = "UPPERCASE"))])) 490 | .unwrap(); 491 | assert!(matches!( 492 | ca.rename_all_fields_deserialize, 493 | Some(RenameAll::Uppercase) 494 | )); 495 | 496 | ca.parse(&syn::parse_quote!(#[serde(rename_all_fields(serialize = "UPPERCASE"))])) 497 | .unwrap(); 498 | assert!(matches!( 499 | ca.rename_all_fields_serialize, 500 | Some(RenameAll::Uppercase) 501 | )); 502 | 503 | ca.parse(&syn::parse_quote!(#[serde(rename_all_fields(deserialize = "lowercase", serialize = "lowercase"))])) 504 | .unwrap(); 505 | assert!(matches!( 506 | ca.rename_all_fields_deserialize, 507 | Some(RenameAll::Lowercase) 508 | )); 509 | assert!(matches!( 510 | ca.rename_all_fields_serialize, 511 | Some(RenameAll::Lowercase) 512 | )); 513 | } 514 | 515 | #[test] 516 | fn parses_container_enum_representation() { 517 | let mut ca = ContainerAttributes::default(); 518 | 519 | ca.parse(&syn::parse_quote!(#[serde(tag = "t")])).unwrap(); 520 | if let EnumRepresentation::Internal { tag } = &ca.enum_representation { 521 | assert_eq!(tag, "t"); 522 | } else { 523 | panic!("failed to parse tag"); 524 | } 525 | 526 | ca.parse(&syn::parse_quote!(#[serde(tag = "t", content = "c")])) 527 | .unwrap(); 528 | if let EnumRepresentation::Adjacent { tag, content } = &ca.enum_representation { 529 | assert_eq!(tag, "t"); 530 | assert_eq!(content, "c"); 531 | } else { 532 | panic!("failed to parse tag and content"); 533 | } 534 | 535 | ca.parse(&syn::parse_quote!(#[serde(untagged)])).unwrap(); 536 | assert!(matches!( 537 | ca.enum_representation, 538 | EnumRepresentation::Untagged 539 | )); 540 | } 541 | 542 | #[test] 543 | fn parses_variant_rename() { 544 | let mut va = VariantAttributes::default(); 545 | 546 | va.parse(&syn::parse_quote!(#[serde(rename = "re")])) 547 | .unwrap(); 548 | assert_eq!(va.rename, Some("re".to_string())); 549 | 550 | va.parse(&syn::parse_quote!(#[serde(rename(deserialize = "de"))])) 551 | .unwrap(); 552 | assert_eq!(va.rename_deserialize, Some("de".to_string())); 553 | 554 | va.parse(&syn::parse_quote!(#[serde(rename(serialize = "se"))])) 555 | .unwrap(); 556 | assert_eq!(va.rename_serialize, Some("se".to_string())); 557 | 558 | va.parse(&syn::parse_quote!(#[serde(rename(deserialize = "de2", serialize = "se2"))])) 559 | .unwrap(); 560 | assert_eq!(va.rename_deserialize, Some("de2".to_string())); 561 | assert_eq!(va.rename_serialize, Some("se2".to_string())); 562 | } 563 | 564 | #[test] 565 | fn parses_variant_rename_all() { 566 | let mut va = VariantAttributes::default(); 567 | 568 | va.parse(&syn::parse_quote!(#[serde(rename_all = "UPPERCASE")])) 569 | .unwrap(); 570 | assert!(matches!(va.rename_all, Some(RenameAll::Uppercase))); 571 | 572 | va.parse(&syn::parse_quote!(#[serde(rename_all(deserialize = "UPPERCASE"))])) 573 | .unwrap(); 574 | assert!(matches!( 575 | va.rename_all_deserialize, 576 | Some(RenameAll::Uppercase) 577 | )); 578 | 579 | va.parse(&syn::parse_quote!(#[serde(rename_all(serialize = "UPPERCASE"))])) 580 | .unwrap(); 581 | assert!(matches!( 582 | va.rename_all_serialize, 583 | Some(RenameAll::Uppercase) 584 | )); 585 | 586 | va.parse(&syn::parse_quote!(#[serde(rename_all(deserialize = "lowercase", serialize = "lowercase"))])) 587 | .unwrap(); 588 | assert!(matches!( 589 | va.rename_all_deserialize, 590 | Some(RenameAll::Lowercase) 591 | )); 592 | assert!(matches!( 593 | va.rename_all_serialize, 594 | Some(RenameAll::Lowercase) 595 | )); 596 | } 597 | 598 | #[test] 599 | fn parses_variant_aliases() { 600 | let mut va = VariantAttributes::default(); 601 | va.parse(&syn::parse_quote!(#[serde(alias = "a")])).unwrap(); 602 | assert_eq!(va.aliases[0], "a"); 603 | va.parse(&syn::parse_quote!(#[serde(alias = "a1", alias = "a2")])) 604 | .unwrap(); 605 | assert_eq!(va.aliases[1], "a1"); 606 | assert_eq!(va.aliases[2], "a2"); 607 | } 608 | 609 | #[test] 610 | fn parses_variant_skip() { 611 | let mut va = VariantAttributes::default(); 612 | va.parse(&syn::parse_quote!(#[serde(skip)])).unwrap(); 613 | assert!(va.skip); 614 | } 615 | 616 | #[test] 617 | fn parses_variant_other() { 618 | let mut va = VariantAttributes::default(); 619 | va.parse(&syn::parse_quote!(#[serde(other)])).unwrap(); 620 | assert!(va.other); 621 | } 622 | 623 | #[test] 624 | fn parses_field_rename() { 625 | let mut fa = FieldAttributes::default(); 626 | 627 | fa.parse(&syn::parse_quote!(#[serde(rename = "re")])) 628 | .unwrap(); 629 | assert_eq!(fa.rename, Some("re".to_string())); 630 | 631 | fa.parse(&syn::parse_quote!(#[serde(rename(deserialize = "de"))])) 632 | .unwrap(); 633 | assert_eq!(fa.rename_deserialize, Some("de".to_string())); 634 | 635 | fa.parse(&syn::parse_quote!(#[serde(rename(serialize = "se"))])) 636 | .unwrap(); 637 | assert_eq!(fa.rename_serialize, Some("se".to_string())); 638 | 639 | fa.parse(&syn::parse_quote!(#[serde(rename(deserialize = "de2", serialize = "se2"))])) 640 | .unwrap(); 641 | assert_eq!(fa.rename_deserialize, Some("de2".to_string())); 642 | assert_eq!(fa.rename_serialize, Some("se2".to_string())); 643 | } 644 | 645 | #[test] 646 | fn parses_field_aliases() { 647 | let mut fa = FieldAttributes::default(); 648 | fa.parse(&syn::parse_quote!(#[serde(alias = "a")])).unwrap(); 649 | assert_eq!(fa.aliases[0], "a"); 650 | fa.parse(&syn::parse_quote!(#[serde(alias = "a1", alias = "a2")])) 651 | .unwrap(); 652 | assert_eq!(fa.aliases[1], "a1"); 653 | assert_eq!(fa.aliases[2], "a2"); 654 | } 655 | 656 | #[test] 657 | fn parses_field_flatten() { 658 | let mut fa = FieldAttributes::default(); 659 | fa.parse(&syn::parse_quote!(#[serde(flatten)])).unwrap(); 660 | assert!(fa.flatten); 661 | } 662 | 663 | #[test] 664 | fn parses_field_skip() { 665 | let mut fa = FieldAttributes::default(); 666 | fa.parse(&syn::parse_quote!(#[serde(skip)])).unwrap(); 667 | assert!(fa.skip); 668 | } 669 | } 670 | } 671 | -------------------------------------------------------------------------------- /elm_rs_derive/src/elm.rs: -------------------------------------------------------------------------------- 1 | //! Derive macro for Elm. 2 | 3 | use super::{EnumVariant, EnumVariantKind, Intermediate, StructField, TypeInfo}; 4 | use proc_macro::TokenStream; 5 | use proc_macro2::TokenStream as TokenStream2; 6 | use quote::quote; 7 | use syn::{parse_macro_input, DeriveInput, Type}; 8 | 9 | pub fn derive(input: TokenStream) -> TokenStream { 10 | let derive_input = parse_macro_input!(input as DeriveInput); 11 | let intermediate = match Intermediate::parse(derive_input) { 12 | Ok(intermediate) => intermediate, 13 | Err(err) => return err.to_compile_error().into(), 14 | }; 15 | let token_stream = intermediate_to_token_stream(intermediate); 16 | TokenStream::from(token_stream) 17 | } 18 | 19 | fn intermediate_to_token_stream( 20 | Intermediate { 21 | ident, 22 | elm_type, 23 | mut generics, 24 | generics_without_bounds, 25 | type_info, 26 | container_attributes: _, 27 | }: Intermediate, 28 | ) -> TokenStream2 { 29 | let type_definition = match type_info { 30 | TypeInfo::Unit => unit(&elm_type), 31 | TypeInfo::Newtype(ty) => newtype(&elm_type, &ty), 32 | TypeInfo::Tuple(tys) => tuple(&elm_type, &tys), 33 | TypeInfo::Struct(fields) => struct_type(&elm_type, fields), 34 | TypeInfo::Enum { variants, .. } => enum_type(&elm_type, variants), 35 | }; 36 | 37 | for p in generics.type_params_mut() { 38 | p.bounds.push(syn::parse_str("::elm_rs::Elm").unwrap()); 39 | } 40 | 41 | quote! { 42 | impl #generics ::elm_rs::Elm for #ident #generics_without_bounds { 43 | fn elm_type() -> ::std::string::String { 44 | ::std::convert::From::from(#elm_type) 45 | } 46 | 47 | fn elm_definition() -> ::std::option::Option<::std::string::String> { 48 | ::std::option::Option::Some(#type_definition) 49 | } 50 | } 51 | } 52 | } 53 | 54 | fn unit(elm_type: &str) -> TokenStream2 { 55 | quote! {::std::format!("\ 56 | type {elm_type} 57 | = {elm_type} 58 | ", 59 | elm_type = #elm_type, 60 | )} 61 | } 62 | 63 | fn newtype(elm_type: &str, ty: &Type) -> TokenStream2 { 64 | quote! {::std::format!("\ 65 | type {elm_type} 66 | = {elm_type} ({inner_type}) 67 | ", 68 | elm_type = #elm_type, 69 | inner_type = <#ty as ::elm_rs::Elm>::elm_type(), 70 | )} 71 | } 72 | 73 | fn tuple(elm_type: &str, ts: &[Type]) -> TokenStream2 { 74 | quote! {::std::format!("\ 75 | type {elm_type} 76 | = {elm_type} {types} 77 | ", 78 | elm_type = #elm_type, 79 | types = 80 | ( 81 | &[ 82 | #(::std::format!("({})", <#ts as ::elm_rs::Elm>::elm_type())),* 83 | ] 84 | ).join(" "), 85 | )} 86 | } 87 | 88 | fn struct_type(elm_type: &str, fields: Vec) -> TokenStream2 { 89 | let ids = fields.iter().map(|field| field.name_elm()); 90 | let tys = fields.iter().map(|field| &field.ty); 91 | quote! {::std::format!("\ 92 | type alias {elm_type} = 93 | {{ {fields} 94 | }} 95 | ", 96 | elm_type = #elm_type, 97 | fields = 98 | ( 99 | &[ 100 | #(::std::format!("{} : {}", #ids, <#tys as ::elm_rs::Elm>::elm_type())),* 101 | ] 102 | ).join("\n , "), 103 | )} 104 | } 105 | 106 | fn enum_type(elm_type: &str, enum_variants: Vec) -> TokenStream2 { 107 | let mut enum_fields: Vec = vec![]; 108 | for enum_variant in enum_variants { 109 | let variant_elm_name = enum_variant.name_elm(); 110 | match &enum_variant.variant { 111 | EnumVariantKind::Unit => { 112 | let field = quote! { 113 | #variant_elm_name 114 | }; 115 | enum_fields.push(field); 116 | } 117 | EnumVariantKind::Newtype(ty) => { 118 | let field = quote! { 119 | ::std::format!("{} ({})", #variant_elm_name, <#ty as ::elm_rs::Elm>::elm_type()) 120 | }; 121 | enum_fields.push(field); 122 | } 123 | EnumVariantKind::Tuple(tys) => { 124 | let field = quote! { 125 | ::std::format!("{name} {types}", 126 | name = #variant_elm_name, 127 | types = 128 | ( 129 | &[ 130 | #(::std::format!("({})", <#tys as ::elm_rs::Elm>::elm_type())),* 131 | ] as &[::std::string::String] 132 | ).join(" ")) 133 | }; 134 | enum_fields.push(field); 135 | } 136 | EnumVariantKind::Struct(fields) => { 137 | let ids = fields.iter().map(|field| field.name_elm()); 138 | let tys = fields.iter().map(|field| &field.ty); 139 | let field = quote! { 140 | ::std::format!("{name} {{ {fields} }}", 141 | name = #variant_elm_name, 142 | fields = 143 | ( 144 | &[ 145 | #(::std::format!("{} : {}", #ids, <#tys as ::elm_rs::Elm>::elm_type())),* 146 | ] as &[::std::string::String] 147 | ).join(", ")) 148 | }; 149 | enum_fields.push(field); 150 | } 151 | } 152 | } 153 | quote! {::std::format!("\ 154 | type {elm_type} 155 | = {enum_fields} 156 | ", 157 | elm_type = #elm_type, 158 | enum_fields = 159 | ( 160 | &[ 161 | #(::std::format!("{}", #enum_fields)),* 162 | ] as &[::std::string::String] 163 | ).join("\n | "), 164 | )} 165 | } 166 | -------------------------------------------------------------------------------- /elm_rs_derive/src/elm_encode.rs: -------------------------------------------------------------------------------- 1 | //! Derive macro for ElmEncode. 2 | 3 | use super::{EnumVariantKind, Intermediate, TypeInfo}; 4 | #[cfg(feature = "serde")] 5 | use crate::attributes::serde::EnumRepresentation; 6 | use crate::{attributes::ContainerAttributes, EnumVariant, StructField}; 7 | use heck::ToLowerCamelCase; 8 | use proc_macro::TokenStream; 9 | use proc_macro2::TokenStream as TokenStream2; 10 | use quote::quote; 11 | use syn::{parse_macro_input, DeriveInput, Type}; 12 | 13 | pub fn derive(input: TokenStream) -> TokenStream { 14 | let derive_input = parse_macro_input!(input as DeriveInput); 15 | let intermediate = match Intermediate::parse(derive_input) { 16 | Ok(intermediate) => intermediate, 17 | Err(err) => return err.to_compile_error().into(), 18 | }; 19 | let token_stream = match intermediate_to_token_stream(intermediate) { 20 | Ok(token_stream) => token_stream, 21 | Err(err) => return err.to_compile_error().into(), 22 | }; 23 | TokenStream::from(token_stream) 24 | } 25 | 26 | fn intermediate_to_token_stream( 27 | Intermediate { 28 | ident, 29 | elm_type, 30 | mut generics, 31 | generics_without_bounds, 32 | type_info, 33 | container_attributes, 34 | }: Intermediate, 35 | ) -> syn::Result { 36 | let encoder_type = format!("{}Encoder", elm_type.to_lower_camel_case()); 37 | 38 | let encoder = match type_info { 39 | TypeInfo::Unit => struct_unit(&elm_type, &encoder_type), 40 | TypeInfo::Newtype(ty) => struct_newtype(&elm_type, &encoder_type, &ty), 41 | TypeInfo::Tuple(tys) => struct_tuple(&elm_type, &encoder_type, &tys), 42 | TypeInfo::Struct(fields) => { 43 | struct_named(&elm_type, &encoder_type, &fields, &container_attributes) 44 | } 45 | TypeInfo::Enum { 46 | variants, 47 | #[cfg(feature = "serde")] 48 | representation, 49 | } => { 50 | #[cfg(feature = "serde")] 51 | let representation = match representation { 52 | EnumRepresentation::External => { 53 | enum_external(&elm_type, &encoder_type, variants, &container_attributes) 54 | } 55 | EnumRepresentation::Internal { tag } => enum_internal( 56 | &elm_type, 57 | &encoder_type, 58 | variants, 59 | &tag, 60 | &container_attributes, 61 | )?, 62 | EnumRepresentation::Adjacent { tag, content } => enum_adjacent( 63 | &elm_type, 64 | &encoder_type, 65 | variants, 66 | &tag, 67 | &content, 68 | &container_attributes, 69 | )?, 70 | EnumRepresentation::Untagged => { 71 | enum_untagged(&elm_type, &encoder_type, variants, &container_attributes)? 72 | } 73 | }; 74 | #[cfg(not(feature = "serde"))] 75 | let representation = 76 | enum_external(&elm_type, &encoder_type, variants, &container_attributes); 77 | representation 78 | } 79 | }; 80 | 81 | for p in generics.type_params_mut() { 82 | p.bounds.push(syn::parse_str("::elm_rs::Elm").unwrap()); 83 | p.bounds 84 | .push(syn::parse_str("::elm_rs::ElmEncode").unwrap()); 85 | } 86 | 87 | let res = quote! { 88 | impl #generics ::elm_rs::ElmEncode for #ident #generics_without_bounds { 89 | fn encoder_type() -> ::std::string::String { 90 | ::std::convert::From::from(#encoder_type) 91 | } 92 | 93 | fn encoder_definition() -> ::std::option::Option<::std::string::String> { 94 | ::std::option::Option::Some(#encoder) 95 | } 96 | } 97 | }; 98 | Ok(res) 99 | } 100 | 101 | // ======= 102 | // structs 103 | // ======= 104 | 105 | /// #[derive(Deserialize, Serialize)] 106 | /// struct Unit; 107 | /// "null" 108 | fn struct_unit(elm_type: &str, encoder_type: &str) -> TokenStream2 { 109 | quote! {::std::format!("\ 110 | {encoder_type} : {elm_type} -> Json.Encode.Value 111 | {encoder_type} _ = 112 | Json.Encode.null 113 | ", 114 | elm_type = #elm_type, 115 | encoder_type = #encoder_type, 116 | )} 117 | } 118 | 119 | /// #[derive(Deserialize, Serialize)] 120 | /// struct Newtype(i32); 121 | /// "0" 122 | fn struct_newtype(elm_type: &str, encoder_type: &str, ty: &Type) -> TokenStream2 { 123 | quote! {::std::format!("\ 124 | {encoder_type} : {elm_type} -> Json.Encode.Value 125 | {encoder_type} ({elm_type} inner) = 126 | ({inner_encoder}) inner 127 | ", 128 | elm_type = #elm_type, 129 | encoder_type = #encoder_type, 130 | inner_encoder = <#ty>::encoder_type(), 131 | )} 132 | } 133 | 134 | /// #[derive(Deserialize, Serialize)] 135 | /// struct Tuple(i32, i32); 136 | /// "[0,0]" 137 | fn struct_tuple(elm_type: &str, encoder_type: &str, inner_types: &[Type]) -> TokenStream2 { 138 | let indices: Vec = inner_types.iter().enumerate().map(|(i, _)| i).collect(); 139 | 140 | quote! {::std::format!("\ 141 | {encoder_type} : {elm_type} -> Json.Encode.Value 142 | {encoder_type} ({elm_type} {params}) = 143 | Json.Encode.list identity 144 | [ {encoders} 145 | ] 146 | ", 147 | elm_type = #elm_type, 148 | encoder_type = #encoder_type, 149 | params = (&[#(::std::format!("t{idx}", 150 | idx = #indices)),* 151 | ] 152 | ).join(" "), 153 | encoders = ( 154 | &[ 155 | #(::std::format!("({encoder}) t{idx}", 156 | encoder = <#inner_types>::encoder_type(), 157 | idx = #indices) 158 | ),* 159 | ] 160 | ).join("\n , "), 161 | )} 162 | } 163 | 164 | /// #[derive(Deserialize, Serialize)] 165 | /// struct Struct { 166 | /// a: i32, 167 | /// }; 168 | /// "{\"a\":0}" 169 | fn struct_named( 170 | elm_type: &str, 171 | encoder_type: &str, 172 | fields: &[StructField], 173 | container_attributes: &ContainerAttributes, 174 | ) -> TokenStream2 { 175 | let mut field_encoders = vec![]; 176 | for field in fields { 177 | let field_name = field.name_elm(); 178 | let field_name_encode = field.name_encode(container_attributes); 179 | let ty = &field.ty; 180 | field_encoders.push( 181 | quote! {::std::format!("( \"{field_name_encode}\", ({encoder}) struct.{field_name} )", 182 | field_name_encode = #field_name_encode, 183 | encoder = <#ty as ::elm_rs::ElmEncode>::encoder_type(), 184 | field_name = #field_name)}, 185 | ) 186 | } 187 | let encoder = quote! {::std::format!("\ 188 | {encoder_type} : {elm_type} -> Json.Encode.Value 189 | {encoder_type} struct = 190 | Json.Encode.object 191 | [ {fields} 192 | ] 193 | ", 194 | elm_type = #elm_type, 195 | encoder_type = #encoder_type, 196 | fields = ( 197 | &[ 198 | #(#field_encoders),* 199 | ] 200 | ).join("\n , "), 201 | )}; 202 | 203 | encoder 204 | } 205 | 206 | // ===== 207 | // enums 208 | // ===== 209 | 210 | // everything is contained under the variant field 211 | /// #[derive(Deserialize, Serialize)] 212 | /// enum External { 213 | /// Unit, 214 | /// Newtype(i32), 215 | /// Tuple(i32, i32), 216 | /// Struct { a: i32 }, 217 | /// } 218 | /// "\"Unit\"" 219 | /// "{\"Newtype\":0}" 220 | /// "{\"Tuple\":[0,0]}" 221 | /// "{\"Struct\":{\"a\":0}}" 222 | fn enum_external( 223 | elm_type: &str, 224 | encoder_name: &str, 225 | variants: Vec, 226 | container_attributes: &ContainerAttributes, 227 | ) -> TokenStream2 { 228 | let mut encoders = vec![]; 229 | for variant in variants { 230 | #[cfg(feature = "serde")] 231 | if variant.serde_attributes.skip { 232 | continue; 233 | } 234 | 235 | let elm_name = variant.name_elm(); 236 | let elm_name_encode = variant.name_encode(container_attributes); 237 | 238 | let encoder = match &variant.variant { 239 | EnumVariantKind::Unit => enum_variant_unit_external(&elm_name, &elm_name_encode), 240 | EnumVariantKind::Newtype(inner) => { 241 | enum_variant_newtype_external(&elm_name, &elm_name_encode, inner) 242 | } 243 | EnumVariantKind::Tuple(types) => { 244 | enum_variant_tuple_external(&elm_name, &elm_name_encode, types) 245 | } 246 | EnumVariantKind::Struct(fields) => enum_variant_struct_external( 247 | &elm_name, 248 | &elm_name_encode, 249 | fields, 250 | container_attributes, 251 | ), 252 | }; 253 | encoders.push(encoder); 254 | } 255 | 256 | let encoder = quote! {::std::format!("\ 257 | {encoder_name} : {elm_type} -> Json.Encode.Value 258 | {encoder_name} enum = 259 | case enum of 260 | {encoders}", 261 | encoder_name = #encoder_name, 262 | elm_type = #elm_type, 263 | encoders = ( 264 | &[ 265 | #(#encoders),* 266 | ] 267 | ).join("\n ") 268 | )}; 269 | encoder 270 | } 271 | 272 | // an object with a tag field 273 | /// #[derive(Deserialize, Serialize)] 274 | /// #[serde(tag = "t")] 275 | /// enum Internal { 276 | /// Unit, 277 | /// Struct { a: i32 }, 278 | /// } 279 | /// "{\"t\":\"Unit\"}" 280 | /// "{\"t\":\"Struct\",\"a\":0}" 281 | #[cfg(feature = "serde")] 282 | fn enum_internal( 283 | elm_type: &str, 284 | encoder_name: &str, 285 | variants: Vec, 286 | tag: &str, 287 | container_attributes: &ContainerAttributes, 288 | ) -> syn::Result { 289 | let mut encoders = vec![]; 290 | for variant in variants { 291 | #[cfg(feature = "serde")] 292 | if variant.serde_attributes.skip { 293 | continue; 294 | } 295 | 296 | let elm_name = variant.name_elm(); 297 | let elm_name_encode = variant.name_encode(container_attributes); 298 | 299 | let encoder = match &variant.variant { 300 | EnumVariantKind::Unit => { 301 | enum_variant_unit_internal_or_adjacent(tag, &elm_name, &elm_name_encode) 302 | } 303 | EnumVariantKind::Newtype(_) => { 304 | return Err(syn::Error::new( 305 | variant.span, 306 | "Internally tagged newtype variants are not supported by serde_json", 307 | )) 308 | } 309 | EnumVariantKind::Tuple(_) => { 310 | return Err(syn::Error::new( 311 | variant.span, 312 | "Internally tagged tuple variants are not supported by serde_json", 313 | )) 314 | } 315 | EnumVariantKind::Struct(fields) => enum_variant_struct_internal( 316 | tag, 317 | &elm_name, 318 | &elm_name_encode, 319 | fields, 320 | container_attributes, 321 | ), 322 | }; 323 | encoders.push(encoder); 324 | } 325 | 326 | let encoder = quote! {::std::format!("\ 327 | {encoder_name} : {elm_type} -> Json.Encode.Value 328 | {encoder_name} enum = 329 | case enum of 330 | {encoders}", 331 | encoder_name = #encoder_name, 332 | elm_type = #elm_type, 333 | encoders = (&[ 334 | #(#encoders),* 335 | ]).join("\n ") 336 | )}; 337 | Ok(encoder) 338 | } 339 | 340 | // an object with tag and content fields 341 | /// #[derive(Deserialize, Serialize)] 342 | /// #[serde(tag = "t", content = "c")] 343 | /// enum Adjacent { 344 | /// Unit, 345 | /// Newtype(i32), 346 | /// Tuple(i32, i32), 347 | /// Struct { a: i32 }, 348 | /// } 349 | /// "{\"t\":\"Unit\"}" 350 | /// "{\"t\":\"Newtype\",\"c\":0}" 351 | /// "{\"t\":\"Tuple\",\"c\":[0,0]}" 352 | /// "{\"t\":\"Struct\",\"c\":{\"a\":0}}" 353 | #[cfg(feature = "serde")] 354 | fn enum_adjacent( 355 | elm_type: &str, 356 | encoder_name: &str, 357 | variants: Vec, 358 | tag: &str, 359 | content: &str, 360 | container_attributes: &ContainerAttributes, 361 | ) -> syn::Result { 362 | let mut encoders = vec![]; 363 | for variant in variants { 364 | #[cfg(feature = "serde")] 365 | if variant.serde_attributes.skip { 366 | continue; 367 | } 368 | 369 | let elm_name = variant.name_elm(); 370 | let elm_name_encode = variant.name_encode(container_attributes); 371 | 372 | let encoder = match &variant.variant { 373 | EnumVariantKind::Unit => { 374 | enum_variant_unit_internal_or_adjacent(tag, &elm_name, &elm_name_encode) 375 | } 376 | EnumVariantKind::Newtype(inner) => { 377 | enum_variant_newtype_adjacent(tag, content, &elm_name, &elm_name_encode, inner) 378 | } 379 | EnumVariantKind::Tuple(types) => { 380 | enum_variant_tuple_adjacent(tag, content, &elm_name, &elm_name_encode, types) 381 | } 382 | EnumVariantKind::Struct(fields) => enum_variant_struct_adjacent( 383 | tag, 384 | content, 385 | &elm_name, 386 | &elm_name_encode, 387 | fields, 388 | container_attributes, 389 | ), 390 | }; 391 | encoders.push(encoder); 392 | } 393 | 394 | let encoder = quote! {::std::format!("\ 395 | {encoder_name} : {elm_type} -> Json.Encode.Value 396 | {encoder_name} enum = 397 | case enum of 398 | {encoders}", 399 | encoder_name = #encoder_name, 400 | elm_type = #elm_type, 401 | encoders = (&[ 402 | #(#encoders),* 403 | ]).join("\n ") 404 | )}; 405 | Ok(encoder) 406 | } 407 | 408 | // no tag 409 | /// #[derive(Deserialize, Serialize)] 410 | /// #[serde(untagged)] 411 | /// enum Untagged { 412 | /// Unit, 413 | /// Newtype(i32), 414 | /// Tuple(i32, i32), 415 | /// Struct { a: i32 }, 416 | /// } 417 | /// "null" 418 | /// "0" 419 | /// "[0,0]" 420 | /// "{\"a\":0}" 421 | #[cfg(feature = "serde")] 422 | fn enum_untagged( 423 | elm_type: &str, 424 | encoder_name: &str, 425 | variants: Vec, 426 | container_attributes: &ContainerAttributes, 427 | ) -> syn::Result { 428 | let mut encoders = vec![]; 429 | for variant in variants { 430 | #[cfg(feature = "serde")] 431 | if variant.serde_attributes.skip { 432 | continue; 433 | } 434 | 435 | let elm_name = variant.name_elm(); 436 | let encoder = match &variant.variant { 437 | EnumVariantKind::Unit => enum_variant_unit_untagged(&elm_name), 438 | EnumVariantKind::Newtype(inner) => enum_variant_newtype_untagged(&elm_name, inner), 439 | EnumVariantKind::Tuple(types) => enum_variant_tuple_untagged(&elm_name, types), 440 | EnumVariantKind::Struct(fields) => { 441 | enum_variant_struct_untagged(&elm_name, fields, container_attributes) 442 | } 443 | }; 444 | encoders.push(encoder); 445 | } 446 | 447 | let encoder = quote! {::std::format!("\ 448 | {encoder_name} : {elm_type} -> Json.Encode.Value 449 | {encoder_name} enum = 450 | case enum of 451 | {encoders}", 452 | encoder_name = #encoder_name, 453 | elm_type = #elm_type, 454 | encoders = ( 455 | &[ 456 | #(#encoders),* 457 | ] 458 | ).join("\n ") 459 | )}; 460 | Ok(encoder) 461 | } 462 | 463 | // ================= 464 | // external variants 465 | // ================= 466 | 467 | /// #[derive(Deserialize, Serialize)] 468 | /// enum External { 469 | /// Unit, 470 | /// } 471 | /// "\"Unit\"" 472 | fn enum_variant_unit_external(variant_name: &str, variant_name_encode: &str) -> TokenStream2 { 473 | quote! {::std::format!("\ 474 | {variant_name} -> 475 | Json.Encode.string \"{variant_name_encode}\"", 476 | variant_name = #variant_name, 477 | variant_name_encode = #variant_name_encode, 478 | )} 479 | } 480 | 481 | /// #[derive(Deserialize, Serialize)] 482 | /// enum External { 483 | /// Newtype(i32), 484 | /// } 485 | /// "{\"Newtype\":0}" 486 | fn enum_variant_newtype_external( 487 | variant_name: &str, 488 | variant_name_encode: &str, 489 | inner_type: &TokenStream2, 490 | ) -> TokenStream2 { 491 | quote! { format!("\ 492 | {variant_name} inner -> 493 | Json.Encode.object [ ( \"{variant_name_encode}\", {encoder} inner ) ]", 494 | variant_name = #variant_name, 495 | variant_name_encode = #variant_name_encode, 496 | encoder = <#inner_type as ::elm_rs::ElmEncode>::encoder_type(), 497 | )} 498 | } 499 | 500 | /// #[derive(Deserialize, Serialize)] 501 | /// enum External { 502 | /// Tuple(i32, i32), 503 | /// } 504 | /// "{\"Tuple\":[0,0]}" 505 | fn enum_variant_tuple_external( 506 | variant_name: &str, 507 | variant_name_encode: &str, 508 | tuple_types: &[TokenStream2], 509 | ) -> TokenStream2 { 510 | let idx: Vec = (0..tuple_types.len()).collect(); 511 | quote! {::std::format!("\ 512 | {variant_name} {fields} -> 513 | Json.Encode.object [ ( \"{variant_name_encode}\", Json.Encode.list identity [ {encoders} ] ) ]", 514 | variant_name = #variant_name, 515 | fields = ( 516 | &[ 517 | #(::std::format!("t{}", #idx) 518 | ),* 519 | ] 520 | ).join(" "), 521 | variant_name_encode = #variant_name_encode, 522 | encoders = ( 523 | &[ 524 | #(::std::format!("{} t{}", 525 | <#tuple_types as ::elm_rs::ElmEncode>::encoder_type(), 526 | #idx 527 | )),* 528 | ] 529 | ).join(", "), 530 | )} 531 | } 532 | 533 | /// #[derive(Deserialize, Serialize)] 534 | /// enum External { 535 | /// Struct { a: i32 }, 536 | /// } 537 | /// "{\"Struct\":{\"a\":0}}" 538 | fn enum_variant_struct_external( 539 | variant_name: &str, 540 | variant_name_encode: &str, 541 | fields: &[StructField], 542 | container_attributes: &ContainerAttributes, 543 | ) -> TokenStream2 { 544 | let (field_names, tys): (Vec<_>, Vec<_>) = fields 545 | .iter() 546 | .map(|field| (field.name_elm(), &field.ty)) 547 | .unzip(); 548 | let field_names_serialize = fields 549 | .iter() 550 | .map(|field| field.name_encode(container_attributes)); 551 | 552 | quote! {::std::format!("\ 553 | {variant_name} {{ {fields} }} -> 554 | Json.Encode.object [ ( \"{variant_name_encode}\", Json.Encode.object [ {encoders} ] ) ]", 555 | variant_name = #variant_name, 556 | variant_name_encode = #variant_name_encode, 557 | fields = ( 558 | &[ 559 | #(::std::format!("{}", #field_names, 560 | )),* 561 | ] 562 | ).join(", "), 563 | encoders = ( 564 | &[ 565 | #(::std::format!("( \"{}\", ({}) {} )", 566 | #field_names_serialize, 567 | <#tys as ::elm_rs::ElmEncode>::encoder_type(), 568 | #field_names, 569 | )),* 570 | ] 571 | ).join(", ") 572 | )} 573 | } 574 | 575 | // ================= 576 | // internal variants 577 | // ================= 578 | 579 | /// #[derive(Deserialize, Serialize)] 580 | /// #[serde(tag = "t")] 581 | /// enum Internal { 582 | /// Unit, 583 | /// } 584 | /// #[derive(Deserialize, Serialize)] 585 | /// #[serde(tag = "t", content = "c")] 586 | /// enum Adjacent { 587 | /// Unit, 588 | /// } 589 | /// "{\"t\":\"Unit\"}" 590 | #[cfg(feature = "serde")] 591 | fn enum_variant_unit_internal_or_adjacent( 592 | tag: &str, 593 | variant_name: &str, 594 | variant_name_encode: &str, 595 | ) -> TokenStream2 { 596 | quote! {::std::format!("\ 597 | {variant_name} -> 598 | Json.Encode.object [ ( \"{tag}\", Json.Encode.string \"{variant_name_encode}\" ) ]", 599 | variant_name = #variant_name, 600 | tag = #tag, 601 | variant_name_encode = #variant_name_encode, 602 | )} 603 | } 604 | 605 | /// #[derive(Deserialize, Serialize)] 606 | /// #[serde(tag = "t")] 607 | /// enum Internal { 608 | /// Struct { a: i32 }, 609 | /// } 610 | /// "{\"t\":\"Struct\",\"a\":0}" 611 | #[cfg(feature = "serde")] 612 | fn enum_variant_struct_internal( 613 | tag: &str, 614 | variant_name: &str, 615 | variant_name_encode: &str, 616 | fields: &[StructField], 617 | container_attributes: &ContainerAttributes, 618 | ) -> TokenStream2 { 619 | let (field_names, tys): (Vec<_>, Vec<_>) = fields 620 | .iter() 621 | .map(|field| (field.name_elm(), &field.ty)) 622 | .unzip(); 623 | let field_names_serialize = fields 624 | .iter() 625 | .map(|field| field.name_encode(container_attributes)); 626 | 627 | quote! {::std::format!("\ 628 | {variant_name} {{ {fields} }} -> 629 | Json.Encode.object [ ( \"{tag}\", Json.Encode.string \"{variant_name_encode}\" ), {encoders} ]", 630 | variant_name = #variant_name, 631 | fields = (&[ 632 | #(#field_names),* 633 | ]).join(", "), 634 | tag = #tag, 635 | variant_name_encode = #variant_name_encode, 636 | encoders = ( 637 | &[ 638 | #(::std::format!("( \"{}\", ({}) {} )", 639 | #field_names_serialize, 640 | <#tys>::encoder_type(), 641 | #field_names, 642 | )),* 643 | ] 644 | ).join(", "), 645 | )} 646 | } 647 | 648 | // ################# 649 | // adjacent variants 650 | // ################# 651 | 652 | /// #[derive(Deserialize, Serialize)] 653 | /// #[serde(tag = "t", content = "c")] 654 | /// enum Adjacent { 655 | /// Newtype(i32), 656 | /// } 657 | /// "{\"t\":\"Newtype\",\"c\":0}" 658 | #[cfg(feature = "serde")] 659 | fn enum_variant_newtype_adjacent( 660 | tag: &str, 661 | content: &str, 662 | variant_name: &str, 663 | variant_name_encode: &str, 664 | inner_type: &TokenStream2, 665 | ) -> TokenStream2 { 666 | quote! { format!("\ 667 | {variant_name} inner -> 668 | Json.Encode.object [ ( \"{tag}\", Json.Encode.string \"{variant_name_encode}\"), ( \"{content}\", {encoder} inner ) ]", 669 | variant_name = #variant_name, 670 | tag = #tag, 671 | content = #content, 672 | variant_name_encode = #variant_name_encode, 673 | encoder = <#inner_type as ::elm_rs::ElmEncode>::encoder_type(), 674 | )} 675 | } 676 | 677 | /// #[derive(Deserialize, Serialize)] 678 | /// #[serde(tag = "t", content = "c")] 679 | /// enum Adjacent { 680 | /// Tuple(i32, i32), 681 | /// } 682 | /// "{\"t\":\"Tuple\",\"c\":[0,0]}" 683 | #[cfg(feature = "serde")] 684 | fn enum_variant_tuple_adjacent( 685 | tag: &str, 686 | content: &str, 687 | variant_name: &str, 688 | variant_name_encode: &str, 689 | tuple_types: &[TokenStream2], 690 | ) -> TokenStream2 { 691 | let idx: Vec = (0..tuple_types.len()).collect(); 692 | 693 | quote! { format!("\ 694 | {variant_name} {params} -> 695 | Json.Encode.object [ ( \"{tag}\", Json.Encode.string \"{variant_name_encode}\"), ( \"{content}\", Json.Encode.list identity [ {encoders} ] ) ]", 696 | variant_name = #variant_name, 697 | params = ( 698 | &[ 699 | #(::std::format!("t{}", #idx) 700 | ),* 701 | ] 702 | ).join(" "), 703 | tag = #tag, 704 | content = #content, 705 | variant_name_encode = #variant_name_encode, 706 | encoders = ( 707 | &[ 708 | #(::std::format!("{} t{}", <#tuple_types as ::elm_rs::ElmEncode>::encoder_type(), #idx) 709 | ),* 710 | ] 711 | ).join(", "), 712 | )} 713 | } 714 | 715 | /// #[derive(Deserialize, Serialize)] 716 | /// #[serde(tag = "t", content = "c")] 717 | /// enum Adjacent { 718 | /// Struct { a: i32 }, 719 | /// } 720 | /// "{\"t\":\"Struct\",\"c\":{\"a\":0}}" 721 | #[cfg(feature = "serde")] 722 | fn enum_variant_struct_adjacent( 723 | tag: &str, 724 | content: &str, 725 | variant_name: &str, 726 | variant_name_encode: &str, 727 | fields: &[StructField], 728 | container_attributes: &ContainerAttributes, 729 | ) -> TokenStream2 { 730 | let (field_names, tys): (Vec<_>, Vec<_>) = fields 731 | .iter() 732 | .map(|field| (field.name_elm(), &field.ty)) 733 | .unzip(); 734 | let field_names_serialize = fields 735 | .iter() 736 | .map(|field| field.name_encode(container_attributes)); 737 | 738 | quote! { format!("\ 739 | {variant_name} {{ {fields} }} -> 740 | Json.Encode.object [ ( \"{tag}\", Json.Encode.string \"{variant_name_encode}\"), ( \"{content}\", Json.Encode.object [ {encoders} ] ) ]", 741 | variant_name = #variant_name, 742 | fields = ( 743 | &[ 744 | #(::std::format!("{}", #field_names)),* 745 | ] 746 | ).join(", "), 747 | tag = #tag, 748 | content = #content, 749 | variant_name_encode = #variant_name_encode, 750 | encoders = ( 751 | &[ 752 | #(::std::format!("( \"{}\", ({}) {} )", 753 | #field_names_serialize, 754 | <#tys as ::elm_rs::ElmEncode>::encoder_type(), 755 | #field_names, 756 | )),* 757 | ] 758 | ).join(", "), 759 | )} 760 | } 761 | 762 | // ################# 763 | // untagged variants 764 | // ################# 765 | 766 | /// #[derive(Deserialize, Serialize)] 767 | /// #[serde(untagged)] 768 | /// enum Untagged { 769 | /// Unit, 770 | /// } 771 | /// "null" 772 | #[cfg(feature = "serde")] 773 | fn enum_variant_unit_untagged(variant_name: &str) -> TokenStream2 { 774 | quote! {::std::format!("\ 775 | {variant_name} -> 776 | Json.Encode.null", 777 | variant_name = #variant_name 778 | )} 779 | } 780 | 781 | /// #[derive(Deserialize, Serialize)] 782 | /// #[serde(untagged)] 783 | /// enum Untagged { 784 | /// Newtype(i32), 785 | /// } 786 | /// "0" 787 | #[cfg(feature = "serde")] 788 | fn enum_variant_newtype_untagged(variant_name: &str, inner: &TokenStream2) -> TokenStream2 { 789 | quote! {::std::format!("\ 790 | {variant_name} inner -> 791 | {encoder} inner", 792 | variant_name = #variant_name, 793 | encoder = <#inner as ::elm_rs::ElmEncode>::encoder_type(), 794 | )} 795 | } 796 | 797 | /// #[derive(Deserialize, Serialize)] 798 | /// #[serde(untagged)] 799 | /// enum Untagged { 800 | /// Tuple(i32, i32), 801 | /// } 802 | /// "[0,0]" 803 | #[cfg(feature = "serde")] 804 | fn enum_variant_tuple_untagged(variant_name: &str, tuple_types: &[TokenStream2]) -> TokenStream2 { 805 | let idx: Vec = (0..tuple_types.len()).collect(); 806 | let encoder = quote! {::std::format!("\ 807 | {variant_name} {params} -> 808 | Json.Encode.list identity 809 | [ {encoders} 810 | ]", 811 | variant_name = #variant_name, 812 | params = ( 813 | &[ 814 | #(::std::format!("t{idx}", 815 | idx = #idx 816 | )),* 817 | ] 818 | ).join(" "), 819 | encoders = ( 820 | &[ 821 | #(::std::format!("({}) t{}", 822 | <#tuple_types as ::elm_rs::ElmEncode>::encoder_type(), 823 | #idx 824 | )),* 825 | ] 826 | ).join("\n , "), 827 | )}; 828 | 829 | encoder 830 | } 831 | 832 | /// #[derive(Deserialize, Serialize)] 833 | /// #[serde(untagged)] 834 | /// enum Untagged { 835 | /// Struct { a: i32 }, 836 | /// } 837 | /// "{\"a\":0}" 838 | #[cfg(feature = "serde")] 839 | fn enum_variant_struct_untagged( 840 | variant_name: &str, 841 | fields: &[StructField], 842 | container_attributes: &ContainerAttributes, 843 | ) -> TokenStream2 { 844 | let (field_names, tys): (Vec<_>, Vec<_>) = fields 845 | .iter() 846 | .map(|field| (field.name_elm(), &field.ty)) 847 | .unzip(); 848 | let field_names_serialize = fields 849 | .iter() 850 | .map(|field| field.name_encode(container_attributes)); 851 | quote! {::std::format!("\ 852 | {variant_name} {{ {fields} }} -> 853 | Json.Encode.object [ {encoders} ]", 854 | variant_name = #variant_name, 855 | fields = ( 856 | &[ 857 | #(::std::format!("{}", #field_names, 858 | )),* 859 | ] 860 | ).join(", "), 861 | encoders = ( 862 | &[ 863 | #(::std::format!("( \"{}\", ({}) {} )", 864 | #field_names_serialize, 865 | <#tys as ::elm_rs::ElmEncode>::encoder_type(), 866 | #field_names, 867 | )),* 868 | ] 869 | ).join(", ") 870 | )} 871 | } 872 | -------------------------------------------------------------------------------- /elm_rs_derive/src/elm_query.rs: -------------------------------------------------------------------------------- 1 | //! Derive macro for ElmQuery. 2 | 3 | use crate::{Intermediate, TypeInfo}; 4 | use proc_macro::TokenStream; 5 | use proc_macro2::TokenStream as TokenStream2; 6 | use quote::quote; 7 | use syn::{parse_macro_input, DeriveInput}; 8 | 9 | pub fn derive(input: TokenStream) -> TokenStream { 10 | let derive_input = parse_macro_input!(input as DeriveInput); 11 | let intermediate = match Intermediate::parse(derive_input) { 12 | Ok(intermediate) => intermediate, 13 | Err(err) => return TokenStream::from(err.to_compile_error()), 14 | }; 15 | let token_stream = match intermediate_to_token_stream(intermediate) { 16 | Ok(token_stream) => token_stream, 17 | Err(err) => return TokenStream::from(err.to_compile_error()), 18 | }; 19 | TokenStream::from(token_stream) 20 | } 21 | 22 | fn intermediate_to_token_stream( 23 | Intermediate { 24 | ident, 25 | elm_type, 26 | mut generics, 27 | generics_without_bounds, 28 | type_info, 29 | container_attributes, 30 | }: Intermediate, 31 | ) -> syn::Result { 32 | let ts = match type_info { 33 | TypeInfo::Struct(fields) => { 34 | let mut query_fields = vec![]; 35 | for field in fields { 36 | let ty = &field.ty; 37 | let field_name = field.name_elm(); 38 | let field_name_encode = field.name_encode(&container_attributes); 39 | query_fields.push(quote! {::std::format!("\ 40 | {field_type} \"{field_name_encode}\" ({query_field_encoder} {field_value})", 41 | field_type = <#ty as ::elm_rs::ElmQueryField>::query_field_type(), 42 | query_field_encoder = <#ty as ::elm_rs::ElmQueryField>::query_field_encoder_name(), 43 | field_name_encode = #field_name_encode, 44 | field_value = ::std::format!("struct.{field_name}", 45 | field_name = #field_name 46 | ), 47 | )}); 48 | } 49 | quote! {::std::format!("\ 50 | urlEncode{elm_type} : {elm_type} -> List Url.Builder.QueryParameter 51 | urlEncode{elm_type} struct = 52 | [ {fields} ] 53 | ", 54 | elm_type = #elm_type, 55 | fields = ( 56 | &[ 57 | #(#query_fields),* 58 | ] 59 | ).join(", ") 60 | )} 61 | } 62 | _ => { 63 | return Err(syn::Error::new( 64 | ident.span(), 65 | "only structs with named fields are allowed", 66 | )) 67 | } 68 | }; 69 | 70 | for p in generics.type_params_mut() { 71 | p.bounds.push(syn::parse_str("::elm_rs::Elm").unwrap()); 72 | p.bounds 73 | .push(syn::parse_str("::elm_rs::ElmQueryField").unwrap()); 74 | } 75 | 76 | let res = quote! { 77 | impl #generics ::elm_rs::ElmQuery for #ident #generics_without_bounds { 78 | fn elm_query() -> ::std::string::String { 79 | #ts 80 | } 81 | } 82 | }; 83 | Ok(res) 84 | } 85 | -------------------------------------------------------------------------------- /elm_rs_derive/src/elm_query_field.rs: -------------------------------------------------------------------------------- 1 | //! Derive macro for ElmQuery. 2 | 3 | use crate::{EnumVariantKind, Intermediate, TypeInfo}; 4 | use proc_macro::TokenStream; 5 | use proc_macro2::TokenStream as TokenStream2; 6 | use quote::quote; 7 | use syn::{parse_macro_input, DeriveInput}; 8 | 9 | pub fn derive(input: TokenStream) -> TokenStream { 10 | let derive_input = parse_macro_input!(input as DeriveInput); 11 | let intermediate = match Intermediate::parse(derive_input) { 12 | Ok(intermediate) => intermediate, 13 | Err(err) => return TokenStream::from(err.to_compile_error()), 14 | }; 15 | let token_stream = match intermediate_to_token_stream(intermediate) { 16 | Ok(token_stream) => token_stream, 17 | Err(err) => return TokenStream::from(err.to_compile_error()), 18 | }; 19 | TokenStream::from(token_stream) 20 | } 21 | 22 | fn intermediate_to_token_stream( 23 | Intermediate { 24 | ident, 25 | elm_type, 26 | mut generics, 27 | generics_without_bounds, 28 | type_info, 29 | container_attributes, 30 | }: Intermediate, 31 | ) -> syn::Result { 32 | let query_field_encoder_name = format!("queryFieldEncoder{elm_type}"); 33 | let ts = match type_info { 34 | TypeInfo::Enum { variants, .. } => { 35 | let mut branches = Vec::new(); 36 | for variant in variants { 37 | if let EnumVariantKind::Unit = variant.variant { 38 | let elm_name = variant.name_elm(); 39 | let name_encode = variant.name_encode(&container_attributes); 40 | branches.push(format!("{elm_name} -> \"{name_encode}\"")); 41 | } else { 42 | return Err(syn::Error::new( 43 | variant.span, 44 | "only unit variants are allowed", 45 | )); 46 | } 47 | } 48 | 49 | quote! {::std::format!("\ 50 | {function_name} : {elm_type} -> String 51 | {function_name} var = 52 | case var of 53 | {branches} 54 | ", 55 | function_name = #query_field_encoder_name, 56 | elm_type = #elm_type, 57 | branches = ( 58 | &[ 59 | #(#branches),* 60 | ] 61 | ).join("\n ") 62 | )} 63 | } 64 | _ => return Err(syn::Error::new(ident.span(), "only enums are allowed")), 65 | }; 66 | 67 | for p in generics.type_params_mut() { 68 | p.bounds.push(syn::parse_str("::elm_rs::Elm").unwrap()); 69 | p.bounds 70 | .push(syn::parse_str("::elm_rs::ElmQueryField").unwrap()); 71 | } 72 | 73 | let res = quote! { 74 | impl #generics ::elm_rs::ElmQueryField for #ident #generics_without_bounds { 75 | fn query_field_type() -> &'static str { 76 | "Url.Builder.string" 77 | } 78 | 79 | fn query_field_encoder_name() -> &'static str { 80 | #query_field_encoder_name 81 | } 82 | 83 | fn query_field_encoder_definition() -> Option { 84 | Some(#ts) 85 | } 86 | } 87 | }; 88 | Ok(res) 89 | } 90 | -------------------------------------------------------------------------------- /elm_rs_derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Derive macros for elm_rs. 2 | 3 | mod attributes; 4 | mod elm; 5 | #[cfg(feature = "json")] 6 | mod elm_decode; 7 | #[cfg(feature = "json")] 8 | mod elm_encode; 9 | #[cfg(feature = "query")] 10 | mod elm_query; 11 | #[cfg(feature = "query")] 12 | mod elm_query_field; 13 | 14 | use self::attributes::{ContainerAttributes, FieldAttributes, VariantAttributes}; 15 | use heck::{ToLowerCamelCase, ToPascalCase}; 16 | use proc_macro::TokenStream; 17 | use proc_macro2::{Span, TokenStream as TokenStream2}; 18 | use quote::ToTokens; 19 | use std::borrow::Cow; 20 | use syn::{ 21 | punctuated::Punctuated, spanned::Spanned, Data, DataEnum, DeriveInput, Fields, FieldsNamed, 22 | Generics, Ident, Type, Variant, 23 | }; 24 | 25 | /// Derive `Elm`. 26 | #[proc_macro_derive(Elm)] 27 | pub fn derive_elm(input: TokenStream) -> TokenStream { 28 | elm::derive(input) 29 | } 30 | 31 | /// Derive `ElmEncode`. 32 | #[cfg(feature = "json")] 33 | #[proc_macro_derive(ElmEncode)] 34 | pub fn derive_elm_serialize(input: TokenStream) -> TokenStream { 35 | elm_encode::derive(input) 36 | } 37 | 38 | /// Derive `ElmDecode`. 39 | #[cfg(feature = "json")] 40 | #[proc_macro_derive(ElmDecode)] 41 | pub fn derive_elm_deserialize(input: TokenStream) -> TokenStream { 42 | elm_decode::derive(input) 43 | } 44 | 45 | /// Derive `ElmQuery`. 46 | #[cfg(feature = "query")] 47 | #[proc_macro_derive(ElmQuery)] 48 | pub fn derive_elm_query(input: TokenStream) -> TokenStream { 49 | elm_query::derive(input) 50 | } 51 | 52 | /// Derive `ElmQueryField`. 53 | #[cfg(feature = "query")] 54 | #[proc_macro_derive(ElmQueryField)] 55 | pub fn derive_elm_query_field(input: TokenStream) -> TokenStream { 56 | elm_query_field::derive(input) 57 | } 58 | 59 | /// Intermediate representation of the derive input for more convenient handling. 60 | struct Intermediate { 61 | ident: Ident, 62 | elm_type: String, 63 | generics: Generics, 64 | generics_without_bounds: Generics, 65 | type_info: TypeInfo, 66 | container_attributes: ContainerAttributes, 67 | } 68 | 69 | impl Intermediate { 70 | // parses the input to an intermediate representation that's convenient to turn into the end result 71 | fn parse(input: DeriveInput) -> syn::Result { 72 | let container_attributes = ContainerAttributes::parse(&input.attrs)?; 73 | let type_info = TypeInfo::parse(input.data, &container_attributes)?; 74 | 75 | let elm_type = input.ident.to_string().to_pascal_case(); 76 | let mut generics_without_bounds = input.generics.clone(); 77 | for p in generics_without_bounds.type_params_mut() { 78 | p.bounds = Punctuated::default(); 79 | } 80 | Ok(Self { 81 | ident: input.ident, 82 | elm_type, 83 | generics: input.generics, 84 | generics_without_bounds, 85 | type_info, 86 | container_attributes, 87 | }) 88 | } 89 | } 90 | 91 | enum TypeInfo { 92 | // struct S; 93 | Unit, 94 | // struct S(String); 95 | Newtype(Box), 96 | // struct S(String, u32); 97 | Tuple(Vec), 98 | // struct S { 99 | // s: String, 100 | // } 101 | Struct(Vec), 102 | // enum E { 103 | // Variant, 104 | // } 105 | Enum { 106 | variants: Vec, 107 | #[cfg(feature = "serde")] 108 | representation: attributes::serde::EnumRepresentation, 109 | }, 110 | } 111 | 112 | impl TypeInfo { 113 | pub fn parse(data: Data, container_attributes: &ContainerAttributes) -> syn::Result { 114 | let type_info = match data { 115 | Data::Struct(data_struct) => match data_struct.fields { 116 | Fields::Unit => TypeInfo::Unit, 117 | Fields::Unnamed(unnamed) => { 118 | if unnamed.unnamed.len() == 1 { 119 | TypeInfo::Newtype(Box::new(unnamed.unnamed.into_iter().next().unwrap().ty)) 120 | } else { 121 | TypeInfo::Tuple(unnamed.unnamed.into_iter().map(|field| field.ty).collect()) 122 | } 123 | } 124 | Fields::Named(named) => { 125 | #[cfg(not(feature = "serde"))] 126 | let transparent = false; 127 | #[cfg(feature = "serde")] 128 | let transparent = container_attributes.serde.transparent; 129 | if transparent && named.named.len() == 1 { 130 | TypeInfo::Newtype(Box::new(named.named.into_iter().next().unwrap().ty)) 131 | } else { 132 | TypeInfo::Struct(StructField::parse(named)?) 133 | } 134 | } 135 | }, 136 | Data::Enum(DataEnum { variants, .. }) => { 137 | if variants.is_empty() { 138 | return Err(syn::Error::new( 139 | variants.span(), 140 | "empty enums are not supported", 141 | )); 142 | } 143 | let variants = variants 144 | .into_iter() 145 | .map(EnumVariant::parse) 146 | .collect::>()?; 147 | 148 | TypeInfo::Enum { 149 | #[cfg(feature = "serde")] 150 | representation: container_attributes.serde.enum_representation.clone(), 151 | variants, 152 | } 153 | } 154 | Data::Union(union) => { 155 | return Err(syn::Error::new( 156 | union.union_token.span(), 157 | "unions are not supported", 158 | )) 159 | } 160 | }; 161 | Ok(type_info) 162 | } 163 | } 164 | 165 | struct StructField { 166 | ident: Ident, 167 | // todo 168 | // aliases: Vec, 169 | ty: TokenStream2, 170 | #[cfg(feature = "serde")] 171 | serde_attributes: attributes::serde::FieldAttributes, 172 | } 173 | 174 | impl StructField { 175 | /// The name in the Elm type definition. Always camelCased for consistency with Elm style guidelines. 176 | fn name_elm(&self) -> String { 177 | self.ident.to_string().to_lower_camel_case() 178 | } 179 | 180 | #[cfg(any(feature = "json", feature = "query"))] 181 | fn name_encode(&self, container_attributes: &ContainerAttributes) -> String { 182 | // rename during Rust deserialization = needs rename during Elm encoding 183 | // explicit rename has priority 184 | #[cfg(feature = "serde")] 185 | if let Some(rename) = self 186 | .serde_attributes 187 | .rename 188 | .as_ref() 189 | .or(self.serde_attributes.rename_deserialize.as_ref()) 190 | { 191 | rename.clone() 192 | } else if let Some(rename_all) = container_attributes 193 | .serde 194 | .rename_all 195 | .or(container_attributes.serde.rename_all_deserialize) 196 | { 197 | rename_all.rename_ident(&self.ident) 198 | } else { 199 | self.ident.to_string() 200 | } 201 | #[cfg(not(feature = "serde"))] 202 | self.ident.to_string() 203 | } 204 | 205 | #[cfg(any(feature = "json", feature = "query"))] 206 | fn name_decode(&self, container_attributes: &ContainerAttributes) -> String { 207 | // rename during Rust serialization = needs rename during Elm decoding 208 | // explicit rename has priority 209 | #[cfg(feature = "serde")] 210 | if let Some(rename) = self 211 | .serde_attributes 212 | .rename 213 | .as_ref() 214 | .or(self.serde_attributes.rename_serialize.as_ref()) 215 | { 216 | rename.to_string() 217 | } else if let Some(rename_all) = container_attributes 218 | .serde 219 | .rename_all 220 | .or(container_attributes.serde.rename_all_serialize) 221 | { 222 | rename_all.rename_ident(&self.ident) 223 | } else { 224 | self.ident.to_string() 225 | } 226 | #[cfg(not(feature = "serde"))] 227 | self.ident.to_string() 228 | } 229 | 230 | fn parse(fields: FieldsNamed) -> syn::Result> { 231 | let mut parsed = Vec::new(); 232 | for field in fields.named { 233 | let attributes = FieldAttributes::parse(&field.attrs)?; 234 | #[cfg(feature = "serde")] 235 | if attributes.serde.skip { 236 | continue; 237 | } 238 | parsed.push(StructField { 239 | ident: field.ident.unwrap(), // only tuple struct fields are unnamed 240 | // todo 241 | // aliases: field_attributes.serde.aliases, 242 | ty: field.ty.to_token_stream(), 243 | #[cfg(feature = "serde")] 244 | serde_attributes: attributes.serde, 245 | }); 246 | } 247 | Ok(parsed) 248 | } 249 | } 250 | 251 | struct EnumVariant { 252 | ident: Ident, 253 | variant: EnumVariantKind, 254 | span: Span, 255 | #[cfg(feature = "serde")] 256 | serde_attributes: attributes::serde::VariantAttributes, 257 | } 258 | 259 | impl EnumVariant { 260 | /// The name in the Elm type definition. Always PascalCased for consistency with Elm style guidelines. 261 | fn name_elm(&'_ self) -> Cow<'_, str> { 262 | self.ident.to_string().to_pascal_case().into() 263 | } 264 | 265 | #[cfg(any(feature = "json", feature = "query"))] 266 | fn name_encode(&self, container_attributes: &ContainerAttributes) -> String { 267 | // rename during Rust deserialization = needs rename during Elm encoding 268 | // explicit rename has priority 269 | #[cfg(feature = "serde")] 270 | if let Some(rename) = self 271 | .serde_attributes 272 | .rename 273 | .as_ref() 274 | .or(self.serde_attributes.rename_deserialize.as_ref()) 275 | { 276 | rename.clone() 277 | } else if let Some(rename_all) = container_attributes 278 | .serde 279 | .rename_all 280 | .or(container_attributes.serde.rename_all_deserialize) 281 | { 282 | rename_all.rename_ident(&self.ident) 283 | } else { 284 | self.ident.to_string() 285 | } 286 | #[cfg(not(feature = "serde"))] 287 | self.ident.to_string() 288 | } 289 | 290 | #[cfg(any(feature = "json", feature = "query"))] 291 | fn name_decode(&self, container_attributes: &ContainerAttributes) -> String { 292 | // rename during Rust serialization = needs rename during Elm decoding 293 | // explicit rename has priority 294 | #[cfg(feature = "serde")] 295 | if let Some(rename) = self 296 | .serde_attributes 297 | .rename 298 | .as_ref() 299 | .or(self.serde_attributes.rename_serialize.as_ref()) 300 | { 301 | rename.to_string() 302 | } else if let Some(rename_all) = container_attributes 303 | .serde 304 | .rename_all 305 | .or(container_attributes.serde.rename_all_serialize) 306 | { 307 | rename_all.rename_ident(&self.ident) 308 | } else { 309 | self.ident.to_string() 310 | } 311 | #[cfg(not(feature = "serde"))] 312 | self.ident.to_string() 313 | } 314 | 315 | fn parse(variant: Variant) -> syn::Result { 316 | let span = variant.span(); 317 | let variant_attributes = VariantAttributes::parse(&variant.attrs)?; 318 | let variant_kind = match variant.fields { 319 | Fields::Unit => EnumVariantKind::Unit, 320 | Fields::Unnamed(unnamed) if unnamed.unnamed.len() == 1 => EnumVariantKind::Newtype( 321 | unnamed 322 | .unnamed 323 | .into_iter() 324 | .next() 325 | .unwrap() 326 | .ty 327 | .to_token_stream(), 328 | ), 329 | Fields::Unnamed(unnamed) => EnumVariantKind::Tuple( 330 | unnamed 331 | .unnamed 332 | .into_iter() 333 | .map(|field| field.ty.to_token_stream()) 334 | .collect(), 335 | ), 336 | Fields::Named(named) => EnumVariantKind::Struct(StructField::parse(named)?), 337 | }; 338 | let variant = EnumVariant { 339 | ident: variant.ident, 340 | variant: variant_kind, 341 | span, 342 | #[cfg(feature = "serde")] 343 | serde_attributes: variant_attributes.serde, 344 | }; 345 | Ok(variant) 346 | } 347 | } 348 | 349 | enum EnumVariantKind { 350 | // Variant, 351 | // "Variant" 352 | Unit, 353 | // Variant(String), 354 | // {"Variant": "string"} 355 | Newtype(TokenStream2), // e.g. Vec 356 | // Variant(String, u32), 357 | // {"Variant": []} 358 | // {"Variant": ["string", 0]} 359 | Tuple(Vec), // e.g. [Vec, String] 360 | // Variant { 361 | // s: String, 362 | // } 363 | // {} 364 | // {"s": "string"} 365 | Struct(Vec), 366 | } 367 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | imports_granularity = "Crate" 2 | group_imports = "One" 3 | --------------------------------------------------------------------------------