├── .cargo └── config.toml ├── .github ├── FUNDING.yml └── workflows │ └── main.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── benchmarks ├── .gitignore ├── Cargo.toml ├── data │ ├── canada.json │ ├── citm_catalog.json │ └── twitter.json ├── index.mjs ├── package-lock.json ├── package.json └── src │ ├── canada.rs │ ├── citm_catalog.rs │ ├── color.rs │ ├── lib.rs │ ├── prim_str.rs │ └── twitter.rs ├── src ├── de.rs ├── error.rs ├── lib.rs └── ser.rs └── tests ├── browser.rs ├── common └── mod.rs └── node.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | # Compare sizes using non-trapping float-to-int conversions. 3 | # This feature has landed long ago in all engines, but Rust doesn't enable it by default yet. 4 | rustflags = ["-C", "target-feature=+nontrapping-fptoint,+bulk-memory"] 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: RReverser # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v2 12 | - name: Install Rust 13 | uses: hecrj/setup-rust-action@v1 14 | with: 15 | components: clippy,rustfmt 16 | - name: Install wasm-pack 17 | uses: jetli/wasm-pack-action@v0.3.0 18 | - name: Run tests in Node.js 19 | run: wasm-pack test --node 20 | - name: Run tests in Chrome 21 | run: wasm-pack test --headless --chrome 22 | - name: Run tests in Firefox 23 | run: wasm-pack test --headless --firefox 24 | - name: Clippy checks 25 | run: cargo clippy --all-targets -- -D warnings 26 | - name: Format 27 | run: cargo fmt 28 | - name: Commit changes if any 29 | uses: stefanzweifel/git-auto-commit-action@v4 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /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 = "autocfg" 7 | version = "1.1.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 10 | 11 | [[package]] 12 | name = "bincode" 13 | version = "1.3.3" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" 16 | dependencies = [ 17 | "serde", 18 | ] 19 | 20 | [[package]] 21 | name = "bitflags" 22 | version = "1.3.2" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 25 | 26 | [[package]] 27 | name = "bumpalo" 28 | version = "3.14.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" 31 | 32 | [[package]] 33 | name = "byteorder" 34 | version = "1.5.0" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 37 | 38 | [[package]] 39 | name = "cfg-if" 40 | version = "1.0.0" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 43 | 44 | [[package]] 45 | name = "console_error_panic_hook" 46 | version = "0.1.7" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" 49 | dependencies = [ 50 | "cfg-if", 51 | "wasm-bindgen", 52 | ] 53 | 54 | [[package]] 55 | name = "getrandom" 56 | version = "0.2.11" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" 59 | dependencies = [ 60 | "cfg-if", 61 | "js-sys", 62 | "libc", 63 | "wasi", 64 | "wasm-bindgen", 65 | ] 66 | 67 | [[package]] 68 | name = "itoa" 69 | version = "1.0.10" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" 72 | 73 | [[package]] 74 | name = "js-sys" 75 | version = "0.3.66" 76 | source = "registry+https://github.com/rust-lang/crates.io-index" 77 | checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" 78 | dependencies = [ 79 | "wasm-bindgen", 80 | ] 81 | 82 | [[package]] 83 | name = "lazy_static" 84 | version = "1.4.0" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 87 | 88 | [[package]] 89 | name = "libc" 90 | version = "0.2.150" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" 93 | 94 | [[package]] 95 | name = "log" 96 | version = "0.4.20" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 99 | 100 | [[package]] 101 | name = "maplit" 102 | version = "1.0.2" 103 | source = "registry+https://github.com/rust-lang/crates.io-index" 104 | checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" 105 | 106 | [[package]] 107 | name = "num-traits" 108 | version = "0.2.17" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" 111 | dependencies = [ 112 | "autocfg", 113 | ] 114 | 115 | [[package]] 116 | name = "once_cell" 117 | version = "1.19.0" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 120 | 121 | [[package]] 122 | name = "paste" 123 | version = "1.0.14" 124 | source = "registry+https://github.com/rust-lang/crates.io-index" 125 | checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" 126 | 127 | [[package]] 128 | name = "ppv-lite86" 129 | version = "0.2.17" 130 | source = "registry+https://github.com/rust-lang/crates.io-index" 131 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 132 | 133 | [[package]] 134 | name = "proc-macro2" 135 | version = "1.0.70" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" 138 | dependencies = [ 139 | "unicode-ident", 140 | ] 141 | 142 | [[package]] 143 | name = "proptest" 144 | version = "1.0.0" 145 | source = "git+https://github.com/RReverser/proptest?branch=fix-wasm#5e29d23494e1b0ead32c4a146c430b98d81a6fcc" 146 | dependencies = [ 147 | "bitflags", 148 | "byteorder", 149 | "lazy_static", 150 | "num-traits", 151 | "quick-error", 152 | "rand", 153 | "rand_chacha", 154 | "rand_xorshift", 155 | "regex-syntax", 156 | ] 157 | 158 | [[package]] 159 | name = "quick-error" 160 | version = "2.0.1" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" 163 | 164 | [[package]] 165 | name = "quote" 166 | version = "1.0.33" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" 169 | dependencies = [ 170 | "proc-macro2", 171 | ] 172 | 173 | [[package]] 174 | name = "rand" 175 | version = "0.8.5" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 178 | dependencies = [ 179 | "libc", 180 | "rand_chacha", 181 | "rand_core", 182 | ] 183 | 184 | [[package]] 185 | name = "rand_chacha" 186 | version = "0.3.1" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 189 | dependencies = [ 190 | "ppv-lite86", 191 | "rand_core", 192 | ] 193 | 194 | [[package]] 195 | name = "rand_core" 196 | version = "0.6.4" 197 | source = "registry+https://github.com/rust-lang/crates.io-index" 198 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 199 | dependencies = [ 200 | "getrandom", 201 | ] 202 | 203 | [[package]] 204 | name = "rand_xorshift" 205 | version = "0.3.0" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" 208 | dependencies = [ 209 | "rand_core", 210 | ] 211 | 212 | [[package]] 213 | name = "regex-syntax" 214 | version = "0.6.29" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 217 | 218 | [[package]] 219 | name = "rmp" 220 | version = "0.8.12" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "7f9860a6cc38ed1da53456442089b4dfa35e7cedaa326df63017af88385e6b20" 223 | dependencies = [ 224 | "byteorder", 225 | "num-traits", 226 | "paste", 227 | ] 228 | 229 | [[package]] 230 | name = "rmp-serde" 231 | version = "1.1.2" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "bffea85eea980d8a74453e5d02a8d93028f3c34725de143085a844ebe953258a" 234 | dependencies = [ 235 | "byteorder", 236 | "rmp", 237 | "serde", 238 | ] 239 | 240 | [[package]] 241 | name = "ryu" 242 | version = "1.0.16" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" 245 | 246 | [[package]] 247 | name = "scoped-tls" 248 | version = "1.0.1" 249 | source = "registry+https://github.com/rust-lang/crates.io-index" 250 | checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" 251 | 252 | [[package]] 253 | name = "serde" 254 | version = "1.0.193" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89" 257 | dependencies = [ 258 | "serde_derive", 259 | ] 260 | 261 | [[package]] 262 | name = "serde-wasm-bindgen" 263 | version = "0.6.5" 264 | dependencies = [ 265 | "bincode", 266 | "getrandom", 267 | "js-sys", 268 | "maplit", 269 | "proptest", 270 | "serde", 271 | "serde_bytes", 272 | "serde_json", 273 | "wasm-bindgen", 274 | "wasm-bindgen-test", 275 | ] 276 | 277 | [[package]] 278 | name = "serde-wasm-bindgen-benches" 279 | version = "0.1.0" 280 | dependencies = [ 281 | "console_error_panic_hook", 282 | "rmp-serde", 283 | "serde", 284 | "serde-wasm-bindgen", 285 | "wasm-bindgen", 286 | ] 287 | 288 | [[package]] 289 | name = "serde_bytes" 290 | version = "0.11.12" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "ab33ec92f677585af6d88c65593ae2375adde54efdbf16d597f2cbc7a6d368ff" 293 | dependencies = [ 294 | "serde", 295 | ] 296 | 297 | [[package]] 298 | name = "serde_derive" 299 | version = "1.0.193" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3" 302 | dependencies = [ 303 | "proc-macro2", 304 | "quote", 305 | "syn", 306 | ] 307 | 308 | [[package]] 309 | name = "serde_json" 310 | version = "1.0.108" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" 313 | dependencies = [ 314 | "itoa", 315 | "ryu", 316 | "serde", 317 | ] 318 | 319 | [[package]] 320 | name = "syn" 321 | version = "2.0.39" 322 | source = "registry+https://github.com/rust-lang/crates.io-index" 323 | checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" 324 | dependencies = [ 325 | "proc-macro2", 326 | "quote", 327 | "unicode-ident", 328 | ] 329 | 330 | [[package]] 331 | name = "unicode-ident" 332 | version = "1.0.12" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 335 | 336 | [[package]] 337 | name = "wasi" 338 | version = "0.11.0+wasi-snapshot-preview1" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 341 | 342 | [[package]] 343 | name = "wasm-bindgen" 344 | version = "0.2.89" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" 347 | dependencies = [ 348 | "cfg-if", 349 | "serde", 350 | "serde_json", 351 | "wasm-bindgen-macro", 352 | ] 353 | 354 | [[package]] 355 | name = "wasm-bindgen-backend" 356 | version = "0.2.89" 357 | source = "registry+https://github.com/rust-lang/crates.io-index" 358 | checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" 359 | dependencies = [ 360 | "bumpalo", 361 | "log", 362 | "once_cell", 363 | "proc-macro2", 364 | "quote", 365 | "syn", 366 | "wasm-bindgen-shared", 367 | ] 368 | 369 | [[package]] 370 | name = "wasm-bindgen-futures" 371 | version = "0.4.39" 372 | source = "registry+https://github.com/rust-lang/crates.io-index" 373 | checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12" 374 | dependencies = [ 375 | "cfg-if", 376 | "js-sys", 377 | "wasm-bindgen", 378 | "web-sys", 379 | ] 380 | 381 | [[package]] 382 | name = "wasm-bindgen-macro" 383 | version = "0.2.89" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" 386 | dependencies = [ 387 | "quote", 388 | "wasm-bindgen-macro-support", 389 | ] 390 | 391 | [[package]] 392 | name = "wasm-bindgen-macro-support" 393 | version = "0.2.89" 394 | source = "registry+https://github.com/rust-lang/crates.io-index" 395 | checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" 396 | dependencies = [ 397 | "proc-macro2", 398 | "quote", 399 | "syn", 400 | "wasm-bindgen-backend", 401 | "wasm-bindgen-shared", 402 | ] 403 | 404 | [[package]] 405 | name = "wasm-bindgen-shared" 406 | version = "0.2.89" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" 409 | 410 | [[package]] 411 | name = "wasm-bindgen-test" 412 | version = "0.3.39" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "2cf9242c0d27999b831eae4767b2a146feb0b27d332d553e605864acd2afd403" 415 | dependencies = [ 416 | "console_error_panic_hook", 417 | "js-sys", 418 | "scoped-tls", 419 | "wasm-bindgen", 420 | "wasm-bindgen-futures", 421 | "wasm-bindgen-test-macro", 422 | ] 423 | 424 | [[package]] 425 | name = "wasm-bindgen-test-macro" 426 | version = "0.3.39" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "794645f5408c9a039fd09f4d113cdfb2e7eba5ff1956b07bcf701cf4b394fe89" 429 | dependencies = [ 430 | "proc-macro2", 431 | "quote", 432 | "syn", 433 | ] 434 | 435 | [[package]] 436 | name = "web-sys" 437 | version = "0.3.66" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" 440 | dependencies = [ 441 | "js-sys", 442 | "wasm-bindgen", 443 | ] 444 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "serde-wasm-bindgen" 3 | version = "0.6.5" 4 | authors = ["Ingvar Stepanyan "] 5 | edition = "2018" 6 | readme = "README.md" 7 | license = "MIT" 8 | repository = "https://github.com/RReverser/serde-wasm-bindgen" 9 | description = "Native Serde adapter for wasm-bindgen" 10 | categories = ["development-tools::ffi", "wasm", "encoding"] 11 | keywords = ["serde", "serialization", "javascript", "wasm", "webassembly"] 12 | 13 | [dependencies] 14 | serde = { version = "1.0.193", features = ["derive"] } 15 | js-sys = "^0.3" 16 | wasm-bindgen = "0.2.83" 17 | 18 | [dev-dependencies] 19 | wasm-bindgen-test = "0.3.24" 20 | serde_bytes = "0.11.1" 21 | serde_json = "1.0.39" 22 | maplit = "1.0.2" 23 | bincode = "1.3.3" 24 | 25 | [dev-dependencies.proptest] 26 | version = "1.0" 27 | # The default feature set includes things like process forking which are not 28 | # supported in Web Assembly. 29 | default-features = false 30 | # Enable using the `std` crate. 31 | features = ["std"] 32 | 33 | [dev-dependencies.getrandom] 34 | version = "0.2" 35 | features = ["js"] 36 | 37 | [workspace] 38 | members = ["benchmarks"] 39 | 40 | [profile.release] 41 | lto = true 42 | codegen-units = 1 43 | debug = true 44 | 45 | [patch.crates-io] 46 | proptest = { git = "https://github.com/RReverser/proptest", branch = "fix-wasm" } 47 | 48 | [package.metadata.docs.rs] 49 | targets = ["wasm32-unknown-unknown"] 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Cloudflare, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Crates.io](https://img.shields.io/crates/d/serde-wasm-bindgen?logo=rust)](https://crates.io/crates/serde-wasm-bindgen) 2 | [![docs.rs](https://img.shields.io/docsrs/serde-wasm-bindgen)](https://docs.rs/serde-wasm-bindgen/) 3 | [![GitHub Sponsors](https://img.shields.io/github/sponsors/rreverser)](https://github.com/sponsors/RReverser) 4 | 5 | This is a native integration of [Serde](https://serde.rs/) with [wasm-bindgen](https://github.com/rustwasm/wasm-bindgen). It allows to convert Rust data types into native JavaScript types and vice versa. 6 | 7 | Initially this library was created while working for [@Cloudflare](https://github.com/cloudflare) as [an alternative implementation](https://github.com/rustwasm/wasm-bindgen/issues/1258) to the JSON-based Serde support built into the `wasm-bindgen` but, [nowadays](https://github.com/rustwasm/wasm-bindgen/pull/3031) `serde-wasm-bindgen` is the officially preferred approach. It provides much smaller code size overhead than JSON, and, in most common cases, provides much faster serialization/deserialization as well. 8 | 9 | ## Usage 10 | 11 | Copied almost verbatim from the [`wasm-bindgen` guide](https://rustwasm.github.io/wasm-bindgen/reference/arbitrary-data-with-serde.html#serializing-and-deserializing-arbitrary-data-into-and-from-jsvalue-with-serde): 12 | 13 | ### Add dependencies 14 | 15 | To use `serde-wasm-bindgen`, you first have to add it as a dependency in your 16 | `Cargo.toml`. You also need the `serde` crate, with the `derive` feature 17 | enabled, to allow your types to be serialized and deserialized with Serde. 18 | 19 | ```toml 20 | [dependencies] 21 | serde = { version = "1.0", features = ["derive"] } 22 | serde-wasm-bindgen = "0.4" 23 | ``` 24 | 25 | ### Derive the `Serialize` and `Deserialize` Traits 26 | 27 | Add `#[derive(Serialize, Deserialize)]` to your type. All of your type 28 | members must also be supported by Serde, i.e. their types must also implement 29 | the `Serialize` and `Deserialize` traits. 30 | 31 | Note that you don't need to use the `#[wasm_bindgen]` macro. 32 | 33 | ```rust 34 | use serde::{Serialize, Deserialize}; 35 | 36 | #[derive(Serialize, Deserialize)] 37 | pub struct Example { 38 | pub field1: HashMap, 39 | pub field2: Vec>, 40 | pub field3: [f32; 4], 41 | } 42 | ``` 43 | 44 | ### Send it to JavaScript with `serde_wasm_bindgen::to_value` 45 | 46 | ```rust 47 | #[wasm_bindgen] 48 | pub fn send_example_to_js() -> Result { 49 | let mut field1 = HashMap::new(); 50 | field1.insert(0, String::from("ex")); 51 | 52 | let example = Example { 53 | field1, 54 | field2: vec![vec![1., 2.], vec![3., 4.]], 55 | field3: [1., 2., 3., 4.] 56 | }; 57 | 58 | Ok(serde_wasm_bindgen::to_value(&example)?) 59 | } 60 | ``` 61 | 62 | ### Receive it from JavaScript with `serde_wasm_bindgen::from_value` 63 | 64 | ```rust 65 | #[wasm_bindgen] 66 | pub fn receive_example_from_js(val: JsValue) -> Result<(), JsValue> { 67 | let example: Example = serde_wasm_bindgen::from_value(val)?; 68 | /* …do something with `example`… */ 69 | Ok(()) 70 | } 71 | ``` 72 | 73 | ### JavaScript Usage 74 | 75 | In the `JsValue` that JavaScript gets, `field1` will be a `Map`, 76 | `field2` will be an `Array>`, and `field3` will be an `Array`. 77 | 78 | ```js 79 | import { send_example_to_js, receive_example_from_js } from "example"; 80 | 81 | // Get the example object from wasm. 82 | let example = send_example_to_js(); 83 | 84 | // Add another "Vec" element to the end of the "Vec>" 85 | example.field2.push([5, 6]); 86 | 87 | // Send the example object back to wasm. 88 | receive_example_from_js(example); 89 | ``` 90 | 91 | ## Supported Types 92 | 93 | Note that, even though it might often be the case, by default this library doesn't attempt 94 | to be strictly compatible with JSON, instead prioritising better 95 | compatibility with common JavaScript idioms and representations. 96 | 97 | If you need JSON compatibility (e.g. you want to serialize `HashMap` 98 | as plain objects instead of JavaScript `Map` instances), use the 99 | [`Serializer::json_compatible()`](https://docs.rs/serde-wasm-bindgen/latest/serde_wasm_bindgen/struct.Serializer.html#method.json_compatible) preset. 100 | 101 | By default, Rust ⬄ JavaScript conversions in `serde-wasm-bindgen` follow this table: 102 | 103 | | Rust | JavaScript | Also supported in `from_value` | 104 | |-----------------------------------|--------------------------------------|--------------------------------| 105 | | `()` and `Option::None` | `undefined` | `null` | 106 | | `bool` | `boolean` | | 107 | | `f32`, `f64` | `number` | | 108 | | `u8`, `i8`, …, `u32`, `i32` | `number` in the [safe integer] range | | 109 | | `u64`, `i64`, `usize`, `isize` | `number` in the [safe integer] range | `bigint` | 110 | | `u128`, `i128` | `bigint` | | 111 | | `String` | `string` | | 112 | | `char` | single-codepoint `string` | | 113 | | `Enum::Variant { … }` | [as configured in Serde] | | 114 | | `HashMap`, `BTreeMap`, etc. | `Map` | any iterable over `[K, V]` | 115 | | `Struct { key1: value1, … }` | `{ key1: value1, … }` object | | 116 | | tuple, `Vec`, `HashSet`, etc. | `T[]` array | any iterable over `T` | 117 | | [`serde_bytes`] byte buffer | `Uint8Array` | `ArrayBuffer`, `Array` | 118 | 119 | [as configured in Serde]: https://serde.rs/enum-representations.html 120 | [safe integer]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isSafeInteger 121 | [`serde_bytes`]: https://github.com/serde-rs/bytes 122 | 123 | The first two columns show idiomatic representations on Rust and JavaScript sides, while the 3rd column shows which JavaScript values 124 | are additionally supported when deserializing from JavaScript to the Rust type. 125 | 126 | ### Serializer configuration options 127 | 128 | You can customize serialization from Rust to JavaScript by setting the following options on the [`Serializer::new()`](https://docs.rs/serde-wasm-bindgen/latest/serde_wasm_bindgen/struct.Serializer.html) instance (all default to false): 129 | 130 | - `.serialize_missing_as_null(true)`: Serialize `()`, unit structs and `Option::None` to `null` instead of `undefined`. 131 | - `.serialize_maps_as_objects(true)`: Serialize maps into plain JavaScript objects instead of ES2015 Maps. 132 | - `.serialize_large_number_types_as_bigints(true)`: Serialize `u64`, `i64`, `usize` and `isize` to `bigint`s instead of attempting to fit them into the [safe integer] `number` or failing. 133 | - `.serialize_bytes_as_arrays(true)`: Serialize bytes into plain JavaScript arrays instead of ES2015 Uint8Arrays. 134 | 135 | You can also use the `Serializer::json_compatible()` preset to create a JSON compatible serializer. It enables `serialize_missing_as_null`, `serialize_maps_as_objects`, and `serialize_bytes_as_arrays` under the hood. 136 | 137 | ### Preserving JavaScript values 138 | 139 | Sometimes you want to preserve original JavaScript value instead of converting it into a Rust type. This is particularly useful for types that can't be converted without losing the data, such as [`Date`](https://docs.rs/js-sys/latest/js_sys/struct.Date.html), [`RegExp`](https://docs.rs/js-sys/latest/js_sys/struct.RegExp.html) or 3rd-party types. 140 | 141 | `serde_wasm_bindgen::preserve` allows you to do just that: 142 | 143 | ```rust 144 | #[derive(Serialize, Deserialize)] 145 | pub struct Example { 146 | pub regular_field: i32, 147 | 148 | #[serde(with = "serde_wasm_bindgen::preserve")] 149 | pub preserved_date: js_sys::Date, 150 | 151 | #[serde(with = "serde_wasm_bindgen::preserve")] 152 | pub preserved_arbitrary_value: JsValue, 153 | } 154 | ``` 155 | 156 | ## TypeScript support 157 | 158 | There's no built-in type generation in this crate, but you can [tsify](https://github.com/madonoharu/tsify) with the `js` feature which integrates with `serde-wasm-bindgen` under the hood. Aside from generating structural typings, it also allows to derive `IntoWasmAbi` / `FromWasmAbi` so that you don't have to write `from_value` / `to_value` by hand. 159 | 160 | ## License 161 | 162 | Licensed under the MIT license. See the 163 | [LICENSE](https://github.com/RReverser/serde-wasm-bindgen/blob/master/LICENSE) 164 | file for details. 165 | -------------------------------------------------------------------------------- /benchmarks/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /results.csv 3 | -------------------------------------------------------------------------------- /benchmarks/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "serde-wasm-bindgen-benches" 3 | version = "0.1.0" 4 | authors = ["Ingvar Stepanyan "] 5 | edition = "2018" 6 | publish = false 7 | 8 | [dependencies] 9 | console_error_panic_hook = "0.1.6" 10 | rmp-serde = { version = "1.1.1", optional = true } 11 | serde = { version = "^1.0", features = ["derive"] } 12 | serde-wasm-bindgen = { path = "..", optional = true } 13 | wasm-bindgen = { version = "^0.2" } 14 | 15 | [lib] 16 | crate-type = ["cdylib", "rlib"] 17 | 18 | [features] 19 | serde-json = ["wasm-bindgen/serde-serialize"] 20 | msgpack = ["rmp-serde"] 21 | 22 | [package.metadata.wasm-pack.profile.profiling] 23 | wasm-opt = ['-O', '-g'] 24 | -------------------------------------------------------------------------------- /benchmarks/index.mjs: -------------------------------------------------------------------------------- 1 | const { Suite } = globalThis.Benchmark ?? (await import('benchmark')).default; 2 | 3 | let suiteOps = { 4 | parse: {}, 5 | serialize: {} 6 | }; 7 | 8 | const libs = ( 9 | await Promise.all( 10 | [ 11 | 'serde-wasm-bindgen', 12 | 'serde-json', 13 | 'serde-wasm-bindgen-reftypes', 14 | 'msgpack' 15 | ].map(async dir => { 16 | try { 17 | const impl = await import(`./pkg/${dir}/serde_wasm_bindgen_benches.js`); 18 | // await impl.default(); // Init Wasm 19 | return { name: dir, impl }; 20 | } catch (err) { 21 | console.warn(err.toString()); 22 | return null; 23 | } 24 | }) 25 | ) 26 | ).filter(Boolean); 27 | 28 | let readFile, filter; 29 | 30 | const isNode = typeof process !== 'undefined'; 31 | 32 | if (isNode) { 33 | const prettyBytes = (await import('pretty-bytes')).default; 34 | const brotliSize = await import('brotli-size'); 35 | 36 | console.log('=== Sizes ==='); 37 | for (let { name } of libs) { 38 | const [js, wasm] = [ 39 | 'serde_wasm_bindgen_benches.js', 40 | 'serde_wasm_bindgen_benches_bg.wasm' 41 | ].map(file => prettyBytes(brotliSize.fileSync(`pkg/${name}/${file}`))); 42 | 43 | console.log(`${name}: JS = ${js}, Wasm = ${wasm}`); 44 | } 45 | console.log(); 46 | 47 | const readFileImpl = (await import('fs/promises')).readFile; 48 | readFile = name => readFileImpl(name, 'utf8'); 49 | 50 | filter = process.argv[2]; 51 | } else { 52 | readFile = name => 53 | fetch(name).then(res => { 54 | if (!res.ok) { 55 | throw new Error(`Failed to fetch ${name}`); 56 | } 57 | return res.text(); 58 | }); 59 | 60 | filter = new URLSearchParams(location.search).get('filter'); 61 | } 62 | 63 | filter = new RegExp(filter ?? '(?:)', 'i'); 64 | 65 | async function loadData(name) { 66 | return JSON.parse(await readFile(`./data/${name}.json`, 'utf8')); 67 | } 68 | 69 | const datasets = { 70 | Canada: await loadData('canada'), 71 | CitmCatalog: await loadData('citm_catalog'), 72 | Twitter: await loadData('twitter') 73 | }; 74 | 75 | for (let { name: libName, impl } of libs) { 76 | for (let [dataName, json] of Object.entries(datasets)) { 77 | let { parse } = impl[dataName]; 78 | 79 | let parseBenchName = `parse ${dataName} ${libName}`; 80 | if (filter.test(parseBenchName)) { 81 | (suiteOps.parse[dataName] ??= new Suite(dataName)).add(libName, () => 82 | parse(json).free() 83 | ); 84 | } 85 | 86 | let serializeBenchName = `serialize ${dataName} ${libName}`; 87 | if (filter.test(serializeBenchName)) { 88 | let parsed = parse(json); 89 | (suiteOps.serialize[dataName] ??= new Suite(dataName)).add( 90 | libName, 91 | () => parsed.serialize(), 92 | { 93 | onComplete: () => parsed.free() 94 | } 95 | ); 96 | } 97 | } 98 | } 99 | 100 | console.log('=== Benchmarks ==='); 101 | 102 | let csv = ''; 103 | 104 | for (let [op, suites] of Object.entries(suiteOps)) { 105 | console.group(op); 106 | for (let suite of Object.values(suites)) { 107 | console.group(suite.name); 108 | await new Promise((resolve, reject) => { 109 | suite 110 | .on('error', event => reject(event.target.error)) 111 | .on('cycle', event => { 112 | console.log(event.target.toString()); 113 | csv += `${op},${suite.name},${event.target.name},${event.target.hz}\n`; 114 | }) 115 | .on('complete', resolve) 116 | .run({ 117 | async: true 118 | }); 119 | }); 120 | console.groupEnd(); 121 | } 122 | console.groupEnd(); 123 | } 124 | 125 | if (isNode) { 126 | (await import('fs')).writeFileSync('results.csv', csv); 127 | } else { 128 | let csvLink = document.createElement('a'); 129 | csvLink.href = URL.createObjectURL(new Blob([csv], { type: 'text/csv' })); 130 | csvLink.download = 'results.csv'; 131 | csvLink.textContent = 'Download CSV'; 132 | document.body.append('Done! ', csvLink); 133 | } 134 | -------------------------------------------------------------------------------- /benchmarks/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@serde-wasm-bindgen/benches", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "name": "@serde-wasm-bindgen/benches", 8 | "dependencies": { 9 | "@msgpack/msgpack": "^2.8.0", 10 | "benchmark": "^2.1.4", 11 | "brotli-size": "^4.0.0", 12 | "cross-env": "^7.0.3", 13 | "pretty-bytes": "^6.0.0" 14 | } 15 | }, 16 | "node_modules/@msgpack/msgpack": { 17 | "version": "2.8.0", 18 | "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-2.8.0.tgz", 19 | "integrity": "sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ==", 20 | "engines": { 21 | "node": ">= 10" 22 | } 23 | }, 24 | "node_modules/benchmark": { 25 | "version": "2.1.4", 26 | "resolved": "https://registry.npmjs.org/benchmark/-/benchmark-2.1.4.tgz", 27 | "integrity": "sha1-CfPeMckWQl1JjMLuVloOvzwqVik=", 28 | "dependencies": { 29 | "lodash": "^4.17.4", 30 | "platform": "^1.3.3" 31 | } 32 | }, 33 | "node_modules/brotli-size": { 34 | "version": "4.0.0", 35 | "resolved": "https://registry.npmjs.org/brotli-size/-/brotli-size-4.0.0.tgz", 36 | "integrity": "sha512-uA9fOtlTRC0iqKfzff1W34DXUA3GyVqbUaeo3Rw3d4gd1eavKVCETXrn3NzO74W+UVkG3UHu8WxUi+XvKI/huA==", 37 | "dependencies": { 38 | "duplexer": "0.1.1" 39 | }, 40 | "engines": { 41 | "node": ">= 10.16.0" 42 | } 43 | }, 44 | "node_modules/cross-env": { 45 | "version": "7.0.3", 46 | "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", 47 | "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", 48 | "dependencies": { 49 | "cross-spawn": "^7.0.1" 50 | }, 51 | "bin": { 52 | "cross-env": "src/bin/cross-env.js", 53 | "cross-env-shell": "src/bin/cross-env-shell.js" 54 | }, 55 | "engines": { 56 | "node": ">=10.14", 57 | "npm": ">=6", 58 | "yarn": ">=1" 59 | } 60 | }, 61 | "node_modules/cross-spawn": { 62 | "version": "7.0.3", 63 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", 64 | "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", 65 | "dependencies": { 66 | "path-key": "^3.1.0", 67 | "shebang-command": "^2.0.0", 68 | "which": "^2.0.1" 69 | }, 70 | "engines": { 71 | "node": ">= 8" 72 | } 73 | }, 74 | "node_modules/duplexer": { 75 | "version": "0.1.1", 76 | "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", 77 | "integrity": "sha512-sxNZ+ljy+RA1maXoUReeqBBpBC6RLKmg5ewzV+x+mSETmWNoKdZN6vcQjpFROemza23hGFskJtFNoUWUaQ+R4Q==" 78 | }, 79 | "node_modules/isexe": { 80 | "version": "2.0.0", 81 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 82 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" 83 | }, 84 | "node_modules/lodash": { 85 | "version": "4.17.21", 86 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 87 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 88 | }, 89 | "node_modules/path-key": { 90 | "version": "3.1.1", 91 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 92 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 93 | "engines": { 94 | "node": ">=8" 95 | } 96 | }, 97 | "node_modules/platform": { 98 | "version": "1.3.5", 99 | "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.5.tgz", 100 | "integrity": "sha512-TuvHS8AOIZNAlE77WUDiR4rySV/VMptyMfcfeoMgs4P8apaZM3JrnbzBiixKUv+XR6i+BXrQh8WAnjaSPFO65Q==" 101 | }, 102 | "node_modules/pretty-bytes": { 103 | "version": "6.0.0", 104 | "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.0.0.tgz", 105 | "integrity": "sha512-6UqkYefdogmzqAZWzJ7laYeJnaXDy2/J+ZqiiMtS7t7OfpXWTlaeGMwX8U6EFvPV/YWWEKRkS8hKS4k60WHTOg==", 106 | "engines": { 107 | "node": "^14.13.1 || >=16.0.0" 108 | }, 109 | "funding": { 110 | "url": "https://github.com/sponsors/sindresorhus" 111 | } 112 | }, 113 | "node_modules/shebang-command": { 114 | "version": "2.0.0", 115 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 116 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 117 | "dependencies": { 118 | "shebang-regex": "^3.0.0" 119 | }, 120 | "engines": { 121 | "node": ">=8" 122 | } 123 | }, 124 | "node_modules/shebang-regex": { 125 | "version": "3.0.0", 126 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 127 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 128 | "engines": { 129 | "node": ">=8" 130 | } 131 | }, 132 | "node_modules/which": { 133 | "version": "2.0.2", 134 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 135 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 136 | "dependencies": { 137 | "isexe": "^2.0.0" 138 | }, 139 | "bin": { 140 | "node-which": "bin/node-which" 141 | }, 142 | "engines": { 143 | "node": ">= 8" 144 | } 145 | } 146 | }, 147 | "dependencies": { 148 | "@msgpack/msgpack": { 149 | "version": "2.8.0", 150 | "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-2.8.0.tgz", 151 | "integrity": "sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ==" 152 | }, 153 | "benchmark": { 154 | "version": "2.1.4", 155 | "resolved": "https://registry.npmjs.org/benchmark/-/benchmark-2.1.4.tgz", 156 | "integrity": "sha1-CfPeMckWQl1JjMLuVloOvzwqVik=", 157 | "requires": { 158 | "lodash": "^4.17.4", 159 | "platform": "^1.3.3" 160 | } 161 | }, 162 | "brotli-size": { 163 | "version": "4.0.0", 164 | "resolved": "https://registry.npmjs.org/brotli-size/-/brotli-size-4.0.0.tgz", 165 | "integrity": "sha512-uA9fOtlTRC0iqKfzff1W34DXUA3GyVqbUaeo3Rw3d4gd1eavKVCETXrn3NzO74W+UVkG3UHu8WxUi+XvKI/huA==", 166 | "requires": { 167 | "duplexer": "0.1.1" 168 | } 169 | }, 170 | "cross-env": { 171 | "version": "7.0.3", 172 | "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", 173 | "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", 174 | "requires": { 175 | "cross-spawn": "^7.0.1" 176 | } 177 | }, 178 | "cross-spawn": { 179 | "version": "7.0.3", 180 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", 181 | "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", 182 | "requires": { 183 | "path-key": "^3.1.0", 184 | "shebang-command": "^2.0.0", 185 | "which": "^2.0.1" 186 | } 187 | }, 188 | "duplexer": { 189 | "version": "0.1.1", 190 | "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", 191 | "integrity": "sha512-sxNZ+ljy+RA1maXoUReeqBBpBC6RLKmg5ewzV+x+mSETmWNoKdZN6vcQjpFROemza23hGFskJtFNoUWUaQ+R4Q==" 192 | }, 193 | "isexe": { 194 | "version": "2.0.0", 195 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 196 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" 197 | }, 198 | "lodash": { 199 | "version": "4.17.21", 200 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 201 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 202 | }, 203 | "path-key": { 204 | "version": "3.1.1", 205 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 206 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" 207 | }, 208 | "platform": { 209 | "version": "1.3.5", 210 | "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.5.tgz", 211 | "integrity": "sha512-TuvHS8AOIZNAlE77WUDiR4rySV/VMptyMfcfeoMgs4P8apaZM3JrnbzBiixKUv+XR6i+BXrQh8WAnjaSPFO65Q==" 212 | }, 213 | "pretty-bytes": { 214 | "version": "6.0.0", 215 | "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.0.0.tgz", 216 | "integrity": "sha512-6UqkYefdogmzqAZWzJ7laYeJnaXDy2/J+ZqiiMtS7t7OfpXWTlaeGMwX8U6EFvPV/YWWEKRkS8hKS4k60WHTOg==" 217 | }, 218 | "shebang-command": { 219 | "version": "2.0.0", 220 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 221 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 222 | "requires": { 223 | "shebang-regex": "^3.0.0" 224 | } 225 | }, 226 | "shebang-regex": { 227 | "version": "3.0.0", 228 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 229 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" 230 | }, 231 | "which": { 232 | "version": "2.0.2", 233 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 234 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 235 | "requires": { 236 | "isexe": "^2.0.0" 237 | } 238 | } 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /benchmarks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@serde-wasm-bindgen/benches", 3 | "private": true, 4 | "dependencies": { 5 | "@msgpack/msgpack": "^2.8.0", 6 | "benchmark": "^2.1.4", 7 | "brotli-size": "^4.0.0", 8 | "cross-env": "^7.0.3", 9 | "pretty-bytes": "^6.0.0" 10 | }, 11 | "scripts": { 12 | "build:swb": "wasm-pack build -t nodejs --out-dir pkg/serde-wasm-bindgen -- --features serde-wasm-bindgen", 13 | "build:swb-reftypes": "cross-env RUSTFLAGS=\"-C target-feature=+reference-types\" WASM_BINDGEN_EXTERNREF=1 wasm-pack build -t nodejs --out-dir pkg/serde-wasm-bindgen-reftypes -- --features serde-wasm-bindgen", 14 | "build:json": "wasm-pack build -t nodejs --out-dir pkg/serde-json -- --features serde-json", 15 | "build:msgpack": "wasm-pack build -t nodejs --out-dir pkg/msgpack -- --features msgpack", 16 | "build": "npm run build:swb && npm run build:json", 17 | "test": "node index.mjs" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /benchmarks/src/canada.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::collections::BTreeMap as Map; 3 | 4 | pub type Canada = FeatureCollection; 5 | 6 | #[derive(Serialize, Deserialize)] 7 | #[serde(deny_unknown_fields)] 8 | pub struct FeatureCollection { 9 | #[serde(rename = "type")] 10 | pub obj_type: ObjType, 11 | pub features: Vec, 12 | } 13 | 14 | #[derive(Serialize, Deserialize)] 15 | #[serde(deny_unknown_fields)] 16 | pub struct Feature { 17 | #[serde(rename = "type")] 18 | pub obj_type: ObjType, 19 | pub properties: Map, 20 | pub geometry: Geometry, 21 | } 22 | 23 | #[derive(Serialize, Deserialize)] 24 | #[serde(deny_unknown_fields)] 25 | pub struct Geometry { 26 | #[serde(rename = "type")] 27 | pub obj_type: ObjType, 28 | pub coordinates: Vec>, 29 | } 30 | 31 | pub type Latitude = f32; 32 | pub type Longitude = f32; 33 | 34 | #[derive(Serialize, Deserialize)] 35 | #[serde(deny_unknown_fields)] 36 | pub enum ObjType { 37 | FeatureCollection, 38 | Feature, 39 | Polygon, 40 | } 41 | -------------------------------------------------------------------------------- /benchmarks/src/citm_catalog.rs: -------------------------------------------------------------------------------- 1 | use crate::prim_str::PrimStr; 2 | use serde::{Deserialize, Serialize}; 3 | use std::collections::BTreeMap as Map; 4 | 5 | #[derive(Serialize, Deserialize)] 6 | #[serde(deny_unknown_fields, rename_all = "camelCase")] 7 | pub struct CitmCatalog { 8 | pub area_names: Map, 9 | pub audience_sub_category_names: Map, 10 | pub block_names: Map, 11 | pub events: Map, 12 | pub performances: Vec, 13 | pub seat_category_names: Map, 14 | pub sub_topic_names: Map, 15 | pub subject_names: Map, 16 | pub topic_names: Map, 17 | pub topic_sub_topics: Map>, 18 | pub venue_names: Map, 19 | } 20 | 21 | pub type Id = u32; 22 | pub type IdStr = PrimStr; 23 | 24 | #[derive(Serialize, Deserialize)] 25 | #[serde(deny_unknown_fields, rename_all = "camelCase")] 26 | pub struct Event { 27 | pub description: (), 28 | pub id: Id, 29 | pub logo: Option, 30 | pub name: String, 31 | pub sub_topic_ids: Vec, 32 | pub subject_code: (), 33 | pub subtitle: (), 34 | pub topic_ids: Vec, 35 | } 36 | 37 | #[derive(Serialize, Deserialize)] 38 | #[serde(deny_unknown_fields, rename_all = "camelCase")] 39 | pub struct Performance { 40 | pub event_id: Id, 41 | pub id: Id, 42 | pub logo: Option, 43 | pub name: (), 44 | pub prices: Vec, 45 | pub seat_categories: Vec, 46 | pub seat_map_image: (), 47 | pub start: u64, 48 | pub venue_code: String, 49 | } 50 | 51 | #[derive(Serialize, Deserialize)] 52 | #[serde(deny_unknown_fields, rename_all = "camelCase")] 53 | pub struct Price { 54 | pub amount: u32, 55 | pub audience_sub_category_id: Id, 56 | pub seat_category_id: Id, 57 | } 58 | 59 | #[derive(Serialize, Deserialize)] 60 | #[serde(deny_unknown_fields, rename_all = "camelCase")] 61 | pub struct SeatCategory { 62 | pub areas: Vec, 63 | pub seat_category_id: Id, 64 | } 65 | 66 | #[derive(Serialize, Deserialize)] 67 | #[serde(deny_unknown_fields, rename_all = "camelCase")] 68 | pub struct Area { 69 | pub area_id: Id, 70 | pub block_ids: [(); 0], 71 | } 72 | -------------------------------------------------------------------------------- /benchmarks/src/color.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::mem::MaybeUninit; 3 | use std::{ptr, slice, str}; 4 | 5 | use serde::de::{self, Deserialize, Deserializer, Unexpected}; 6 | use serde::ser::{Serialize, Serializer}; 7 | 8 | #[derive(Clone, Copy)] 9 | pub struct Color(u32); 10 | 11 | const HEX_LUT: &[u8] = b"\ 12 | 000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F\ 13 | 202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F\ 14 | 404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F\ 15 | 606162636465666768696A6B6C6D6E6F707172737475767778797A7B7C7D7E7F\ 16 | 808182838485868788898A8B8C8D8E8F909192939495969798999A9B9C9D9E9F\ 17 | A0A1A2A3A4A5A6A7A8A9AAABACADAEAFB0B1B2B3B4B5B6B7B8B9BABBBCBDBEBF\ 18 | C0C1C2C3C4C5C6C7C8C9CACBCCCDCECFD0D1D2D3D4D5D6D7D8D9DADBDCDDDEDF\ 19 | E0E1E2E3E4E5E6E7E8E9EAEBECEDEEEFF0F1F2F3F4F5F6F7F8F9FAFBFCFDFEFF"; 20 | 21 | impl Color { 22 | fn as_str(self, buf: &mut MaybeUninit<[u8; 6]>) -> &str { 23 | let buf_len = 6; 24 | let buf_ptr = buf.as_mut_ptr() as *mut u8; 25 | let lut_ptr = HEX_LUT.as_ptr(); 26 | 27 | let r = ((self.0 & 0xFF0000) >> 15) as isize; 28 | let g = ((self.0 & 0x00FF00) >> 7) as isize; 29 | let b = ((self.0 & 0x0000FF) << 1) as isize; 30 | 31 | unsafe { 32 | ptr::copy_nonoverlapping(lut_ptr.offset(r), buf_ptr, 2); 33 | ptr::copy_nonoverlapping(lut_ptr.offset(g), buf_ptr.offset(2), 2); 34 | ptr::copy_nonoverlapping(lut_ptr.offset(b), buf_ptr.offset(4), 2); 35 | 36 | str::from_utf8(slice::from_raw_parts(buf_ptr, buf_len)).unwrap() 37 | } 38 | } 39 | } 40 | 41 | impl Serialize for Color { 42 | fn serialize(&self, serializer: S) -> Result 43 | where 44 | S: Serializer, 45 | { 46 | let mut buf = MaybeUninit::uninit(); 47 | serializer.serialize_str(self.as_str(&mut buf)) 48 | } 49 | } 50 | 51 | impl<'de> Deserialize<'de> for Color { 52 | fn deserialize(deserializer: D) -> Result 53 | where 54 | D: Deserializer<'de>, 55 | { 56 | struct Visitor; 57 | 58 | impl<'de> de::Visitor<'de> for Visitor { 59 | type Value = Color; 60 | 61 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 62 | formatter.write_str("color string") 63 | } 64 | 65 | fn visit_str(self, value: &str) -> Result 66 | where 67 | E: de::Error, 68 | { 69 | match u32::from_str_radix(value, 16) { 70 | Ok(hex) => Ok(Color(hex)), 71 | Err(_) => Err(E::invalid_value(Unexpected::Str(value), &self)), 72 | } 73 | } 74 | } 75 | 76 | deserializer.deserialize_str(Visitor) 77 | } 78 | } 79 | 80 | #[test] 81 | fn test_color() { 82 | let mut buf = MaybeUninit::uninit(); 83 | let string = Color(0xA0A0A0).as_str(&mut buf); 84 | assert_eq!(string, "A0A0A0"); 85 | } 86 | -------------------------------------------------------------------------------- /benchmarks/src/lib.rs: -------------------------------------------------------------------------------- 1 | use serde::{de::DeserializeOwned, Deserialize, Serialize}; 2 | use wasm_bindgen::prelude::*; 3 | use wasm_bindgen::JsValue; 4 | 5 | mod color; 6 | mod prim_str; 7 | 8 | #[wasm_bindgen(start)] 9 | pub fn init_console() { 10 | console_error_panic_hook::set_once(); 11 | } 12 | 13 | macro_rules! serde_impl { 14 | (|$input:ident| { 15 | $($feature:literal => $(# $attr:tt)* { 16 | parse: $parse:expr, 17 | serialize: $serialize:expr, 18 | },)* 19 | }) => { 20 | $( 21 | #[cfg(feature = $feature)] 22 | $(# $attr)* 23 | mod serde_impl { 24 | use super::*; 25 | 26 | pub fn parse($input: JsValue) -> T { 27 | $parse 28 | } 29 | 30 | pub fn serialize($input: &T) -> JsValue { 31 | $serialize 32 | } 33 | } 34 | )* 35 | }; 36 | } 37 | 38 | serde_impl!(|input| { 39 | "serde-wasm-bindgen" => { 40 | parse: serde_wasm_bindgen::from_value(input).unwrap_throw(), 41 | serialize: { 42 | const SERIALIZER: serde_wasm_bindgen::Serializer = serde_wasm_bindgen::Serializer::json_compatible(); 43 | input.serialize(&SERIALIZER).unwrap_throw() 44 | }, 45 | }, 46 | 47 | "serde-json" => #[allow(deprecated)] { 48 | parse: input.into_serde().unwrap_throw(), 49 | serialize: JsValue::from_serde(input).unwrap_throw(), 50 | }, 51 | 52 | "msgpack" => { 53 | parse: { 54 | #[wasm_bindgen(module = "@msgpack/msgpack")] 55 | extern "C" { 56 | fn encode(input: &JsValue) -> Vec; 57 | } 58 | 59 | let input = encode(&input); 60 | rmp_serde::from_slice(&input).unwrap_throw() 61 | }, 62 | 63 | serialize: { 64 | #[wasm_bindgen(module = "@msgpack/msgpack")] 65 | extern "C" { 66 | fn decode(input: &[u8]) -> JsValue; 67 | } 68 | 69 | let input = rmp_serde::to_vec(input).unwrap_throw(); 70 | decode(&input) 71 | }, 72 | }, 73 | }); 74 | 75 | macro_rules! datasets { 76 | ($($mod:ident :: $ty:ident,)*) => { 77 | $( 78 | mod $mod; 79 | 80 | #[wasm_bindgen] 81 | #[derive(Serialize, Deserialize)] 82 | pub struct $ty($mod::$ty); 83 | 84 | #[wasm_bindgen] 85 | impl $ty { 86 | #[wasm_bindgen] 87 | pub fn parse(input: JsValue) -> $ty { 88 | serde_impl::parse(input) 89 | } 90 | 91 | #[wasm_bindgen] 92 | pub fn serialize(&self) -> JsValue { 93 | serde_impl::serialize(self) 94 | } 95 | } 96 | )* 97 | }; 98 | } 99 | 100 | datasets! { 101 | canada::Canada, 102 | citm_catalog::CitmCatalog, 103 | twitter::Twitter, 104 | } 105 | -------------------------------------------------------------------------------- /benchmarks/src/prim_str.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::fmt::Display; 3 | use std::str::FromStr; 4 | 5 | use serde::de::{self, Deserialize, Deserializer, Unexpected}; 6 | use serde::ser::{Serialize, Serializer}; 7 | 8 | #[derive(Clone, Copy, Ord, PartialOrd, Eq, PartialEq)] 9 | pub struct PrimStr(T) 10 | where 11 | T: Copy + PartialOrd + Display + FromStr; 12 | 13 | impl Serialize for PrimStr 14 | where 15 | T: Copy + PartialOrd + Display + FromStr, 16 | { 17 | fn serialize(&self, serializer: S) -> Result 18 | where 19 | S: Serializer, 20 | { 21 | serializer.collect_str(&self.0) 22 | } 23 | } 24 | 25 | impl<'de, T> Deserialize<'de> for PrimStr 26 | where 27 | T: Copy + PartialOrd + Display + FromStr, 28 | { 29 | fn deserialize(deserializer: D) -> Result 30 | where 31 | D: Deserializer<'de>, 32 | { 33 | use std::marker::PhantomData; 34 | struct Visitor(PhantomData); 35 | 36 | impl<'de, T> de::Visitor<'de> for Visitor 37 | where 38 | T: Copy + PartialOrd + Display + FromStr, 39 | { 40 | type Value = PrimStr; 41 | 42 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 43 | formatter.write_str("number represented as string") 44 | } 45 | 46 | fn visit_str(self, value: &str) -> Result, E> 47 | where 48 | E: de::Error, 49 | { 50 | match T::from_str(value) { 51 | Ok(id) => Ok(PrimStr(id)), 52 | Err(_) => Err(E::invalid_value(Unexpected::Str(value), &self)), 53 | } 54 | } 55 | } 56 | 57 | deserializer.deserialize_str(Visitor(PhantomData)) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /benchmarks/src/twitter.rs: -------------------------------------------------------------------------------- 1 | use crate::color::Color; 2 | use crate::prim_str::PrimStr; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Serialize, Deserialize)] 6 | #[serde(deny_unknown_fields)] 7 | pub struct Twitter { 8 | pub statuses: Vec, 9 | pub search_metadata: SearchMetadata, 10 | } 11 | 12 | // This was originally u64, but many of the given values are not safe integers. 13 | pub type LongId = f64; 14 | pub type ShortId = u32; 15 | pub type LongIdStr = PrimStr; 16 | pub type ShortIdStr = PrimStr; 17 | 18 | #[derive(Serialize, Deserialize)] 19 | #[serde(deny_unknown_fields)] 20 | pub struct Status { 21 | pub metadata: Metadata, 22 | pub created_at: String, 23 | pub id: LongId, 24 | pub id_str: LongIdStr, 25 | pub text: String, 26 | pub source: String, 27 | pub truncated: bool, 28 | pub in_reply_to_status_id: Option, 29 | pub in_reply_to_status_id_str: Option, 30 | pub in_reply_to_user_id: Option, 31 | pub in_reply_to_user_id_str: Option, 32 | pub in_reply_to_screen_name: Option, 33 | pub user: User, 34 | pub geo: (), 35 | pub coordinates: (), 36 | pub place: (), 37 | pub contributors: (), 38 | pub retweeted_status: Option>, 39 | pub retweet_count: u32, 40 | pub favorite_count: u32, 41 | pub entities: StatusEntities, 42 | pub favorited: bool, 43 | pub retweeted: bool, 44 | pub possibly_sensitive: Option, 45 | pub lang: LanguageCode, 46 | } 47 | 48 | #[derive(Serialize, Deserialize)] 49 | #[serde(deny_unknown_fields)] 50 | pub struct Metadata { 51 | pub result_type: ResultType, 52 | pub iso_language_code: LanguageCode, 53 | } 54 | 55 | #[derive(Serialize, Deserialize)] 56 | #[serde(deny_unknown_fields)] 57 | pub struct User { 58 | pub id: ShortId, 59 | pub id_str: ShortIdStr, 60 | pub name: String, 61 | pub screen_name: String, 62 | pub location: String, 63 | pub description: String, 64 | pub url: Option, 65 | pub entities: UserEntities, 66 | pub protected: bool, 67 | pub followers_count: u32, 68 | pub friends_count: u32, 69 | pub listed_count: u32, 70 | pub created_at: String, 71 | pub favourites_count: u32, 72 | pub utc_offset: Option, 73 | pub time_zone: Option, 74 | pub geo_enabled: bool, 75 | pub verified: bool, 76 | pub statuses_count: u32, 77 | pub lang: LanguageCode, 78 | pub contributors_enabled: bool, 79 | pub is_translator: bool, 80 | pub is_translation_enabled: bool, 81 | pub profile_background_color: Color, 82 | pub profile_background_image_url: String, 83 | pub profile_background_image_url_https: String, 84 | pub profile_background_tile: bool, 85 | pub profile_image_url: String, 86 | pub profile_image_url_https: String, 87 | pub profile_banner_url: Option, 88 | pub profile_link_color: Color, 89 | pub profile_sidebar_border_color: Color, 90 | pub profile_sidebar_fill_color: Color, 91 | pub profile_text_color: Color, 92 | pub profile_use_background_image: bool, 93 | pub default_profile: bool, 94 | pub default_profile_image: bool, 95 | pub following: bool, 96 | pub follow_request_sent: bool, 97 | pub notifications: bool, 98 | } 99 | 100 | #[derive(Serialize, Deserialize)] 101 | #[serde(deny_unknown_fields)] 102 | pub struct UserEntities { 103 | pub url: Option, 104 | pub description: UserEntitiesDescription, 105 | } 106 | 107 | #[derive(Serialize, Deserialize)] 108 | #[serde(deny_unknown_fields)] 109 | pub struct UserUrl { 110 | pub urls: Vec, 111 | } 112 | 113 | #[derive(Serialize, Deserialize)] 114 | #[serde(deny_unknown_fields)] 115 | pub struct Url { 116 | pub url: String, 117 | pub expanded_url: String, 118 | pub display_url: String, 119 | pub indices: Indices, 120 | } 121 | 122 | #[derive(Serialize, Deserialize)] 123 | #[serde(deny_unknown_fields)] 124 | pub struct UserEntitiesDescription { 125 | pub urls: Vec, 126 | } 127 | 128 | #[derive(Serialize, Deserialize)] 129 | #[serde(deny_unknown_fields)] 130 | pub struct StatusEntities { 131 | pub hashtags: Vec, 132 | pub symbols: [(); 0], 133 | pub urls: Vec, 134 | pub user_mentions: Vec, 135 | pub media: Option>, 136 | } 137 | 138 | #[derive(Serialize, Deserialize)] 139 | #[serde(deny_unknown_fields)] 140 | pub struct Hashtag { 141 | pub text: String, 142 | pub indices: Indices, 143 | } 144 | 145 | #[derive(Serialize, Deserialize)] 146 | #[serde(deny_unknown_fields)] 147 | pub struct UserMention { 148 | pub screen_name: String, 149 | pub name: String, 150 | pub id: ShortId, 151 | pub id_str: ShortIdStr, 152 | pub indices: Indices, 153 | } 154 | 155 | #[derive(Serialize, Deserialize)] 156 | #[serde(deny_unknown_fields)] 157 | pub struct Media { 158 | pub id: LongId, 159 | pub id_str: LongIdStr, 160 | pub indices: Indices, 161 | pub media_url: String, 162 | pub media_url_https: String, 163 | pub url: String, 164 | pub display_url: String, 165 | pub expanded_url: String, 166 | #[serde(rename = "type")] 167 | pub media_type: String, 168 | pub sizes: Sizes, 169 | pub source_status_id: Option, 170 | pub source_status_id_str: Option, 171 | } 172 | 173 | #[derive(Serialize, Deserialize)] 174 | #[serde(deny_unknown_fields)] 175 | pub struct Sizes { 176 | pub medium: Size, 177 | pub small: Size, 178 | pub thumb: Size, 179 | pub large: Size, 180 | } 181 | 182 | #[derive(Serialize, Deserialize)] 183 | #[serde(deny_unknown_fields)] 184 | pub struct Size { 185 | pub w: u16, 186 | pub h: u16, 187 | pub resize: Resize, 188 | } 189 | 190 | pub type Indices = (u8, u8); 191 | 192 | #[derive(Serialize, Deserialize)] 193 | #[serde(deny_unknown_fields)] 194 | pub struct SearchMetadata { 195 | pub completed_in: f32, 196 | pub max_id: LongId, 197 | pub max_id_str: LongIdStr, 198 | pub next_results: String, 199 | pub query: String, 200 | pub refresh_url: String, 201 | pub count: u8, 202 | pub since_id: LongId, 203 | pub since_id_str: LongIdStr, 204 | } 205 | 206 | #[derive(Serialize, Deserialize)] 207 | #[serde(deny_unknown_fields)] 208 | pub enum Resize { 209 | #[serde(rename = "fit")] 210 | Fit, 211 | #[serde(rename = "crop")] 212 | Crop, 213 | } 214 | 215 | #[derive(Serialize, Deserialize)] 216 | #[serde(deny_unknown_fields)] 217 | pub enum LanguageCode { 218 | #[serde(rename = "zh-cn")] 219 | Cn, 220 | #[serde(rename = "en")] 221 | En, 222 | #[serde(rename = "es")] 223 | Es, 224 | #[serde(rename = "it")] 225 | It, 226 | #[serde(rename = "ja")] 227 | Ja, 228 | #[serde(rename = "zh")] 229 | Zh, 230 | } 231 | 232 | #[derive(Serialize, Deserialize)] 233 | #[serde(deny_unknown_fields)] 234 | pub enum ResultType { 235 | #[serde(rename = "recent")] 236 | Recent, 237 | } 238 | -------------------------------------------------------------------------------- /src/de.rs: -------------------------------------------------------------------------------- 1 | use js_sys::{Array, ArrayBuffer, JsString, Map, Number, Object, Symbol, Uint8Array}; 2 | use serde::de::value::{MapDeserializer, SeqDeserializer}; 3 | use serde::de::{self, IntoDeserializer}; 4 | use std::convert::TryFrom; 5 | use wasm_bindgen::convert::IntoWasmAbi; 6 | use wasm_bindgen::{JsCast, JsValue, UnwrapThrowExt}; 7 | 8 | use crate::preserve::PRESERVED_VALUE_MAGIC; 9 | use crate::{static_str_to_js, Error, ObjectExt, Result}; 10 | 11 | /// Provides [`de::SeqAccess`] from any JS iterator. 12 | struct SeqAccess { 13 | iter: js_sys::IntoIter, 14 | } 15 | 16 | impl<'de> de::SeqAccess<'de> for SeqAccess { 17 | type Error = Error; 18 | 19 | fn next_element_seed>( 20 | &mut self, 21 | seed: T, 22 | ) -> Result> { 23 | Ok(match self.iter.next().transpose()? { 24 | Some(value) => Some(seed.deserialize(Deserializer::from(value))?), 25 | None => None, 26 | }) 27 | } 28 | } 29 | 30 | /// Provides [`de::MapAccess`] from any JS iterator that returns `[key, value]` pairs. 31 | struct MapAccess { 32 | iter: js_sys::IntoIter, 33 | next_value: Option, 34 | } 35 | 36 | impl MapAccess { 37 | const fn new(iter: js_sys::IntoIter) -> Self { 38 | Self { 39 | iter, 40 | next_value: None, 41 | } 42 | } 43 | } 44 | 45 | impl<'de> de::MapAccess<'de> for MapAccess { 46 | type Error = Error; 47 | 48 | fn next_key_seed>(&mut self, seed: K) -> Result> { 49 | debug_assert!(self.next_value.is_none()); 50 | 51 | Ok(match self.iter.next().transpose()? { 52 | Some(pair) => { 53 | let (key, value) = convert_pair(pair); 54 | self.next_value = Some(value); 55 | Some(seed.deserialize(key)?) 56 | } 57 | None => None, 58 | }) 59 | } 60 | 61 | fn next_value_seed>(&mut self, seed: V) -> Result { 62 | seed.deserialize(self.next_value.take().unwrap_throw()) 63 | } 64 | } 65 | 66 | struct ObjectAccess { 67 | obj: ObjectExt, 68 | fields: std::slice::Iter<'static, &'static str>, 69 | next_value: Option, 70 | } 71 | 72 | impl ObjectAccess { 73 | fn new(obj: ObjectExt, fields: &'static [&'static str]) -> Self { 74 | Self { 75 | obj, 76 | fields: fields.iter(), 77 | next_value: None, 78 | } 79 | } 80 | } 81 | 82 | fn str_deserializer(s: &str) -> de::value::StrDeserializer { 83 | de::IntoDeserializer::into_deserializer(s) 84 | } 85 | 86 | impl<'de> de::MapAccess<'de> for ObjectAccess { 87 | type Error = Error; 88 | 89 | fn next_key_seed>(&mut self, seed: K) -> Result> { 90 | debug_assert!(self.next_value.is_none()); 91 | 92 | for field in &mut self.fields { 93 | let js_field = static_str_to_js(field); 94 | let next_value = self.obj.get_with_ref_key(&js_field); 95 | // If this value is `undefined`, it might be actually a missing field; 96 | // double-check with an `in` operator if so. 97 | let is_missing_field = next_value.is_undefined() && !js_field.js_in(&self.obj); 98 | if !is_missing_field { 99 | self.next_value = Some(Deserializer::from(next_value)); 100 | return Ok(Some(seed.deserialize(str_deserializer(field))?)); 101 | } 102 | } 103 | 104 | Ok(None) 105 | } 106 | 107 | fn next_value_seed>(&mut self, seed: V) -> Result { 108 | seed.deserialize(self.next_value.take().unwrap_throw()) 109 | } 110 | } 111 | 112 | enum PreservedValueAccess { 113 | OnMagic(JsValue), 114 | OnValue(JsValue), 115 | Done, 116 | } 117 | 118 | impl<'de> de::SeqAccess<'de> for PreservedValueAccess { 119 | type Error = Error; 120 | 121 | fn next_element_seed>( 122 | &mut self, 123 | seed: T, 124 | ) -> Result> { 125 | // Temporary replacement to avoid borrow checker issues when moving out `JsValue`. 126 | let this = std::mem::replace(self, Self::Done); 127 | match this { 128 | Self::OnMagic(value) => { 129 | *self = Self::OnValue(value); 130 | seed.deserialize(str_deserializer(PRESERVED_VALUE_MAGIC)) 131 | .map(Some) 132 | } 133 | Self::OnValue(value) => seed 134 | .deserialize(Deserializer { 135 | value: JsValue::from(value.into_abi()), 136 | }) 137 | .map(Some), 138 | Self::Done => Ok(None), 139 | } 140 | } 141 | } 142 | 143 | /// Provides [`de::EnumAccess`] from given JS values for the `tag` and the `payload`. 144 | struct EnumAccess { 145 | tag: Deserializer, 146 | payload: Deserializer, 147 | } 148 | 149 | impl<'de> de::EnumAccess<'de> for EnumAccess { 150 | type Error = Error; 151 | type Variant = Deserializer; 152 | 153 | fn variant_seed>( 154 | self, 155 | seed: V, 156 | ) -> Result<(V::Value, Self::Variant)> { 157 | Ok((seed.deserialize(self.tag)?, self.payload)) 158 | } 159 | } 160 | 161 | /// A newtype that allows using any [`JsValue`] as a [`de::Deserializer`]. 162 | pub struct Deserializer { 163 | value: JsValue, 164 | } 165 | 166 | impl From for Deserializer { 167 | fn from(value: JsValue) -> Self { 168 | Self { value } 169 | } 170 | } 171 | 172 | // Ideally this would be implemented for `JsValue` instead, but we can't because 173 | // of the orphan rule. 174 | impl<'de> IntoDeserializer<'de, Error> for Deserializer { 175 | type Deserializer = Self; 176 | 177 | fn into_deserializer(self) -> Self::Deserializer { 178 | self 179 | } 180 | } 181 | 182 | /// Destructures a JS `[key, value]` pair into a tuple of [`Deserializer`]s. 183 | fn convert_pair(pair: JsValue) -> (Deserializer, Deserializer) { 184 | let pair = pair.unchecked_into::(); 185 | (pair.get(0).into(), pair.get(1).into()) 186 | } 187 | 188 | impl Deserializer { 189 | fn as_object_entries(&self) -> Option { 190 | if self.value.is_object() { 191 | Some(Object::entries(self.value.unchecked_ref())) 192 | } else { 193 | None 194 | } 195 | } 196 | 197 | fn is_nullish(&self) -> bool { 198 | self.value.loose_eq(&JsValue::NULL) 199 | } 200 | 201 | fn as_bytes(&self) -> Option> { 202 | let temp; 203 | 204 | let v = if let Some(v) = self.value.dyn_ref::() { 205 | v 206 | } else if let Some(v) = self.value.dyn_ref::() { 207 | temp = Uint8Array::new(v); 208 | &temp 209 | } else { 210 | return None; 211 | }; 212 | 213 | Some(v.to_vec()) 214 | } 215 | 216 | #[cold] 217 | fn invalid_type_(&self, visitor: &dyn de::Expected) -> Error { 218 | let string; 219 | let bytes; 220 | 221 | let unexpected = if self.is_nullish() { 222 | de::Unexpected::Unit 223 | } else if let Some(v) = self.value.as_bool() { 224 | de::Unexpected::Bool(v) 225 | } else if let Some(v) = self.value.as_f64() { 226 | de::Unexpected::Float(v) 227 | } else if let Some(v) = self.value.as_string() { 228 | string = v; 229 | de::Unexpected::Str(&string) 230 | } else if let Some(v) = self.as_bytes() { 231 | bytes = v; 232 | de::Unexpected::Bytes(&bytes) 233 | } else { 234 | string = format!("{:?}", self.value); 235 | de::Unexpected::Other(&string) 236 | }; 237 | 238 | de::Error::invalid_type(unexpected, visitor) 239 | } 240 | 241 | fn invalid_type<'de, V: de::Visitor<'de>>(&self, visitor: V) -> Result { 242 | Err(self.invalid_type_(&visitor)) 243 | } 244 | 245 | fn as_safe_integer(&self) -> Option { 246 | if Number::is_safe_integer(&self.value) { 247 | return Some(self.value.unchecked_into_f64() as i64); 248 | } 249 | None 250 | } 251 | 252 | fn deserialize_from_js_number_signed<'de, V: de::Visitor<'de>>( 253 | &self, 254 | visitor: V, 255 | ) -> Result { 256 | match self.as_safe_integer() { 257 | Some(v) => visitor.visit_i64(v), 258 | _ => self.invalid_type(visitor), 259 | } 260 | } 261 | 262 | fn deserialize_from_js_number_unsigned<'de, V: de::Visitor<'de>>( 263 | &self, 264 | visitor: V, 265 | ) -> Result { 266 | match self.as_safe_integer() { 267 | Some(v) if v >= 0 => visitor.visit_u64(v as _), 268 | _ => self.invalid_type(visitor), 269 | } 270 | } 271 | 272 | fn deserialize_from_array<'de, V: de::Visitor<'de>>( 273 | &self, 274 | visitor: V, 275 | array: &Array, 276 | ) -> Result { 277 | visitor.visit_seq(SeqDeserializer::new(array.iter().map(Deserializer::from))) 278 | } 279 | } 280 | 281 | impl<'de> de::Deserializer<'de> for Deserializer { 282 | type Error = Error; 283 | 284 | fn deserialize_any>(self, visitor: V) -> Result { 285 | if self.is_nullish() { 286 | // Ideally we would only treat `undefined` as `()` / `None` which would be semantically closer 287 | // to JS definitions, but, unfortunately, WebIDL generates missing values as `null` 288 | // and we probably want to support these as well. 289 | visitor.visit_unit() 290 | } else if let Some(v) = self.value.as_bool() { 291 | visitor.visit_bool(v) 292 | } else if self.value.is_bigint() { 293 | match i64::try_from(self.value) { 294 | Ok(v) => visitor.visit_i64(v), 295 | Err(value) => match u64::try_from(value) { 296 | Ok(v) => visitor.visit_u64(v), 297 | Err(_) => Err(de::Error::custom("Couldn't deserialize i64 or u64 from a BigInt outside i64::MIN..u64::MAX bounds")) 298 | } 299 | } 300 | } else if let Some(v) = self.value.as_f64() { 301 | if Number::is_safe_integer(&self.value) { 302 | visitor.visit_i64(v as i64) 303 | } else { 304 | visitor.visit_f64(v) 305 | } 306 | } else if let Some(v) = self.value.as_string() { 307 | visitor.visit_string(v) 308 | } else if Array::is_array(&self.value) { 309 | self.deserialize_seq(visitor) 310 | } else if let Some(bytes) = self.as_bytes() { 311 | // We need to handle this here because serde uses `deserialize_any` 312 | // for internally tagged enums 313 | visitor.visit_byte_buf(bytes) 314 | } else if self.value.is_object() && 315 | // The only reason we want to support objects here is because serde uses 316 | // `deserialize_any` for internally tagged enums 317 | // (see https://github.com/RReverser/serde-wasm-bindgen/pull/4#discussion_r352245020). 318 | // 319 | // We expect such enums to be represented via plain JS objects, so let's explicitly 320 | // exclude Sets and other iterables. These should be deserialized via concrete 321 | // `deserialize_*` methods instead of us trying to guess the right target type. 322 | // 323 | // We still do support Map, so that the format described here stays a self-describing 324 | // format: we happen to serialize to Map, and it is not ambiguous. 325 | // 326 | // Hopefully we can rid of these hacks altogether once 327 | // https://github.com/serde-rs/serde/issues/1183 is implemented / fixed on serde side. 328 | (!Symbol::iterator().js_in(&self.value) || self.value.has_type::()) 329 | { 330 | self.deserialize_map(visitor) 331 | } else { 332 | self.invalid_type(visitor) 333 | } 334 | } 335 | 336 | fn deserialize_unit>(self, visitor: V) -> Result { 337 | if self.is_nullish() { 338 | visitor.visit_unit() 339 | } else { 340 | self.invalid_type(visitor) 341 | } 342 | } 343 | 344 | fn deserialize_unit_struct>( 345 | self, 346 | _name: &'static str, 347 | visitor: V, 348 | ) -> Result { 349 | self.deserialize_unit(visitor) 350 | } 351 | 352 | fn deserialize_bool>(self, visitor: V) -> Result { 353 | if let Some(v) = self.value.as_bool() { 354 | visitor.visit_bool(v) 355 | } else { 356 | self.invalid_type(visitor) 357 | } 358 | } 359 | 360 | // Serde happily converts `f64` to `f32` (with checks), so we can forward. 361 | fn deserialize_f32>(self, visitor: V) -> Result { 362 | self.deserialize_f64(visitor) 363 | } 364 | 365 | fn deserialize_f64>(self, visitor: V) -> Result { 366 | if let Some(v) = self.value.as_f64() { 367 | visitor.visit_f64(v) 368 | } else { 369 | self.invalid_type(visitor) 370 | } 371 | } 372 | 373 | fn deserialize_identifier>(self, visitor: V) -> Result { 374 | self.deserialize_str(visitor) 375 | } 376 | 377 | fn deserialize_str>(self, visitor: V) -> Result { 378 | self.deserialize_string(visitor) 379 | } 380 | 381 | fn deserialize_string>(self, visitor: V) -> Result { 382 | if let Some(v) = self.value.as_string() { 383 | visitor.visit_string(v) 384 | } else { 385 | self.invalid_type(visitor) 386 | } 387 | } 388 | 389 | // Serde happily converts any integer to any integer (with checks), so let's forward all of 390 | // these to 64-bit methods to save some space in the generated WASM. 391 | 392 | fn deserialize_i8>(self, visitor: V) -> Result { 393 | self.deserialize_from_js_number_signed(visitor) 394 | } 395 | 396 | fn deserialize_i16>(self, visitor: V) -> Result { 397 | self.deserialize_from_js_number_signed(visitor) 398 | } 399 | 400 | fn deserialize_i32>(self, visitor: V) -> Result { 401 | self.deserialize_from_js_number_signed(visitor) 402 | } 403 | 404 | fn deserialize_u8>(self, visitor: V) -> Result { 405 | self.deserialize_from_js_number_unsigned(visitor) 406 | } 407 | 408 | fn deserialize_u16>(self, visitor: V) -> Result { 409 | self.deserialize_from_js_number_unsigned(visitor) 410 | } 411 | 412 | fn deserialize_u32>(self, visitor: V) -> Result { 413 | self.deserialize_from_js_number_unsigned(visitor) 414 | } 415 | 416 | /// Supported inputs: 417 | /// - `BigInt` within `i64` boundaries. 418 | /// - number within safe integer boundaries. 419 | fn deserialize_i64>(self, visitor: V) -> Result { 420 | if self.value.is_bigint() { 421 | match i64::try_from(self.value) { 422 | Ok(v) => visitor.visit_i64(v), 423 | Err(_) => Err(de::Error::custom( 424 | "Couldn't deserialize i64 from a BigInt outside i64::MIN..i64::MAX bounds", 425 | )), 426 | } 427 | } else { 428 | self.deserialize_from_js_number_signed(visitor) 429 | } 430 | } 431 | 432 | /// Supported inputs: 433 | /// - `BigInt` within `u64` boundaries. 434 | /// - number within safe integer boundaries. 435 | fn deserialize_u64>(self, visitor: V) -> Result { 436 | if self.value.is_bigint() { 437 | match u64::try_from(self.value) { 438 | Ok(v) => visitor.visit_u64(v), 439 | Err(_) => Err(de::Error::custom( 440 | "Couldn't deserialize u64 from a BigInt outside u64::MIN..u64::MAX bounds", 441 | )), 442 | } 443 | } else { 444 | self.deserialize_from_js_number_unsigned(visitor) 445 | } 446 | } 447 | 448 | /// Supported inputs: 449 | /// - `BigInt` within `i128` boundaries. 450 | fn deserialize_i128>(self, visitor: V) -> Result { 451 | if self.value.is_bigint() { 452 | match i128::try_from(self.value) { 453 | Ok(v) => visitor.visit_i128(v), 454 | Err(_) => Err(de::Error::custom( 455 | "Couldn't deserialize i128 from a BigInt outside i128::MIN..i128::MAX bounds", 456 | )), 457 | } 458 | } else { 459 | self.invalid_type(visitor) 460 | } 461 | } 462 | 463 | /// Supported inputs: 464 | /// - `BigInt` within `u128` boundaries. 465 | fn deserialize_u128>(self, visitor: V) -> Result { 466 | if self.value.is_bigint() { 467 | match u128::try_from(self.value) { 468 | Ok(v) => visitor.visit_u128(v), 469 | Err(_) => Err(de::Error::custom( 470 | "Couldn't deserialize u128 from a BigInt outside u128::MIN..u128::MAX bounds", 471 | )), 472 | } 473 | } else { 474 | self.invalid_type(visitor) 475 | } 476 | } 477 | 478 | /// Converts a JavaScript string to a Rust `char`. 479 | /// 480 | /// By default we don't perform detection of single chars because it's pretty complicated, 481 | /// but if we get a hint that they're expected, this methods allows to avoid heap allocations 482 | /// of an intermediate `String` by directly converting numeric codepoints instead. 483 | fn deserialize_char>(self, visitor: V) -> Result { 484 | if let Some(s) = self.value.dyn_ref::() { 485 | if let Some(c) = s.as_char() { 486 | return visitor.visit_char(c); 487 | } 488 | } 489 | self.invalid_type(visitor) 490 | } 491 | 492 | /// Deserializes `undefined` or `null` into `None` and any other value into `Some(value)`. 493 | // Serde can deserialize `visit_unit` into `None`, but can't deserialize arbitrary value 494 | // as `Some`, so we need to provide own simple implementation. 495 | fn deserialize_option>(self, visitor: V) -> Result { 496 | if !self.is_nullish() { 497 | visitor.visit_some(self) 498 | } else { 499 | visitor.visit_none() 500 | } 501 | } 502 | 503 | /// Forwards to deserializing newtype contents. 504 | fn deserialize_newtype_struct>( 505 | self, 506 | _name: &'static str, 507 | visitor: V, 508 | ) -> Result { 509 | visitor.visit_newtype_struct(self) 510 | } 511 | 512 | /// Supported inputs: 513 | /// - JS iterable (an object with [`[Symbol.iterator]`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/iterator)). 514 | /// 515 | /// Supported outputs: 516 | /// - Any Rust sequence from Serde point of view ([`Vec`], [`HashSet`](std::collections::HashSet), etc.) 517 | fn deserialize_seq>(self, visitor: V) -> Result { 518 | if let Some(arr) = self.value.dyn_ref::() { 519 | self.deserialize_from_array(visitor, arr) 520 | } else if let Some(iter) = js_sys::try_iter(&self.value)? { 521 | visitor.visit_seq(SeqAccess { iter }) 522 | } else { 523 | self.invalid_type(visitor) 524 | } 525 | } 526 | 527 | /// Forwards to [`Self::deserialize_seq`](#method.deserialize_seq). 528 | fn deserialize_tuple>(self, _len: usize, visitor: V) -> Result { 529 | self.deserialize_seq(visitor) 530 | } 531 | 532 | /// Forwards to [`Self::deserialize_tuple`](#method.deserialize_tuple). 533 | fn deserialize_tuple_struct>( 534 | self, 535 | name: &'static str, 536 | len: usize, 537 | visitor: V, 538 | ) -> Result { 539 | if name == PRESERVED_VALUE_MAGIC { 540 | return visitor.visit_seq(PreservedValueAccess::OnMagic(self.value)); 541 | } 542 | self.deserialize_tuple(len, visitor) 543 | } 544 | 545 | /// Supported inputs: 546 | /// - A JS iterable that is expected to return `[key, value]` pairs. 547 | /// - A JS object, which will be iterated using [`Object.entries`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries). 548 | /// 549 | /// Supported outputs: 550 | /// - A Rust key-value map ([`HashMap`](std::collections::HashMap), [`BTreeMap`](std::collections::BTreeMap), etc.). 551 | /// - A typed Rust structure with `#[derive(Deserialize)]`. 552 | fn deserialize_map>(self, visitor: V) -> Result { 553 | match js_sys::try_iter(&self.value)? { 554 | Some(iter) => visitor.visit_map(MapAccess::new(iter)), 555 | None => match self.as_object_entries() { 556 | Some(arr) => visitor.visit_map(MapDeserializer::new(arr.iter().map(convert_pair))), 557 | None => self.invalid_type(visitor), 558 | }, 559 | } 560 | } 561 | 562 | /// Supported inputs: 563 | /// - A plain JS object. 564 | /// 565 | /// Supported outputs: 566 | /// - A typed Rust structure with `#[derive(Deserialize)]`. 567 | fn deserialize_struct>( 568 | self, 569 | _name: &'static str, 570 | fields: &'static [&'static str], 571 | visitor: V, 572 | ) -> Result { 573 | let obj = if self.value.is_object() { 574 | self.value.unchecked_into::() 575 | } else { 576 | return self.invalid_type(visitor); 577 | }; 578 | visitor.visit_map(ObjectAccess::new(obj, fields)) 579 | } 580 | 581 | /// Here we try to be compatible with `serde-json`, which means supporting: 582 | /// - `"Variant"` - gets converted to a unit variant `MyEnum::Variant` 583 | /// - `{ Variant: ...payload... }` - gets converted to a `MyEnum::Variant { ...payload... }`. 584 | fn deserialize_enum>( 585 | self, 586 | _name: &'static str, 587 | _variants: &'static [&'static str], 588 | visitor: V, 589 | ) -> Result { 590 | let access = if self.value.is_string() { 591 | EnumAccess { 592 | tag: self.value.into(), 593 | payload: JsValue::UNDEFINED.into(), 594 | } 595 | } else if let Some(entries) = self.as_object_entries() { 596 | if entries.length() != 1 { 597 | return Err(de::Error::invalid_length(entries.length() as _, &"1")); 598 | } 599 | let entry = entries.get(0); 600 | let (tag, payload) = convert_pair(entry); 601 | EnumAccess { tag, payload } 602 | } else { 603 | return self.invalid_type(visitor); 604 | }; 605 | visitor.visit_enum(access) 606 | } 607 | 608 | /// Ignores any value without calling to the JS side even to check its type. 609 | fn deserialize_ignored_any>(self, visitor: V) -> Result { 610 | visitor.visit_unit() 611 | } 612 | 613 | /// We can't take references to JS memory, so forwards to an owned [`Self::deserialize_byte_buf`](#method.deserialize_byte_buf). 614 | fn deserialize_bytes>(self, visitor: V) -> Result { 615 | self.deserialize_byte_buf(visitor) 616 | } 617 | 618 | /// Serde expects `visit_byte_buf` to be called only in response to an explicit `deserialize_bytes`, 619 | /// so we provide conversions here. 620 | /// 621 | /// Supported inputs: 622 | /// - `ArrayBuffer` - converted to an `Uint8Array` view first. 623 | /// - `Uint8Array`, `Array` - copied to a newly created `Vec` on the Rust side. 624 | fn deserialize_byte_buf>(self, visitor: V) -> Result { 625 | if let Some(bytes) = self.as_bytes() { 626 | visitor.visit_byte_buf(bytes) 627 | } else if let Some(arr) = self.value.dyn_ref::() { 628 | self.deserialize_from_array(visitor, arr) 629 | } else { 630 | self.invalid_type(visitor) 631 | } 632 | } 633 | 634 | fn is_human_readable(&self) -> bool { 635 | true 636 | } 637 | } 638 | 639 | impl<'de> de::VariantAccess<'de> for Deserializer { 640 | type Error = Error; 641 | 642 | fn unit_variant(self) -> Result<()> { 643 | de::Deserialize::deserialize(self) 644 | } 645 | 646 | fn newtype_variant_seed>(self, seed: T) -> Result { 647 | seed.deserialize(self) 648 | } 649 | 650 | fn tuple_variant>(self, len: usize, visitor: V) -> Result { 651 | de::Deserializer::deserialize_tuple(self, len, visitor) 652 | } 653 | 654 | fn struct_variant>( 655 | self, 656 | fields: &'static [&'static str], 657 | visitor: V, 658 | ) -> Result { 659 | de::Deserializer::deserialize_struct(self, "", fields, visitor) 660 | } 661 | } 662 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::prelude::*; 2 | 3 | /// A newtype that represents Serde errors as JavaScript exceptions. 4 | #[derive(Debug)] 5 | pub struct Error(JsValue); 6 | 7 | impl std::fmt::Display for Error { 8 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 9 | #[wasm_bindgen] 10 | extern "C" { 11 | #[wasm_bindgen(js_name = String)] 12 | pub fn to_string(value: &JsValue) -> String; 13 | } 14 | 15 | to_string(&self.0).fmt(f) 16 | } 17 | } 18 | 19 | impl std::error::Error for Error {} 20 | 21 | impl Error { 22 | /// Creates a JavaScript `Error` with a given message. 23 | pub fn new(msg: T) -> Self { 24 | Error(JsError::new(&msg.to_string()).into()) 25 | } 26 | } 27 | 28 | impl serde::ser::Error for Error { 29 | fn custom(msg: T) -> Self { 30 | Error::new(msg) 31 | } 32 | } 33 | 34 | impl serde::de::Error for Error { 35 | fn custom(msg: T) -> Self { 36 | Error::new(msg) 37 | } 38 | } 39 | 40 | /// This conversion is needed for `?` to just work when using wasm-bindgen 41 | /// imports that return JavaScript exceptions as `Result`. 42 | impl From for Error { 43 | fn from(error: JsValue) -> Error { 44 | Error(error) 45 | } 46 | } 47 | 48 | // This conversion is needed for `?` to just work in wasm-bindgen exports 49 | // that return `Result` to throw JavaScript exceptions. 50 | impl From for JsValue { 51 | fn from(error: Error) -> JsValue { 52 | error.0 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc = include_str!("../README.md")] 2 | #![warn(missing_docs)] 3 | #![warn(clippy::missing_const_for_fn)] 4 | 5 | use js_sys::JsString; 6 | use wasm_bindgen::prelude::*; 7 | 8 | mod de; 9 | mod error; 10 | mod ser; 11 | 12 | pub use de::Deserializer; 13 | pub use error::Error; 14 | pub use ser::Serializer; 15 | 16 | type Result = std::result::Result; 17 | 18 | fn static_str_to_js(s: &'static str) -> JsString { 19 | use std::cell::RefCell; 20 | use std::collections::HashMap; 21 | 22 | #[derive(Default)] 23 | struct PtrHasher { 24 | addr: usize, 25 | } 26 | 27 | impl std::hash::Hasher for PtrHasher { 28 | fn write(&mut self, _bytes: &[u8]) { 29 | unreachable!(); 30 | } 31 | 32 | fn write_usize(&mut self, addr_or_len: usize) { 33 | if self.addr == 0 { 34 | self.addr = addr_or_len; 35 | } 36 | } 37 | 38 | fn finish(&self) -> u64 { 39 | self.addr as _ 40 | } 41 | } 42 | 43 | type PtrBuildHasher = std::hash::BuildHasherDefault; 44 | 45 | thread_local! { 46 | // Since we're mainly optimising for converting the exact same string literal over and over again, 47 | // which will always have the same pointer, we can speed things up by indexing by the string's pointer 48 | // instead of its value. 49 | static CACHE: RefCell> = Default::default(); 50 | } 51 | CACHE.with(|cache| { 52 | cache 53 | .borrow_mut() 54 | .entry(s) 55 | .or_insert_with(|| s.into()) 56 | .clone() 57 | }) 58 | } 59 | 60 | /// Custom bindings to avoid using fallible `Reflect` for plain objects. 61 | #[wasm_bindgen] 62 | extern "C" { 63 | type ObjectExt; 64 | 65 | #[wasm_bindgen(method, indexing_getter)] 66 | fn get_with_ref_key(this: &ObjectExt, key: &JsString) -> JsValue; 67 | 68 | #[wasm_bindgen(method, indexing_setter)] 69 | fn set(this: &ObjectExt, key: JsString, value: JsValue); 70 | } 71 | 72 | /// Converts [`JsValue`] into a Rust type. 73 | pub fn from_value(value: JsValue) -> Result { 74 | T::deserialize(Deserializer::from(value)) 75 | } 76 | 77 | /// Converts a Rust value into a [`JsValue`]. 78 | pub fn to_value(value: &T) -> Result { 79 | value.serialize(&Serializer::new()) 80 | } 81 | 82 | /// Serialization and deserialization functions that pass JavaScript objects through unchanged. 83 | /// 84 | /// This module is compatible with the `serde(with)` annotation, so for example if you create 85 | /// the struct 86 | /// 87 | /// ```rust 88 | /// #[derive(serde::Serialize)] 89 | /// struct MyStruct { 90 | /// int_field: i32, 91 | /// #[serde(with = "serde_wasm_bindgen::preserve")] 92 | /// js_field: js_sys::Int8Array, 93 | /// } 94 | /// 95 | /// let s = MyStruct { 96 | /// int_field: 5, 97 | /// js_field: js_sys::Int8Array::new_with_length(1000), 98 | /// }; 99 | /// ``` 100 | /// 101 | /// then `serde_wasm_bindgen::to_value(&s)` 102 | /// will return a JsValue representing an object with two fields (`int_field` and `js_field`), where 103 | /// `js_field` will be an `Int8Array` pointing to the same underlying JavaScript object as `s.js_field` does. 104 | pub mod preserve { 105 | use serde::{de::Error, Deserialize, Serialize}; 106 | use wasm_bindgen::{ 107 | convert::{FromWasmAbi, IntoWasmAbi}, 108 | JsCast, JsValue, 109 | }; 110 | 111 | // Some arbitrary string that no one will collide with unless they try. 112 | pub(crate) const PRESERVED_VALUE_MAGIC: &str = "1fc430ca-5b7f-4295-92de-33cf2b145d38"; 113 | 114 | struct Magic; 115 | 116 | impl<'de> serde::de::Deserialize<'de> for Magic { 117 | fn deserialize>(de: D) -> Result { 118 | struct Visitor; 119 | 120 | impl<'de> serde::de::Visitor<'de> for Visitor { 121 | type Value = Magic; 122 | 123 | fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { 124 | formatter.write_str("serde-wasm-bindgen's magic string") 125 | } 126 | 127 | fn visit_str(self, s: &str) -> Result { 128 | if s == PRESERVED_VALUE_MAGIC { 129 | Ok(Magic) 130 | } else { 131 | Err(E::invalid_value(serde::de::Unexpected::Str(s), &self)) 132 | } 133 | } 134 | } 135 | 136 | de.deserialize_str(Visitor) 137 | } 138 | } 139 | 140 | #[derive(Serialize)] 141 | #[serde(rename = "1fc430ca-5b7f-4295-92de-33cf2b145d38")] 142 | struct PreservedValueSerWrapper(u32); 143 | 144 | // Intentionally asymmetrical wrapper to ensure that only serde-wasm-bindgen preserves roundtrip. 145 | #[derive(Deserialize)] 146 | #[serde(rename = "1fc430ca-5b7f-4295-92de-33cf2b145d38")] 147 | struct PreservedValueDeWrapper(Magic, u32); 148 | 149 | /// Serialize any `JsCast` value. 150 | /// 151 | /// When used with the `Serializer` in `serde_wasm_bindgen`, this serializes the value by 152 | /// passing it through as a `JsValue`. 153 | /// 154 | /// This function is compatible with the `serde(serialize_with)` derive annotation. 155 | pub fn serialize(val: &T, ser: S) -> Result { 156 | // It's responsibility of serde-wasm-bindgen's Serializer to clone the value. 157 | // For all other serializers, using reference instead of cloning here will ensure that we don't 158 | // create accidental leaks. 159 | PreservedValueSerWrapper(val.as_ref().into_abi()).serialize(ser) 160 | } 161 | 162 | /// Deserialize any `JsCast` value. 163 | /// 164 | /// When used with the `Derializer` in `serde_wasm_bindgen`, this serializes the value by 165 | /// passing it through as a `JsValue` and casting it. 166 | /// 167 | /// This function is compatible with the `serde(deserialize_with)` derive annotation. 168 | pub fn deserialize<'de, D: serde::Deserializer<'de>, T: JsCast>(de: D) -> Result { 169 | let wrap = PreservedValueDeWrapper::deserialize(de)?; 170 | // When used with our deserializer this unsafe is correct, because the 171 | // deserializer just converted a JsValue into_abi. 172 | // 173 | // Other deserializers are unlikely to end up here, thanks 174 | // to the asymmetry between PreservedValueSerWrapper and 175 | // PreservedValueDeWrapper. Even if some other deserializer ends up 176 | // here, this may be incorrect but it shouldn't be UB because JsValues 177 | // are represented using indices into a JS-side (i.e. bounds-checked) 178 | // array. 179 | let val: JsValue = unsafe { FromWasmAbi::from_abi(wrap.1) }; 180 | val.dyn_into().map_err(|e| { 181 | D::Error::custom(format_args!( 182 | "incompatible JS value {e:?} for type {}", 183 | std::any::type_name::() 184 | )) 185 | }) 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/ser.rs: -------------------------------------------------------------------------------- 1 | use js_sys::{Array, JsString, Map, Number, Object, Uint8Array}; 2 | use serde::ser::{self, Error as _, Serialize}; 3 | use wasm_bindgen::convert::RefFromWasmAbi; 4 | use wasm_bindgen::prelude::*; 5 | use wasm_bindgen::JsCast; 6 | 7 | use crate::preserve::PRESERVED_VALUE_MAGIC; 8 | use crate::{static_str_to_js, Error, ObjectExt}; 9 | 10 | type Result = super::Result; 11 | 12 | /// Wraps other serializers into an enum tagged variant form. 13 | /// 14 | /// Results in `{"Variant": ...payload...}` for compatibility with serde-json. 15 | pub struct VariantSerializer { 16 | variant: &'static str, 17 | inner: S, 18 | } 19 | 20 | impl VariantSerializer { 21 | const fn new(variant: &'static str, inner: S) -> Self { 22 | Self { variant, inner } 23 | } 24 | 25 | fn end(self, inner: impl FnOnce(S) -> Result) -> Result { 26 | let value = inner(self.inner)?; 27 | let obj = Object::new(); 28 | obj.unchecked_ref::() 29 | .set(static_str_to_js(self.variant), value); 30 | Ok(obj.into()) 31 | } 32 | } 33 | 34 | impl> ser::SerializeTupleVariant 35 | for VariantSerializer 36 | { 37 | type Ok = JsValue; 38 | type Error = Error; 39 | 40 | fn serialize_field(&mut self, value: &T) -> Result<()> { 41 | self.inner.serialize_field(value) 42 | } 43 | 44 | fn end(self) -> Result { 45 | self.end(S::end) 46 | } 47 | } 48 | 49 | impl> ser::SerializeStructVariant 50 | for VariantSerializer 51 | { 52 | type Ok = JsValue; 53 | type Error = Error; 54 | 55 | fn serialize_field( 56 | &mut self, 57 | key: &'static str, 58 | value: &T, 59 | ) -> Result<()> { 60 | self.inner.serialize_field(key, value) 61 | } 62 | 63 | fn end(self) -> Result { 64 | self.end(S::end) 65 | } 66 | } 67 | 68 | /// Serializes Rust iterables and tuples into JS arrays. 69 | pub struct ArraySerializer<'s> { 70 | serializer: &'s Serializer, 71 | target: Array, 72 | idx: u32, 73 | } 74 | 75 | impl<'s> ArraySerializer<'s> { 76 | fn new(serializer: &'s Serializer) -> Self { 77 | Self { 78 | serializer, 79 | target: Array::new(), 80 | idx: 0, 81 | } 82 | } 83 | } 84 | 85 | impl ser::SerializeSeq for ArraySerializer<'_> { 86 | type Ok = JsValue; 87 | type Error = Error; 88 | 89 | fn serialize_element(&mut self, value: &T) -> Result<()> { 90 | self.target.set(self.idx, value.serialize(self.serializer)?); 91 | self.idx += 1; 92 | Ok(()) 93 | } 94 | 95 | fn end(self) -> Result { 96 | Ok(self.target.into()) 97 | } 98 | } 99 | 100 | impl ser::SerializeTuple for ArraySerializer<'_> { 101 | type Ok = JsValue; 102 | type Error = Error; 103 | 104 | fn serialize_element(&mut self, value: &T) -> Result<()> { 105 | ser::SerializeSeq::serialize_element(self, value) 106 | } 107 | 108 | fn end(self) -> Result { 109 | ser::SerializeSeq::end(self) 110 | } 111 | } 112 | 113 | impl ser::SerializeTupleStruct for ArraySerializer<'_> { 114 | type Ok = JsValue; 115 | type Error = Error; 116 | 117 | fn serialize_field(&mut self, value: &T) -> Result<()> { 118 | ser::SerializeTuple::serialize_element(self, value) 119 | } 120 | 121 | fn end(self) -> Result { 122 | ser::SerializeTuple::end(self) 123 | } 124 | } 125 | 126 | pub enum MapResult { 127 | Map(Map), 128 | Object(Object), 129 | } 130 | 131 | /// Serializes Rust maps into JS `Map` or plain JS objects. 132 | /// 133 | /// Plain JS objects are used if `serialize_maps_as_objects` is set to `true`, 134 | /// but then only string keys are supported. 135 | pub struct MapSerializer<'s> { 136 | serializer: &'s Serializer, 137 | target: MapResult, 138 | next_key: Option, 139 | } 140 | 141 | impl<'s> MapSerializer<'s> { 142 | pub fn new(serializer: &'s Serializer, as_object: bool) -> Self { 143 | Self { 144 | serializer, 145 | target: if as_object { 146 | MapResult::Object(Object::new()) 147 | } else { 148 | MapResult::Map(Map::new()) 149 | }, 150 | next_key: None, 151 | } 152 | } 153 | } 154 | 155 | impl ser::SerializeMap for MapSerializer<'_> { 156 | type Ok = JsValue; 157 | type Error = Error; 158 | 159 | fn serialize_key(&mut self, key: &T) -> Result<()> { 160 | debug_assert!(self.next_key.is_none()); 161 | self.next_key = Some(key.serialize(self.serializer)?); 162 | Ok(()) 163 | } 164 | 165 | fn serialize_value(&mut self, value: &T) -> Result<()> { 166 | let key = self.next_key.take().unwrap_throw(); 167 | let value_ser = value.serialize(self.serializer)?; 168 | match &self.target { 169 | MapResult::Map(map) => { 170 | map.set(&key, &value_ser); 171 | } 172 | MapResult::Object(object) => { 173 | let key = key.dyn_into::().map_err(|_| { 174 | Error::custom("Map key is not a string and cannot be an object key") 175 | })?; 176 | object.unchecked_ref::().set(key, value_ser); 177 | } 178 | } 179 | Ok(()) 180 | } 181 | 182 | fn end(self) -> Result { 183 | debug_assert!(self.next_key.is_none()); 184 | match self.target { 185 | MapResult::Map(map) => Ok(map.into()), 186 | MapResult::Object(object) => Ok(object.into()), 187 | } 188 | } 189 | } 190 | 191 | /// Serializes Rust structs into plain JS objects. 192 | pub struct ObjectSerializer<'s> { 193 | serializer: &'s Serializer, 194 | target: ObjectExt, 195 | } 196 | 197 | impl<'s> ObjectSerializer<'s> { 198 | pub fn new(serializer: &'s Serializer) -> Self { 199 | Self { 200 | serializer, 201 | target: Object::new().unchecked_into::(), 202 | } 203 | } 204 | } 205 | 206 | impl ser::SerializeStruct for ObjectSerializer<'_> { 207 | type Ok = JsValue; 208 | type Error = Error; 209 | 210 | fn serialize_field( 211 | &mut self, 212 | key: &'static str, 213 | value: &T, 214 | ) -> Result<()> { 215 | let value = value.serialize(self.serializer)?; 216 | self.target.set(static_str_to_js(key), value); 217 | Ok(()) 218 | } 219 | 220 | fn end(self) -> Result { 221 | Ok(self.target.into()) 222 | } 223 | } 224 | 225 | /// A [`serde::Serializer`] that converts supported Rust values into a [`JsValue`]. 226 | #[derive(Default)] 227 | pub struct Serializer { 228 | serialize_missing_as_null: bool, 229 | serialize_maps_as_objects: bool, 230 | serialize_large_number_types_as_bigints: bool, 231 | serialize_bytes_as_arrays: bool, 232 | } 233 | 234 | impl Serializer { 235 | /// Creates a new default [`Serializer`]. 236 | pub const fn new() -> Self { 237 | Self { 238 | serialize_missing_as_null: false, 239 | serialize_maps_as_objects: false, 240 | serialize_large_number_types_as_bigints: false, 241 | serialize_bytes_as_arrays: false, 242 | } 243 | } 244 | 245 | /// Creates a JSON compatible serializer. This uses null instead of undefined, and 246 | /// uses plain objects instead of ES maps. So you will get the same result of 247 | /// `JsValue::from_serde`, and you can stringify results to JSON and store 248 | /// it without data loss. 249 | pub const fn json_compatible() -> Self { 250 | Self { 251 | serialize_missing_as_null: true, 252 | serialize_maps_as_objects: true, 253 | serialize_large_number_types_as_bigints: false, 254 | serialize_bytes_as_arrays: true, 255 | } 256 | } 257 | 258 | /// Set to `true` to serialize `()`, unit structs and `Option::None` to `null` 259 | /// instead of `undefined` in JS. `false` by default. 260 | pub const fn serialize_missing_as_null(mut self, value: bool) -> Self { 261 | self.serialize_missing_as_null = value; 262 | self 263 | } 264 | 265 | /// Set to `true` to serialize maps into plain JavaScript objects instead of 266 | /// ES2015 `Map`s. `false` by default. 267 | pub const fn serialize_maps_as_objects(mut self, value: bool) -> Self { 268 | self.serialize_maps_as_objects = value; 269 | self 270 | } 271 | 272 | /// Set to `true` to serialize 64-bit numbers to JavaScript `BigInt` instead of 273 | /// plain numbers. `false` by default. 274 | pub const fn serialize_large_number_types_as_bigints(mut self, value: bool) -> Self { 275 | self.serialize_large_number_types_as_bigints = value; 276 | self 277 | } 278 | 279 | /// Set to `true` to serialize bytes into plain JavaScript arrays instead of 280 | /// ES2015 `Uint8Array`s. `false` by default. 281 | pub const fn serialize_bytes_as_arrays(mut self, value: bool) -> Self { 282 | self.serialize_bytes_as_arrays = value; 283 | self 284 | } 285 | } 286 | 287 | macro_rules! forward_to_into { 288 | ($($name:ident($ty:ty);)*) => { 289 | $(fn $name(self, v: $ty) -> Result { 290 | Ok(v.into()) 291 | })* 292 | }; 293 | } 294 | 295 | impl<'s> ser::Serializer for &'s Serializer { 296 | type Ok = JsValue; 297 | type Error = Error; 298 | 299 | type SerializeSeq = ArraySerializer<'s>; 300 | type SerializeTuple = ArraySerializer<'s>; 301 | type SerializeTupleStruct = ArraySerializer<'s>; 302 | type SerializeTupleVariant = VariantSerializer>; 303 | type SerializeMap = MapSerializer<'s>; 304 | type SerializeStruct = ObjectSerializer<'s>; 305 | type SerializeStructVariant = VariantSerializer>; 306 | 307 | forward_to_into! { 308 | serialize_bool(bool); 309 | 310 | serialize_i8(i8); 311 | serialize_i16(i16); 312 | serialize_i32(i32); 313 | 314 | serialize_u8(u8); 315 | serialize_u16(u16); 316 | serialize_u32(u32); 317 | 318 | serialize_f32(f32); 319 | serialize_f64(f64); 320 | 321 | serialize_str(&str); 322 | } 323 | 324 | /// Serializes `i64` into a `BigInt` or a JS number. 325 | /// 326 | /// If `serialize_large_number_types_as_bigints` is set to `false`, 327 | /// `i64` is serialized as a JS number. But in this mode only numbers 328 | /// within the safe integer range are supported. 329 | fn serialize_i64(self, v: i64) -> Result { 330 | if self.serialize_large_number_types_as_bigints { 331 | return Ok(v.into()); 332 | } 333 | 334 | // Note: don't try to "simplify" by using `.abs()` as it can overflow, 335 | // but range check can't. 336 | const MIN_SAFE_INTEGER: i64 = Number::MIN_SAFE_INTEGER as i64; 337 | const MAX_SAFE_INTEGER: i64 = Number::MAX_SAFE_INTEGER as i64; 338 | 339 | if (MIN_SAFE_INTEGER..=MAX_SAFE_INTEGER).contains(&v) { 340 | self.serialize_f64(v as _) 341 | } else { 342 | Err(Error::custom(format_args!( 343 | "{} can't be represented as a JavaScript number", 344 | v 345 | ))) 346 | } 347 | } 348 | 349 | /// Serializes `u64` into a `BigInt` or a JS number. 350 | /// 351 | /// If `serialize_large_number_types_as_bigints` is set to `false`, 352 | /// `u64` is serialized as a JS number. But in this mode only numbers 353 | /// within the safe integer range are supported. 354 | fn serialize_u64(self, v: u64) -> Result { 355 | if self.serialize_large_number_types_as_bigints { 356 | return Ok(v.into()); 357 | } 358 | 359 | if v <= Number::MAX_SAFE_INTEGER as u64 { 360 | self.serialize_f64(v as _) 361 | } else { 362 | Err(Error::custom(format_args!( 363 | "{} can't be represented as a JavaScript number", 364 | v 365 | ))) 366 | } 367 | } 368 | 369 | /// Serializes `i128` into a `BigInt`. 370 | fn serialize_i128(self, v: i128) -> Result { 371 | Ok(JsValue::from(v)) 372 | } 373 | 374 | /// Serializes `u128` into a `BigInt`. 375 | fn serialize_u128(self, v: u128) -> Result { 376 | Ok(JsValue::from(v)) 377 | } 378 | 379 | /// Serializes `char` into a JS string. 380 | fn serialize_char(self, v: char) -> Result { 381 | Ok(JsString::from(v).into()) 382 | } 383 | 384 | /// Serializes `bytes` into a JS `Uint8Array` or a plain JS array. 385 | /// 386 | /// If `serialize_bytes_as_arrays` is set to `true`, bytes are serialized as plain JS arrays. 387 | fn serialize_bytes(self, v: &[u8]) -> Result { 388 | // Create a `Uint8Array` view into a Rust slice, and immediately copy it to the JS memory. 389 | // 390 | // This is necessary because any allocation in WebAssembly can require reallocation of the 391 | // backing memory, which will invalidate existing views (including `Uint8Array`). 392 | let view = unsafe { Uint8Array::view(v) }; 393 | if self.serialize_bytes_as_arrays { 394 | Ok(JsValue::from(Array::from(view.as_ref()))) 395 | } else { 396 | Ok(JsValue::from(Uint8Array::new(view.as_ref()))) 397 | } 398 | } 399 | 400 | /// Serializes `None` into `undefined` or `null`. 401 | /// 402 | /// If `serialize_missing_as_null` is set to `true`, `None` is serialized as `null`. 403 | fn serialize_none(self) -> Result { 404 | self.serialize_unit() 405 | } 406 | 407 | /// Serializes `Some(T)` as `T`. 408 | fn serialize_some(self, value: &T) -> Result { 409 | value.serialize(self) 410 | } 411 | 412 | /// Serializes `()` into `undefined` or `null`. 413 | /// 414 | /// If `serialize_missing_as_null` is set to `true`, `()` is serialized as `null`. 415 | fn serialize_unit(self) -> Result { 416 | Ok(if self.serialize_missing_as_null { 417 | JsValue::NULL 418 | } else { 419 | JsValue::UNDEFINED 420 | }) 421 | } 422 | 423 | /// Serializes unit structs into `undefined` or `null`. 424 | fn serialize_unit_struct(self, _name: &'static str) -> Result { 425 | self.serialize_unit() 426 | } 427 | 428 | /// For compatibility with serde-json, serializes unit variants as "Variant" strings. 429 | fn serialize_unit_variant( 430 | self, 431 | _name: &'static str, 432 | _variant_index: u32, 433 | variant: &'static str, 434 | ) -> Result { 435 | Ok(static_str_to_js(variant).into()) 436 | } 437 | 438 | /// Serializes newtype structs as their inner values. 439 | fn serialize_newtype_struct( 440 | self, 441 | name: &'static str, 442 | value: &T, 443 | ) -> Result { 444 | if name == PRESERVED_VALUE_MAGIC { 445 | let abi = value.serialize(self)?.unchecked_into_f64() as u32; 446 | // `PreservedValueSerWrapper` gives us ABI of a reference to a `JsValue` that is 447 | // guaranteed to be alive only during this call. 448 | // We must clone it before giving away the value to the caller. 449 | return Ok(unsafe { JsValue::ref_from_abi(abi) }.as_ref().clone()); 450 | } 451 | value.serialize(self) 452 | } 453 | 454 | /// Serializes newtype variants as their inner values. 455 | fn serialize_newtype_variant( 456 | self, 457 | _name: &'static str, 458 | _variant_index: u32, 459 | variant: &'static str, 460 | value: &T, 461 | ) -> Result { 462 | VariantSerializer::new(variant, self.serialize_newtype_struct(variant, value)?).end(Ok) 463 | } 464 | 465 | /// Serializes any Rust iterable as a JS Array. 466 | // TODO: Figure out if there is a way to detect and serialize `Set` differently. 467 | fn serialize_seq(self, _len: Option) -> Result { 468 | Ok(ArraySerializer::new(self)) 469 | } 470 | 471 | /// Serializes Rust tuples as JS arrays. 472 | fn serialize_tuple(self, len: usize) -> Result { 473 | self.serialize_seq(Some(len)) 474 | } 475 | 476 | /// Serializes Rust tuple structs as JS arrays. 477 | fn serialize_tuple_struct( 478 | self, 479 | _name: &'static str, 480 | len: usize, 481 | ) -> Result { 482 | self.serialize_tuple(len) 483 | } 484 | 485 | /// Serializes Rust tuple variants as `{"Variant": [ ...tuple... ]}`. 486 | fn serialize_tuple_variant( 487 | self, 488 | _name: &'static str, 489 | _variant_index: u32, 490 | variant: &'static str, 491 | len: usize, 492 | ) -> Result { 493 | Ok(VariantSerializer::new( 494 | variant, 495 | self.serialize_tuple_struct(variant, len)?, 496 | )) 497 | } 498 | 499 | /// Serializes Rust maps into JS `Map` or plain JS objects. 500 | /// 501 | /// See [`MapSerializer`] for more details. 502 | fn serialize_map(self, _len: Option) -> Result { 503 | Ok(MapSerializer::new(self, self.serialize_maps_as_objects)) 504 | } 505 | 506 | /// Serializes Rust typed structs into plain JS objects. 507 | fn serialize_struct(self, _name: &'static str, _len: usize) -> Result { 508 | Ok(ObjectSerializer::new(self)) 509 | } 510 | 511 | /// Serializes Rust struct-like variants into `{"Variant": { ...fields... }}`. 512 | fn serialize_struct_variant( 513 | self, 514 | _name: &'static str, 515 | _variant_index: u32, 516 | variant: &'static str, 517 | len: usize, 518 | ) -> Result { 519 | Ok(VariantSerializer::new( 520 | variant, 521 | self.serialize_struct(variant, len)?, 522 | )) 523 | } 524 | } 525 | -------------------------------------------------------------------------------- /tests/browser.rs: -------------------------------------------------------------------------------- 1 | wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); 2 | 3 | mod common; 4 | -------------------------------------------------------------------------------- /tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | use js_sys::{BigInt, JsString, Number, Object}; 2 | use maplit::{btreemap, hashmap, hashset}; 3 | use proptest::prelude::*; 4 | use serde::de::DeserializeOwned; 5 | use serde::ser::Error as SerError; 6 | use serde::{Deserialize, Serialize}; 7 | use serde_wasm_bindgen::{from_value, to_value, Error, Serializer}; 8 | use std::collections::{BTreeMap, HashMap}; 9 | use std::fmt::Debug; 10 | use std::hash::Hash; 11 | use wasm_bindgen::{JsCast, JsValue}; 12 | use wasm_bindgen_test::wasm_bindgen_test; 13 | 14 | const SERIALIZER: Serializer = Serializer::new(); 15 | 16 | const JSON_SERIALIZER: Serializer = Serializer::json_compatible(); 17 | 18 | const BIGINT_SERIALIZER: Serializer = 19 | Serializer::new().serialize_large_number_types_as_bigints(true); 20 | 21 | const MAP_OBJECT_SERIALIZER: Serializer = Serializer::new().serialize_maps_as_objects(true); 22 | 23 | fn test_via_round_trip_with_config(value: T, serializer: &Serializer) -> JsValue 24 | where 25 | T: Serialize + DeserializeOwned + PartialEq + Debug, 26 | { 27 | let serialized = value.serialize(serializer).unwrap(); 28 | let round_trip = from_value(serialized.clone()).unwrap(); 29 | assert_eq!( 30 | value, round_trip, 31 | "{:?} == from_value({:?})", 32 | value, serialized 33 | ); 34 | serialized 35 | } 36 | 37 | fn test_via_into_with_config(lhs: L, rhs: R, serializer: &Serializer) 38 | where 39 | L: Serialize + DeserializeOwned + PartialEq + Clone + Debug, 40 | R: Into + Clone + Debug, 41 | { 42 | let serialized = test_via_round_trip_with_config(lhs.clone(), serializer); 43 | assert_eq!( 44 | serialized, 45 | rhs.clone().into(), 46 | "to_value({:?}) == JsValue::from({:?})", 47 | lhs, 48 | rhs 49 | ); 50 | } 51 | 52 | fn test_via_into(lhs: L, rhs: R) 53 | where 54 | L: Serialize + DeserializeOwned + PartialEq + Clone + Debug, 55 | R: Into + Clone + Debug, 56 | { 57 | test_via_into_with_config(lhs, rhs, &SERIALIZER) 58 | } 59 | 60 | fn test_primitive_with_config(value: T, serializer: &Serializer) 61 | where 62 | T: Copy + Serialize + Into + DeserializeOwned + PartialEq + Debug, 63 | { 64 | test_via_into_with_config(value, value, serializer) 65 | } 66 | 67 | fn test_primitive(value: T) 68 | where 69 | T: Copy + Serialize + Into + DeserializeOwned + PartialEq + Debug, 70 | { 71 | test_via_into(value, value) 72 | } 73 | 74 | #[derive(PartialEq)] 75 | enum ValueKind { 76 | Null, 77 | Undefined, 78 | Boolean, 79 | PosFloat, 80 | NegFloat, 81 | NaN, 82 | PosInfinity, 83 | NegInfinity, 84 | PosInt, 85 | NegInt, 86 | PosBigInt, 87 | NegBigInt, 88 | String, 89 | Object, 90 | } 91 | 92 | fn sample_js_values() -> Vec<(ValueKind, JsValue)> { 93 | vec![ 94 | (ValueKind::Null, JsValue::NULL), 95 | (ValueKind::Undefined, JsValue::UNDEFINED), 96 | (ValueKind::Boolean, JsValue::TRUE), 97 | (ValueKind::PosFloat, JsValue::from(0.5)), 98 | (ValueKind::NegFloat, JsValue::from(-0.5)), 99 | (ValueKind::NaN, JsValue::from(std::f64::NAN)), 100 | (ValueKind::PosInfinity, JsValue::from(std::f64::INFINITY)), 101 | (ValueKind::NegInfinity, JsValue::from(-std::f64::INFINITY)), 102 | (ValueKind::PosInt, JsValue::from(1)), 103 | (ValueKind::NegInt, JsValue::from(-1)), 104 | (ValueKind::PosBigInt, JsValue::from(BigInt::from(1_i64))), 105 | (ValueKind::NegBigInt, JsValue::from(BigInt::from(-1_i64))), 106 | (ValueKind::String, JsValue::from("1")), 107 | (ValueKind::Object, Object::new().into()), 108 | ] 109 | } 110 | 111 | macro_rules! test_value_compatibility { 112 | ($ty:ty { $(ValueKind::$kind:ident $delim:tt $rust_value:expr,)* }) => { 113 | for (kind, js_value) in sample_js_values() { 114 | match kind { 115 | $(ValueKind::$kind => { 116 | let rust_value: $ty = $rust_value; 117 | test_value_compatibility!(@compare $ty, $kind $delim rust_value, js_value); 118 | })* 119 | _ => { 120 | from_value::<$ty>(js_value).unwrap_err(); 121 | } 122 | } 123 | } 124 | }; 125 | 126 | (@compare $ty:ty, NaN => $rust_value:ident, $js_value:ident) => {{ 127 | let mut rust_value: $ty = $rust_value; 128 | assert!(rust_value.is_nan(), "{:?} is not NaN", rust_value); 129 | 130 | let js_value = to_value(&rust_value).unwrap(); 131 | assert!(Number::is_nan(&js_value), "{:?} is not NaN", js_value); 132 | 133 | rust_value = from_value(js_value).unwrap(); 134 | assert!(rust_value.is_nan(), "{:?} is not NaN", rust_value); 135 | }}; 136 | 137 | (@compare $ty:ty, $kind:ident -> $rust_value:ident, $js_value:ident) => {{ 138 | assert_ne!(to_value(&$rust_value).ok().as_ref(), Some(&$js_value), "to_value({:?}) != Ok({:?})", $rust_value, $js_value); 139 | let rust_value: $ty = from_value($js_value.clone()).unwrap(); 140 | assert_eq!(rust_value, $rust_value, "from_value from {:?}", $js_value); 141 | }}; 142 | 143 | (@compare $ty:ty, $kind:ident => $rust_value:ident, $js_value:ident) => ( 144 | test_via_into::<$ty, JsValue>($rust_value, $js_value) 145 | ); 146 | } 147 | 148 | #[wasm_bindgen_test] 149 | fn unit() { 150 | test_via_into((), JsValue::UNDEFINED); 151 | test_value_compatibility!(() { 152 | ValueKind::Undefined => (), 153 | // Special case: one-way only conversion. 154 | ValueKind::Null -> (), 155 | }); 156 | } 157 | 158 | mod proptests { 159 | use super::*; 160 | 161 | proptest! { 162 | #[wasm_bindgen_test] 163 | fn bool(value: bool) { 164 | test_primitive(value); 165 | } 166 | 167 | #[wasm_bindgen_test] 168 | fn i8(value: i8) { 169 | test_primitive(value); 170 | } 171 | 172 | #[wasm_bindgen_test] 173 | fn i16(value: i16) { 174 | test_primitive(value); 175 | } 176 | 177 | #[wasm_bindgen_test] 178 | fn i32(value: i32) { 179 | test_primitive(value); 180 | } 181 | 182 | #[wasm_bindgen_test] 183 | fn u8(value: u8) { 184 | test_primitive(value); 185 | } 186 | 187 | #[wasm_bindgen_test] 188 | fn u16(value: u16) { 189 | test_primitive(value); 190 | } 191 | 192 | #[wasm_bindgen_test] 193 | fn u32(value: u32) { 194 | test_primitive(value); 195 | } 196 | 197 | #[wasm_bindgen_test] 198 | fn f32(value: f32) { 199 | test_primitive(value); 200 | } 201 | 202 | #[wasm_bindgen_test] 203 | fn f64(value: f64) { 204 | test_primitive(value); 205 | } 206 | 207 | #[wasm_bindgen_test] 208 | fn i64_bigints(value: i64) { 209 | test_primitive_with_config(value, &BIGINT_SERIALIZER); 210 | } 211 | 212 | #[wasm_bindgen_test] 213 | fn u64_bigints(value: i64) { 214 | test_primitive_with_config(value, &BIGINT_SERIALIZER); 215 | } 216 | 217 | #[wasm_bindgen_test] 218 | fn i64_numbers(value in Number::MIN_SAFE_INTEGER as i64..=Number::MAX_SAFE_INTEGER as i64) { 219 | test_via_into(value, value as f64); 220 | } 221 | 222 | #[wasm_bindgen_test] 223 | fn u64_numbers(value in 0..=Number::MAX_SAFE_INTEGER as u64) { 224 | test_via_into(value, value as f64); 225 | } 226 | 227 | #[wasm_bindgen_test] 228 | fn isize(value: isize) { 229 | test_via_into(value, value as f64); 230 | test_via_into_with_config(value, value as i64, &BIGINT_SERIALIZER); 231 | } 232 | 233 | #[wasm_bindgen_test] 234 | fn usize(value: usize) { 235 | test_via_into(value, value as f64); 236 | test_via_into_with_config(value, value as u64, &BIGINT_SERIALIZER); 237 | } 238 | 239 | #[wasm_bindgen_test] 240 | fn i128(value: i128) { 241 | test_primitive(value); 242 | } 243 | 244 | #[wasm_bindgen_test] 245 | fn u128(value: u128) { 246 | test_primitive(value); 247 | } 248 | 249 | #[wasm_bindgen_test] 250 | fn char(c: char) { 251 | test_via_into(c, String::from(c)); 252 | } 253 | 254 | #[wasm_bindgen_test] 255 | fn string(s: String) { 256 | test_via_into(s.clone(), s); 257 | } 258 | } 259 | } 260 | 261 | mod compat { 262 | use super::*; 263 | 264 | macro_rules! test_int_boundaries { 265 | ($ty:ident) => { 266 | test_primitive::<$ty>($ty::MIN); 267 | test_primitive::<$ty>($ty::MAX); 268 | 269 | let too_small = f64::from($ty::MIN) - 1.0; 270 | from_value::<$ty>(too_small.into()).unwrap_err(); 271 | 272 | let too_large = f64::from($ty::MAX) + 1.0; 273 | from_value::<$ty>(too_large.into()).unwrap_err(); 274 | }; 275 | } 276 | 277 | macro_rules! test_bigint_boundaries { 278 | ($ty:ident $(as $as:ident)?) => { 279 | test_via_into_with_config($ty::MIN, $ty::MIN $(as $as)?, &BIGINT_SERIALIZER); 280 | test_via_into_with_config($ty::MAX, $ty::MAX $(as $as)?, &BIGINT_SERIALIZER); 281 | 282 | let too_small = BigInt::from($ty::MIN $(as $as)?) - BigInt::from(1); 283 | from_value::<$ty>(too_small.into()).unwrap_err(); 284 | 285 | let too_large = BigInt::from($ty::MAX $(as $as)?) + BigInt::from(1); 286 | from_value::<$ty>(too_large.into()).unwrap_err(); 287 | }; 288 | } 289 | 290 | macro_rules! test_safe_int_boundaries { 291 | (signed $ty:ident) => { 292 | test_via_into(Number::MIN_SAFE_INTEGER as $ty, Number::MIN_SAFE_INTEGER); 293 | from_value::<$ty>(JsValue::from(Number::MIN_SAFE_INTEGER - 1.)).unwrap_err(); 294 | test_primitive_with_config((Number::MIN_SAFE_INTEGER - 1.) as $ty, &BIGINT_SERIALIZER); 295 | 296 | test_safe_int_boundaries!(unsigned $ty); 297 | }; 298 | 299 | (unsigned $ty:ident) => { 300 | test_via_into(Number::MAX_SAFE_INTEGER as $ty, Number::MAX_SAFE_INTEGER); 301 | from_value::<$ty>(JsValue::from(Number::MAX_SAFE_INTEGER + 1.)).unwrap_err(); 302 | test_primitive_with_config((Number::MAX_SAFE_INTEGER + 1.) as $ty, &BIGINT_SERIALIZER); 303 | }; 304 | } 305 | 306 | #[wasm_bindgen_test] 307 | fn bool() { 308 | test_value_compatibility!(bool { 309 | ValueKind::Boolean => true, 310 | }); 311 | } 312 | 313 | #[wasm_bindgen_test] 314 | fn i8() { 315 | test_int_boundaries!(i8); 316 | test_value_compatibility!(i8 { 317 | ValueKind::PosInt => 1, 318 | ValueKind::NegInt => -1, 319 | }); 320 | } 321 | 322 | #[wasm_bindgen_test] 323 | fn i16() { 324 | test_int_boundaries!(i16); 325 | test_value_compatibility!(i16 { 326 | ValueKind::PosInt => 1, 327 | ValueKind::NegInt => -1, 328 | }); 329 | } 330 | 331 | #[wasm_bindgen_test] 332 | fn i32() { 333 | test_int_boundaries!(i32); 334 | test_value_compatibility!(i32 { 335 | ValueKind::PosInt => 1, 336 | ValueKind::NegInt => -1, 337 | }); 338 | } 339 | 340 | #[wasm_bindgen_test] 341 | fn u8() { 342 | test_int_boundaries!(u8); 343 | test_value_compatibility!(u8 { 344 | ValueKind::PosInt => 1, 345 | }); 346 | } 347 | 348 | #[wasm_bindgen_test] 349 | fn u16() { 350 | test_int_boundaries!(u16); 351 | test_value_compatibility!(u16 { 352 | ValueKind::PosInt => 1, 353 | }); 354 | } 355 | 356 | #[wasm_bindgen_test] 357 | fn u32() { 358 | test_int_boundaries!(u32); 359 | test_value_compatibility!(u32 { 360 | ValueKind::PosInt => 1, 361 | }); 362 | } 363 | 364 | #[wasm_bindgen_test] 365 | fn i64() { 366 | test_bigint_boundaries!(i64); 367 | test_safe_int_boundaries!(signed i64); 368 | test_value_compatibility!(i64 { 369 | ValueKind::PosInt => 1, 370 | ValueKind::NegInt => -1, 371 | ValueKind::PosBigInt -> 1, 372 | ValueKind::NegBigInt -> -1, 373 | }); 374 | } 375 | 376 | #[wasm_bindgen_test] 377 | fn u64() { 378 | test_bigint_boundaries!(u64); 379 | test_safe_int_boundaries!(unsigned u64); 380 | test_value_compatibility!(u64 { 381 | ValueKind::PosInt => 1, 382 | ValueKind::PosBigInt -> 1, 383 | }); 384 | } 385 | 386 | #[wasm_bindgen_test] 387 | fn i128() { 388 | test_bigint_boundaries!(i128); 389 | test_value_compatibility!(i128 { 390 | ValueKind::PosBigInt => 1, 391 | ValueKind::NegBigInt => -1, 392 | }); 393 | } 394 | 395 | #[wasm_bindgen_test] 396 | fn u128() { 397 | test_bigint_boundaries!(u128); 398 | test_value_compatibility!(u128 { 399 | ValueKind::PosBigInt => 1, 400 | }); 401 | } 402 | 403 | #[wasm_bindgen_test] 404 | fn isize() { 405 | test_bigint_boundaries!(isize as i64); 406 | test_value_compatibility!(isize { 407 | ValueKind::PosInt => 1, 408 | ValueKind::NegInt => -1, 409 | ValueKind::PosBigInt -> 1, 410 | ValueKind::NegBigInt -> -1, 411 | }); 412 | } 413 | 414 | #[wasm_bindgen_test] 415 | fn usize() { 416 | test_bigint_boundaries!(usize as u64); 417 | test_value_compatibility!(usize { 418 | ValueKind::PosInt => 1, 419 | ValueKind::PosBigInt -> 1, 420 | }); 421 | } 422 | 423 | #[wasm_bindgen_test] 424 | fn f32() { 425 | test_value_compatibility!(f32 { 426 | ValueKind::PosFloat => 0.5, 427 | ValueKind::NegFloat => -0.5, 428 | ValueKind::NaN => f32::NAN, 429 | ValueKind::PosInfinity => f32::INFINITY, 430 | ValueKind::NegInfinity => f32::NEG_INFINITY, 431 | ValueKind::PosInt => 1.0, 432 | ValueKind::NegInt => -1.0, 433 | }); 434 | } 435 | 436 | #[wasm_bindgen_test] 437 | fn f64() { 438 | test_value_compatibility!(f64 { 439 | ValueKind::PosFloat => 0.5, 440 | ValueKind::NegFloat => -0.5, 441 | ValueKind::NaN => f64::NAN, 442 | ValueKind::PosInfinity => f64::INFINITY, 443 | ValueKind::NegInfinity => f64::NEG_INFINITY, 444 | ValueKind::PosInt => 1.0, 445 | ValueKind::NegInt => -1.0, 446 | }); 447 | } 448 | 449 | #[wasm_bindgen_test] 450 | fn string() { 451 | let lone_surrogate = JsString::from_char_code(&[0xDC00]); 452 | // Lone surrogates currently become a replacement character. 453 | assert_eq!( 454 | from_value::(lone_surrogate.into()).unwrap(), 455 | "\u{FFFD}" 456 | ); 457 | } 458 | } 459 | 460 | #[wasm_bindgen_test] 461 | fn bytes() { 462 | // Create a backing storage. 463 | let mut src = [1, 2, 3]; 464 | // Store the original separately for the mutation test 465 | let orig_src = src; 466 | // Convert to a JS value 467 | let value = to_value(&serde_bytes::Bytes::new(&src)).unwrap(); 468 | // Modify the original storage to make sure that JS value is a copy. 469 | src[0] = 10; 470 | 471 | // Make sure the JS value is a Uint8Array 472 | let res = value.dyn_ref::().unwrap(); 473 | // Copy it into another Rust storage 474 | let mut dst = [0; 3]; 475 | res.copy_to(&mut dst); 476 | // Finally, compare that resulting storage with the original. 477 | assert_eq!(orig_src, dst); 478 | 479 | // Now, try to deserialize back. 480 | let deserialized: serde_bytes::ByteBuf = from_value(value).unwrap(); 481 | assert_eq!(deserialized.as_ref(), orig_src); 482 | } 483 | 484 | #[wasm_bindgen_test] 485 | fn bytes_as_array() { 486 | let src = [1, 2, 3]; 487 | // Convert to a JS value 488 | let serializer = Serializer::new().serialize_bytes_as_arrays(true); 489 | let bytes = &serde_bytes::Bytes::new(&src); 490 | let value = bytes.serialize(&serializer).unwrap(); 491 | // Make sure the JS value is an Array. 492 | value.dyn_ref::().unwrap(); 493 | // Now, try to deserialize back. 494 | let deserialized: serde_bytes::ByteBuf = from_value(value).unwrap(); 495 | assert_eq!(deserialized.as_ref(), src); 496 | } 497 | 498 | #[wasm_bindgen_test] 499 | fn bytes_from_mixed_array() { 500 | // The string "xyz" cannot convert to a u8 501 | let value = (100, "xyz".to_string(), true) 502 | .serialize(&SERIALIZER) 503 | .unwrap(); 504 | from_value::(value).unwrap_err(); 505 | // The number 256 cannot convert to a u8 506 | let value = (100, 256, 100).serialize(&SERIALIZER).unwrap(); 507 | from_value::(value).unwrap_err(); 508 | } 509 | 510 | #[wasm_bindgen_test] 511 | fn options() { 512 | test_via_into(Some(0_u32), 0_u32); 513 | test_via_into(Some(32_u32), 32_u32); 514 | test_via_into(None::, JsValue::UNDEFINED); 515 | 516 | test_via_into(Some("".to_string()), ""); 517 | test_via_into(Some("abc".to_string()), "abc"); 518 | test_via_into(None::, JsValue::UNDEFINED); 519 | 520 | // This one is an unfortunate edge case that won't roundtrip, 521 | // but it's pretty unlikely in real-world code. 522 | assert_eq!(to_value(&Some(())).unwrap(), JsValue::UNDEFINED); 523 | assert_eq!(to_value(&None::<()>).unwrap(), JsValue::UNDEFINED); 524 | assert_eq!(to_value(&Some(Some(()))).unwrap(), JsValue::UNDEFINED); 525 | assert_eq!(to_value(&Some(None::<()>)).unwrap(), JsValue::UNDEFINED); 526 | } 527 | 528 | fn assert_json(lhs_value: JsValue, rhs: R) 529 | where 530 | R: Serialize + DeserializeOwned + PartialEq + Debug, 531 | { 532 | if lhs_value.is_undefined() { 533 | assert_eq!("null", serde_json::to_string(&rhs).unwrap()) 534 | } else { 535 | assert_eq!( 536 | js_sys::JSON::stringify(&lhs_value).unwrap(), 537 | serde_json::to_string(&rhs).unwrap(), 538 | ); 539 | } 540 | 541 | let restored_lhs: R = from_value(lhs_value.clone()).unwrap(); 542 | assert_eq!(restored_lhs, rhs, "from_value from {:?}", lhs_value); 543 | } 544 | 545 | fn test_via_json_with_config(value: T, serializer: &Serializer) 546 | where 547 | T: Serialize + DeserializeOwned + PartialEq + Debug, 548 | { 549 | assert_json(value.serialize(serializer).unwrap(), value); 550 | } 551 | 552 | fn test_via_json(value: T) 553 | where 554 | T: Serialize + DeserializeOwned + PartialEq + Debug, 555 | { 556 | test_via_json_with_config(value, &SERIALIZER); 557 | } 558 | 559 | #[wasm_bindgen_test] 560 | fn enums() { 561 | macro_rules! test_enum { 562 | ($(# $attr:tt)* $name:ident) => {{ 563 | #[derive(Debug, PartialEq, Serialize, Deserialize)] 564 | $(# $attr)* 565 | enum $name where A: Debug + Ord + Eq { 566 | Unit, 567 | Newtype(A), 568 | Tuple(A, B), 569 | Struct { a: A, b: B }, 570 | Map(BTreeMap), 571 | Seq { seq: Vec } // internal tags cannot be directly embedded in arrays 572 | } 573 | 574 | test_via_json($name::Unit::); 575 | test_via_json($name::Newtype::<_, i32>("newtype content".to_string())); 576 | test_via_json($name::Tuple("tuple content".to_string(), 42)); 577 | test_via_json($name::Struct { 578 | a: "struct content".to_string(), 579 | b: 42, 580 | }); 581 | test_via_json_with_config($name::Map::( 582 | btreemap!{ 583 | "a".to_string() => 12, 584 | "abc".to_string() => -1161, 585 | "b".to_string() => 64, 586 | } 587 | ), &MAP_OBJECT_SERIALIZER); 588 | test_via_json($name::Seq:: { seq: vec![5, 63, 0, -62, 6] }); 589 | }}; 590 | } 591 | 592 | test_enum! { 593 | ExternallyTagged 594 | } 595 | 596 | #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] 597 | #[serde(tag = "tag")] 598 | enum InternallyTagged 599 | where 600 | A: Ord, 601 | { 602 | Unit, 603 | Struct { 604 | a: A, 605 | b: B, 606 | }, 607 | Sequence { 608 | seq: Vec, 609 | }, 610 | Map(BTreeMap), 611 | Bytes { 612 | #[serde(with = "serde_bytes")] 613 | serde_bytes: Vec, 614 | raw: Vec, 615 | }, 616 | } 617 | 618 | test_via_json(InternallyTagged::Unit::<(), ()>); 619 | test_via_json(InternallyTagged::Struct { 620 | a: "struct content".to_string(), 621 | b: 42, 622 | }); 623 | test_via_json(InternallyTagged::Struct { 624 | a: "struct content".to_string(), 625 | b: 42.2, 626 | }); 627 | test_via_json(InternallyTagged::::Sequence { 628 | seq: vec![12, 41, -11, -65, 961], 629 | }); 630 | 631 | // Internal tags with maps are not properly deserialized from Map values due to the exclusion 632 | // of Iterables during deserialize_any(). They can be deserialized properly from plain objects 633 | // so we can test that. 634 | test_via_json_with_config( 635 | InternallyTagged::Map(btreemap! { 636 | "a".to_string() => 12, 637 | "abc".to_string() => -1161, 638 | "b".to_string() => 64, 639 | }), 640 | &MAP_OBJECT_SERIALIZER, 641 | ); 642 | 643 | test_via_round_trip_with_config( 644 | InternallyTagged::Struct { 645 | a: 10_u64, 646 | b: -10_i64, 647 | }, 648 | &BIGINT_SERIALIZER, 649 | ); 650 | 651 | test_via_round_trip_with_config( 652 | InternallyTagged::<(), ()>::Bytes { 653 | serde_bytes: vec![0, 1, 2], 654 | raw: vec![3, 4, 5], 655 | }, 656 | &SERIALIZER, 657 | ); 658 | 659 | test_enum! { 660 | #[serde(tag = "tag", content = "content")] 661 | AdjacentlyTagged 662 | } 663 | test_enum! { 664 | #[serde(untagged)] 665 | Untagged 666 | } 667 | } 668 | 669 | #[wasm_bindgen_test] 670 | fn serde_json_value_with_json() { 671 | test_via_round_trip_with_config( 672 | serde_json::from_str::("[0, \"foo\"]").unwrap(), 673 | &JSON_SERIALIZER, 674 | ); 675 | test_via_round_trip_with_config( 676 | serde_json::from_str::(r#"{"foo": "bar"}"#).unwrap(), 677 | &JSON_SERIALIZER, 678 | ); 679 | } 680 | 681 | #[wasm_bindgen_test] 682 | fn serde_json_value_with_default() { 683 | test_via_round_trip_with_config( 684 | serde_json::from_str::("[0, \"foo\"]").unwrap(), 685 | &SERIALIZER, 686 | ); 687 | test_via_round_trip_with_config( 688 | serde_json::from_str::(r#"{"foo": "bar"}"#).unwrap(), 689 | &SERIALIZER, 690 | ); 691 | } 692 | 693 | #[wasm_bindgen_test] 694 | fn preserved_value() { 695 | #[derive(serde::Deserialize, serde::Serialize, PartialEq, Clone, Debug)] 696 | #[serde(bound = "T: JsCast")] 697 | struct PreservedValue(#[serde(with = "serde_wasm_bindgen::preserve")] T); 698 | 699 | test_via_into(PreservedValue(JsValue::from_f64(42.0)), 42); 700 | test_via_into(PreservedValue(JsValue::from_str("hello")), "hello"); 701 | 702 | let res: PreservedValue = from_value(JsValue::from_f64(42.0)).unwrap(); 703 | assert_eq!(res.0.as_f64(), Some(42.0)); 704 | 705 | // Check that object identity is preserved. 706 | let big_array = js_sys::Int8Array::new_with_length(64); 707 | let val = PreservedValue(big_array); 708 | let res = to_value(&val).unwrap(); 709 | assert_eq!(res, JsValue::from(val.0)); 710 | 711 | // The JsCasts are checked on deserialization. 712 | let bool = js_sys::Boolean::from(true); 713 | let serialized = to_value(&PreservedValue(bool)).unwrap(); 714 | let res: Result, _> = from_value(serialized); 715 | assert_eq!( 716 | res.unwrap_err().to_string(), 717 | Error::custom("incompatible JS value JsValue(true) for type js_sys::Number").to_string() 718 | ); 719 | 720 | // serde_json must fail to round-trip our special wrapper 721 | let s = serde_json::to_string(&PreservedValue(JsValue::from_f64(42.0))).unwrap(); 722 | serde_json::from_str::>(&s).unwrap_err(); 723 | 724 | // bincode must fail to round-trip our special wrapper 725 | let s = bincode::serialize(&PreservedValue(JsValue::from_f64(42.0))).unwrap(); 726 | bincode::deserialize::>(&s).unwrap_err(); 727 | } 728 | 729 | #[wasm_bindgen_test] 730 | fn structs() { 731 | #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] 732 | struct Unit; 733 | 734 | test_via_into(Unit, JsValue::UNDEFINED); 735 | 736 | #[derive(Debug, PartialEq, Serialize, Deserialize)] 737 | struct Newtype(A); 738 | 739 | test_via_json(Newtype("newtype content".to_string())); 740 | 741 | #[derive(Debug, PartialEq, Serialize, Deserialize)] 742 | struct Tuple(A, B); 743 | 744 | test_via_json(Tuple("tuple content".to_string(), 42)); 745 | 746 | #[derive(Debug, PartialEq, Serialize, Deserialize)] 747 | struct Struct { 748 | a: A, 749 | b: B, 750 | } 751 | 752 | test_via_json(Struct { 753 | a: "struct content".to_string(), 754 | b: 42, 755 | }); 756 | } 757 | 758 | #[wasm_bindgen_test] 759 | fn sequences() { 760 | test_via_json([1, 2]); 761 | test_via_json(["".to_string(), "x".to_string(), "xyz".to_string()]); 762 | test_via_json((100, "xyz".to_string(), true)); 763 | 764 | // Sets are currently indistinguishable from other sequences for 765 | // Serde serializers, so this will become an array on the JS side. 766 | test_via_json(hashset! {false, true}); 767 | } 768 | 769 | #[wasm_bindgen_test] 770 | fn maps() { 771 | #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] 772 | struct Struct { 773 | a: A, 774 | b: B, 775 | } 776 | 777 | // Create a Rust HashMap with non-string keys to make sure 778 | // that we support real arbitrary maps. 779 | let src = hashmap! { 780 | Struct { 781 | a: 1, 782 | b: "smth".to_string(), 783 | } => Struct { 784 | a: 2, 785 | b: "SMTH".to_string(), 786 | }, 787 | Struct { 788 | a: 42, 789 | b: "something".to_string(), 790 | } => Struct { 791 | a: 84, 792 | b: "SOMETHING".to_string(), 793 | }, 794 | }; 795 | 796 | // Convert to a JS value 797 | let res = to_value(&src).unwrap(); 798 | 799 | // Make sure that the result is an ES6 Map. 800 | let res = res.dyn_into::().unwrap(); 801 | assert_eq!(res.size() as usize, src.len()); 802 | 803 | // Compare values one by one (it's ok to use JSON for invidivual structs). 804 | res.entries() 805 | .into_iter() 806 | .map(|kv| kv.unwrap()) 807 | .zip(src) 808 | .for_each(|(lhs_kv, rhs_kv)| { 809 | assert_json(lhs_kv, rhs_kv); 810 | }); 811 | } 812 | 813 | #[wasm_bindgen_test] 814 | fn maps_objects_string_key() { 815 | #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] 816 | struct Struct { 817 | a: A, 818 | b: B, 819 | } 820 | 821 | let src = hashmap! { 822 | "a".to_string() => Struct { 823 | a: 2, 824 | b: "S".to_string(), 825 | }, 826 | "b".to_string() => Struct { 827 | a: 3, 828 | b: "T".to_string(), 829 | }, 830 | }; 831 | 832 | test_via_json_with_config(src, &MAP_OBJECT_SERIALIZER); 833 | } 834 | 835 | #[wasm_bindgen_test] 836 | fn serialize_json_compatible() { 837 | #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] 838 | struct Struct { 839 | a: HashMap, 840 | b: Option, 841 | } 842 | let x = Struct { 843 | a: hashmap! { 844 | "foo".to_string() => (), 845 | "bar".to_string() => (), 846 | }, 847 | b: None, 848 | }; 849 | test_via_json_with_config(x, &Serializer::json_compatible()); 850 | } 851 | 852 | #[wasm_bindgen_test] 853 | fn maps_objects_object_key() { 854 | #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] 855 | struct Struct { 856 | a: A, 857 | b: B, 858 | } 859 | 860 | let src = hashmap! { 861 | Struct { 862 | a: 1, 863 | b: "smth".to_string(), 864 | } => Struct { 865 | a: 2, 866 | b: "SMTH".to_string(), 867 | }, 868 | Struct { 869 | a: 42, 870 | b: "something".to_string(), 871 | } => Struct { 872 | a: 84, 873 | b: "SOMETHING".to_string(), 874 | }, 875 | }; 876 | 877 | let res = src.serialize(&MAP_OBJECT_SERIALIZER).unwrap_err(); 878 | assert_eq!( 879 | res.to_string(), 880 | Error::custom("Map key is not a string and cannot be an object key").to_string() 881 | ); 882 | } 883 | 884 | #[wasm_bindgen_test] 885 | fn serde_default_fields() { 886 | #[derive(Deserialize)] 887 | #[allow(dead_code)] 888 | struct Struct { 889 | data: String, 890 | #[serde(default)] 891 | missing: bool, 892 | opt_field: Option, 893 | unit_field: (), 894 | } 895 | 896 | let json = r#"{"data": "testing", "unit_field": null}"#; 897 | let obj = js_sys::JSON::parse(json).unwrap(); 898 | 899 | // Check that it parses successfully despite the missing field. 900 | let _struct: Struct = from_value(obj).unwrap(); 901 | } 902 | 903 | #[wasm_bindgen_test] 904 | fn field_aliases() { 905 | #[derive(Debug, Serialize, Deserialize, PartialEq)] 906 | struct Struct { 907 | #[serde(alias = "b")] 908 | a: i32, 909 | c: i32, 910 | } 911 | 912 | test_via_round_trip_with_config(Struct { a: 42, c: 84 }, &SERIALIZER); 913 | } 914 | -------------------------------------------------------------------------------- /tests/node.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | --------------------------------------------------------------------------------