diff --git a/.cargo/config.toml b/.cargo/config.toml index 61b55184a..650f04425 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,7 +1,7 @@ [alias] # Neon defines mutually exclusive feature flags which prevents using `cargo clippy --all-features` # The following aliases simplify linting the entire workspace -neon-check = " check --all --all-targets --features napi-experimental,futures,external-buffers" -neon-clippy = "clippy --all --all-targets --features napi-experimental,futures,external-buffers -- -A clippy::missing_safety_doc" -neon-test = " test --all --features=doc-comment,napi-experimental,futures,external-buffers" -neon-doc = " rustdoc -p neon --features=doc-dependencies,napi-experimental,futures,external-buffers -- --cfg docsrs" +neon-check = " check --all --all-targets --features napi-experimental,futures,external-buffers,serde" +neon-clippy = "clippy --all --all-targets --features napi-experimental,futures,external-buffers,serde -- -A clippy::missing_safety_doc" +neon-test = " test --all --features=doc-comment,napi-experimental,futures,external-buffers,serde" +neon-doc = " rustdoc -p neon --features=doc-dependencies,napi-experimental,futures,external-buffers,serde -- --cfg docsrs" diff --git a/Cargo.lock b/Cargo.lock index 40c63133b..471a8b644 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "aho-corasick" version = "0.7.19" @@ -11,6 +26,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + [[package]] name = "ansi_term" version = "0.12.1" @@ -32,7 +53,7 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a941c39708478e8eea39243b5983f1c42d2717b3620ee91f4a52115fd02ac43f" dependencies = [ - "itertools", + "itertools 0.9.0", "proc-macro-error", "proc-macro2", "quote", @@ -56,6 +77,21 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "bindgen" version = "0.57.0" @@ -65,7 +101,7 @@ dependencies = [ "bitflags", "cexpr", "clang-sys", - "clap", + "clap 2.34.0", "env_logger", "lazy_static", "lazycell", @@ -76,7 +112,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "which", + "which 3.1.1", ] [[package]] @@ -85,6 +121,39 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "block-buffer" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" + +[[package]] +name = "bytes" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" + [[package]] name = "cexpr" version = "0.4.0" @@ -100,6 +169,33 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "ciborium" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c137568cc60b904a7724001b35ce2630fd00d5d84805fbb608ab89509d788f" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346de753af073cc87b52b2083a506b38ac176a44cfb05497b622e27be899b369" + +[[package]] +name = "ciborium-ll" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213030a2b5a4e0c0892b6652260cf6ccac84827b83a85a534e178e3906c4cf1b" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "clang-sys" version = "1.3.3" @@ -121,11 +217,158 @@ dependencies = [ "atty", "bitflags", "strsim", - "textwrap", + "textwrap 0.11.0", "unicode-width", "vec_map", ] +[[package]] +name = "clap" +version = "3.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" +dependencies = [ + "bitflags", + "clap_lex", + "indexmap", + "textwrap 0.16.0", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "cpp_demangle" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b446fd40bcc17eddd6a4a78f24315eb90afdb3334999ddfd4909985c47722442" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "cpufeatures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +dependencies = [ + "libc", +] + +[[package]] +name = "criterion" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c76e09c1aae2bc52b3d2f29e13c6572553b30c4aa1b8a49fd70de6412654cb" +dependencies = [ + "anes", + "atty", + "cast", + "ciborium", + "clap 3.2.23", + "criterion-plot", + "itertools 0.10.5", + "lazy_static", + "num-traits", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "uuid", +] + +[[package]] +name = "digest" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "doc-comment" version = "0.3.3" @@ -158,6 +401,43 @@ dependencies = [ "termcolor", ] +[[package]] +name = "fastrand" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +dependencies = [ + "instant", +] + +[[package]] +name = "findshlibs" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40b9e59cd0f7e0806cca4be089683ecb6434e602038df21fe6bf6711b2f07f64" +dependencies = [ + "cc", + "lazy_static", + "libc", + "winapi", +] + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "generic-array" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.7" @@ -169,12 +449,36 @@ dependencies = [ "wasi", ] +[[package]] +name = "gimli" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec7af912d60cdbd3677c1af9352ebae6fb8394d165568a2234df0fa00f87793" + [[package]] name = "glob" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -190,6 +494,25 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "indexmap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + [[package]] name = "itertools" version = "0.9.0" @@ -199,6 +522,30 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" + +[[package]] +name = "js-sys" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -227,6 +574,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.17" @@ -242,12 +599,48 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memmap2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b182332558b18d807c4ce1ca8ca983b34c3ee32765e47b3f0f69b90355cc1dc" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ + "adler", +] + +[[package]] +name = "multimap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" + [[package]] name = "napi-tests" version = "0.1.0" dependencies = [ "neon", "once_cell", + "serde", + "serde_bytes", + "serde_json", "tokio", ] @@ -265,11 +658,25 @@ dependencies = [ "once_cell", "psd", "semver", + "serde", + "serde_json", "smallvec", "tokio", "widestring", ] +[[package]] +name = "neon-bench" +version = "0.1.0" +dependencies = [ + "criterion", + "neon", + "once_cell", + "pprof", + "serde", + "serde_json", +] + [[package]] name = "neon-macros" version = "1.0.0-alpha.2" @@ -279,6 +686,17 @@ dependencies = [ "syn-mid", ] +[[package]] +name = "nix" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + [[package]] name = "nodejs-sys" version = "0.13.0" @@ -298,6 +716,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.13.1" @@ -308,6 +735,15 @@ dependencies = [ "libc", ] +[[package]] +name = "object" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d864c91689fdc196779b98dba0aceac6118594c2df6ee5d943eb6a8df4d107a" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.14.0" @@ -315,19 +751,126 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" [[package]] -name = "peeking_take_while" -version = "0.1.2" +name = "oorandom" +version = "11.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" +checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" [[package]] -name = "pin-project-lite" -version = "0.2.9" +name = "os_str_bytes" +version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" [[package]] -name = "proc-macro-error" +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "petgraph" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5014253a1331579ce62aa67443b4a658c5e7dd03d4bc6d302b94474888143" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "plotters" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2538b639e642295546c50fcd545198c9d64ee2a38620a628724a3b266d5fbf97" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "193228616381fecdc1224c62e96946dfbc73ff4384fba576e052ff8c1bea8142" + +[[package]] +name = "plotters-svg" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a81d2759aae1dae668f783c308bc5c8ebd191ff4184aaa1b37f65a6ae5a56f" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "pprof" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e20150f965e0e4c925982b9356da71c84bcd56cb66ef4e894825837cbcf6613e" +dependencies = [ + "backtrace", + "cfg-if", + "findshlibs", + "libc", + "log", + "nix", + "once_cell", + "parking_lot", + "prost", + "prost-build", + "prost-derive", + "sha2", + "smallvec", + "symbolic-demangle", + "tempfile", + "thiserror", +] + +[[package]] +name = "prettyplease" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e97e3215779627f01ee256d2fad52f3d95e8e1c11e9fc6fd08f7cd455d5d5c78" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro-error" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" @@ -359,6 +902,61 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c01db6702aa05baa3f57dec92b8eeeeb4cb19e894e73996b32a4093289e54592" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5320c680de74ba083512704acb90fe00f28f79207286a848e730c45dd73ed6" +dependencies = [ + "bytes", + "heck", + "itertools 0.10.5", + "lazy_static", + "log", + "multimap", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn", + "tempfile", + "which 4.3.0", +] + +[[package]] +name = "prost-derive" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8842bad1a5419bca14eac663ba798f6bc19c413c2fdceb5f3ba3b0932d96720" +dependencies = [ + "anyhow", + "itertools 0.10.5", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-types" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "017f79637768cde62820bc2d4fe0e45daaa027755c323ad077767c6c5f173091" +dependencies = [ + "bytes", + "prost", +] + [[package]] name = "psd" version = "0.3.4" @@ -377,6 +975,37 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rayon" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cac410af5d00ab6884528b4ab69d1e8e146e8d471201800fa1b4524126de6ad3" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + [[package]] name = "regex" version = "1.6.0" @@ -394,18 +1023,105 @@ version = "0.6.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + [[package]] name = "rustc-hash" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "ryu" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + [[package]] name = "semver" version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93f6841e709003d68bb2deee8c343572bf446003ec20a583e76f7b15cebf3711" +[[package]] +name = "serde" +version = "1.0.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "718dc5fff5b36f99093fc49b280cfc96ce6fc824317783bff5a1fed0c7a64819" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "0.1.1" @@ -418,12 +1134,41 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "strsim" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +[[package]] +name = "symbolic-common" +version = "10.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b55cdc318ede251d0957f07afe5fed912119b8c1bc5a7804151826db999e737" +dependencies = [ + "debugid", + "memmap2", + "stable_deref_trait", + "uuid", +] + +[[package]] +name = "symbolic-demangle" +version = "10.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79be897be8a483a81fff6a3a4e195b4ac838ef73ca42d348b3f722da9902e489" +dependencies = [ + "cpp_demangle", + "rustc-demangle", + "symbolic-common", +] + [[package]] name = "syn" version = "1.0.99" @@ -446,6 +1191,20 @@ dependencies = [ "syn", ] +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + [[package]] name = "termcolor" version = "1.1.3" @@ -464,6 +1223,12 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" + [[package]] name = "thiserror" version = "1.0.35" @@ -484,6 +1249,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tokio" version = "1.21.1" @@ -496,6 +1271,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + [[package]] name = "unicode-ident" version = "1.0.4" @@ -508,6 +1289,12 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +[[package]] +name = "uuid" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c" + [[package]] name = "vec_map" version = "0.8.2" @@ -520,12 +1307,87 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasm-bindgen" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" + +[[package]] +name = "web-sys" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "which" version = "3.1.1" @@ -535,6 +1397,17 @@ dependencies = [ "libc", ] +[[package]] +name = "which" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c831fbbee9e129a8cf93e7747a82da9d95ba8e16621cae60ec2cdc849bacb7b" +dependencies = [ + "either", + "libc", + "once_cell", +] + [[package]] name = "widestring" version = "1.0.2" @@ -571,3 +1444,60 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" diff --git a/Cargo.toml b/Cargo.toml index 937e60cb3..d8e7ac409 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,3 +3,6 @@ members = [ "crates/*", "test/*", ] + +[profile.release] +lto = true diff --git a/crates/neon/Cargo.toml b/crates/neon/Cargo.toml index f52660a8c..d88e81e0b 100644 --- a/crates/neon/Cargo.toml +++ b/crates/neon/Cargo.toml @@ -29,6 +29,8 @@ once_cell = "1.10.0" neon-macros = { version = "=1.0.0-alpha.2", path = "../neon-macros" } aquamarine = { version = "0.1.11", optional = true } doc-comment = { version = "0.3.3", optional = true } +serde = { version = "1", optional = true } +serde_json = { version = "1", optional = true } [dependencies.tokio] version = "1.18.2" @@ -48,6 +50,9 @@ external-buffers = [] # https://github.com/neon-bindings/rfcs/pull/46 futures = ["tokio"] +# Convert between JavaScript and Rust values using serde and JSON serialization +serde = ["dep:serde", "serde_json"] + # Default N-API version. Prefer to select a minimum required version. # DEPRECATED: This is an alias that should be removed napi-runtime = ["napi-8"] @@ -88,4 +93,5 @@ features = [ "futures", "napi-experimental", "doc-dependencies", + "serde", ] diff --git a/crates/neon/src/context/mod.rs b/crates/neon/src/context/mod.rs index 598ba21ab..59828f01c 100644 --- a/crates/neon/src/context/mod.rs +++ b/crates/neon/src/context/mod.rs @@ -145,6 +145,9 @@ use std::{convert::Into, marker::PhantomData, panic::UnwindSafe}; pub use crate::types::buffer::lock::Lock; +#[cfg(feature = "serde")] +pub use crate::serde::{FromArg, FromArgs}; + use crate::{ event::TaskBuilder, handle::{Handle, Managed}, @@ -203,6 +206,34 @@ impl CallbackInfo<'_> { unsafe { sys::call::argv(cx.env().to_raw(), self.info) } } + #[cfg(feature = "serde")] + pub(crate) fn argv_exact<'b, C: Context<'b>, const N: usize>( + &self, + cx: &mut C, + ) -> [Handle<'b, JsValue>; N] { + use std::ptr; + + let mut argv = [JsValue::new_internal(ptr::null_mut()); N]; + let mut argc = argv.len(); + + unsafe { + assert_eq!( + sys::get_cb_info( + cx.env().to_raw(), + self.info, + &mut argc, + argv.as_mut_ptr().cast(), + ptr::null_mut(), + ptr::null_mut(), + ), + sys::Status::Ok, + ); + } + + // Empty values will be filled with `undefined` + argv + } + pub fn this<'b, C: Context<'b>>(&self, cx: &mut C) -> raw::Local { let env = cx.env(); unsafe { @@ -603,7 +634,7 @@ impl<'a> Context<'a> for ComputeContext<'a> {} /// The type parameter `T` is the type of the `this`-binding. pub struct FunctionContext<'a> { env: Env, - info: &'a CallbackInfo<'a>, + pub(crate) info: &'a CallbackInfo<'a>, arguments: Option, } @@ -659,6 +690,45 @@ impl<'a> FunctionContext<'a> { } } + #[cfg_attr(docsrs, doc(cfg(feature = "serde")))] + #[cfg(feature = "serde")] + /// Attempt to read arguments into a Rust tuple using serde + /// ``` + /// # use neon::prelude::*; + /// fn greet(mut cx: FunctionContext) -> JsResult { + /// let (greeting, name): (String, String) = cx.deserialize_args()?; + /// + /// Ok(cx.string(format!("{}, {}!", greeting, name))) + /// } + /// ``` + pub fn deserialize_args(&mut self) -> NeonResult + where + T: FromArgs<'a>, + { + crate::serde::from_args(self) + } + + #[cfg_attr(docsrs, doc(cfg(feature = "serde")))] + #[cfg(feature = "serde")] + /// Attempt to read the first argument using serde. + /// ``` + /// # use neon::prelude::*; + /// fn greet(mut cx: FunctionContext) -> JsResult { + /// let name: String = cx.deserialize_arg()?; + /// + /// Ok(cx.string(format!("Hello, {}!", name))) + /// } + /// ``` + /// + /// Equivalent to `let (v,) = cx.deserialize_args()?;`. + pub fn deserialize_arg(&mut self) -> NeonResult + where + T: FromArg<'a>, + { + let (arg,) = crate::serde::from_args(self)?; + Ok(arg) + } + /// Produces a handle to the `this`-binding and attempts to downcast as a specific type. /// Equivalent to calling `cx.this_value().downcast_or_throw(&mut cx)`. /// @@ -671,6 +741,11 @@ impl<'a> FunctionContext<'a> { pub fn this_value(&mut self) -> Handle<'a, JsValue> { JsValue::new_internal(self.info.this(self)) } + + #[cfg(feature = "serde")] + pub(crate) fn argv(&mut self) -> [Handle<'a, JsValue>; N] { + self.info.argv_exact(self) + } } impl<'a> ContextInternal<'a> for FunctionContext<'a> { diff --git a/crates/neon/src/lib.rs b/crates/neon/src/lib.rs index 959acb7ab..65b93b2c0 100644 --- a/crates/neon/src/lib.rs +++ b/crates/neon/src/lib.rs @@ -105,6 +105,12 @@ pub use neon_macros::*; #[cfg(feature = "napi-6")] mod lifecycle; +#[cfg(feature = "serde")] +mod serde; + +#[cfg(feature = "serde")] +pub use crate::serde::{deserialize, serialize}; + #[cfg(feature = "napi-8")] static MODULE_TAG: once_cell::sync::Lazy = once_cell::sync::Lazy::new(|| { let mut lower = [0; std::mem::size_of::()]; diff --git a/crates/neon/src/serde/args.rs b/crates/neon/src/serde/args.rs new file mode 100644 index 000000000..d28a4350e --- /dev/null +++ b/crates/neon/src/serde/args.rs @@ -0,0 +1,121 @@ +use serde::de; + +use crate::{ + context::FunctionContext, + handle::{Handle, Root}, + object::Object, + result::NeonResult, + types::{JsValue, Value}, +}; + +#[cfg_attr(docsrs, doc(cfg(feature = "serde")))] +/// Trait specifying that a value may be deserialized from a function argument. +pub trait FromArg<'cx>: private::FromArgInternal<'cx> {} + +impl<'cx, V> FromArg<'cx> for Handle<'cx, V> where V: Value {} + +impl<'cx, V> private::FromArgInternal<'cx> for Handle<'cx, V> +where + V: Value, +{ + fn from_arg(cx: &mut FunctionContext<'cx>, v: Handle<'cx, JsValue>) -> NeonResult { + v.downcast_or_throw(cx) + } +} + +impl<'cx, V> FromArg<'cx> for V where V: de::DeserializeOwned + ?Sized {} + +impl<'cx, V> private::FromArgInternal<'cx> for V +where + V: de::DeserializeOwned + ?Sized, +{ + fn from_arg(cx: &mut FunctionContext<'cx>, v: Handle<'cx, JsValue>) -> NeonResult { + super::deserialize(cx, v) + } +} + +impl<'cx, O> FromArg<'cx> for Root where O: Object {} + +impl<'cx, O> private::FromArgInternal<'cx> for Root +where + O: Object, +{ + fn from_arg(cx: &mut FunctionContext<'cx>, v: Handle<'cx, JsValue>) -> NeonResult { + Ok(v.downcast_or_throw::(cx)?.root(cx)) + } +} + +#[cfg_attr(docsrs, doc(cfg(feature = "serde")))] +/// Trait specifying values that may be deserialized from function arguments. +/// +/// **Note:** This trait is implemented for tuples of up to 32 values, but for +/// the sake of brevity, only tuples up to size 8 are shown in this documentation. +pub trait FromArgs<'cx>: private::FromArgsInternal<'cx> {} + +impl<'cx> FromArgs<'cx> for () {} + +impl<'cx> private::FromArgsInternal<'cx> for () { + fn from_args(_cx: &mut FunctionContext<'cx>) -> NeonResult { + Ok(()) + } +} + +pub(crate) fn from_args<'cx, T>(cx: &mut FunctionContext<'cx>) -> NeonResult +where + T: FromArgs<'cx>, +{ + private::FromArgsInternal::from_args(cx) +} + +macro_rules! impl_arguments { + ($(#[$attrs:meta])? [$($head:ident),*], []) => {}; + + ($(#[$attrs:meta])? [$($head:ident),*], [$cur:ident $(, $tail:ident)*]) => { + $(#[$attrs])? + impl<'cx, $($head,)* $cur> FromArgs<'cx> for ($($head,)* $cur,) + where + $($head: FromArg<'cx>,)* + $cur: FromArg<'cx>, + {} + + impl<'cx, $($head,)* $cur> private::FromArgsInternal<'cx> for ($($head,)* $cur,) + where + $($head: FromArg<'cx>,)* + $cur: FromArg<'cx>, + { + fn from_args(cx: &mut FunctionContext<'cx>) -> NeonResult { + #[allow(non_snake_case)] + let [$($head,)* $cur] = cx.argv(); + + Ok(( + $($head::from_arg(cx, $head)?,)* + $cur::from_arg(cx, $cur)?, + )) + } + } + + impl_arguments!($(#[$attrs])? [$($head,)* $cur], [$($tail),*]); + }; +} + +impl_arguments!([], [T1, T2, T3, T4, T5, T6, T7, T8]); +impl_arguments!( + #[doc(hidden)] + [T1, T2, T3, T4, T5, T6, T7, T8], + [ + T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, T23, T24, T25, T26, + T27, T28, T29, T30, T31, T32 + ] +); + +mod private { + use crate::{context::FunctionContext, handle::Handle, result::NeonResult, types::JsValue}; + + pub trait FromArgInternal<'cx>: Sized { + fn from_arg(cx: &mut FunctionContext<'cx>, v: Handle<'cx, JsValue>) -> NeonResult; + } + + pub trait FromArgsInternal<'cx>: Sized { + fn from_args(cx: &mut FunctionContext<'cx>) -> NeonResult; + } +} diff --git a/crates/neon/src/serde/de.rs b/crates/neon/src/serde/de.rs new file mode 100644 index 000000000..a05092774 --- /dev/null +++ b/crates/neon/src/serde/de.rs @@ -0,0 +1,409 @@ +use serde::de; + +use crate::{ + context::Context, + handle::Handle, + serde::{stringify, sys, Error}, + types::{JsString, Value}, +}; + +pub(super) fn deserialize<'cx, T, V, C>(cx: &mut C, value: Handle) -> Result +where + T: de::DeserializeOwned + ?Sized, + V: Value, + C: Context<'cx>, +{ + let env = cx.env().to_raw(); + let v = value.to_raw(); + + match T::deserialize(unsafe { Deserializer::new(env, v) }) { + Err(Error::FallbackJson) => {} + res => return res, + } + + let this = cx.undefined(); + let s = stringify(cx)? + .call(cx, this, [value.upcast()])? + .downcast_or_throw::(cx)? + .value(cx); + + Ok(serde_json::from_str(&s)?) +} + +struct Deserializer { + env: sys::Env, + value: sys::Value, +} + +impl Deserializer { + unsafe fn new(env: sys::Env, value: sys::Value) -> Self { + Self { env, value } + } +} + +#[derive(Debug, Copy, Clone)] +struct Number(f64); + +impl Number { + unsafe fn new(env: sys::Env, value: sys::Value) -> Result { + sys::get_value_double(env, value).map(Self) + } + + fn check_int(self, max: f64) -> Result { + let Self(n) = self; + + if n.is_nan() { + return Err(Error::NaN); + } + + if n.fract() != 0.0 { + return Err(Error::NotInt(n)); + } + + if n > max { + return Err(Error::Overflow(n)); + } + + Ok(n) + } + + fn check_signed(self, min: f64, max: f64) -> Result { + if self.0 < min { + return Err(Error::Underflow(self.0)); + } + + self.check_int(max) + } + + fn check_unsigned(self, max: f64) -> Result { + if self.0.is_sign_negative() { + return Err(Error::Underflow(self.0)); + } + + self.check_int(max) + } + + fn into_u8(self) -> Result { + self.check_unsigned(u8::MAX as f64).map(|v| v as u8) + } + + fn into_u16(self) -> Result { + self.check_unsigned(u16::MAX as f64).map(|v| v as u16) + } + + fn into_u32(self) -> Result { + self.check_unsigned(u32::MAX as f64).map(|v| v as u32) + } + + fn into_u64(self) -> Result { + self.check_unsigned(u64::MAX as f64).map(|v| v as u64) + } + + fn into_u128(self) -> Result { + self.check_unsigned(u128::MAX as f64).map(|v| v as u128) + } + + fn into_i8(self) -> Result { + self.check_signed(i8::MIN as f64, i8::MAX as f64) + .map(|v| v as i8) + } + + fn into_i16(self) -> Result { + self.check_signed(i16::MIN as f64, i16::MAX as f64) + .map(|v| v as i16) + } + + fn into_i32(self) -> Result { + self.check_signed(i32::MIN as f64, i32::MAX as f64) + .map(|v| v as i32) + } + + // FIXME: Does this work? + fn into_i64(self) -> Result { + self.check_signed(i64::MIN as f64, i64::MAX as f64) + .map(|v| v as i64) + } + + // FIXME: Does this work? + fn into_i128(self) -> Result { + self.check_signed(i128::MIN as f64, i128::MAX as f64) + .map(|v| v as i128) + } +} + +impl de::Deserializer<'static> for Deserializer { + type Error = Error; + + fn deserialize_any(self, visitor: V) -> Result + where + V: de::Visitor<'static>, + { + match unsafe { sys::typeof_value(self.env, self.value)? } { + sys::ValueType::Undefined => self.deserialize_unit(visitor), + sys::ValueType::Null => self.deserialize_unit(visitor), + sys::ValueType::Boolean => self.deserialize_bool(visitor), + sys::ValueType::Number => { + let n = unsafe { sys::get_value_double(self.env, self.value)? }; + + match (n.fract() == 0.0, n.is_sign_positive()) { + (true, true) => visitor.visit_u64(n as u64), + (true, false) => visitor.visit_i64(n as i64), + _ => visitor.visit_f64(n), + } + } + sys::ValueType::String => self.deserialize_string(visitor), + sys::ValueType::Object => match self.deserialize_byte_buf(visitor) { + Err(Error::Status(sys::Status::InvalidArg)) => Err(Error::FallbackJson), + res => res, + }, + typ => Err(Error::Unsupported(typ)), + } + } + + fn deserialize_bool(self, visitor: V) -> Result + where + V: de::Visitor<'static>, + { + visitor.visit_bool(unsafe { sys::get_value_bool(self.env, self.value)? }) + } + + fn deserialize_i8(self, visitor: V) -> Result + where + V: de::Visitor<'static>, + { + visitor.visit_i8(unsafe { Number::new(self.env, self.value)?.into_i8()? }) + } + + fn deserialize_i16(self, visitor: V) -> Result + where + V: de::Visitor<'static>, + { + visitor.visit_i16(unsafe { Number::new(self.env, self.value)?.into_i16()? }) + } + + fn deserialize_i32(self, visitor: V) -> Result + where + V: de::Visitor<'static>, + { + visitor.visit_i32(unsafe { Number::new(self.env, self.value)?.into_i32()? }) + } + + fn deserialize_i64(self, visitor: V) -> Result + where + V: de::Visitor<'static>, + { + visitor.visit_i64(unsafe { Number::new(self.env, self.value)?.into_i64()? }) + } + + fn deserialize_i128(self, visitor: V) -> Result + where + V: de::Visitor<'static>, + { + visitor.visit_i128(unsafe { Number::new(self.env, self.value)?.into_i128()? }) + } + + fn deserialize_u8(self, visitor: V) -> Result + where + V: de::Visitor<'static>, + { + visitor.visit_u8(unsafe { Number::new(self.env, self.value)?.into_u8()? }) + } + + fn deserialize_u16(self, visitor: V) -> Result + where + V: de::Visitor<'static>, + { + visitor.visit_u16(unsafe { Number::new(self.env, self.value)?.into_u16()? }) + } + + fn deserialize_u32(self, visitor: V) -> Result + where + V: de::Visitor<'static>, + { + visitor.visit_u32(unsafe { Number::new(self.env, self.value)?.into_u32()? }) + } + + fn deserialize_u64(self, visitor: V) -> Result + where + V: de::Visitor<'static>, + { + visitor.visit_u64(unsafe { Number::new(self.env, self.value)?.into_u64()? }) + } + + fn deserialize_u128(self, visitor: V) -> Result + where + V: de::Visitor<'static>, + { + visitor.visit_u128(unsafe { Number::new(self.env, self.value)?.into_u128()? }) + } + + fn deserialize_f32(self, visitor: V) -> Result + where + V: de::Visitor<'static>, + { + visitor.visit_f32(unsafe { sys::get_value_double(self.env, self.value)? as f32 }) + } + + fn deserialize_f64(self, visitor: V) -> Result + where + V: de::Visitor<'static>, + { + visitor.visit_f64(unsafe { sys::get_value_double(self.env, self.value)? }) + } + + fn deserialize_char(self, visitor: V) -> Result + where + V: de::Visitor<'static>, + { + self.deserialize_string(visitor) + } + + fn deserialize_str(self, visitor: V) -> Result + where + V: de::Visitor<'static>, + { + self.deserialize_string(visitor) + } + + fn deserialize_string(self, visitor: V) -> Result + where + V: de::Visitor<'static>, + { + visitor.visit_string(unsafe { sys::get_value_string(self.env, self.value)? }) + } + + fn deserialize_bytes(self, visitor: V) -> Result + where + V: de::Visitor<'static>, + { + self.deserialize_byte_buf(visitor) + } + + fn deserialize_byte_buf(self, visitor: V) -> Result + where + V: de::Visitor<'static>, + { + match unsafe { sys::get_value_arraybuffer(self.env, self.value) } { + Ok(v) => visitor.visit_byte_buf(v), + Err(err) if err == sys::Status::InvalidArg => { + visitor.visit_byte_buf(unsafe { sys::get_value_arrayview(self.env, self.value)? }) + } + Err(err) => Err(err.into()), + } + } + + fn deserialize_option(self, visitor: V) -> Result + where + V: de::Visitor<'static>, + { + match unsafe { sys::typeof_value(self.env, self.value)? } { + sys::ValueType::Null | sys::ValueType::Undefined => visitor.visit_none(), + _ => visitor.visit_some(self), + } + } + + fn deserialize_unit(self, visitor: V) -> Result + where + V: de::Visitor<'static>, + { + visitor.visit_unit() + } + + fn deserialize_unit_struct( + self, + _name: &'static str, + visitor: V, + ) -> Result + where + V: de::Visitor<'static>, + { + visitor.visit_unit() + } + + fn deserialize_newtype_struct( + self, + _name: &'static str, + visitor: V, + ) -> Result + where + V: de::Visitor<'static>, + { + visitor.visit_newtype_struct(self) + } + + fn deserialize_seq(self, _visitor: V) -> Result + where + V: de::Visitor<'static>, + { + Err(Error::FallbackJson) + } + + fn deserialize_tuple(self, _len: usize, visitor: V) -> Result + where + V: de::Visitor<'static>, + { + self.deserialize_seq(visitor) + } + + fn deserialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + visitor: V, + ) -> Result + where + V: de::Visitor<'static>, + { + self.deserialize_seq(visitor) + } + + fn deserialize_map(self, _visitor: V) -> Result + where + V: de::Visitor<'static>, + { + Err(Error::FallbackJson) + } + + fn deserialize_struct( + self, + _name: &'static str, + _fields: &'static [&'static str], + _visitor: V, + ) -> Result + where + V: de::Visitor<'static>, + { + Err(Error::FallbackJson) + } + + fn deserialize_enum( + self, + _name: &'static str, + _variants: &'static [&'static str], + visitor: V, + ) -> Result + where + V: de::Visitor<'static>, + { + // No-value enums are serialized as `string` + if let Ok(s) = unsafe { sys::get_value_string(self.env, self.value) } { + visitor.visit_enum(de::IntoDeserializer::into_deserializer(s)) + } else { + Err(Error::FallbackJson) + } + } + + fn deserialize_identifier(self, visitor: V) -> Result + where + V: de::Visitor<'static>, + { + self.deserialize_string(visitor) + } + + fn deserialize_ignored_any(self, visitor: V) -> Result + where + V: de::Visitor<'static>, + { + visitor.visit_unit() + } +} diff --git a/crates/neon/src/serde/mod.rs b/crates/neon/src/serde/mod.rs new file mode 100644 index 000000000..d3e4aa49b --- /dev/null +++ b/crates/neon/src/serde/mod.rs @@ -0,0 +1,168 @@ +use std::{error, fmt}; + +use crate::{ + context::Context, + handle::Handle, + object::Object, + result::{JsResult, NeonResult, Throw}, + types::{JsFunction, JsObject, JsValue, Value}, +}; + +#[cfg(feature = "napi-6")] +use crate::{handle::Root, thread::LocalKey}; + +pub(crate) use args::from_args; +pub use args::{FromArg, FromArgs}; + +mod args; +mod de; +mod ser; +mod sys; + +#[derive(Debug)] +pub(super) enum Error { + Custom(String), + Unsupported(sys::ValueType), + Status(sys::Status), + FallbackJson, + Overflow(f64), + Underflow(f64), + NotInt(f64), + NaN, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Error::Custom(msg) => f.write_str(msg), + Error::Unsupported(typ) => write!(f, "Unsupported({:?})", typ), + Error::Status(status) => write!(f, "Status({:?})", status), + Error::FallbackJson => f.write_str("FallbackJson"), + Error::Overflow(n) => write!(f, "Overflow: {}", n), + Error::Underflow(n) => write!(f, "Underflow: {}", n), + Error::NotInt(n) => write!(f, "NotInt: {}", n), + Error::NaN => f.write_str("NaN"), + } + } +} + +impl error::Error for Error {} + +impl serde::de::Error for Error { + fn custom(msg: T) -> Self + where + T: fmt::Display, + { + Self::Custom(msg.to_string()) + } +} + +impl serde::ser::Error for Error { + fn custom(msg: T) -> Self + where + T: fmt::Display, + { + Self::Custom(msg.to_string()) + } +} + +impl From for Error { + fn from(status: sys::Status) -> Self { + Self::Status(status) + } +} + +impl From for Error { + fn from(_: Throw) -> Self { + Self::Status(sys::Status::PendingException) + } +} + +impl From for Error { + fn from(err: serde_json::Error) -> Self { + Self::Custom(err.to_string()) + } +} + +fn parse<'cx, C: Context<'cx>>(cx: &mut C) -> JsResult<'cx, JsFunction> { + fn parse<'cx, C: Context<'cx>>(cx: &mut C) -> JsResult<'cx, JsFunction> { + cx.global() + .get::(cx, "JSON")? + .get::(cx, "parse") + } + + #[cfg(feature = "napi-6")] + { + static PARSE: LocalKey> = LocalKey::new(); + + PARSE + .get_or_try_init(cx, |cx| Ok(parse(cx)?.root(cx))) + .map(|v| v.to_inner(cx)) + } + + #[cfg(not(feature = "napi-6"))] + { + parse(cx) + } +} + +fn stringify<'cx, C: Context<'cx>>(cx: &mut C) -> JsResult<'cx, JsFunction> { + fn stringify<'cx, C: Context<'cx>>(cx: &mut C) -> JsResult<'cx, JsFunction> { + cx.global() + .get::(cx, "JSON")? + .get::(cx, "stringify") + } + + #[cfg(feature = "napi-6")] + { + static STRINGIFY: LocalKey> = LocalKey::new(); + + STRINGIFY + .get_or_try_init(cx, |cx| Ok(stringify(cx)?.root(cx))) + .map(|v| v.to_inner(cx)) + } + + #[cfg(not(feature = "napi-6"))] + { + stringify(cx) + } +} + +/// Attempts to read a JavaScript value into a Rust data type using serde +pub fn deserialize<'cx, T, V, C>(cx: &mut C, v: Handle) -> NeonResult +where + T: serde::de::DeserializeOwned + ?Sized, + V: Value, + C: Context<'cx>, +{ + de::deserialize(cx, v).or_else(|err| cx.throw_error(err.to_string())) +} + +/// Attempts to write Rust data into a JavaScript value using serde +pub fn serialize<'cx, T, V, C>(cx: &mut C, v: &V) -> JsResult<'cx, T> +where + T: Value, + V: serde::ser::Serialize + ?Sized, + C: Context<'cx>, +{ + let v = match v.serialize(unsafe { ser::Serializer::new(cx.env().to_raw()) }) { + Ok(v) => JsValue::new_internal(v), + Err(Error::FallbackJson) => { + let mut writer = Vec::with_capacity(128); + let mut serializer = serde_json::Serializer::new(&mut writer); + + v.serialize(ser::JsonSerializer::new(&mut serializer)) + .or_else(|err| cx.throw_error(err.to_string()))?; + + // Safety: JSON is always valid UTF-8 + let s = unsafe { String::from_utf8_unchecked(writer) }; + let s = cx.string(s); + let this = cx.undefined(); + + parse(cx)?.call(cx, this, [s.upcast()])? + } + Err(err) => return cx.throw_error(err.to_string()), + }; + + v.downcast_or_throw(cx) +} diff --git a/crates/neon/src/serde/ser.rs b/crates/neon/src/serde/ser.rs new file mode 100644 index 000000000..b942b21ab --- /dev/null +++ b/crates/neon/src/serde/ser.rs @@ -0,0 +1,637 @@ +//! Implements a serde serializer for JavaScript values +use std::fmt; + +use serde::{ser, Serialize}; + +use crate::serde::{sys, Error}; + +const MAX_SAFE_INTEGER: u64 = 9_007_199_254_740_991; +const MIN_SAFE_INTEGER: i64 = -9_007_199_254_740_991; + +pub(super) struct Serializer { + env: sys::Env, +} + +impl Serializer { + pub(super) unsafe fn new(env: sys::Env) -> Self { + Self { env } + } +} + +impl ser::Serializer for Serializer { + type Ok = sys::Value; + type Error = Error; + + type SerializeSeq = ser::Impossible; + type SerializeTuple = ser::Impossible; + type SerializeTupleStruct = ser::Impossible; + type SerializeTupleVariant = ser::Impossible; + type SerializeMap = ser::Impossible; + type SerializeStruct = ser::Impossible; + type SerializeStructVariant = ser::Impossible; + + fn serialize_bool(self, v: bool) -> Result { + Ok(unsafe { sys::create_bool(self.env, v)? }) + } + + fn serialize_i8(self, v: i8) -> Result { + Ok(unsafe { sys::create_double(self.env, v)? }) + } + + fn serialize_i16(self, v: i16) -> Result { + Ok(unsafe { sys::create_double(self.env, v)? }) + } + + fn serialize_i32(self, v: i32) -> Result { + Ok(unsafe { sys::create_double(self.env, v)? }) + } + + fn serialize_i64(self, v: i64) -> Result { + if v > MAX_SAFE_INTEGER as i64 { + return Err(ser::Error::custom("i64 greater than MAX_SAFE_INTEGER")); + } + + if v < MIN_SAFE_INTEGER { + return Err(ser::Error::custom("i64 less than MAX_SAFE_INTEGER")); + } + + Ok(unsafe { sys::create_double(self.env, v as f64)? }) + } + + fn serialize_i128(self, v: i128) -> Result { + if v > MAX_SAFE_INTEGER as i128 { + return Err(ser::Error::custom("i128 greater than MAX_SAFE_INTEGER")); + } + + if v < MIN_SAFE_INTEGER as i128 { + return Err(ser::Error::custom("i128 less than MAX_SAFE_INTEGER")); + } + + Ok(unsafe { sys::create_double(self.env, v as f64)? }) + } + + fn serialize_u8(self, v: u8) -> Result { + Ok(unsafe { sys::create_double(self.env, v)? }) + } + + fn serialize_u16(self, v: u16) -> Result { + Ok(unsafe { sys::create_double(self.env, v)? }) + } + + fn serialize_u32(self, v: u32) -> Result { + Ok(unsafe { sys::create_double(self.env, v)? }) + } + + fn serialize_u64(self, v: u64) -> Result { + if v > MAX_SAFE_INTEGER { + return Err(ser::Error::custom("u64 greater than MAX_SAFE_INTEGER")); + } + + Ok(unsafe { sys::create_double(self.env, v as f64)? }) + } + + fn serialize_u128(self, v: u128) -> Result { + if v > MAX_SAFE_INTEGER.into() { + return Err(ser::Error::custom("u128 greater than MAX_SAFE_INTEGER")); + } + + Ok(unsafe { sys::create_double(self.env, v as f64)? }) + } + + fn serialize_f32(self, v: f32) -> Result { + Ok(unsafe { sys::create_double(self.env, v)? }) + } + + fn serialize_f64(self, v: f64) -> Result { + Ok(unsafe { sys::create_double(self.env, v)? }) + } + fn serialize_char(self, v: char) -> Result { + Ok(unsafe { sys::create_string(self.env, v.to_string())? }) + } + + fn serialize_str(self, v: &str) -> Result { + Ok(unsafe { sys::create_string(self.env, v)? }) + } + + fn serialize_bytes(self, v: &[u8]) -> Result { + Ok(unsafe { sys::create_arraybuffer(self.env, v)? }) + } + + fn serialize_none(self) -> Result { + self.serialize_unit() + } + + fn serialize_some(self, value: &T) -> Result + where + T: Serialize, + { + value.serialize(self) + } + + fn serialize_unit(self) -> Result { + Ok(unsafe { sys::get_null(self.env)? }) + } + + fn serialize_unit_struct(self, _name: &'static str) -> Result { + self.serialize_unit() + } + + fn serialize_unit_variant( + self, + _name: &'static str, + _variant_index: u32, + variant: &'static str, + ) -> Result { + self.serialize_str(variant) + } + + fn serialize_newtype_struct( + self, + _name: &'static str, + value: &T, + ) -> Result + where + T: Serialize, + { + value.serialize(self) + } + + fn serialize_newtype_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _value: &T, + ) -> Result + where + T: Serialize, + { + Err(Error::FallbackJson) + } + + fn serialize_seq(self, _len: Option) -> Result { + Err(Error::FallbackJson) + } + + fn serialize_tuple(self, _len: usize) -> Result { + Err(Error::FallbackJson) + } + + fn serialize_tuple_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Err(Error::FallbackJson) + } + + fn serialize_tuple_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(Error::FallbackJson) + } + + fn serialize_map(self, _len: Option) -> Result { + Err(Error::FallbackJson) + } + + fn serialize_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Err(Error::FallbackJson) + } + + fn serialize_struct_variant( + self, + _name: &'static str, + _variant_index: u32, + _variant: &'static str, + _len: usize, + ) -> Result { + Err(Error::FallbackJson) + } +} + +// Wrapper around `serde_json` that errors serializing values that cannot be +// handled by `JSON.parse`. +pub(super) struct JsonSerializer(S); + +impl JsonSerializer { + pub(super) fn new(s: S) -> Self { + Self(s) + } +} + +impl ser::Serializer for JsonSerializer +where + S: ser::Serializer, +{ + type Ok = S::Ok; + type Error = S::Error; + + type SerializeSeq = JsonSerializer; + type SerializeTuple = JsonSerializer; + type SerializeTupleStruct = JsonSerializer; + type SerializeTupleVariant = JsonSerializer; + type SerializeMap = JsonSerializer; + type SerializeStruct = JsonSerializer; + type SerializeStructVariant = JsonSerializer; + + fn serialize_bool(self, v: bool) -> Result { + self.0.serialize_bool(v) + } + + fn serialize_i8(self, v: i8) -> Result { + self.0.serialize_i8(v) + } + + fn serialize_i16(self, v: i16) -> Result { + self.0.serialize_i16(v) + } + + fn serialize_i32(self, v: i32) -> Result { + self.0.serialize_i32(v) + } + + fn serialize_i64(self, v: i64) -> Result { + if v > MAX_SAFE_INTEGER as i64 { + return Err(ser::Error::custom("i64 greater than MAX_SAFE_INTEGER")); + } + + if v < MIN_SAFE_INTEGER { + return Err(ser::Error::custom("i64 less than MAX_SAFE_INTEGER")); + } + + self.0.serialize_i64(v) + } + + fn serialize_i128(self, v: i128) -> Result { + if v > MAX_SAFE_INTEGER as i128 { + return Err(ser::Error::custom("i128 greater than MAX_SAFE_INTEGER")); + } + + if v < MIN_SAFE_INTEGER as i128 { + return Err(ser::Error::custom("i128 less than MAX_SAFE_INTEGER")); + } + + self.0.serialize_i128(v) + } + + fn serialize_u8(self, v: u8) -> Result { + self.0.serialize_u8(v) + } + + fn serialize_u16(self, v: u16) -> Result { + self.0.serialize_u16(v) + } + + fn serialize_u32(self, v: u32) -> Result { + self.0.serialize_u32(v) + } + + fn serialize_u64(self, v: u64) -> Result { + if v > MAX_SAFE_INTEGER { + return Err(ser::Error::custom("u64 greater than MAX_SAFE_INTEGER")); + } + + self.0.serialize_u64(v) + } + + fn serialize_u128(self, v: u128) -> Result { + if v > MAX_SAFE_INTEGER.into() { + return Err(ser::Error::custom("u64 greater than MAX_SAFE_INTEGER")); + } + + self.0.serialize_u128(v) + } + + fn serialize_f32(self, v: f32) -> Result { + self.0.serialize_f32(v) + } + + fn serialize_f64(self, v: f64) -> Result { + self.0.serialize_f64(v) + } + + fn serialize_char(self, v: char) -> Result { + self.0.serialize_char(v) + } + + fn serialize_str(self, v: &str) -> Result { + self.0.serialize_str(v) + } + + fn serialize_bytes(self, v: &[u8]) -> Result { + self.0.serialize_bytes(v) + } + + fn serialize_none(self) -> Result { + self.0.serialize_none() + } + + fn serialize_some(self, v: &T) -> Result + where + T: Serialize, + { + self.0.serialize_some(v) + } + + fn serialize_unit(self) -> Result { + self.0.serialize_unit() + } + + fn serialize_unit_struct(self, name: &'static str) -> Result { + self.0.serialize_unit_struct(name) + } + + fn serialize_unit_variant( + self, + name: &'static str, + i: u32, + v: &'static str, + ) -> Result { + self.0.serialize_unit_variant(name, i, v) + } + + fn serialize_newtype_struct( + self, + name: &'static str, + v: &T, + ) -> Result + where + T: Serialize, + { + self.0.serialize_newtype_struct(name, v) + } + + fn serialize_newtype_variant( + self, + name: &'static str, + i: u32, + variant: &'static str, + v: &T, + ) -> Result + where + T: Serialize, + { + self.0.serialize_newtype_variant(name, i, variant, v) + } + + fn serialize_seq(self, len: Option) -> Result { + self.0.serialize_seq(len).map(JsonSerializer) + } + + fn serialize_tuple(self, len: usize) -> Result { + self.0.serialize_tuple(len).map(JsonSerializer) + } + + fn serialize_tuple_struct( + self, + name: &'static str, + len: usize, + ) -> Result { + self.0.serialize_tuple_struct(name, len).map(JsonSerializer) + } + + fn serialize_tuple_variant( + self, + name: &'static str, + variant_index: u32, + variant: &'static str, + len: usize, + ) -> Result { + self.0 + .serialize_tuple_variant(name, variant_index, variant, len) + .map(JsonSerializer) + } + + fn serialize_map(self, len: Option) -> Result { + self.0.serialize_map(len).map(JsonSerializer) + } + + fn serialize_struct( + self, + name: &'static str, + len: usize, + ) -> Result { + self.0.serialize_struct(name, len).map(JsonSerializer) + } + + fn serialize_struct_variant( + self, + name: &'static str, + variant_index: u32, + variant: &'static str, + len: usize, + ) -> Result { + self.0 + .serialize_struct_variant(name, variant_index, variant, len) + .map(JsonSerializer) + } + + fn collect_seq(self, iter: I) -> Result + where + I: IntoIterator, + ::Item: Serialize, + { + self.0.collect_seq(iter) + } + + fn collect_map(self, iter: I) -> Result + where + K: Serialize, + V: Serialize, + I: IntoIterator, + { + self.0.collect_map(iter) + } + + fn collect_str(self, value: &T) -> Result + where + T: fmt::Display, + { + self.0.collect_str(value) + } + + fn is_human_readable(&self) -> bool { + self.0.is_human_readable() + } +} + +impl ser::Serialize for JsonSerializer +where + T: Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: ser::Serializer, + { + self.0.serialize(JsonSerializer(serializer)) + } +} + +impl ser::SerializeSeq for JsonSerializer +where + S: ser::SerializeSeq, +{ + type Ok = S::Ok; + type Error = S::Error; + + fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + self.0.serialize_element(&JsonSerializer(value)) + } + + fn end(self) -> Result { + self.0.end() + } +} + +impl ser::SerializeTuple for JsonSerializer +where + S: ser::SerializeTuple, +{ + type Ok = S::Ok; + type Error = S::Error; + + fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + self.0.serialize_element(&JsonSerializer(value)) + } + + fn end(self) -> Result { + self.0.end() + } +} + +impl ser::SerializeTupleStruct for JsonSerializer +where + S: ser::SerializeTupleStruct, +{ + type Ok = S::Ok; + type Error = S::Error; + + fn serialize_field(&mut self, value: &T) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + self.0.serialize_field(&JsonSerializer(value)) + } + + fn end(self) -> Result { + self.0.end() + } +} + +impl ser::SerializeTupleVariant for JsonSerializer +where + S: ser::SerializeTupleVariant, +{ + type Ok = S::Ok; + type Error = S::Error; + + fn serialize_field(&mut self, value: &T) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + self.0.serialize_field(&JsonSerializer(value)) + } + + fn end(self) -> Result { + self.0.end() + } +} +impl ser::SerializeMap for JsonSerializer +where + S: ser::SerializeMap, +{ + type Ok = S::Ok; + type Error = S::Error; + + fn serialize_key(&mut self, key: &T) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + self.0.serialize_key(&JsonSerializer(key)) + } + + fn serialize_value(&mut self, value: &T) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + self.0.serialize_value(&JsonSerializer(value)) + } + + fn serialize_entry(&mut self, key: &K, value: &V) -> Result<(), Self::Error> + where + K: ?Sized + Serialize, + V: ?Sized + Serialize, + { + self.0 + .serialize_entry(&JsonSerializer(key), &JsonSerializer(value)) + } + + fn end(self) -> Result { + self.0.end() + } +} + +impl ser::SerializeStruct for JsonSerializer +where + S: ser::SerializeStruct, +{ + type Ok = S::Ok; + type Error = S::Error; + + fn serialize_field( + &mut self, + key: &'static str, + value: &T, + ) -> Result<(), Self::Error> + where + T: Serialize, + { + self.0.serialize_field(key, &JsonSerializer(value)) + } + + fn skip_field(&mut self, key: &'static str) -> Result<(), Self::Error> { + self.0.skip_field(key) + } + + fn end(self) -> Result { + self.0.end() + } +} + +impl ser::SerializeStructVariant for JsonSerializer +where + S: ser::SerializeStructVariant, +{ + type Ok = S::Ok; + type Error = S::Error; + + fn serialize_field(&mut self, key: &'static str, value: &T) -> Result<(), Self::Error> + where + T: ?Sized + Serialize, + { + self.0.serialize_field(key, &JsonSerializer(value)) + } + + fn skip_field(&mut self, key: &'static str) -> Result<(), Self::Error> { + self.0.skip_field(key) + } + + fn end(self) -> Result { + self.0.end() + } +} diff --git a/crates/neon/src/serde/sys.rs b/crates/neon/src/serde/sys.rs new file mode 100644 index 000000000..00aaebe45 --- /dev/null +++ b/crates/neon/src/serde/sys.rs @@ -0,0 +1,165 @@ +//! Node-API wrappers used by serde transcoding +//! +//! In many cases, these functions provide similar functionality to functions +//! available elsewhere in `neon-`. However, keeping serde fully self contained +//! has a few benefits: +//! +//! * Wrappers can be written, altered, combined and otherwise optimized for +//! providing the most efficient possible serde implementation +//! * All errors can be forwarded to avoid additional checks and panics +//! * The serde implementation remains self contained for potential extraction +//! into a separate crate +//! +//! _Do not export anything from this file outside of the serde module._ + +use std::{mem::MaybeUninit, ptr, slice}; + +use crate::sys; + +pub(super) use crate::sys::{Env, Status, Value, ValueType}; + +// Extension trait to more easily return early from a failed Node-API call +trait Verify { + fn verify(self) -> Result<(), Status>; +} + +impl Verify for Status { + fn verify(self) -> Result<(), Status> { + if self == Status::Ok { + Ok(()) + } else { + Err(self) + } + } +} + +pub(super) unsafe fn typeof_value(env: Env, value: Value) -> Result { + let mut out = MaybeUninit::uninit(); + sys::typeof_value(env, value, out.as_mut_ptr()).verify()?; + Ok(out.assume_init()) +} + +pub(super) unsafe fn get_value_bool(env: Env, value: Value) -> Result { + let mut out = false; + sys::get_value_bool(env, value, &mut out as *mut bool).verify()?; + Ok(out) +} + +pub(super) unsafe fn get_value_double(env: Env, value: Value) -> Result { + let mut out = 0f64; + sys::get_value_double(env, value, &mut out as *mut f64).verify()?; + Ok(out) +} + +unsafe fn get_string_len(env: Env, value: Value) -> Result { + let mut out = 0usize; + sys::get_value_string_utf8(env, value, ptr::null_mut(), 0, &mut out as *mut usize).verify()?; + Ok(out) +} + +pub(super) unsafe fn get_value_string(env: Env, value: Value) -> Result { + let mut out = 0usize; + let string_len = get_string_len(env, value)?; + let buf_len = string_len + 1; + let mut buf = Vec::::with_capacity(buf_len); + + sys::get_value_string_utf8( + env, + value, + buf.as_mut_ptr().cast(), + buf_len, + &mut out as *mut usize, + ) + .verify()?; + + debug_assert_eq!(out, string_len); + buf.set_len(string_len); + + Ok(String::from_utf8_unchecked(buf)) +} + +pub(super) unsafe fn get_value_arraybuffer(env: Env, value: Value) -> Result, Status> { + let mut len = 0usize; + let mut out = MaybeUninit::uninit(); + + sys::get_arraybuffer_info(env, value, out.as_mut_ptr(), &mut len as *mut usize).verify()?; + + let buf = if len == 0 { + &[] + } else { + slice::from_raw_parts(out.assume_init().cast(), len) + }; + + Ok(buf.to_vec()) +} + +pub(super) unsafe fn get_value_arrayview(env: Env, value: Value) -> Result, Status> { + let mut len = 0usize; + let mut typ = MaybeUninit::uninit(); + let mut out = MaybeUninit::uninit(); + + sys::get_typedarray_info( + env, + value, + typ.as_mut_ptr(), + &mut len, + out.as_mut_ptr(), + ptr::null_mut(), + ptr::null_mut(), + ) + .verify()?; + + if !matches!( + typ.assume_init(), + sys::TypedArrayType::U8 | sys::TypedArrayType::U8Clamped + ) { + return Err(Status::InvalidArg); + } + + let buf = if len == 0 { + &[] + } else { + slice::from_raw_parts(out.assume_init().cast(), len) + }; + + Ok(buf.to_vec()) +} + +pub(super) unsafe fn create_bool(env: Env, v: bool) -> Result { + let mut value = MaybeUninit::uninit(); + sys::get_boolean(env, v, value.as_mut_ptr()).verify()?; + Ok(value.assume_init()) +} + +pub(super) unsafe fn create_double(env: Env, v: impl Into) -> Result { + let mut value = MaybeUninit::uninit(); + sys::create_double(env, v.into(), value.as_mut_ptr()).verify()?; + Ok(value.assume_init()) +} + +pub(super) unsafe fn create_string(env: Env, v: impl AsRef) -> Result { + let mut value = MaybeUninit::uninit(); + let v = v.as_ref(); + sys::create_string_utf8(env, v.as_ptr().cast(), v.len(), value.as_mut_ptr()).verify()?; + Ok(value.assume_init()) +} + +pub(super) unsafe fn create_arraybuffer(env: Env, v: &[u8]) -> Result { + let mut value = MaybeUninit::uninit(); + let mut data = MaybeUninit::uninit(); + + sys::create_arraybuffer(env, v.len(), data.as_mut_ptr(), value.as_mut_ptr()).verify()?; + + let value = value.assume_init(); + let data = slice::from_raw_parts_mut(data.assume_init().cast(), v.len()); + + data.copy_from_slice(v); + + Ok(value) +} + +pub(super) unsafe fn get_null(env: Env) -> Result { + let mut value = MaybeUninit::uninit(); + sys::get_null(env, value.as_mut_ptr()).verify()?; + Ok(value.assume_init()) +} diff --git a/package-lock.json b/package-lock.json index 9c6907b64..c94e4623b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1480,6 +1480,10 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, + "node_modules/neon-bench": { + "resolved": "test/bench", + "link": true + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -2316,6 +2320,14 @@ "typescript": "^4.7.4" } }, + "test/bench": { + "version": "0.1.0", + "hasInstallScript": true, + "license": "MIT", + "devDependencies": { + "cargo-cp-artifact": "^0.1" + } + }, "test/electron": { "name": "electron-tests", "version": "0.1.0", @@ -3503,6 +3515,12 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, + "neon-bench": { + "version": "file:test/bench", + "requires": { + "cargo-cp-artifact": "^0.1" + } + }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", diff --git a/test/bench/.gitignore b/test/bench/.gitignore new file mode 100644 index 000000000..6ca71fb5f --- /dev/null +++ b/test/bench/.gitignore @@ -0,0 +1,5 @@ +target +index.node +**/node_modules +**/.DS_Store +npm-debug.log* diff --git a/test/bench/Cargo.toml b/test/bench/Cargo.toml new file mode 100644 index 000000000..552aa156f --- /dev/null +++ b/test/bench/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "neon-bench" +version = "0.1.0" +license = "MIT" +edition = "2018" +exclude = ["index.node"] + +[lib] +crate-type = ["cdylib"] + +[dependencies] +criterion = "0.4" +once_cell = "1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +[dependencies.neon] +version = "1.0.0-alpha.2" +path = "../../crates/neon" +features = ["external-buffers", "futures", "napi-experimental", "serde"] + +[target.'cfg(not(windows))'.dependencies] +pprof = { version = "0.11", features = ["prost-codec", "frame-pointer"] } diff --git a/test/bench/README.md b/test/bench/README.md new file mode 100644 index 000000000..159f64e80 --- /dev/null +++ b/test/bench/README.md @@ -0,0 +1,30 @@ +# Benchmark + +These benchmarks are intended for evaluating the performance impact of Neon changes. + +## Run + +```shell +# All benchmarks +node bench + +# Single benchmark +node bench serialize + +# Execute only one part of a bench group +node bench --json +node bench --json serialize + +# Generate a CPU profile +# Note: While it is not necessary to limit to a single bench function, +# the generated report will be easier to read. +node bench.js --neon --report=report.proto deserialize +``` + +## View Report + +CPU reports are generated with `pprof` and can use the standard tooling for evaluation. + +```shell +go tool pprof -http=:8080 report.proto +``` \ No newline at end of file diff --git a/test/bench/bench.js b/test/bench/bench.js new file mode 100644 index 000000000..ee4af5515 --- /dev/null +++ b/test/bench/bench.js @@ -0,0 +1,29 @@ +"use strict"; + +const bench = require("."); + +const args = process.argv.slice(2); +const benchArgs = args.filter((f) => !f.startsWith("-")); +const benches = new Set(benchArgs.length ? benchArgs : Object.keys(bench)); + +const flags = new Set( + args + .filter((f) => f.startsWith("-") && !f.startsWith("--report")) + .map((f) => f.slice(2)) +); + +const [reportFile] = args + .filter((f) => f.startsWith("--report=")) + .map((f) => f.slice(9)); + +const options = { + neon: flags.size === 0 ? true : flags.has("neon"), + json: flags.size === 0 ? true : flags.has("json"), + reportFile, +}; + +for (const [name, b] of Object.entries(bench)) { + if (benches.has(name)) { + b(options); + } +} diff --git a/test/bench/data/pokemon.json b/test/bench/data/pokemon.json new file mode 100644 index 000000000..0f1051943 --- /dev/null +++ b/test/bench/data/pokemon.json @@ -0,0 +1,3478 @@ +{ + "pokemon": [ + { + "id": 1, + "num": "001", + "name": "Bulbasaur", + "img": "http://www.serebii.net/pokemongo/pokemon/001.png", + "type": ["Grass", "Poison"], + "height": "0.71 m", + "weight": "6.9 kg", + "candy": "Bulbasaur Candy", + "candy_count": 25, + "egg": "2 km", + "spawn_chance": 0.69, + "avg_spawns": 69, + "spawn_time": "20:00", + "multipliers": [1.58], + "weaknesses": ["Fire", "Ice", "Flying", "Psychic"], + "next_evolution": [ + { + "num": "002", + "name": "Ivysaur" + }, + { + "num": "003", + "name": "Venusaur" + } + ] + }, + { + "id": 2, + "num": "002", + "name": "Ivysaur", + "img": "http://www.serebii.net/pokemongo/pokemon/002.png", + "type": ["Grass", "Poison"], + "height": "0.99 m", + "weight": "13.0 kg", + "candy": "Bulbasaur Candy", + "candy_count": 100, + "egg": "Not in Eggs", + "spawn_chance": 0.042, + "avg_spawns": 4.2, + "spawn_time": "07:00", + "multipliers": [1.2, 1.6], + "weaknesses": ["Fire", "Ice", "Flying", "Psychic"], + "prev_evolution": [ + { + "num": "001", + "name": "Bulbasaur" + } + ], + "next_evolution": [ + { + "num": "003", + "name": "Venusaur" + } + ] + }, + { + "id": 3, + "num": "003", + "name": "Venusaur", + "img": "http://www.serebii.net/pokemongo/pokemon/003.png", + "type": ["Grass", "Poison"], + "height": "2.01 m", + "weight": "100.0 kg", + "candy": "Bulbasaur Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.017, + "avg_spawns": 1.7, + "spawn_time": "11:30", + "multipliers": null, + "weaknesses": ["Fire", "Ice", "Flying", "Psychic"], + "prev_evolution": [ + { + "num": "001", + "name": "Bulbasaur" + }, + { + "num": "002", + "name": "Ivysaur" + } + ] + }, + { + "id": 4, + "num": "004", + "name": "Charmander", + "img": "http://www.serebii.net/pokemongo/pokemon/004.png", + "type": ["Fire"], + "height": "0.61 m", + "weight": "8.5 kg", + "candy": "Charmander Candy", + "candy_count": 25, + "egg": "2 km", + "spawn_chance": 0.253, + "avg_spawns": 25.3, + "spawn_time": "08:45", + "multipliers": [1.65], + "weaknesses": ["Water", "Ground", "Rock"], + "next_evolution": [ + { + "num": "005", + "name": "Charmeleon" + }, + { + "num": "006", + "name": "Charizard" + } + ] + }, + { + "id": 5, + "num": "005", + "name": "Charmeleon", + "img": "http://www.serebii.net/pokemongo/pokemon/005.png", + "type": ["Fire"], + "height": "1.09 m", + "weight": "19.0 kg", + "candy": "Charmander Candy", + "candy_count": 100, + "egg": "Not in Eggs", + "spawn_chance": 0.012, + "avg_spawns": 1.2, + "spawn_time": "19:00", + "multipliers": [1.79], + "weaknesses": ["Water", "Ground", "Rock"], + "prev_evolution": [ + { + "num": "004", + "name": "Charmander" + } + ], + "next_evolution": [ + { + "num": "006", + "name": "Charizard" + } + ] + }, + { + "id": 6, + "num": "006", + "name": "Charizard", + "img": "http://www.serebii.net/pokemongo/pokemon/006.png", + "type": ["Fire", "Flying"], + "height": "1.70 m", + "weight": "90.5 kg", + "candy": "Charmander Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.0031, + "avg_spawns": 0.31, + "spawn_time": "13:34", + "multipliers": null, + "weaknesses": ["Water", "Electric", "Rock"], + "prev_evolution": [ + { + "num": "004", + "name": "Charmander" + }, + { + "num": "005", + "name": "Charmeleon" + } + ] + }, + { + "id": 7, + "num": "007", + "name": "Squirtle", + "img": "http://www.serebii.net/pokemongo/pokemon/007.png", + "type": ["Water"], + "height": "0.51 m", + "weight": "9.0 kg", + "candy": "Squirtle Candy", + "candy_count": 25, + "egg": "2 km", + "spawn_chance": 0.58, + "avg_spawns": 58, + "spawn_time": "04:25", + "multipliers": [2.1], + "weaknesses": ["Electric", "Grass"], + "next_evolution": [ + { + "num": "008", + "name": "Wartortle" + }, + { + "num": "009", + "name": "Blastoise" + } + ] + }, + { + "id": 8, + "num": "008", + "name": "Wartortle", + "img": "http://www.serebii.net/pokemongo/pokemon/008.png", + "type": ["Water"], + "height": "0.99 m", + "weight": "22.5 kg", + "candy": "Squirtle Candy", + "candy_count": 100, + "egg": "Not in Eggs", + "spawn_chance": 0.034, + "avg_spawns": 3.4, + "spawn_time": "07:02", + "multipliers": [1.4], + "weaknesses": ["Electric", "Grass"], + "prev_evolution": [ + { + "num": "007", + "name": "Squirtle" + } + ], + "next_evolution": [ + { + "num": "009", + "name": "Blastoise" + } + ] + }, + { + "id": 9, + "num": "009", + "name": "Blastoise", + "img": "http://www.serebii.net/pokemongo/pokemon/009.png", + "type": ["Water"], + "height": "1.60 m", + "weight": "85.5 kg", + "candy": "Squirtle Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.0067, + "avg_spawns": 0.67, + "spawn_time": "00:06", + "multipliers": null, + "weaknesses": ["Electric", "Grass"], + "prev_evolution": [ + { + "num": "007", + "name": "Squirtle" + }, + { + "num": "008", + "name": "Wartortle" + } + ] + }, + { + "id": 10, + "num": "010", + "name": "Caterpie", + "img": "http://www.serebii.net/pokemongo/pokemon/010.png", + "type": ["Bug"], + "height": "0.30 m", + "weight": "2.9 kg", + "candy": "Caterpie Candy", + "candy_count": 12, + "egg": "2 km", + "spawn_chance": 3.032, + "avg_spawns": 303.2, + "spawn_time": "16:35", + "multipliers": [1.05], + "weaknesses": ["Fire", "Flying", "Rock"], + "next_evolution": [ + { + "num": "011", + "name": "Metapod" + }, + { + "num": "012", + "name": "Butterfree" + } + ] + }, + { + "id": 11, + "num": "011", + "name": "Metapod", + "img": "http://www.serebii.net/pokemongo/pokemon/011.png", + "type": ["Bug"], + "height": "0.71 m", + "weight": "9.9 kg", + "candy": "Caterpie Candy", + "candy_count": 50, + "egg": "Not in Eggs", + "spawn_chance": 0.187, + "avg_spawns": 18.7, + "spawn_time": "02:11", + "multipliers": [3.55, 3.79], + "weaknesses": ["Fire", "Flying", "Rock"], + "prev_evolution": [ + { + "num": "010", + "name": "Caterpie" + } + ], + "next_evolution": [ + { + "num": "012", + "name": "Butterfree" + } + ] + }, + { + "id": 12, + "num": "012", + "name": "Butterfree", + "img": "http://www.serebii.net/pokemongo/pokemon/012.png", + "type": ["Bug", "Flying"], + "height": "1.09 m", + "weight": "32.0 kg", + "candy": "Caterpie Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.022, + "avg_spawns": 2.2, + "spawn_time": "05:23", + "multipliers": null, + "weaknesses": ["Fire", "Electric", "Ice", "Flying", "Rock"], + "prev_evolution": [ + { + "num": "010", + "name": "Caterpie" + }, + { + "num": "011", + "name": "Metapod" + } + ] + }, + { + "id": 13, + "num": "013", + "name": "Weedle", + "img": "http://www.serebii.net/pokemongo/pokemon/013.png", + "type": ["Bug", "Poison"], + "height": "0.30 m", + "weight": "3.2 kg", + "candy": "Weedle Candy", + "candy_count": 12, + "egg": "2 km", + "spawn_chance": 7.12, + "avg_spawns": 712, + "spawn_time": "02:21", + "multipliers": [1.01, 1.09], + "weaknesses": ["Fire", "Flying", "Psychic", "Rock"], + "next_evolution": [ + { + "num": "014", + "name": "Kakuna" + }, + { + "num": "015", + "name": "Beedrill" + } + ] + }, + { + "id": 14, + "num": "014", + "name": "Kakuna", + "img": "http://www.serebii.net/pokemongo/pokemon/014.png", + "type": ["Bug", "Poison"], + "height": "0.61 m", + "weight": "10.0 kg", + "candy": "Weedle Candy", + "candy_count": 50, + "egg": "Not in Eggs", + "spawn_chance": 0.44, + "avg_spawns": 44, + "spawn_time": "02:30", + "multipliers": [3.01, 3.41], + "weaknesses": ["Fire", "Flying", "Psychic", "Rock"], + "prev_evolution": [ + { + "num": "013", + "name": "Weedle" + } + ], + "next_evolution": [ + { + "num": "015", + "name": "Beedrill" + } + ] + }, + { + "id": 15, + "num": "015", + "name": "Beedrill", + "img": "http://www.serebii.net/pokemongo/pokemon/015.png", + "type": ["Bug", "Poison"], + "height": "0.99 m", + "weight": "29.5 kg", + "candy": "Weedle Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.051, + "avg_spawns": 5.1, + "spawn_time": "04:50", + "multipliers": null, + "weaknesses": ["Fire", "Flying", "Psychic", "Rock"], + "prev_evolution": [ + { + "num": "013", + "name": "Weedle" + }, + { + "num": "014", + "name": "Kakuna" + } + ] + }, + { + "id": 16, + "num": "016", + "name": "Pidgey", + "img": "http://www.serebii.net/pokemongo/pokemon/016.png", + "type": ["Normal", "Flying"], + "height": "0.30 m", + "weight": "1.8 kg", + "candy": "Pidgey Candy", + "candy_count": 12, + "egg": "2 km", + "spawn_chance": 15.98, + "avg_spawns": 1.598, + "spawn_time": "01:34", + "multipliers": [1.71, 1.92], + "weaknesses": ["Electric", "Rock"], + "next_evolution": [ + { + "num": "017", + "name": "Pidgeotto" + }, + { + "num": "018", + "name": "Pidgeot" + } + ] + }, + { + "id": 17, + "num": "017", + "name": "Pidgeotto", + "img": "http://www.serebii.net/pokemongo/pokemon/017.png", + "type": ["Normal", "Flying"], + "height": "1.09 m", + "weight": "30.0 kg", + "candy": "Pidgey Candy", + "candy_count": 50, + "egg": "Not in Eggs", + "spawn_chance": 1.02, + "avg_spawns": 102, + "spawn_time": "01:30", + "multipliers": [1.79], + "weaknesses": ["Electric", "Rock"], + "prev_evolution": [ + { + "num": "016", + "name": "Pidgey" + } + ], + "next_evolution": [ + { + "num": "018", + "name": "Pidgeot" + } + ] + }, + { + "id": 18, + "num": "018", + "name": "Pidgeot", + "img": "http://www.serebii.net/pokemongo/pokemon/018.png", + "type": ["Normal", "Flying"], + "height": "1.50 m", + "weight": "39.5 kg", + "candy": "Pidgey Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.13, + "avg_spawns": 13, + "spawn_time": "01:50", + "multipliers": null, + "weaknesses": ["Electric", "Rock"], + "prev_evolution": [ + { + "num": "016", + "name": "Pidgey" + }, + { + "num": "017", + "name": "Pidgeotto" + } + ] + }, + { + "id": 19, + "num": "019", + "name": "Rattata", + "img": "http://www.serebii.net/pokemongo/pokemon/019.png", + "type": ["Normal"], + "height": "0.30 m", + "weight": "3.5 kg", + "candy": "Rattata Candy", + "candy_count": 25, + "egg": "2 km", + "spawn_chance": 13.05, + "avg_spawns": 1.305, + "spawn_time": "01:55", + "multipliers": [2.55, 2.73], + "weaknesses": ["Fighting"], + "next_evolution": [ + { + "num": "020", + "name": "Raticate" + } + ] + }, + { + "id": 20, + "num": "020", + "name": "Raticate", + "img": "http://www.serebii.net/pokemongo/pokemon/020.png", + "type": ["Normal"], + "height": "0.71 m", + "weight": "18.5 kg", + "candy": "Rattata Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.41, + "avg_spawns": 41, + "spawn_time": "01:56", + "multipliers": null, + "weaknesses": ["Fighting"], + "prev_evolution": [ + { + "num": "019", + "name": "Rattata" + } + ] + }, + { + "id": 21, + "num": "021", + "name": "Spearow", + "img": "http://www.serebii.net/pokemongo/pokemon/021.png", + "type": ["Normal", "Flying"], + "height": "0.30 m", + "weight": "2.0 kg", + "candy": "Spearow Candy", + "candy_count": 50, + "egg": "2 km", + "spawn_chance": 4.73, + "avg_spawns": 473, + "spawn_time": "12:25", + "multipliers": [2.66, 2.68], + "weaknesses": ["Electric", "Rock"], + "next_evolution": [ + { + "num": "022", + "name": "Fearow" + } + ] + }, + { + "id": 22, + "num": "022", + "name": "Fearow", + "img": "http://www.serebii.net/pokemongo/pokemon/022.png", + "type": ["Normal", "Flying"], + "height": "1.19 m", + "weight": "38.0 kg", + "candy": "Spearow Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.15, + "avg_spawns": 15, + "spawn_time": "01:11", + "multipliers": null, + "weaknesses": ["Electric", "Rock"], + "prev_evolution": [ + { + "num": "021", + "name": "Spearow" + } + ] + }, + { + "id": 23, + "num": "023", + "name": "Ekans", + "img": "http://www.serebii.net/pokemongo/pokemon/023.png", + "type": ["Poison"], + "height": "2.01 m", + "weight": "6.9 kg", + "candy": "Ekans Candy", + "candy_count": 50, + "egg": "5 km", + "spawn_chance": 2.27, + "avg_spawns": 227, + "spawn_time": "12:20", + "multipliers": [2.21, 2.27], + "weaknesses": ["Ground", "Psychic"], + "next_evolution": [ + { + "num": "024", + "name": "Arbok" + } + ] + }, + { + "id": 24, + "num": "024", + "name": "Arbok", + "img": "http://www.serebii.net/pokemongo/pokemon/024.png", + "type": ["Poison"], + "height": "3.51 m", + "weight": "65.0 kg", + "candy": "Ekans Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.072, + "avg_spawns": 7.2, + "spawn_time": "01:50", + "multipliers": null, + "weaknesses": ["Ground", "Psychic"], + "prev_evolution": [ + { + "num": "023", + "name": "Ekans" + } + ] + }, + { + "id": 25, + "num": "025", + "name": "Pikachu", + "img": "http://www.serebii.net/pokemongo/pokemon/025.png", + "type": ["Electric"], + "height": "0.41 m", + "weight": "6.0 kg", + "candy": "Pikachu Candy", + "candy_count": 50, + "egg": "2 km", + "spawn_chance": 0.21, + "avg_spawns": 21, + "spawn_time": "04:00", + "multipliers": [2.34], + "weaknesses": ["Ground"], + "next_evolution": [ + { + "num": "026", + "name": "Raichu" + } + ] + }, + { + "id": 26, + "num": "026", + "name": "Raichu", + "img": "http://www.serebii.net/pokemongo/pokemon/026.png", + "type": ["Electric"], + "height": "0.79 m", + "weight": "30.0 kg", + "candy": "Pikachu Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.0076, + "avg_spawns": 0.76, + "spawn_time": "23:58", + "multipliers": null, + "weaknesses": ["Ground"], + "prev_evolution": [ + { + "num": "025", + "name": "Pikachu" + } + ] + }, + { + "id": 27, + "num": "027", + "name": "Sandshrew", + "img": "http://www.serebii.net/pokemongo/pokemon/027.png", + "type": ["Ground"], + "height": "0.61 m", + "weight": "12.0 kg", + "candy": "Sandshrew Candy", + "candy_count": 50, + "egg": "5 km", + "spawn_chance": 1.11, + "avg_spawns": 111, + "spawn_time": "01:58", + "multipliers": [2.45], + "weaknesses": ["Water", "Grass", "Ice"], + "next_evolution": [ + { + "num": "028", + "name": "Sandslash" + } + ] + }, + { + "id": 28, + "num": "028", + "name": "Sandslash", + "img": "http://www.serebii.net/pokemongo/pokemon/028.png", + "type": ["Ground"], + "height": "0.99 m", + "weight": "29.5 kg", + "candy": "Sandshrew Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.037, + "avg_spawns": 3.7, + "spawn_time": "12:34", + "multipliers": null, + "weaknesses": ["Water", "Grass", "Ice"], + "prev_evolution": [ + { + "num": "027", + "name": "Sandshrew" + } + ] + }, + { + "id": 29, + "num": "029", + "name": "Nidoran ♀ (Female)", + "img": "http://www.serebii.net/pokemongo/pokemon/029.png", + "type": ["Poison"], + "height": "0.41 m", + "weight": "7.0 kg", + "candy": "Nidoran ♀ (Female) Candy", + "candy_count": 25, + "egg": "5 km", + "spawn_chance": 1.38, + "avg_spawns": 138, + "spawn_time": "01:51", + "multipliers": [1.63, 2.48], + "weaknesses": ["Ground", "Psychic"], + "next_evolution": [ + { + "num": "030", + "name": "Nidorina" + }, + { + "num": "031", + "name": "Nidoqueen" + } + ] + }, + { + "id": 30, + "num": "030", + "name": "Nidorina", + "img": "http://www.serebii.net/pokemongo/pokemon/030.png", + "type": ["Poison"], + "height": "0.79 m", + "weight": "20.0 kg", + "candy": "Nidoran ♀ (Female) Candy", + "candy_count": 100, + "egg": "Not in Eggs", + "spawn_chance": 0.088, + "avg_spawns": 8.8, + "spawn_time": "07:22", + "multipliers": [1.83, 2.48], + "weaknesses": ["Ground", "Psychic"], + "prev_evolution": [ + { + "num": "029", + "name": "Nidoran(Female)" + } + ], + "next_evolution": [ + { + "num": "031", + "name": "Nidoqueen" + } + ] + }, + { + "id": 31, + "num": "031", + "name": "Nidoqueen", + "img": "http://www.serebii.net/pokemongo/pokemon/031.png", + "type": ["Poison", "Ground"], + "height": "1.30 m", + "weight": "60.0 kg", + "candy": "Nidoran ♀ (Female) Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.012, + "avg_spawns": 1.2, + "spawn_time": "12:35", + "multipliers": null, + "weaknesses": ["Water", "Ice", "Ground", "Psychic"], + "prev_evolution": [ + { + "num": "029", + "name": "Nidoran(Female)" + }, + { + "num": "030", + "name": "Nidorina" + } + ] + }, + { + "id": 32, + "num": "032", + "name": "Nidoran ♂ (Male)", + "img": "http://www.serebii.net/pokemongo/pokemon/032.png", + "type": ["Poison"], + "height": "0.51 m", + "weight": "9.0 kg", + "candy": "Nidoran ♂ (Male) Candy", + "candy_count": 25, + "egg": "5 km", + "spawn_chance": 1.31, + "avg_spawns": 131, + "spawn_time": "01:12", + "multipliers": [1.64, 1.7], + "weaknesses": ["Ground", "Psychic"], + "next_evolution": [ + { + "num": "033", + "name": "Nidorino" + }, + { + "num": "034", + "name": "Nidoking" + } + ] + }, + { + "id": 33, + "num": "033", + "name": "Nidorino", + "img": "http://www.serebii.net/pokemongo/pokemon/033.png", + "type": ["Poison"], + "height": "0.89 m", + "weight": "19.5 kg", + "candy": "Nidoran ♂ (Male) Candy", + "candy_count": 100, + "egg": "Not in Eggs", + "spawn_chance": 0.083, + "avg_spawns": 8.3, + "spawn_time": "09:02", + "multipliers": [1.83], + "weaknesses": ["Ground", "Psychic"], + "prev_evolution": [ + { + "num": "032", + "name": "Nidoran(Male)" + } + ], + "next_evolution": [ + { + "num": "034", + "name": "Nidoking" + } + ] + }, + { + "id": 34, + "num": "034", + "name": "Nidoking", + "img": "http://www.serebii.net/pokemongo/pokemon/034.png", + "type": ["Poison", "Ground"], + "height": "1.40 m", + "weight": "62.0 kg", + "candy": "Nidoran ♂ (Male) Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.017, + "avg_spawns": 1.7, + "spawn_time": "12:16", + "multipliers": null, + "weaknesses": ["Water", "Ice", "Ground", "Psychic"], + "prev_evolution": [ + { + "num": "032", + "name": "Nidoran(Male)" + }, + { + "num": "033", + "name": "Nidorino" + } + ] + }, + { + "id": 35, + "num": "035", + "name": "Clefairy", + "img": "http://www.serebii.net/pokemongo/pokemon/035.png", + "type": ["Normal"], + "height": "0.61 m", + "weight": "7.5 kg", + "candy": "Clefairy Candy", + "candy_count": 50, + "egg": "2 km", + "spawn_chance": 0.92, + "avg_spawns": 92, + "spawn_time": "03:30", + "multipliers": [2.03, 2.14], + "weaknesses": ["Fighting"], + "next_evolution": [ + { + "num": "036", + "name": "Clefable" + } + ] + }, + { + "id": 36, + "num": "036", + "name": "Clefable", + "img": "http://www.serebii.net/pokemongo/pokemon/036.png", + "type": ["Normal"], + "height": "1.30 m", + "weight": "40.0 kg", + "candy": "Clefairy Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.012, + "avg_spawns": 1.2, + "spawn_time": "03:29", + "multipliers": null, + "weaknesses": ["Fighting"], + "prev_evolution": [ + { + "num": "035", + "name": "Clefairy" + } + ] + }, + { + "id": 37, + "num": "037", + "name": "Vulpix", + "img": "http://www.serebii.net/pokemongo/pokemon/037.png", + "type": ["Fire"], + "height": "0.61 m", + "weight": "9.9 kg", + "candy": "Vulpix Candy", + "candy_count": 50, + "egg": "5 km", + "spawn_chance": 0.22, + "avg_spawns": 22, + "spawn_time": "13:43", + "multipliers": [2.74, 2.81], + "weaknesses": ["Water", "Ground", "Rock"], + "next_evolution": [ + { + "num": "038", + "name": "Ninetales" + } + ] + }, + { + "id": 38, + "num": "038", + "name": "Ninetales", + "img": "http://www.serebii.net/pokemongo/pokemon/038.png", + "type": ["Fire"], + "height": "1.09 m", + "weight": "19.9 kg", + "candy": "Vulpix Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.0077, + "avg_spawns": 0.77, + "spawn_time": "01:32", + "multipliers": null, + "weaknesses": ["Water", "Ground", "Rock"], + "prev_evolution": [ + { + "num": "037", + "name": "Vulpix" + } + ] + }, + { + "id": 39, + "num": "039", + "name": "Jigglypuff", + "img": "http://www.serebii.net/pokemongo/pokemon/039.png", + "type": ["Normal"], + "height": "0.51 m", + "weight": "5.5 kg", + "candy": "Jigglypuff Candy", + "candy_count": 50, + "egg": "2 km", + "spawn_chance": 0.39, + "avg_spawns": 39, + "spawn_time": "08:46", + "multipliers": [1.85], + "weaknesses": ["Fighting"], + "next_evolution": [ + { + "num": "040", + "name": "Wigglytuff" + } + ] + }, + { + "id": 40, + "num": "040", + "name": "Wigglytuff", + "img": "http://www.serebii.net/pokemongo/pokemon/040.png", + "type": ["Normal"], + "height": "0.99 m", + "weight": "12.0 kg", + "candy": "Jigglypuff Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.018, + "avg_spawns": 1.8, + "spawn_time": "12:28", + "multipliers": null, + "weaknesses": ["Fighting"], + "prev_evolution": [ + { + "num": "039", + "name": "Jigglypuff" + } + ] + }, + { + "id": 41, + "num": "041", + "name": "Zubat", + "img": "http://www.serebii.net/pokemongo/pokemon/041.png", + "type": ["Poison", "Flying"], + "height": "0.79 m", + "weight": "7.5 kg", + "candy": "Zubat Candy", + "candy_count": 50, + "egg": "2 km", + "spawn_chance": 6.52, + "avg_spawns": 652, + "spawn_time": "12:28", + "multipliers": [2.6, 3.67], + "weaknesses": ["Electric", "Ice", "Psychic", "Rock"], + "next_evolution": [ + { + "num": "042", + "name": "Golbat" + } + ] + }, + { + "id": 42, + "num": "042", + "name": "Golbat", + "img": "http://www.serebii.net/pokemongo/pokemon/042.png", + "type": ["Poison", "Flying"], + "height": "1.60 m", + "weight": "55.0 kg", + "candy": "Zubat Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.42, + "avg_spawns": 42, + "spawn_time": "02:15", + "multipliers": null, + "weaknesses": ["Electric", "Ice", "Psychic", "Rock"], + "prev_evolution": [ + { + "num": "041", + "name": "Zubat" + } + ] + }, + { + "id": 43, + "num": "043", + "name": "Oddish", + "img": "http://www.serebii.net/pokemongo/pokemon/043.png", + "type": ["Grass", "Poison"], + "height": "0.51 m", + "weight": "5.4 kg", + "candy": "Oddish Candy", + "candy_count": 25, + "egg": "5 km", + "spawn_chance": 1.02, + "avg_spawns": 102, + "spawn_time": "03:58", + "multipliers": [1.5], + "weaknesses": ["Fire", "Ice", "Flying", "Psychic"], + "next_evolution": [ + { + "num": "044", + "name": "Gloom" + }, + { + "num": "045", + "name": "Vileplume" + } + ] + }, + { + "id": 44, + "num": "044", + "name": "Gloom", + "img": "http://www.serebii.net/pokemongo/pokemon/044.png", + "type": ["Grass", "Poison"], + "height": "0.79 m", + "weight": "8.6 kg", + "candy": "Oddish Candy", + "candy_count": 100, + "egg": "Not in Eggs", + "spawn_chance": 0.064, + "avg_spawns": 6.4, + "spawn_time": "11:33", + "multipliers": [1.49], + "weaknesses": ["Fire", "Ice", "Flying", "Psychic"], + "prev_evolution": [ + { + "num": "043", + "name": "Oddish" + } + ], + "next_evolution": [ + { + "num": "045", + "name": "Vileplume" + } + ] + }, + { + "id": 45, + "num": "045", + "name": "Vileplume", + "img": "http://www.serebii.net/pokemongo/pokemon/045.png", + "type": ["Grass", "Poison"], + "height": "1.19 m", + "weight": "18.6 kg", + "candy": "Oddish Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.0097, + "avg_spawns": 0.97, + "spawn_time": "23:58", + "multipliers": null, + "weaknesses": ["Fire", "Ice", "Flying", "Psychic"], + "prev_evolution": [ + { + "num": "043", + "name": "Oddish" + }, + { + "num": "044", + "name": "Gloom" + } + ] + }, + { + "id": 46, + "num": "046", + "name": "Paras", + "img": "http://www.serebii.net/pokemongo/pokemon/046.png", + "type": ["Bug", "Grass"], + "height": "0.30 m", + "weight": "5.4 kg", + "candy": "Paras Candy", + "candy_count": 50, + "egg": "5 km", + "spawn_chance": 2.36, + "avg_spawns": 236, + "spawn_time": "01:42", + "multipliers": [2.02], + "weaknesses": ["Fire", "Ice", "Poison", "Flying", "Bug", "Rock"], + "next_evolution": [ + { + "num": "047", + "name": "Parasect" + } + ] + }, + { + "id": 47, + "num": "047", + "name": "Parasect", + "img": "http://www.serebii.net/pokemongo/pokemon/047.png", + "type": ["Bug", "Grass"], + "height": "0.99 m", + "weight": "29.5 kg", + "candy": "Paras Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.074, + "avg_spawns": 7.4, + "spawn_time": "01:22", + "multipliers": null, + "weaknesses": ["Fire", "Ice", "Poison", "Flying", "Bug", "Rock"], + "prev_evolution": [ + { + "num": "046", + "name": "Paras" + } + ] + }, + { + "id": 48, + "num": "048", + "name": "Venonat", + "img": "http://www.serebii.net/pokemongo/pokemon/048.png", + "type": ["Bug", "Poison"], + "height": "0.99 m", + "weight": "30.0 kg", + "candy": "Venonat Candy", + "candy_count": 50, + "egg": "5 km", + "spawn_chance": 2.28, + "avg_spawns": 228, + "spawn_time": "02:31", + "multipliers": [1.86, 1.9], + "weaknesses": ["Fire", "Flying", "Psychic", "Rock"], + "next_evolution": [ + { + "num": "049", + "name": "Venomoth" + } + ] + }, + { + "id": 49, + "num": "049", + "name": "Venomoth", + "img": "http://www.serebii.net/pokemongo/pokemon/049.png", + "type": ["Bug", "Poison"], + "height": "1.50 m", + "weight": "12.5 kg", + "candy": "Venonat Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.072, + "avg_spawns": 7.2, + "spawn_time": "23:40", + "multipliers": null, + "weaknesses": ["Fire", "Flying", "Psychic", "Rock"], + "prev_evolution": [ + { + "num": "048", + "name": "Venonat" + } + ] + }, + { + "id": 50, + "num": "050", + "name": "Diglett", + "img": "http://www.serebii.net/pokemongo/pokemon/050.png", + "type": ["Ground"], + "height": "0.20 m", + "weight": "0.8 kg", + "candy": "Diglett Candy", + "candy_count": 50, + "egg": "5 km", + "spawn_chance": 0.4, + "avg_spawns": 40, + "spawn_time": "02:22", + "multipliers": [2.69], + "weaknesses": ["Water", "Grass", "Ice"], + "next_evolution": [ + { + "num": "051", + "name": "Dugtrio" + } + ] + }, + { + "id": 51, + "num": "051", + "name": "Dugtrio", + "img": "http://www.serebii.net/pokemongo/pokemon/051.png", + "type": ["Ground"], + "height": "0.71 m", + "weight": "33.3 kg", + "candy": "Dugtrio", + "egg": "Not in Eggs", + "spawn_chance": 0.014, + "avg_spawns": 1.4, + "spawn_time": "12:37", + "multipliers": null, + "weaknesses": ["Water", "Grass", "Ice"], + "prev_evolution": [ + { + "num": "050", + "name": "Diglett" + } + ] + }, + { + "id": 52, + "num": "052", + "name": "Meowth", + "img": "http://www.serebii.net/pokemongo/pokemon/052.png", + "type": ["Normal"], + "height": "0.41 m", + "weight": "4.2 kg", + "candy": "Meowth Candy", + "candy_count": 50, + "egg": "5 km", + "spawn_chance": 0.86, + "avg_spawns": 86, + "spawn_time": "02:54", + "multipliers": [1.98], + "weaknesses": ["Fighting"], + "next_evolution": [ + { + "num": "053", + "name": "Persian" + } + ] + }, + { + "id": 53, + "num": "053", + "name": "Persian", + "img": "http://www.serebii.net/pokemongo/pokemon/053.png", + "type": ["Normal"], + "height": "0.99 m", + "weight": "32.0 kg", + "candy": "Meowth Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.022, + "avg_spawns": 2.2, + "spawn_time": "02:44", + "multipliers": null, + "weaknesses": ["Fighting"], + "prev_evolution": [ + { + "num": "052", + "name": "Meowth" + } + ] + }, + { + "id": 54, + "num": "054", + "name": "Psyduck", + "img": "http://www.serebii.net/pokemongo/pokemon/054.png", + "type": ["Water"], + "height": "0.79 m", + "weight": "19.6 kg", + "candy": "Psyduck Candy", + "candy_count": 50, + "egg": "5 km", + "spawn_chance": 2.54, + "avg_spawns": 254, + "spawn_time": "03:41", + "multipliers": [2.27], + "weaknesses": ["Electric", "Grass"], + "next_evolution": [ + { + "num": "055", + "name": "Golduck" + } + ] + }, + { + "id": 55, + "num": "055", + "name": "Golduck", + "img": "http://www.serebii.net/pokemongo/pokemon/055.png", + "type": ["Water"], + "height": "1.70 m", + "weight": "76.6 kg", + "candy": "Psyduck Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.087, + "avg_spawns": 8.7, + "spawn_time": "23:06", + "multipliers": null, + "weaknesses": ["Electric", "Grass"], + "prev_evolution": [ + { + "num": "054", + "name": "Psyduck" + } + ] + }, + { + "id": 56, + "num": "056", + "name": "Mankey", + "img": "http://www.serebii.net/pokemongo/pokemon/056.png", + "type": ["Fighting"], + "height": "0.51 m", + "weight": "28.0 kg", + "candy": "Mankey Candy", + "candy_count": 50, + "egg": "5 km", + "spawn_chance": 0.92, + "avg_spawns": 92, + "spawn_time": "12:52", + "multipliers": [2.17, 2.28], + "weaknesses": ["Flying", "Psychic", "Fairy"], + "next_evolution": [ + { + "num": "057", + "name": "Primeape" + } + ] + }, + { + "id": 57, + "num": "057", + "name": "Primeape", + "img": "http://www.serebii.net/pokemongo/pokemon/057.png", + "type": ["Fighting"], + "height": "0.99 m", + "weight": "32.0 kg", + "candy": "Mankey Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.031, + "avg_spawns": 3.1, + "spawn_time": "12:33", + "multipliers": null, + "weaknesses": ["Flying", "Psychic", "Fairy"], + "prev_evolution": [ + { + "num": "056", + "name": "Mankey" + } + ] + }, + { + "id": 58, + "num": "058", + "name": "Growlithe", + "img": "http://www.serebii.net/pokemongo/pokemon/058.png", + "type": ["Fire"], + "height": "0.71 m", + "weight": "19.0 kg", + "candy": "Growlithe Candy", + "candy_count": 50, + "egg": "5 km", + "spawn_chance": 0.92, + "avg_spawns": 92, + "spawn_time": "03:57", + "multipliers": [2.31, 2.36], + "weaknesses": ["Water", "Ground", "Rock"], + "next_evolution": [ + { + "num": "059", + "name": "Arcanine" + } + ] + }, + { + "id": 59, + "num": "059", + "name": "Arcanine", + "img": "http://www.serebii.net/pokemongo/pokemon/059.png", + "type": ["Fire"], + "height": "1.91 m", + "weight": "155.0 kg", + "candy": "Growlithe Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.017, + "avg_spawns": 1.7, + "spawn_time": "03:11", + "multipliers": null, + "weaknesses": ["Water", "Ground", "Rock"], + "prev_evolution": [ + { + "num": "058", + "name": "Growlithe" + } + ] + }, + { + "id": 60, + "num": "060", + "name": "Poliwag", + "img": "http://www.serebii.net/pokemongo/pokemon/060.png", + "type": ["Water"], + "height": "0.61 m", + "weight": "12.4 kg", + "candy": "Poliwag Candy", + "candy_count": 25, + "egg": "5 km", + "spawn_chance": 2.19, + "avg_spawns": 219, + "spawn_time": "03:40", + "multipliers": [1.72, 1.73], + "weaknesses": ["Electric", "Grass"], + "next_evolution": [ + { + "num": "061", + "name": "Poliwhirl" + }, + { + "num": "062", + "name": "Poliwrath" + } + ] + }, + { + "id": 61, + "num": "061", + "name": "Poliwhirl", + "img": "http://www.serebii.net/pokemongo/pokemon/061.png", + "type": ["Water"], + "height": "0.99 m", + "weight": "20.0 kg", + "candy": "Poliwag Candy", + "candy_count": 100, + "egg": "Not in Eggs", + "spawn_chance": 0.13, + "avg_spawns": 13, + "spawn_time": "09:14", + "multipliers": [1.95], + "weaknesses": ["Electric", "Grass"], + "prev_evolution": [ + { + "num": "060", + "name": "Poliwag" + } + ], + "next_evolution": [ + { + "num": "062", + "name": "Poliwrath" + } + ] + }, + { + "id": 62, + "num": "062", + "name": "Poliwrath", + "img": "http://www.serebii.net/pokemongo/pokemon/062.png", + "type": ["Water", "Fighting"], + "height": "1.30 m", + "weight": "54.0 kg", + "candy": "Poliwag Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.011, + "avg_spawns": 1.1, + "spawn_time": "01:32", + "multipliers": null, + "weaknesses": ["Electric", "Grass", "Flying", "Psychic", "Fairy"], + "prev_evolution": [ + { + "num": "060", + "name": "Poliwag" + }, + { + "num": "061", + "name": "Poliwhirl" + } + ] + }, + { + "id": 63, + "num": "063", + "name": "Abra", + "img": "http://www.serebii.net/pokemongo/pokemon/063.png", + "type": ["Psychic"], + "height": "0.89 m", + "weight": "19.5 kg", + "candy": "Abra Candy", + "candy_count": 25, + "egg": "5 km", + "spawn_chance": 0.42, + "avg_spawns": 42, + "spawn_time": "04:30", + "multipliers": [1.36, 1.95], + "weaknesses": ["Bug", "Ghost", "Dark"], + "next_evolution": [ + { + "num": "064", + "name": "Kadabra" + }, + { + "num": "065", + "name": "Alakazam" + } + ] + }, + { + "id": 64, + "num": "064", + "name": "Kadabra", + "img": "http://www.serebii.net/pokemongo/pokemon/064.png", + "type": ["Psychic"], + "height": "1.30 m", + "weight": "56.5 kg", + "candy": "Abra Candy", + "candy_count": 100, + "egg": "Not in Eggs", + "spawn_chance": 0.027, + "avg_spawns": 2.7, + "spawn_time": "11:25", + "multipliers": [1.4], + "weaknesses": ["Bug", "Ghost", "Dark"], + "prev_evolution": [ + { + "num": "063", + "name": "Abra" + } + ], + "next_evolution": [ + { + "num": "065", + "name": "Alakazam" + } + ] + }, + { + "id": 65, + "num": "065", + "name": "Alakazam", + "img": "http://www.serebii.net/pokemongo/pokemon/065.png", + "type": ["Psychic"], + "height": "1.50 m", + "weight": "48.0 kg", + "candy": "Abra Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.0073, + "avg_spawns": 0.73, + "spawn_time": "12:33", + "multipliers": null, + "weaknesses": ["Bug", "Ghost", "Dark"], + "prev_evolution": [ + { + "num": "063", + "name": "Abra" + }, + { + "num": "064", + "name": "Kadabra" + } + ] + }, + { + "id": 66, + "num": "066", + "name": "Machop", + "img": "http://www.serebii.net/pokemongo/pokemon/066.png", + "type": ["Fighting"], + "height": "0.79 m", + "weight": "19.5 kg", + "candy": "Machop Candy", + "candy_count": 25, + "egg": "5 km", + "spawn_chance": 0.49, + "avg_spawns": 49, + "spawn_time": "01:55", + "multipliers": [1.64, 1.65], + "weaknesses": ["Flying", "Psychic", "Fairy"], + "next_evolution": [ + { + "num": "067", + "name": "Machoke" + }, + { + "num": "068", + "name": "Machamp" + } + ] + }, + { + "id": 67, + "num": "067", + "name": "Machoke", + "img": "http://www.serebii.net/pokemongo/pokemon/067.png", + "type": ["Fighting"], + "height": "1.50 m", + "weight": "70.5 kg", + "candy": "Machop Candy", + "candy_count": 100, + "egg": "Not in Eggs", + "spawn_chance": 0.034, + "avg_spawns": 3.4, + "spawn_time": "10:32", + "multipliers": [1.7], + "weaknesses": ["Flying", "Psychic", "Fairy"], + "prev_evolution": [ + { + "num": "066", + "name": "Machop" + } + ], + "next_evolution": [ + { + "num": "068", + "name": "Machamp" + } + ] + }, + { + "id": 68, + "num": "068", + "name": "Machamp", + "img": "http://www.serebii.net/pokemongo/pokemon/068.png", + "type": ["Fighting"], + "height": "1.60 m", + "weight": "130.0 kg", + "candy": "Machop Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.0068, + "avg_spawns": 0.68, + "spawn_time": "02:55", + "multipliers": null, + "weaknesses": ["Flying", "Psychic", "Fairy"], + "prev_evolution": [ + { + "num": "066", + "name": "Machop" + }, + { + "num": "067", + "name": "Machoke" + } + ] + }, + { + "id": 69, + "num": "069", + "name": "Bellsprout", + "img": "http://www.serebii.net/pokemongo/pokemon/069.png", + "type": ["Grass", "Poison"], + "height": "0.71 m", + "weight": "4.0 kg", + "candy": "Bellsprout Candy", + "candy_count": 25, + "egg": "5 km", + "spawn_chance": 1.15, + "avg_spawns": 115, + "spawn_time": "04:10", + "multipliers": [1.57], + "weaknesses": ["Fire", "Ice", "Flying", "Psychic"], + "next_evolution": [ + { + "num": "070", + "name": "Weepinbell" + }, + { + "num": "071", + "name": "Victreebel" + } + ] + }, + { + "id": 70, + "num": "070", + "name": "Weepinbell", + "img": "http://www.serebii.net/pokemongo/pokemon/070.png", + "type": ["Grass", "Poison"], + "height": "0.99 m", + "weight": "6.4 kg", + "candy": "Bellsprout Candy", + "candy_count": 100, + "egg": "Not in Eggs", + "spawn_chance": 0.072, + "avg_spawns": 7.2, + "spawn_time": "09:45", + "multipliers": [1.59], + "weaknesses": ["Fire", "Ice", "Flying", "Psychic"], + "prev_evolution": [ + { + "num": "069", + "name": "Bellsprout" + } + ], + "next_evolution": [ + { + "num": "071", + "name": "Victreebel" + } + ] + }, + { + "id": 71, + "num": "071", + "name": "Victreebel", + "img": "http://www.serebii.net/pokemongo/pokemon/071.png", + "type": ["Grass", "Poison"], + "height": "1.70 m", + "weight": "15.5 kg", + "candy": "Bellsprout Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.0059, + "avg_spawns": 0.59, + "spawn_time": "12:19", + "multipliers": null, + "weaknesses": ["Fire", "Ice", "Flying", "Psychic"], + "prev_evolution": [ + { + "num": "069", + "name": "Bellsprout" + }, + { + "num": "070", + "name": "Weepinbell" + } + ] + }, + { + "id": 72, + "num": "072", + "name": "Tentacool", + "img": "http://www.serebii.net/pokemongo/pokemon/072.png", + "type": ["Water", "Poison"], + "height": "0.89 m", + "weight": "45.5 kg", + "candy": "Tentacool Candy", + "candy_count": 50, + "egg": "5 km", + "spawn_chance": 0.81, + "avg_spawns": 81, + "spawn_time": "03:20", + "multipliers": [2.52], + "weaknesses": ["Electric", "Ground", "Psychic"], + "next_evolution": [ + { + "num": "073", + "name": "Tentacruel" + } + ] + }, + { + "id": 73, + "num": "073", + "name": "Tentacruel", + "img": "http://www.serebii.net/pokemongo/pokemon/073.png", + "type": ["Water", "Poison"], + "height": "1.60 m", + "weight": "55.0 kg", + "candy": "Tentacool Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.082, + "avg_spawns": 8.2, + "spawn_time": "23:36", + "multipliers": null, + "weaknesses": ["Electric", "Ground", "Psychic"], + "prev_evolution": [ + { + "num": "072", + "name": "Tentacool" + } + ] + }, + { + "id": 74, + "num": "074", + "name": "Geodude", + "img": "http://www.serebii.net/pokemongo/pokemon/074.png", + "type": ["Rock", "Ground"], + "height": "0.41 m", + "weight": "20.0 kg", + "candy": "Geodude Candy", + "candy_count": 25, + "egg": "2 km", + "spawn_chance": 1.19, + "avg_spawns": 119, + "spawn_time": "12:40", + "multipliers": [1.75, 1.76], + "weaknesses": ["Water", "Grass", "Ice", "Fighting", "Ground", "Steel"], + "next_evolution": [ + { + "num": "075", + "name": "Graveler" + }, + { + "num": "076", + "name": "Golem" + } + ] + }, + { + "id": 75, + "num": "075", + "name": "Graveler", + "img": "http://www.serebii.net/pokemongo/pokemon/075.png", + "type": ["Rock", "Ground"], + "height": "0.99 m", + "weight": "105.0 kg", + "candy": "Geodude Candy", + "candy_count": 100, + "egg": "Not in Eggs", + "spawn_chance": 0.071, + "avg_spawns": 7.1, + "spawn_time": "04:53", + "multipliers": [1.64, 1.72], + "weaknesses": ["Water", "Grass", "Ice", "Fighting", "Ground", "Steel"], + "prev_evolution": [ + { + "num": "074", + "name": "Geodude" + } + ], + "next_evolution": [ + { + "num": "076", + "name": "Golem" + } + ] + }, + { + "id": 76, + "num": "076", + "name": "Golem", + "img": "http://www.serebii.net/pokemongo/pokemon/076.png", + "type": ["Rock", "Ground"], + "height": "1.40 m", + "weight": "300.0 kg", + "candy": "Geodude Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.0047, + "avg_spawns": 0.47, + "spawn_time": "12:16", + "multipliers": null, + "weaknesses": ["Water", "Grass", "Ice", "Fighting", "Ground", "Steel"], + "prev_evolution": [ + { + "num": "074", + "name": "Geodude" + }, + { + "num": "075", + "name": "Graveler" + } + ] + }, + { + "id": 77, + "num": "077", + "name": "Ponyta", + "img": "http://www.serebii.net/pokemongo/pokemon/077.png", + "type": ["Fire"], + "height": "0.99 m", + "weight": "30.0 kg", + "candy": "Ponyta Candy", + "candy_count": 50, + "egg": "5 km", + "spawn_chance": 0.51, + "avg_spawns": 51, + "spawn_time": "02:50", + "multipliers": [1.48, 1.5], + "weaknesses": ["Water", "Ground", "Rock"], + "next_evolution": [ + { + "num": "078", + "name": "Rapidash" + } + ] + }, + { + "id": 78, + "num": "078", + "name": "Rapidash", + "img": "http://www.serebii.net/pokemongo/pokemon/078.png", + "type": ["Fire"], + "height": "1.70 m", + "weight": "95.0 kg", + "candy": "Ponyta Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.011, + "avg_spawns": 1.1, + "spawn_time": "04:00", + "multipliers": null, + "weaknesses": ["Water", "Ground", "Rock"], + "prev_evolution": [ + { + "num": "077", + "name": "Ponyta" + } + ] + }, + { + "id": 79, + "num": "079", + "name": "Slowpoke", + "img": "http://www.serebii.net/pokemongo/pokemon/079.png", + "type": ["Water", "Psychic"], + "height": "1.19 m", + "weight": "36.0 kg", + "candy": "Slowpoke Candy", + "candy_count": 50, + "egg": "5 km", + "spawn_chance": 1.05, + "avg_spawns": 105, + "spawn_time": "07:12", + "multipliers": [2.21], + "weaknesses": ["Electric", "Grass", "Bug", "Ghost", "Dark"], + "next_evolution": [ + { + "num": "080", + "name": "Slowbro" + } + ] + }, + { + "id": 80, + "num": "080", + "name": "Slowbro", + "img": "http://www.serebii.net/pokemongo/pokemon/080.png", + "type": ["Water", "Psychic"], + "height": "1.60 m", + "weight": "78.5 kg", + "candy": "Slowpoke Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.036, + "avg_spawns": 3.6, + "spawn_time": "02:56", + "multipliers": null, + "weaknesses": ["Electric", "Grass", "Bug", "Ghost", "Dark"], + "prev_evolution": [ + { + "num": "079", + "name": "Slowpoke" + } + ] + }, + { + "id": 81, + "num": "081", + "name": "Magnemite", + "img": "http://www.serebii.net/pokemongo/pokemon/081.png", + "type": ["Electric"], + "height": "0.30 m", + "weight": "6.0 kg", + "candy": "Magnemite Candy", + "candy_count": 50, + "egg": "5 km", + "spawn_chance": 0.71, + "avg_spawns": 71, + "spawn_time": "04:04", + "multipliers": [2.16, 2.17], + "weaknesses": ["Fire", "Water", "Ground"], + "next_evolution": [ + { + "num": "082", + "name": "Magneton" + } + ] + }, + { + "id": 82, + "num": "082", + "name": "Magneton", + "img": "http://www.serebii.net/pokemongo/pokemon/082.png", + "type": ["Electric"], + "height": "0.99 m", + "weight": "60.0 kg", + "candy": "Magnemite Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.023, + "avg_spawns": 2.3, + "spawn_time": "15:25", + "multipliers": null, + "weaknesses": ["Fire", "Water", "Ground"], + "prev_evolution": [ + { + "num": "081", + "name": "Magnemite" + } + ] + }, + { + "id": 83, + "num": "083", + "name": "Farfetch'd", + "img": "http://www.serebii.net/pokemongo/pokemon/083.png", + "type": ["Normal", "Flying"], + "height": "0.79 m", + "weight": "15.0 kg", + "candy": "None", + "egg": "5 km", + "spawn_chance": 0.0212, + "avg_spawns": 2.12, + "spawn_time": "01:09", + "multipliers": null, + "weaknesses": ["Electric", "Rock"] + }, + { + "id": 84, + "num": "084", + "name": "Doduo", + "img": "http://www.serebii.net/pokemongo/pokemon/084.png", + "type": ["Normal", "Flying"], + "height": "1.40 m", + "weight": "39.2 kg", + "candy": "Doduo Candy", + "candy_count": 50, + "egg": "5 km", + "spawn_chance": 0.52, + "avg_spawns": 52, + "spawn_time": "05:10", + "multipliers": [2.19, 2.24], + "weaknesses": ["Electric", "Rock"], + "next_evolution": [ + { + "num": "085", + "name": "Dodrio" + } + ] + }, + { + "id": 85, + "num": "085", + "name": "Dodrio", + "img": "http://www.serebii.net/pokemongo/pokemon/085.png", + "type": ["Normal", "Flying"], + "height": "1.80 m", + "weight": "85.2 kg", + "candy": "Doduo Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.22, + "avg_spawns": 22, + "spawn_time": "02:12", + "multipliers": null, + "weaknesses": ["Electric", "Rock"], + "prev_evolution": [ + { + "num": "084", + "name": "Doduo" + } + ] + }, + { + "id": 86, + "num": "086", + "name": "Seel", + "img": "http://www.serebii.net/pokemongo/pokemon/086.png", + "type": ["Water"], + "height": "1.09 m", + "weight": "90.0 kg", + "candy": "Seel Candy", + "candy_count": 50, + "egg": "5 km", + "spawn_chance": 0.28, + "avg_spawns": 28, + "spawn_time": "06:46", + "multipliers": [1.04, 1.96], + "weaknesses": ["Electric", "Grass"], + "next_evolution": [ + { + "num": "087", + "name": "Dewgong" + } + ] + }, + { + "id": 87, + "num": "087", + "name": "Dewgong", + "img": "http://www.serebii.net/pokemongo/pokemon/087.png", + "type": ["Water", "Ice"], + "height": "1.70 m", + "weight": "120.0 kg", + "candy": "Seel Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.013, + "avg_spawns": 1.3, + "spawn_time": "06:04", + "multipliers": null, + "weaknesses": ["Electric", "Grass", "Fighting", "Rock"], + "prev_evolution": [ + { + "num": "086", + "name": "Seel" + } + ] + }, + { + "id": 88, + "num": "088", + "name": "Grimer", + "img": "http://www.serebii.net/pokemongo/pokemon/088.png", + "type": ["Poison"], + "height": "0.89 m", + "weight": "30.0 kg", + "candy": "Grimer Candy", + "candy_count": 50, + "egg": "5 km", + "spawn_chance": 0.052, + "avg_spawns": 5.2, + "spawn_time": "15:11", + "multipliers": [2.44], + "weaknesses": ["Ground", "Psychic"], + "next_evolution": [ + { + "num": "089", + "name": "Muk" + } + ] + }, + { + "id": 89, + "num": "089", + "name": "Muk", + "img": "http://www.serebii.net/pokemongo/pokemon/089.png", + "type": ["Poison"], + "height": "1.19 m", + "weight": "30.0 kg", + "candy": "Grimer Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.0031, + "avg_spawns": 0.31, + "spawn_time": "01:28", + "multipliers": null, + "weaknesses": ["Ground", "Psychic"], + "prev_evolution": [ + { + "num": "088", + "name": "Grimer" + } + ] + }, + { + "id": 90, + "num": "090", + "name": "Shellder", + "img": "http://www.serebii.net/pokemongo/pokemon/090.png", + "type": ["Water"], + "height": "0.30 m", + "weight": "4.0 kg", + "candy": "Shellder Candy", + "candy_count": 50, + "egg": "5 km", + "spawn_chance": 0.52, + "avg_spawns": 52, + "spawn_time": "07:39", + "multipliers": [2.65], + "weaknesses": ["Electric", "Grass"], + "next_evolution": [ + { + "num": "091", + "name": "Cloyster" + } + ] + }, + { + "id": 91, + "num": "091", + "name": "Cloyster", + "img": "http://www.serebii.net/pokemongo/pokemon/091.png", + "type": ["Water", "Ice"], + "height": "1.50 m", + "weight": "132.5 kg", + "candy": "Shellder Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.015, + "avg_spawns": 1.5, + "spawn_time": "02:33", + "multipliers": null, + "weaknesses": ["Electric", "Grass", "Fighting", "Rock"], + "prev_evolution": [ + { + "num": "090", + "name": "Shellder" + } + ] + }, + { + "id": 92, + "num": "092", + "name": "Gastly", + "img": "http://www.serebii.net/pokemongo/pokemon/092.png", + "type": ["Ghost", "Poison"], + "height": "1.30 m", + "weight": "0.1 kg", + "candy": "Gastly Candy", + "candy_count": 25, + "egg": "5 km", + "spawn_chance": 0.79, + "avg_spawns": 79, + "spawn_time": "04:21", + "multipliers": [1.78], + "weaknesses": ["Ground", "Psychic", "Ghost", "Dark"], + "next_evolution": [ + { + "num": "093", + "name": "Haunter" + }, + { + "num": "094", + "name": "Gengar" + } + ] + }, + { + "id": 93, + "num": "093", + "name": "Haunter", + "img": "http://www.serebii.net/pokemongo/pokemon/093.png", + "type": ["Ghost", "Poison"], + "height": "1.60 m", + "weight": "0.1 kg", + "candy": "Gastly Candy", + "candy_count": 100, + "egg": "Not in Eggs", + "spawn_chance": 0.052, + "avg_spawns": 5.2, + "spawn_time": "00:10", + "multipliers": [1.56, 1.8], + "weaknesses": ["Ground", "Psychic", "Ghost", "Dark"], + "prev_evolution": [ + { + "num": "092", + "name": "Gastly" + } + ], + "next_evolution": [ + { + "num": "094", + "name": "Gengar" + } + ] + }, + { + "id": 94, + "num": "094", + "name": "Gengar", + "img": "http://www.serebii.net/pokemongo/pokemon/094.png", + "type": ["Ghost", "Poison"], + "height": "1.50 m", + "weight": "40.5 kg", + "candy": "Gastly Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.0067, + "avg_spawns": 0.67, + "spawn_time": "03:55", + "multipliers": null, + "weaknesses": ["Ground", "Psychic", "Ghost", "Dark"], + "prev_evolution": [ + { + "num": "092", + "name": "Gastly" + }, + { + "num": "093", + "name": "Haunter" + } + ] + }, + { + "id": 95, + "num": "095", + "name": "Onix", + "img": "http://www.serebii.net/pokemongo/pokemon/095.png", + "type": ["Rock", "Ground"], + "height": "8.79 m", + "weight": "210.0 kg", + "candy": "None", + "egg": "10 km", + "spawn_chance": 0.1, + "avg_spawns": 10, + "spawn_time": "01:18", + "multipliers": null, + "weaknesses": ["Water", "Grass", "Ice", "Fighting", "Ground", "Steel"] + }, + { + "id": 96, + "num": "096", + "name": "Drowzee", + "img": "http://www.serebii.net/pokemongo/pokemon/096.png", + "type": ["Psychic"], + "height": "0.99 m", + "weight": "32.4 kg", + "candy": "Drowzee Candy", + "candy_count": 50, + "egg": "5 km", + "spawn_chance": 3.21, + "avg_spawns": 321, + "spawn_time": "01:51", + "multipliers": [2.08, 2.09], + "weaknesses": ["Bug", "Ghost", "Dark"], + "next_evolution": [ + { + "num": "097", + "name": "Hypno" + } + ] + }, + { + "id": 97, + "num": "097", + "name": "Hypno", + "img": "http://www.serebii.net/pokemongo/pokemon/097.png", + "type": ["Psychic"], + "height": "1.60 m", + "weight": "75.6 kg", + "candy": "Drowzee Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.1, + "avg_spawns": 10, + "spawn_time": "02:17", + "multipliers": null, + "weaknesses": ["Bug", "Ghost", "Dark"], + "prev_evolution": [ + { + "num": "096", + "name": "Drowzee" + } + ] + }, + { + "id": 98, + "num": "098", + "name": "Krabby", + "img": "http://www.serebii.net/pokemongo/pokemon/098.png", + "type": ["Water"], + "height": "0.41 m", + "weight": "6.5 kg", + "candy": "Krabby Candy", + "candy_count": 50, + "egg": "5 km", + "spawn_chance": 2.12, + "avg_spawns": 212, + "spawn_time": "03:33", + "multipliers": [2.36, 2.4], + "weaknesses": ["Electric", "Grass"], + "next_evolution": [ + { + "num": "099", + "name": "Kingler" + } + ] + }, + { + "id": 99, + "num": "099", + "name": "Kingler", + "img": "http://www.serebii.net/pokemongo/pokemon/099.png", + "type": ["Water"], + "height": "1.30 m", + "weight": "60.0 kg", + "candy": "Krabby Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.062, + "avg_spawns": 6.2, + "spawn_time": "03:44", + "multipliers": null, + "weaknesses": ["Electric", "Grass"], + "prev_evolution": [ + { + "num": "098", + "name": "Krabby" + } + ] + }, + { + "id": 100, + "num": "100", + "name": "Voltorb", + "img": "http://www.serebii.net/pokemongo/pokemon/100.png", + "type": ["Electric"], + "height": "0.51 m", + "weight": "10.4 kg", + "candy": "Voltorb Candy", + "candy_count": 50, + "egg": "5 km", + "spawn_chance": 0.65, + "avg_spawns": 65, + "spawn_time": "04:36", + "multipliers": [2.01, 2.02], + "weaknesses": ["Ground"], + "next_evolution": [ + { + "num": "101", + "name": "Electrode" + } + ] + }, + { + "id": 101, + "num": "101", + "name": "Electrode", + "img": "http://www.serebii.net/pokemongo/pokemon/101.png", + "type": ["Electric"], + "height": "1.19 m", + "weight": "66.6 kg", + "candy": "Voltorb Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.02, + "avg_spawns": 2, + "spawn_time": "04:10", + "multipliers": null, + "weaknesses": ["Ground"], + "prev_evolution": [ + { + "num": "100", + "name": "Voltorb" + } + ] + }, + { + "id": 102, + "num": "102", + "name": "Exeggcute", + "img": "http://www.serebii.net/pokemongo/pokemon/102.png", + "type": ["Grass", "Psychic"], + "height": "0.41 m", + "weight": "2.5 kg", + "candy": "Exeggcute Candy", + "candy_count": 50, + "egg": "5 km", + "spawn_chance": 0.78, + "avg_spawns": 78, + "spawn_time": "09:09", + "multipliers": [2.7, 3.18], + "weaknesses": ["Fire", "Ice", "Poison", "Flying", "Bug", "Ghost", "Dark"], + "next_evolution": [ + { + "num": "103", + "name": "Exeggutor" + } + ] + }, + { + "id": 103, + "num": "103", + "name": "Exeggutor", + "img": "http://www.serebii.net/pokemongo/pokemon/103.png", + "type": ["Grass", "Psychic"], + "height": "2.01 m", + "weight": "120.0 kg", + "candy": "Exeggcute Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.014, + "avg_spawns": 1.4, + "spawn_time": "12:34", + "multipliers": null, + "weaknesses": ["Fire", "Ice", "Poison", "Flying", "Bug", "Ghost", "Dark"], + "prev_evolution": [ + { + "num": "102", + "name": "Exeggcute" + } + ] + }, + { + "id": 104, + "num": "104", + "name": "Cubone", + "img": "http://www.serebii.net/pokemongo/pokemon/104.png", + "type": ["Ground"], + "height": "0.41 m", + "weight": "6.5 kg", + "candy": "Cubone Candy", + "candy_count": 50, + "egg": "5 km", + "spawn_chance": 0.61, + "avg_spawns": 61, + "spawn_time": "01:51", + "multipliers": [1.67], + "weaknesses": ["Water", "Grass", "Ice"], + "next_evolution": [ + { + "num": "105", + "name": "Marowak" + } + ] + }, + { + "id": 105, + "num": "105", + "name": "Marowak", + "img": "http://www.serebii.net/pokemongo/pokemon/105.png", + "type": ["Ground"], + "height": "0.99 m", + "weight": "45.0 kg", + "candy": "Cubone Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.02, + "avg_spawns": 2, + "spawn_time": "03:59", + "multipliers": null, + "weaknesses": ["Water", "Grass", "Ice"], + "prev_evolution": [ + { + "num": "104", + "name": "Cubone" + } + ] + }, + { + "id": 106, + "num": "106", + "name": "Hitmonlee", + "img": "http://www.serebii.net/pokemongo/pokemon/106.png", + "type": ["Fighting"], + "height": "1.50 m", + "weight": "49.8 kg", + "candy": "None", + "egg": "10 km", + "spawn_chance": 0.02, + "avg_spawns": 2, + "spawn_time": "03:59", + "multipliers": null, + "weaknesses": ["Flying", "Psychic", "Fairy"] + }, + { + "id": 107, + "num": "107", + "name": "Hitmonchan", + "img": "http://www.serebii.net/pokemongo/pokemon/107.png", + "type": ["Fighting"], + "height": "1.40 m", + "weight": "50.2 kg", + "candy": "None", + "egg": "10 km", + "spawn_chance": 0.022, + "avg_spawns": 2.2, + "spawn_time": "05:58", + "multipliers": null, + "weaknesses": ["Flying", "Psychic", "Fairy"] + }, + { + "id": 108, + "num": "108", + "name": "Lickitung", + "img": "http://www.serebii.net/pokemongo/pokemon/108.png", + "type": ["Normal"], + "height": "1.19 m", + "weight": "65.5 kg", + "candy": "None", + "egg": "5 km", + "spawn_chance": 0.011, + "avg_spawns": 1.1, + "spawn_time": "02:46", + "multipliers": null, + "weaknesses": ["Fighting"] + }, + { + "id": 109, + "num": "109", + "name": "Koffing", + "img": "http://www.serebii.net/pokemongo/pokemon/109.png", + "type": ["Poison"], + "height": "0.61 m", + "weight": "1.0 kg", + "candy": "Koffing Candy", + "candy_count": 50, + "egg": "5 km", + "spawn_chance": 0.2, + "avg_spawns": 20, + "spawn_time": "08:16", + "multipliers": [1.11], + "weaknesses": ["Ground", "Psychic"], + "next_evolution": [ + { + "num": "110", + "name": "Weezing" + } + ] + }, + { + "id": 110, + "num": "110", + "name": "Weezing", + "img": "http://www.serebii.net/pokemongo/pokemon/110.png", + "type": ["Poison"], + "height": "1.19 m", + "weight": "9.5 kg", + "candy": "Koffing Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.016, + "avg_spawns": 1.6, + "spawn_time": "12:17", + "multipliers": null, + "weaknesses": ["Ground", "Psychic"], + "prev_evolution": [ + { + "num": "109", + "name": "Koffing" + } + ] + }, + { + "id": 111, + "num": "111", + "name": "Rhyhorn", + "img": "http://www.serebii.net/pokemongo/pokemon/111.png", + "type": ["Ground", "Rock"], + "height": "0.99 m", + "weight": "115.0 kg", + "candy": "Rhyhorn Candy", + "candy_count": 50, + "egg": "5 km", + "spawn_chance": 0.63, + "avg_spawns": 63, + "spawn_time": "03:21", + "multipliers": [1.91], + "weaknesses": ["Water", "Grass", "Ice", "Fighting", "Ground", "Steel"], + "next_evolution": [ + { + "num": "112", + "name": "Rhydon" + } + ] + }, + { + "id": 112, + "num": "112", + "name": "Rhydon", + "img": "http://www.serebii.net/pokemongo/pokemon/112.png", + "type": ["Ground", "Rock"], + "height": "1.91 m", + "weight": "120.0 kg", + "candy": "Rhyhorn Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.022, + "avg_spawns": 2.2, + "spawn_time": "05:50", + "multipliers": null, + "weaknesses": ["Water", "Grass", "Ice", "Fighting", "Ground", "Steel"], + "prev_evolution": [ + { + "num": "111", + "name": "Rhyhorn" + } + ] + }, + { + "id": 113, + "num": "113", + "name": "Chansey", + "img": "http://www.serebii.net/pokemongo/pokemon/113.png", + "type": ["Normal"], + "height": "1.09 m", + "weight": "34.6 kg", + "candy": "None", + "egg": "10 km", + "spawn_chance": 0.013, + "avg_spawns": 1.3, + "spawn_time": "04:46", + "multipliers": null, + "weaknesses": ["Fighting"] + }, + { + "id": 114, + "num": "114", + "name": "Tangela", + "img": "http://www.serebii.net/pokemongo/pokemon/114.png", + "type": ["Grass"], + "height": "0.99 m", + "weight": "35.0 kg", + "candy": "None", + "egg": "5 km", + "spawn_chance": 0.228, + "avg_spawns": 22.8, + "spawn_time": "23:13", + "multipliers": null, + "weaknesses": ["Fire", "Ice", "Poison", "Flying", "Bug"] + }, + { + "id": 115, + "num": "115", + "name": "Kangaskhan", + "img": "http://www.serebii.net/pokemongo/pokemon/115.png", + "type": ["Normal"], + "height": "2.21 m", + "weight": "80.0 kg", + "candy": "None", + "egg": "5 km", + "spawn_chance": 0.0086, + "avg_spawns": 0.86, + "spawn_time": "02:40", + "multipliers": null, + "weaknesses": ["Fighting"] + }, + { + "id": 116, + "num": "116", + "name": "Horsea", + "img": "http://www.serebii.net/pokemongo/pokemon/116.png", + "type": ["Water"], + "height": "0.41 m", + "weight": "8.0 kg", + "candy": "Horsea Candy", + "candy_count": 50, + "egg": "5 km", + "spawn_chance": 1.13, + "avg_spawns": 113, + "spawn_time": "02:53", + "multipliers": [2.23], + "weaknesses": ["Electric", "Grass"], + "next_evolution": [ + { + "num": "117", + "name": "Seadra" + } + ] + }, + { + "id": 117, + "num": "117", + "name": "Seadra", + "img": "http://www.serebii.net/pokemongo/pokemon/117.png", + "type": ["Water"], + "height": "1.19 m", + "weight": "25.0 kg", + "candy": "Horsea Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.034, + "avg_spawns": 3.4, + "spawn_time": "03:18", + "multipliers": null, + "weaknesses": ["Electric", "Grass"], + "prev_evolution": [ + { + "num": "116", + "name": "Horsea" + } + ] + }, + { + "id": 118, + "num": "118", + "name": "Goldeen", + "img": "http://www.serebii.net/pokemongo/pokemon/118.png", + "type": ["Water"], + "height": "0.61 m", + "weight": "15.0 kg", + "candy": "Goldeen Candy", + "candy_count": 50, + "egg": "5 km", + "spawn_chance": 2.18, + "avg_spawns": 218, + "spawn_time": "03:14", + "multipliers": [2.15, 2.2], + "weaknesses": ["Electric", "Grass"], + "next_evolution": [ + { + "num": "119", + "name": "Seaking" + } + ] + }, + { + "id": 119, + "num": "119", + "name": "Seaking", + "img": "http://www.serebii.net/pokemongo/pokemon/119.png", + "type": ["Water"], + "height": "1.30 m", + "weight": "39.0 kg", + "candy": "Goldeen Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.08, + "avg_spawns": 8, + "spawn_time": "05:21", + "multipliers": null, + "weaknesses": ["Electric", "Grass"], + "prev_evolution": [ + { + "num": "118", + "name": "Goldeen" + } + ] + }, + { + "id": 120, + "num": "120", + "name": "Staryu", + "img": "http://www.serebii.net/pokemongo/pokemon/120.png", + "type": ["Water"], + "height": "0.79 m", + "weight": "34.5 kg", + "candy": "Staryu Candy", + "candy_count": 50, + "egg": "5 km", + "spawn_chance": 1.95, + "avg_spawns": 195, + "spawn_time": "22:59", + "multipliers": [2.38, 2.41], + "weaknesses": ["Electric", "Grass"], + "next_evolution": [ + { + "num": "121", + "name": "Starmie" + } + ] + }, + { + "id": 121, + "num": "121", + "name": "Starmie", + "img": "http://www.serebii.net/pokemongo/pokemon/121.png", + "type": ["Water", "Psychic"], + "height": "1.09 m", + "weight": "80.0 kg", + "candy": "Staryu Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.034, + "avg_spawns": 3.4, + "spawn_time": "06:57", + "multipliers": null, + "weaknesses": ["Electric", "Grass", "Bug", "Ghost", "Dark"], + "prev_evolution": [ + { + "num": "120", + "name": "Staryu" + } + ] + }, + { + "id": 122, + "num": "122", + "name": "Mr. Mime", + "img": "http://www.serebii.net/pokemongo/pokemon/122.png", + "type": ["Psychic"], + "height": "1.30 m", + "weight": "54.5 kg", + "candy": "None", + "egg": "10 km", + "spawn_chance": 0.0031, + "avg_spawns": 0.31, + "spawn_time": "01:51", + "multipliers": null, + "weaknesses": ["Bug", "Ghost", "Dark"] + }, + { + "id": 123, + "num": "123", + "name": "Scyther", + "img": "http://www.serebii.net/pokemongo/pokemon/123.png", + "type": ["Bug", "Flying"], + "height": "1.50 m", + "weight": "56.0 kg", + "candy": "None", + "egg": "10 km", + "spawn_chance": 0.14, + "avg_spawns": 14, + "spawn_time": "05:43", + "multipliers": null, + "weaknesses": ["Fire", "Electric", "Ice", "Flying", "Rock"] + }, + { + "id": 124, + "num": "124", + "name": "Jynx", + "img": "http://www.serebii.net/pokemongo/pokemon/124.png", + "type": ["Ice", "Psychic"], + "height": "1.40 m", + "weight": "40.6 kg", + "candy": "None", + "egg": "10 km", + "spawn_chance": 0.35, + "avg_spawns": 35, + "spawn_time": "05:41", + "multipliers": null, + "weaknesses": ["Fire", "Bug", "Rock", "Ghost", "Dark", "Steel"] + }, + { + "id": 125, + "num": "125", + "name": "Electabuzz", + "img": "http://www.serebii.net/pokemongo/pokemon/125.png", + "type": ["Electric"], + "height": "1.09 m", + "weight": "30.0 kg", + "candy": "None", + "egg": "10 km", + "spawn_chance": 0.074, + "avg_spawns": 7.4, + "spawn_time": "04:28", + "multipliers": null, + "weaknesses": ["Ground"] + }, + { + "id": 126, + "num": "126", + "name": "Magmar", + "img": "http://www.serebii.net/pokemongo/pokemon/126.png", + "type": ["Fire"], + "height": "1.30 m", + "weight": "44.5 kg", + "candy": "None", + "egg": "10 km", + "spawn_chance": 0.1, + "avg_spawns": 10, + "spawn_time": "20:36", + "multipliers": null, + "weaknesses": ["Water", "Ground", "Rock"] + }, + { + "id": 127, + "num": "127", + "name": "Pinsir", + "img": "http://www.serebii.net/pokemongo/pokemon/127.png", + "type": ["Bug"], + "height": "1.50 m", + "weight": "55.0 kg", + "candy": "None", + "egg": "10 km", + "spawn_chance": 0.99, + "avg_spawns": 99, + "spawn_time": "03:25", + "multipliers": null, + "weaknesses": ["Fire", "Flying", "Rock"] + }, + { + "id": 128, + "num": "128", + "name": "Tauros", + "img": "http://www.serebii.net/pokemongo/pokemon/128.png", + "type": ["Normal"], + "height": "1.40 m", + "weight": "88.4 kg", + "candy": "None", + "egg": "5 km", + "spawn_chance": 0.12, + "avg_spawns": 12, + "spawn_time": "00:37", + "multipliers": null, + "weaknesses": ["Fighting"] + }, + { + "id": 129, + "num": "129", + "name": "Magikarp", + "img": "http://www.serebii.net/pokemongo/pokemon/129.png", + "type": ["Water"], + "height": "0.89 m", + "weight": "10.0 kg", + "candy": "Magikarp Candy", + "candy_count": 400, + "egg": "2 km", + "spawn_chance": 4.78, + "avg_spawns": 478, + "spawn_time": "14:26", + "multipliers": [10.1, 11.8], + "weaknesses": ["Electric", "Grass"], + "next_evolution": [ + { + "num": "130", + "name": "Gyarados" + } + ] + }, + { + "id": 130, + "num": "130", + "name": "Gyarados", + "img": "http://www.serebii.net/pokemongo/pokemon/130.png", + "type": ["Water", "Flying"], + "height": "6.50 m", + "weight": "235.0 kg", + "candy": "Magikarp Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.0032, + "avg_spawns": 0.32, + "spawn_time": "02:15", + "multipliers": null, + "weaknesses": ["Electric", "Rock"], + "prev_evolution": [ + { + "num": "129", + "name": "Magikarp" + } + ] + }, + { + "id": 131, + "num": "131", + "name": "Lapras", + "img": "http://www.serebii.net/pokemongo/pokemon/131.png", + "type": ["Water", "Ice"], + "height": "2.49 m", + "weight": "220.0 kg", + "candy": "None", + "egg": "10 km", + "spawn_chance": 0.006, + "avg_spawns": 0.6, + "spawn_time": "08:59", + "multipliers": null, + "weaknesses": ["Electric", "Grass", "Fighting", "Rock"] + }, + { + "id": 132, + "num": "132", + "name": "Ditto", + "img": "http://www.serebii.net/pokemongo/pokemon/132.png", + "type": ["Normal"], + "height": "0.30 m", + "weight": "4.0 kg", + "candy": "None", + "egg": "Not in Eggs", + "spawn_chance": 0, + "avg_spawns": 0, + "spawn_time": "N/A", + "multipliers": null, + "weaknesses": ["Fighting"] + }, + { + "id": 133, + "num": "133", + "name": "Eevee", + "img": "http://www.serebii.net/pokemongo/pokemon/133.png", + "type": ["Normal"], + "height": "0.30 m", + "weight": "6.5 kg", + "candy": "Eevee Candy", + "candy_count": 25, + "egg": "10 km", + "spawn_chance": 2.75, + "avg_spawns": 275, + "spawn_time": "05:32", + "multipliers": [2.02, 2.64], + "weaknesses": ["Fighting"], + "next_evolution": [ + { + "num": "134", + "name": "Vaporeon" + }, + { + "num": "135", + "name": "Jolteon" + }, + { + "num": "136", + "name": "Flareon" + } + ] + }, + { + "id": 134, + "num": "134", + "name": "Vaporeon", + "img": "http://www.serebii.net/pokemongo/pokemon/134.png", + "type": ["Water"], + "height": "0.99 m", + "weight": "29.0 kg", + "candy": "Eevee Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.014, + "avg_spawns": 1.4, + "spawn_time": "10:54", + "multipliers": null, + "weaknesses": ["Electric", "Grass"], + "prev_evolution": [ + { + "num": "133", + "name": "Eevee" + } + ] + }, + { + "id": 135, + "num": "135", + "name": "Jolteon", + "img": "http://www.serebii.net/pokemongo/pokemon/135.png", + "type": ["Electric"], + "height": "0.79 m", + "weight": "24.5 kg", + "candy": "None", + "egg": "Not in Eggs", + "spawn_chance": 0.012, + "avg_spawns": 1.2, + "spawn_time": "02:30", + "multipliers": null, + "weaknesses": ["Ground"], + "prev_evolution": [ + { + "num": "133", + "name": "Eevee" + } + ] + }, + { + "id": 136, + "num": "136", + "name": "Flareon", + "img": "http://www.serebii.net/pokemongo/pokemon/136.png", + "type": ["Fire"], + "height": "0.89 m", + "weight": "25.0 kg", + "candy": "Eevee Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.017, + "avg_spawns": 1.7, + "spawn_time": "07:02", + "multipliers": null, + "weaknesses": ["Water", "Ground", "Rock"], + "prev_evolution": [ + { + "num": "133", + "name": "Eevee" + } + ] + }, + { + "id": 137, + "num": "137", + "name": "Porygon", + "img": "http://www.serebii.net/pokemongo/pokemon/137.png", + "type": ["Normal"], + "height": "0.79 m", + "weight": "36.5 kg", + "candy": "None", + "egg": "5 km", + "spawn_chance": 0.012, + "avg_spawns": 1.2, + "spawn_time": "02:49", + "multipliers": null, + "weaknesses": ["Fighting"] + }, + { + "id": 138, + "num": "138", + "name": "Omanyte", + "img": "http://www.serebii.net/pokemongo/pokemon/138.png", + "type": ["Rock", "Water"], + "height": "0.41 m", + "weight": "7.5 kg", + "candy": "Omanyte Candy", + "candy_count": 50, + "egg": "10 km", + "spawn_chance": 0.14, + "avg_spawns": 14, + "spawn_time": "10:23", + "multipliers": [2.12], + "weaknesses": ["Electric", "Grass", "Fighting", "Ground"], + "next_evolution": [ + { + "num": "139", + "name": "Omastar" + } + ] + }, + { + "id": 139, + "num": "139", + "name": "Omastar", + "img": "http://www.serebii.net/pokemongo/pokemon/139.png", + "type": ["Rock", "Water"], + "height": "0.99 m", + "weight": "35.0 kg", + "candy": "None", + "egg": "Omanyte Candy", + "spawn_chance": 0.0061, + "avg_spawns": 0.61, + "spawn_time": "05:04", + "multipliers": null, + "weaknesses": ["Electric", "Grass", "Fighting", "Ground"], + "prev_evolution": [ + { + "num": "138", + "name": "Omanyte" + } + ] + }, + { + "id": 140, + "num": "140", + "name": "Kabuto", + "img": "http://www.serebii.net/pokemongo/pokemon/140.png", + "type": ["Rock", "Water"], + "height": "0.51 m", + "weight": "11.5 kg", + "candy": "Kabuto Candy", + "candy_count": 50, + "egg": "10 km", + "spawn_chance": 0.1, + "avg_spawns": 10, + "spawn_time": "00:05", + "multipliers": [1.97, 2.37], + "weaknesses": ["Electric", "Grass", "Fighting", "Ground"], + "next_evolution": [ + { + "num": "141", + "name": "Kabutops" + } + ] + }, + { + "id": 141, + "num": "141", + "name": "Kabutops", + "img": "http://www.serebii.net/pokemongo/pokemon/141.png", + "type": ["Rock", "Water"], + "height": "1.30 m", + "weight": "40.5 kg", + "candy": "Kabuto Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.0032, + "avg_spawns": 0.32, + "spawn_time": "23:40", + "multipliers": null, + "weaknesses": ["Electric", "Grass", "Fighting", "Ground"], + "prev_evolution": [ + { + "num": "140", + "name": "Kabuto" + } + ] + }, + { + "id": 142, + "num": "142", + "name": "Aerodactyl", + "img": "http://www.serebii.net/pokemongo/pokemon/142.png", + "type": ["Rock", "Flying"], + "height": "1.80 m", + "weight": "59.0 kg", + "candy": "None", + "egg": "10 km", + "spawn_chance": 0.018, + "avg_spawns": 1.8, + "spawn_time": "23:40", + "multipliers": null, + "weaknesses": ["Water", "Electric", "Ice", "Rock", "Steel"] + }, + { + "id": 143, + "num": "143", + "name": "Snorlax", + "img": "http://www.serebii.net/pokemongo/pokemon/143.png", + "type": ["Normal"], + "height": "2.11 m", + "weight": "460.0 kg", + "candy": "None", + "egg": "10 km", + "spawn_chance": 0.016, + "avg_spawns": 1.6, + "spawn_time": "23:40", + "multipliers": null, + "weaknesses": ["Fighting"] + }, + { + "id": 144, + "num": "144", + "name": "Articuno", + "img": "http://www.serebii.net/pokemongo/pokemon/144.png", + "type": ["Ice", "Flying"], + "height": "1.70 m", + "weight": "55.4 kg", + "candy": "None", + "egg": "Not in Eggs", + "spawn_chance": 0, + "avg_spawns": 0, + "spawn_time": "N/A", + "multipliers": null, + "weaknesses": ["Fire", "Electric", "Rock", "Steel"] + }, + { + "id": 145, + "num": "145", + "name": "Zapdos", + "img": "http://www.serebii.net/pokemongo/pokemon/145.png", + "type": ["Electric", "Flying"], + "height": "1.60 m", + "weight": "52.6 kg", + "candy": "None", + "egg": "Not in Eggs", + "spawn_chance": 0, + "avg_spawns": 0, + "spawn_time": "N/A", + "multipliers": null, + "weaknesses": ["Ice", "Rock"] + }, + { + "id": 146, + "num": "146", + "name": "Moltres", + "img": "http://www.serebii.net/pokemongo/pokemon/146.png", + "type": ["Fire", "Flying"], + "height": "2.01 m", + "weight": "60.0 kg", + "candy": "None", + "egg": "Not in Eggs", + "spawn_chance": 0, + "avg_spawns": 0, + "spawn_time": "N/A", + "multipliers": null, + "weaknesses": ["Water", "Electric", "Rock"] + }, + { + "id": 147, + "num": "147", + "name": "Dratini", + "img": "http://www.serebii.net/pokemongo/pokemon/147.png", + "type": ["Dragon"], + "height": "1.80 m", + "weight": "3.3 kg", + "candy": "Dratini Candy", + "candy_count": 25, + "egg": "10 km", + "spawn_chance": 0.3, + "avg_spawns": 30, + "spawn_time": "06:41", + "multipliers": [1.83, 1.84], + "weaknesses": ["Ice", "Dragon", "Fairy"], + "next_evolution": [ + { + "num": "148", + "name": "Dragonair" + }, + { + "num": "149", + "name": "Dragonite" + } + ] + }, + { + "id": 148, + "num": "148", + "name": "Dragonair", + "img": "http://www.serebii.net/pokemongo/pokemon/148.png", + "type": ["Dragon"], + "height": "3.99 m", + "weight": "16.5 kg", + "candy": "Dratini Candy", + "candy_count": 100, + "egg": "Not in Eggs", + "spawn_chance": 0.02, + "avg_spawns": 2, + "spawn_time": "11:57", + "multipliers": [2.05], + "weaknesses": ["Ice", "Dragon", "Fairy"], + "prev_evolution": [ + { + "num": "147", + "name": "Dratini" + } + ], + "next_evolution": [ + { + "num": "149", + "name": "Dragonite" + } + ] + }, + { + "id": 149, + "num": "149", + "name": "Dragonite", + "img": "http://www.serebii.net/pokemongo/pokemon/149.png", + "type": ["Dragon", "Flying"], + "height": "2.21 m", + "weight": "210.0 kg", + "candy": "Dratini Candy", + "egg": "Not in Eggs", + "spawn_chance": 0.0011, + "avg_spawns": 0.11, + "spawn_time": "23:38", + "multipliers": null, + "weaknesses": ["Ice", "Rock", "Dragon", "Fairy"], + "prev_evolution": [ + { + "num": "147", + "name": "Dratini" + }, + { + "num": "148", + "name": "Dragonair" + } + ] + }, + { + "id": 150, + "num": "150", + "name": "Mewtwo", + "img": "http://www.serebii.net/pokemongo/pokemon/150.png", + "type": ["Psychic"], + "height": "2.01 m", + "weight": "122.0 kg", + "candy": "None", + "egg": "Not in Eggs", + "spawn_chance": 0, + "avg_spawns": 0, + "spawn_time": "N/A", + "multipliers": null, + "weaknesses": ["Bug", "Ghost", "Dark"] + }, + { + "id": 151, + "num": "151", + "name": "Mew", + "img": "http://www.serebii.net/pokemongo/pokemon/151.png", + "type": ["Psychic"], + "height": "0.41 m", + "weight": "4.0 kg", + "candy": "None", + "egg": "Not in Eggs", + "spawn_chance": 0, + "avg_spawns": 0, + "spawn_time": "N/A", + "multipliers": null, + "weaknesses": ["Bug", "Ghost", "Dark"] + } + ] +} diff --git a/test/bench/package.json b/test/bench/package.json new file mode 100644 index 000000000..f753acdc1 --- /dev/null +++ b/test/bench/package.json @@ -0,0 +1,18 @@ +{ + "name": "neon-bench", + "version": "0.1.0", + "description": "Neon Benchmarks", + "main": "index.node", + "scripts": { + "build": "cargo-cp-artifact -nc index.node -- cargo build --message-format=json-render-diagnostics", + "build-debug": "npm run build --", + "build-release": "npm run build -- --release", + "install": "npm run build-release", + "test": "cargo test" + }, + "author": "The Neon Community", + "license": "MIT", + "devDependencies": { + "cargo-cp-artifact": "^0.1" + } +} diff --git a/test/bench/src/lib.rs b/test/bench/src/lib.rs new file mode 100644 index 000000000..f69510169 --- /dev/null +++ b/test/bench/src/lib.rs @@ -0,0 +1,232 @@ +use serde::Deserialize; +use std::{fmt, time::Duration}; + +use criterion::Criterion; +use neon::prelude::*; + +#[cfg(not(windows))] +use std::{fs, io, path}; + +mod pokemon; + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct BenchOptions { + #[cfg(not(windows))] + report_file: Option, + #[serde(default)] + neon: bool, + #[serde(default)] + json: bool, +} + +fn serialize(mut cx: FunctionContext) -> JsResult { + let options = cx + .argument_opt(0) + .unwrap_or_else(|| cx.empty_object().upcast()); + + let options = neon::deserialize::(&mut cx, options)?; + let mut c = Criterion::default(); + let mut group = c.benchmark_group("Serialize"); + let pokedex = pokemon::pokedex(); + + group.measurement_time(Duration::from_secs(10)); + + let parse = cx + .global() + .get::(&mut cx, "JSON")? + .get::(&mut cx, "parse")?; + + #[cfg(not(windows))] + let guard = options + .report_file + .is_some() + .then(|| pprof::ProfilerGuardBuilder::default().build()) + .transpose() + .or_else(|err| cx.throw_error(err.to_string()))?; + + if options.neon { + group.bench_function("neon", |b| { + b.iter(|| { + cx.execute_scoped(|mut cx| { + criterion::black_box(neon::serialize::(&mut cx, pokedex)) + }) + }) + }); + } + + if options.json { + group.bench_function("serde_json", |b| { + b.iter(|| { + cx.execute_scoped(|mut cx| { + let s = serde_json::to_string(pokedex).or_throw(&mut cx)?; + let s = cx.string(s).upcast(); + + criterion::black_box(parse.call(&mut cx, parse, [s])) + }) + }) + }); + } + + group.finish(); + + #[cfg(not(windows))] + if let (Some(guard), Some(report_file)) = (guard, options.report_file.as_ref()) { + let report = guard.report().build().or_throw(&mut cx)?; + let profile = report.pprof().or_throw(&mut cx)?; + let mut file = fs::File::create(report_file).or_throw(&mut cx)?; + + let mut content = Vec::new(); + + pprof::protos::Message::encode(&profile, &mut content).or_throw(&mut cx)?; + io::Write::write_all(&mut file, &content).or_throw(&mut cx)?; + } + + Ok(cx.undefined()) +} + +fn deserialize(mut cx: FunctionContext) -> JsResult { + let options = cx + .argument_opt(0) + .unwrap_or_else(|| cx.empty_object().upcast()); + + let options = neon::deserialize::(&mut cx, options)?; + let mut c = Criterion::default(); + let mut group = c.benchmark_group("Deserialize"); + let pokedex = neon::serialize::(&mut cx, pokemon::pokedex())?; + + group.measurement_time(Duration::from_secs(10)); + + let stringify = cx + .global() + .get::(&mut cx, "JSON")? + .get::(&mut cx, "stringify")?; + + #[cfg(not(windows))] + let guard = options + .report_file + .is_some() + .then(|| pprof::ProfilerGuardBuilder::default().build()) + .transpose() + .or_else(|err| cx.throw_error(err.to_string()))?; + + if options.neon { + group.bench_function("neon", |b| { + b.iter(|| { + cx.execute_scoped(|mut cx| { + criterion::black_box(neon::deserialize::( + &mut cx, pokedex, + )) + }) + }) + }); + } + + if options.json { + group.bench_function("serde_json", |b| { + b.iter(|| { + cx.execute_scoped(|mut cx| { + let pokedex = stringify + .call(&mut cx, stringify, [pokedex])? + .downcast_or_throw::(&mut cx)? + .value(&mut cx); + + criterion::black_box( + serde_json::from_str::(&pokedex).or_throw(&mut cx), + ) + }) + }) + }); + } + + group.finish(); + + #[cfg(not(windows))] + if let (Some(guard), Some(report_file)) = (guard, options.report_file.as_ref()) { + let report = guard.report().build().or_throw(&mut cx)?; + let profile = report.pprof().or_throw(&mut cx)?; + let mut file = fs::File::create(report_file).or_throw(&mut cx)?; + + let mut content = Vec::new(); + + pprof::protos::Message::encode(&profile, &mut content).or_throw(&mut cx)?; + io::Write::write_all(&mut file, &content).or_throw(&mut cx)?; + } + + Ok(cx.undefined()) +} + +fn arguments(mut cx: FunctionContext) -> JsResult { + let mut c = Criterion::default(); + let mut group = c.benchmark_group("Arguments"); + + group.measurement_time(Duration::from_secs(10)); + + let a = cx.number(1).upcast(); + let b = cx.number(2).upcast(); + + let manual = JsFunction::new(&mut cx, |mut cx| { + let a = cx.argument::(0)?.value(&mut cx); + let b = cx.argument::(0)?.value(&mut cx); + + Ok(cx.number(a + b)) + })?; + + let partial = JsFunction::new(&mut cx, |mut cx| { + let (a, b): (f64, f64) = cx.deserialize_args()?; + + Ok(cx.number(a + b)) + })?; + + let full = JsFunction::new(&mut cx, |mut cx| { + let (a, b): (f64, f64) = cx.deserialize_args()?; + + neon::serialize::(&mut cx, &(a + b)) + })?; + + group.bench_function("manual", |bencher| { + bencher.iter(|| manual.call(&mut cx, manual, [a, b])) + }); + + group.bench_function("partial", |bencher| { + bencher.iter(|| partial.call(&mut cx, partial, [a, b])) + }); + + group.bench_function("full", |bencher| { + bencher.iter(|| full.call(&mut cx, full, [a, b])) + }); + + group.finish(); + + Ok(cx.undefined()) +} + +#[neon::main] +fn main(mut cx: ModuleContext) -> NeonResult<()> { + cx.export_function("serialize", serialize)?; + cx.export_function("deserialize", deserialize)?; + cx.export_function("arguments", arguments)?; + + Ok(()) +} + +trait ResultExt { + fn or_throw<'cx, C>(self, cx: &mut C) -> NeonResult + where + C: Context<'cx>; +} + +impl ResultExt for Result +where + E: fmt::Display, +{ + fn or_throw<'cx, C>(self, cx: &mut C) -> NeonResult + where + C: Context<'cx>, + { + match self { + Ok(v) => Ok(v), + Err(e) => cx.throw_error(e.to_string()), + } + } +} diff --git a/test/bench/src/pokemon.rs b/test/bench/src/pokemon.rs new file mode 100644 index 000000000..7fc76a4c5 --- /dev/null +++ b/test/bench/src/pokemon.rs @@ -0,0 +1,81 @@ +use once_cell::sync::Lazy; +use serde::{Deserialize, Serialize}; + +static POKEDEX: Lazy = Lazy::new(|| { + static POKEMON: &str = include_str!("../data/pokemon.json"); + + serde_json::from_str(POKEMON).unwrap() +}); + +pub fn pokedex() -> &'static Pokedex { + Lazy::force(&POKEDEX) +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Pokedex { + pub pokemon: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Pokemon { + pub id: i64, + pub num: String, + pub name: String, + pub img: String, + #[serde(rename = "type")] + pub pokemon_type: Vec, + pub height: String, + pub weight: String, + pub candy: String, + pub candy_count: Option, + pub egg: Egg, + pub spawn_chance: f64, + pub avg_spawns: f64, + pub spawn_time: String, + pub multipliers: Option>, + pub weaknesses: Vec, + pub next_evolution: Option>, + pub prev_evolution: Option>, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Evolution { + pub num: String, + pub name: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum Egg { + #[serde(rename = "Not in Eggs")] + NotInEggs, + #[serde(rename = "Omanyte Candy")] + OmanyteCandy, + #[serde(rename = "10 km")] + The10Km, + #[serde(rename = "2 km")] + The2Km, + #[serde(rename = "5 km")] + The5Km, +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum Type { + Bug, + Dark, + Dragon, + Electric, + Fairy, + Fighting, + Fire, + Flying, + Ghost, + Grass, + Ground, + Ice, + Normal, + Poison, + Psychic, + Rock, + Steel, + Water, +} diff --git a/test/napi/Cargo.toml b/test/napi/Cargo.toml index 02aea60eb..c8b752b45 100644 --- a/test/napi/Cargo.toml +++ b/test/napi/Cargo.toml @@ -11,9 +11,12 @@ crate-type = ["cdylib"] [dependencies] once_cell = "1" +serde = { version = "1", features = ["derive"] } +serde_bytes = "0.11" +serde_json = "1" tokio = { version = "1", features = ["rt-multi-thread"] } [dependencies.neon] version = "1.0.0-alpha.2" path = "../../crates/neon" -features = ["futures", "napi-experimental", "external-buffers"] +features = ["external-buffers", "futures", "napi-experimental", "serde"] diff --git a/test/napi/lib/functions.js b/test/napi/lib/functions.js index 2f1ce976b..f82a92fe4 100644 --- a/test/napi/lib/functions.js +++ b/test/napi/lib/functions.js @@ -231,6 +231,13 @@ describe("JsFunction", function () { assert.strictEqual(addon.count_called() + 1, addon.count_called()); }); + it("should be able to deserialize arguments with serde", function () { + assert.strictEqual( + addon.deserialize_greet("Hello", "World", [5, 3]), + "Hello, World!" + ); + }); + (global.gc ? it : it.skip)( "should drop function when going out of scope", function (cb) { diff --git a/test/napi/lib/serde.js b/test/napi/lib/serde.js new file mode 100644 index 000000000..d08b533f9 --- /dev/null +++ b/test/napi/lib/serde.js @@ -0,0 +1,9 @@ +const addon = require(".."); + +describe("Serde", () => { + const suite = addon.build_serde_test_suite(); + + for (const [k, v] of Object.entries(suite)) { + it(k, v); + } +}); diff --git a/test/napi/package.json b/test/napi/package.json index fa9a6c6d5..692bda2f0 100644 --- a/test/napi/package.json +++ b/test/napi/package.json @@ -6,6 +6,7 @@ "license": "MIT", "scripts": { "install": "cargo-cp-artifact -nc index.node -- cargo build --message-format=json-render-diagnostics", + "mocha": "mocha", "test": "mocha --v8-expose-gc --timeout 5000 --recursive lib" }, "devDependencies": { diff --git a/test/napi/src/js/functions.rs b/test/napi/src/js/functions.rs index 0a099774a..d1bddac9d 100644 --- a/test/napi/src/js/functions.rs +++ b/test/napi/src/js/functions.rs @@ -245,6 +245,13 @@ pub fn is_construct(mut cx: FunctionContext) -> JsResult { Ok(this) } +pub fn deserialize_greet(mut cx: FunctionContext) -> JsResult { + let (greeting, name): (String, Handle) = cx.deserialize_args()?; + let msg = format!("{}, {}!", greeting, name.value(&mut cx)); + + Ok(cx.string(msg)) +} + // `function caller_with_drop_callback(wrappedCallback, dropCallback)` // // `wrappedCallback` will be called each time the returned function is diff --git a/test/napi/src/js/serde.rs b/test/napi/src/js/serde.rs new file mode 100644 index 000000000..356930c55 --- /dev/null +++ b/test/napi/src/js/serde.rs @@ -0,0 +1,1328 @@ +// Adapted from https://github.com/serde-rs/json/blob/master/tests/test.rs + +use std::{any, collections::BTreeMap, fmt, marker::PhantomData, panic}; + +use neon::{prelude::*, types::buffer::TypedArray}; + +use serde::{ + de::{self, DeserializeOwned}, + ser, Deserialize, Serialize, +}; + +use serde_bytes::{ByteBuf, Bytes}; + +use serde_json::json; + +const MAX_SAFE_INTEGER: u64 = 9_007_199_254_740_991; +const MIN_SAFE_INTEGER: i64 = -9_007_199_254_740_991; + +macro_rules! json_str { + ([]) => { + "[]" + }; + ([ $e0:tt $(, $e:tt)* $(,)? ]) => { + concat!("[", + json_str!($e0), + $(",", json_str!($e),)* + "]") + }; + ({}) => { + "{}" + }; + ({ $k0:tt : $v0:tt $(, $k:tt : $v:tt)* $(,)? }) => { + concat!("{", + stringify!($k0), ":", json_str!($v0), + $(",", stringify!($k), ":", json_str!($v),)* + "}") + }; + (($other:tt)) => { + $other + }; + ($other:tt) => { + stringify!($other) + }; +} + +macro_rules! treemap { + () => { + BTreeMap::new() + }; + ($($k:expr => $v:expr),+) => { + { + let mut m = BTreeMap::new(); + $( + m.insert($k, $v); + )+ + m + } + }; +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +enum Animal { + Dog, + Frog(String, Vec), + Cat { age: usize, name: String }, + AntHive(Vec), +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +struct Inner { + a: (), + b: usize, + c: Vec, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +struct Outer { + inner: Vec, +} + +fn export(cx: &mut FunctionContext, o: &JsObject, f: F) -> NeonResult<()> +where + F: Fn(&mut FunctionContext) + 'static, +{ + let f = JsFunction::new(cx, move |mut cx| { + panic::catch_unwind(panic::AssertUnwindSafe(|| f(&mut cx))).or_else(|panic| { + if let Some(s) = panic.downcast_ref::<&str>() { + cx.throw_error(s) + } else if let Some(s) = panic.downcast_ref::() { + cx.throw_error(s) + } else { + panic::resume_unwind(panic) + } + })?; + + Ok(cx.undefined()) + })?; + + o.set(cx, any::type_name::(), f)?; + + Ok(()) +} + +fn test_encode_ok(cx: &mut FunctionContext, tests: &[(T, &str)]) +where + T: PartialEq + fmt::Debug + ser::Serialize + de::DeserializeOwned, +{ + for &(ref value, out) in tests { + let out = out.to_string(); + let s = to_string(cx, value).unwrap(); + assert_eq!(s, out); + + // Make sure we can round trip + let v = neon::serialize(cx, value).unwrap(); + let d = neon::deserialize::(cx, v).unwrap(); + assert_eq!(value, &d); + } +} + +fn test_parse_ok(cx: &mut FunctionContext, tests: Vec<(&str, T)>) +where + T: Clone + fmt::Debug + PartialEq + ser::Serialize + de::DeserializeOwned, +{ + for (s, value) in tests { + let v: T = from_str(cx, s).unwrap(); + assert_eq!(v, value.clone()); + + // Make sure we can round trip + let s2 = to_string(cx, &v).unwrap(); + let v2 = from_str(cx, &s2).unwrap(); + assert_eq!(v, v2); + } +} + +// For testing representations that the deserializer accepts but the serializer +// never generates. These do not survive a round-trip. +fn test_parse_unusual_ok(cx: &mut FunctionContext, tests: Vec<(&str, T)>) +where + T: Clone + fmt::Debug + PartialEq + ser::Serialize + de::DeserializeOwned, +{ + for (s, value) in tests { + let v: T = from_str(cx, s).unwrap(); + assert_eq!(v, value); + } +} + +fn to_string(cx: &mut FunctionContext, v: &T) -> NeonResult +where + T: ?Sized + serde::Serialize, +{ + let v = neon::serialize(cx, v)?; + let s = cx + .global() + .get::(cx, "JSON")? + .get::(cx, "stringify")? + .call_with(cx) + .arg::(v) + .apply::(cx)?; + + Ok(s.value(cx)) +} + +fn from_str(cx: &mut FunctionContext, s: &str) -> NeonResult +where + T: de::DeserializeOwned, +{ + let v = cx + .global() + .get::(cx, "JSON")? + .get::(cx, "parse")? + .call_with(cx) + .arg(cx.string(s)) + .apply::(cx)?; + + neon::deserialize(cx, v) +} + +fn test_write_null(cx: &mut FunctionContext) { + let tests = &[((), "null")]; + test_encode_ok(cx, tests); +} + +fn test_write_u64(cx: &mut FunctionContext) { + let tests = &[ + (3u64, "3"), + (MAX_SAFE_INTEGER, &MAX_SAFE_INTEGER.to_string()), + ]; + test_encode_ok(cx, tests); +} + +fn test_write_i64(cx: &mut FunctionContext) { + let tests = &[ + (3i64, "3"), + (-2i64, "-2"), + (-1234i64, "-1234"), + (MIN_SAFE_INTEGER, &MIN_SAFE_INTEGER.to_string()), + ]; + test_encode_ok(cx, tests); +} + +fn test_write_f64(cx: &mut FunctionContext) { + let tests = &[ + (3.0, "3"), + (3.1, "3.1"), + (-1.5, "-1.5"), + (0.5, "0.5"), + (f64::MIN, "-1.7976931348623157e+308"), + (f64::MAX, "1.7976931348623157e+308"), + (f64::EPSILON, "2.220446049250313e-16"), + ]; + test_encode_ok(cx, tests); +} + +fn test_encode_nonfinite_float_yields_null(cx: &mut FunctionContext) { + let v = to_string(cx, &f64::NAN).unwrap(); + assert_eq!(v, "null"); + + let v = to_string(cx, &f64::INFINITY).unwrap(); + assert_eq!(v, "null"); + + let v = to_string(cx, &f32::NAN).unwrap(); + assert_eq!(v, "null"); + + let v = to_string(cx, &f32::INFINITY).unwrap(); + assert_eq!(v, "null"); +} + +fn test_write_str(cx: &mut FunctionContext) { + let tests = &[("".to_owned(), "\"\""), ("foo".to_owned(), "\"foo\"")]; + test_encode_ok(cx, tests); +} + +fn test_write_bool(cx: &mut FunctionContext) { + let tests = &[(true, "true"), (false, "false")]; + test_encode_ok(cx, tests); +} + +fn test_write_char(cx: &mut FunctionContext) { + let tests = &[ + ('n', "\"n\""), + ('"', "\"\\\"\""), + ('\\', "\"\\\\\""), + ('/', "\"/\""), + ('\x08', "\"\\b\""), + ('\x0C', "\"\\f\""), + ('\n', "\"\\n\""), + ('\r', "\"\\r\""), + ('\t', "\"\\t\""), + ('\x0B', "\"\\u000b\""), + ('\u{3A3}', "\"\u{3A3}\""), + ]; + test_encode_ok(cx, tests); +} + +fn test_write_list(cx: &mut FunctionContext) { + test_encode_ok( + cx, + &[ + (vec![], "[]"), + (vec![true], "[true]"), + (vec![true, false], "[true,false]"), + ], + ); + + test_encode_ok( + cx, + &[ + (vec![vec![], vec![], vec![]], "[[],[],[]]"), + (vec![vec![1, 2, 3], vec![], vec![]], "[[1,2,3],[],[]]"), + (vec![vec![], vec![1, 2, 3], vec![]], "[[],[1,2,3],[]]"), + (vec![vec![], vec![], vec![1, 2, 3]], "[[],[],[1,2,3]]"), + ], + ); + + let long_test_list = json!([false, null, ["foo\nbar", 3.5]]); + + test_encode_ok( + cx, + &[(long_test_list, json_str!([false, null, ["foo\nbar", 3.5]]))], + ); +} + +fn test_write_object(cx: &mut FunctionContext) { + test_encode_ok( + cx, + &[ + (treemap!(), "{}"), + (treemap!("a".to_string() => true), "{\"a\":true}"), + ( + treemap!( + "a".to_string() => true, + "b".to_string() => false + ), + "{\"a\":true,\"b\":false}", + ), + ], + ); + + test_encode_ok( + cx, + &[ + ( + treemap![ + "a".to_string() => treemap![], + "b".to_string() => treemap![], + "c".to_string() => treemap![] + ], + "{\"a\":{},\"b\":{},\"c\":{}}", + ), + ( + treemap![ + "a".to_string() => treemap![ + "a".to_string() => treemap!["a".to_string() => vec![1,2,3]], + "b".to_string() => treemap![], + "c".to_string() => treemap![] + ], + "b".to_string() => treemap![], + "c".to_string() => treemap![] + ], + "{\"a\":{\"a\":{\"a\":[1,2,3]},\"b\":{},\"c\":{}},\"b\":{},\"c\":{}}", + ), + ( + treemap![ + "a".to_string() => treemap![], + "b".to_string() => treemap![ + "a".to_string() => treemap!["a".to_string() => vec![1,2,3]], + "b".to_string() => treemap![], + "c".to_string() => treemap![] + ], + "c".to_string() => treemap![] + ], + "{\"a\":{},\"b\":{\"a\":{\"a\":[1,2,3]},\"b\":{},\"c\":{}},\"c\":{}}", + ), + ( + treemap![ + "a".to_string() => treemap![], + "b".to_string() => treemap![], + "c".to_string() => treemap![ + "a".to_string() => treemap!["a".to_string() => vec![1,2,3]], + "b".to_string() => treemap![], + "c".to_string() => treemap![] + ] + ], + "{\"a\":{},\"b\":{},\"c\":{\"a\":{\"a\":[1,2,3]},\"b\":{},\"c\":{}}}", + ), + ], + ); + + test_encode_ok(cx, &[(treemap!['c' => ()], "{\"c\":null}")]); + + let complex_obj = json!({ + "b": [ + {"c": "\x0c\x1f\r"}, + {"d": ""} + ] + }); + + test_encode_ok( + cx, + &[( + complex_obj, + json_str!({ + "b": [ + { + "c": (r#""\f\u001f\r""#) + }, + { + "d": "" + } + ] + }), + )], + ); +} + +fn test_write_tuple(cx: &mut FunctionContext) { + test_encode_ok(cx, &[((5,), "[5]")]); + + test_encode_ok(cx, &[((5, (6, "abc".to_owned())), "[5,[6,\"abc\"]]")]); +} + +fn test_write_enum(cx: &mut FunctionContext) { + test_encode_ok( + cx, + &[ + (Animal::Dog, "\"Dog\""), + ( + Animal::Frog("Henry".to_string(), vec![]), + "{\"Frog\":[\"Henry\",[]]}", + ), + ( + Animal::Frog("Henry".to_string(), vec![349]), + "{\"Frog\":[\"Henry\",[349]]}", + ), + ( + Animal::Frog("Henry".to_string(), vec![349, 102]), + "{\"Frog\":[\"Henry\",[349,102]]}", + ), + ( + Animal::Cat { + age: 5, + name: "Kate".to_string(), + }, + "{\"Cat\":{\"age\":5,\"name\":\"Kate\"}}", + ), + ( + Animal::AntHive(vec!["Bob".to_string(), "Stuart".to_string()]), + "{\"AntHive\":[\"Bob\",\"Stuart\"]}", + ), + ], + ); +} + +fn test_write_option(cx: &mut FunctionContext) { + test_encode_ok( + cx, + &[ + (None, "null"), + (Some("jodhpurs".to_owned()), "\"jodhpurs\""), + ], + ); + + test_encode_ok( + cx, + &[ + (None, "null"), + ( + Some(vec!["foo".to_owned(), "bar".to_owned()]), + "[\"foo\",\"bar\"]", + ), + ], + ); +} + +fn test_write_newtype_struct(cx: &mut FunctionContext) { + #[derive(Clone, Deserialize, Serialize, PartialEq, Debug)] + struct Newtype(BTreeMap); + + let inner = Newtype(treemap!(String::from("inner") => 123)); + + test_encode_ok(cx, &[(inner.clone(), r#"{"inner":123}"#)]); + + let outer = treemap!(String::from("outer") => inner); + + test_encode_ok(cx, &[(outer, r#"{"outer":{"inner":123}}"#)]); +} + +fn test_deserialize_number_to_untagged_enum(cx: &mut FunctionContext) { + #[derive(PartialEq, Deserialize, Debug)] + #[serde(untagged)] + enum E { + N(T), + } + + fn test(h: Handle, v: T, cx: &mut FunctionContext) + where + T: PartialEq + DeserializeOwned + fmt::Debug, + { + assert_eq!(neon::deserialize::, _, _>(cx, h).unwrap(), E::N(v)); + } + + test(cx.number(5), 5i64, cx); + test(cx.number(0), 0i64, cx); + test(cx.number(-0), 0i64, cx); + test(cx.number(-5), -5i64, cx); + test(cx.number(0), 0u64, cx); + test(cx.number(5), 5u64, cx); + test(cx.number(-5), -5f64, cx); + test(cx.number(-5.5), -5.5f64, cx); + test(cx.number(0), 0f64, cx); + test(cx.number(5), 5f64, cx); + test(cx.number(5.5), 5.5f64, cx); +} + +fn test_parse_null(cx: &mut FunctionContext) { + test_parse_ok(cx, vec![("null", ())]); +} + +fn test_parse_bool(cx: &mut FunctionContext) { + test_parse_ok( + cx, + vec![ + ("true", true), + (" true ", true), + ("false", false), + (" false ", false), + ], + ); +} + +fn test_parse_char(cx: &mut FunctionContext) { + test_parse_ok( + cx, + vec![ + ("\"n\"", 'n'), + ("\"\\\"\"", '"'), + ("\"\\\\\"", '\\'), + ("\"/\"", '/'), + ("\"\\b\"", '\x08'), + ("\"\\f\"", '\x0C'), + ("\"\\n\"", '\n'), + ("\"\\r\"", '\r'), + ("\"\\t\"", '\t'), + ("\"\\u000b\"", '\x0B'), + ("\"\\u000B\"", '\x0B'), + ("\"\u{3A3}\"", '\u{3A3}'), + ], + ); +} + +fn test_parse_i64(cx: &mut FunctionContext) { + test_parse_ok( + cx, + vec![ + ("-2", -2), + ("-1234", -1234), + (" -1234 ", -1234), + (&MIN_SAFE_INTEGER.to_string(), MIN_SAFE_INTEGER), + (&MAX_SAFE_INTEGER.to_string(), MAX_SAFE_INTEGER as i64), + ], + ); +} + +fn test_parse_u64(cx: &mut FunctionContext) { + test_parse_ok( + cx, + vec![ + ("0", 0u64), + ("3", 3u64), + ("1234", 1234), + (&MAX_SAFE_INTEGER.to_string(), MAX_SAFE_INTEGER), + ], + ); +} + +fn test_parse_f64(cx: &mut FunctionContext) { + test_parse_ok( + cx, + vec![ + ("0.0", 0.0f64), + ("3.0", 3.0f64), + ("3.1", 3.1), + ("-1.2", -1.2), + ("0.4", 0.4), + // Edge case from: + // https://github.com/serde-rs/json/issues/536#issuecomment-583714900 + ("2.638344616030823e-256", 2.638344616030823e-256), + ], + ); + + test_parse_ok( + cx, + vec![ + // With arbitrary-precision enabled, this parses as Number{"3.00"} + // but the float is Number{"3.0"} + ("3.00", 3.0f64), + ("0.4e5", 0.4e5), + ("0.4e+5", 0.4e5), + ("0.4e15", 0.4e15), + ("0.4e+15", 0.4e15), + ("0.4e-01", 0.4e-1), + (" 0.4e-01 ", 0.4e-1), + ("0.4e-001", 0.4e-1), + ("0.4e-0", 0.4e0), + ("0.00e00", 0.0), + ("0.00e+00", 0.0), + ("0.00e-00", 0.0), + ("3.5E-2147483647", 0.0), + ("0.0100000000000000000001", 0.01), + ( + &format!("{}", (i64::MIN as f64) - 1.0), + (i64::MIN as f64) - 1.0, + ), + ( + &format!("{}", (u64::MAX as f64) + 1.0), + (u64::MAX as f64) + 1.0, + ), + (&format!("{}", f64::EPSILON), f64::EPSILON), + ( + "0.0000000000000000000000000000000000000000000000000123e50", + 1.23, + ), + ("100e-777777777777777777777777777", 0.0), + ( + "1010101010101010101010101010101010101010", + 1.010_101_010_101_01e39, + ), + ( + "0.1010101010101010101010101010101010101010", + 0.101_010_101_010_101_01, + ), + ("0e1000000000000000000000000000000000000000000000", 0.0), + ( + "1000000000000000000000000000000000000000000000000000000000000\ + 000000000000000000000000000000000000000000000000000000000000\ + 000000000000000000000000000000000000000000000000000000000000\ + 000000000000000000000000000000000000000000000000000000000000\ + 000000000000000000000000000000000000000000000000000000000000\ + 00000000", + 1e308, + ), + ( + "1000000000000000000000000000000000000000000000000000000000000\ + 000000000000000000000000000000000000000000000000000000000000\ + 000000000000000000000000000000000000000000000000000000000000\ + 000000000000000000000000000000000000000000000000000000000000\ + 000000000000000000000000000000000000000000000000000000000000\ + .0e8", + 1e308, + ), + ( + "1000000000000000000000000000000000000000000000000000000000000\ + 000000000000000000000000000000000000000000000000000000000000\ + 000000000000000000000000000000000000000000000000000000000000\ + 000000000000000000000000000000000000000000000000000000000000\ + 000000000000000000000000000000000000000000000000000000000000\ + e8", + 1e308, + ), + ( + "1000000000000000000000000000000000000000000000000000000000000\ + 000000000000000000000000000000000000000000000000000000000000\ + 000000000000000000000000000000000000000000000000000000000000\ + 000000000000000000000000000000000000000000000000000000000000\ + 000000000000000000000000000000000000000000000000000000000000\ + 000000000000000000e-10", + 1e308, + ), + ], + ); +} + +fn test_value_as_f64(cx: &mut FunctionContext) { + test_parse_unusual_ok( + cx, + vec![ + ("1e1000", f64::INFINITY), // Serializes as `null` + ], + ); +} + +fn test_roundtrip_f64(cx: &mut FunctionContext) { + for &float in &[ + // Samples from quickcheck-ing roundtrip with `input: f64`. Comments + // indicate the value returned by the old deserializer. + 51.24817837550540_4, // 51.2481783755054_1 + -93.3113703768803_3, // -93.3113703768803_2 + -36.5739948427534_36, // -36.5739948427534_4 + 52.31400820410624_4, // 52.31400820410624_ + 97.4536532003468_5, // 97.4536532003468_4 + // Samples from `rng.next_u64` + `f64::from_bits` + `is_finite` filter. + 2.0030397744267762e-253, + 7.101215824554616e260, + 1.769268377902049e74, + -1.6727517818542075e58, + 3.9287532173373315e299, + ] { + let json = to_string(cx, &float).unwrap(); + let output: f64 = from_str(cx, &json).unwrap(); + assert_eq!(float, output); + } +} + +fn test_roundtrip_f32(cx: &mut FunctionContext) { + let float = 7.038531e-26; + let json = to_string(cx, &float).unwrap(); + let output: f64 = from_str(cx, &json).unwrap(); + assert_eq!(float, output); +} + +fn test_serialize_char(cx: &mut FunctionContext) { + let value = json!( + ({ + let mut map = BTreeMap::new(); + map.insert('c', ()); + map + }) + ); + + neon::serialize::(cx, &value) + .unwrap() + .get::(cx, "c") + .unwrap(); +} + +fn test_parse_number(cx: &mut FunctionContext) { + test_parse_ok( + cx, + vec![ + ("0.0", 0.0f64), + ("3.0", 3.0f64), + ("3.1", 3.1), + ("-1.2", -1.2), + ("0.4", 0.4), + ], + ); +} + +fn test_parse_string(cx: &mut FunctionContext) { + test_parse_ok( + cx, + vec![ + ("\"\"", String::new()), + ("\"foo\"", "foo".to_string()), + (" \"foo\" ", "foo".to_string()), + ("\"\\\"\"", "\"".to_string()), + ("\"\\b\"", "\x08".to_string()), + ("\"\\n\"", "\n".to_string()), + ("\"\\r\"", "\r".to_string()), + ("\"\\t\"", "\t".to_string()), + ("\"\\u12ab\"", "\u{12ab}".to_string()), + ("\"\\uAB12\"", "\u{AB12}".to_string()), + ("\"\\uD83C\\uDF95\"", "\u{1F395}".to_string()), + ], + ); +} + +fn test_parse_list(cx: &mut FunctionContext) { + test_parse_ok( + cx, + vec![ + ("[]", vec![]), + ("[ ]", vec![]), + ("[null]", vec![()]), + (" [ null ] ", vec![()]), + ], + ); + + test_parse_ok(cx, vec![("[true]", vec![true])]); + + test_parse_ok( + cx, + vec![("[3,1]", vec![3u64, 1]), (" [ 3 , 1 ] ", vec![3, 1])], + ); + + test_parse_ok(cx, vec![("[[3], [1, 2]]", vec![vec![3u64], vec![1, 2]])]); + + test_parse_ok(cx, vec![("[1]", (1u64,))]); + + test_parse_ok(cx, vec![("[1, 2]", (1u64, 2u64))]); + + test_parse_ok(cx, vec![("[1, 2, 3]", (1u64, 2u64, 3u64))]); + + test_parse_ok(cx, vec![("[1, [2, 3]]", (1u64, (2u64, 3u64)))]); +} + +fn test_parse_object(cx: &mut FunctionContext) { + test_parse_ok( + cx, + vec![ + ("{}", treemap!()), + ("{ }", treemap!()), + ("{\"a\":3}", treemap!("a".to_string() => 3u64)), + ("{ \"a\" : 3 }", treemap!("a".to_string() => 3)), + ( + "{\"a\":3,\"b\":4}", + treemap!("a".to_string() => 3, "b".to_string() => 4), + ), + ( + " { \"a\" : 3 , \"b\" : 4 } ", + treemap!("a".to_string() => 3, "b".to_string() => 4), + ), + ], + ); + + test_parse_ok( + cx, + vec![( + "{\"a\": {\"b\": 3, \"c\": 4}}", + treemap!( + "a".to_string() => treemap!( + "b".to_string() => 3u64, + "c".to_string() => 4 + ) + ), + )], + ); + + test_parse_ok(cx, vec![("{\"c\":null}", treemap!('c' => ()))]); +} + +fn test_parse_struct(cx: &mut FunctionContext) { + test_parse_ok( + cx, + vec![ + ( + "{ + \"inner\": [] + }", + Outer { inner: vec![] }, + ), + ( + "{ + \"inner\": [ + { \"a\": null, \"b\": 2, \"c\": [\"abc\", \"xyz\"] } + ] + }", + Outer { + inner: vec![Inner { + a: (), + b: 2, + c: vec!["abc".to_string(), "xyz".to_string()], + }], + }, + ), + ], + ); + + let v: Outer = from_str( + cx, + "[ + [ + [ null, 2, [\"abc\", \"xyz\"] ] + ] + ]", + ) + .unwrap(); + + assert_eq!( + v, + Outer { + inner: vec![Inner { + a: (), + b: 2, + c: vec!["abc".to_string(), "xyz".to_string()], + }], + } + ); +} + +fn test_parse_option(cx: &mut FunctionContext) { + test_parse_ok( + cx, + vec![ + ("null", None::), + ("\"jodhpurs\"", Some("jodhpurs".to_string())), + ], + ); + + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] + struct Foo { + x: Option, + } + + let value: Foo = from_str(cx, "{}").unwrap(); + assert_eq!(value, Foo { x: None }); + + test_parse_ok( + cx, + vec![ + ("{\"x\": null}", Foo { x: None }), + ("{\"x\": 5}", Foo { x: Some(5) }), + ], + ); +} + +fn test_parse_enum(cx: &mut FunctionContext) { + test_parse_ok( + cx, + vec![ + ("\"Dog\"", Animal::Dog), + (" \"Dog\" ", Animal::Dog), + ( + "{\"Frog\":[\"Henry\",[]]}", + Animal::Frog("Henry".to_string(), vec![]), + ), + ( + " { \"Frog\": [ \"Henry\" , [ 349, 102 ] ] } ", + Animal::Frog("Henry".to_string(), vec![349, 102]), + ), + ( + "{\"Cat\": {\"age\": 5, \"name\": \"Kate\"}}", + Animal::Cat { + age: 5, + name: "Kate".to_string(), + }, + ), + ( + " { \"Cat\" : { \"age\" : 5 , \"name\" : \"Kate\" } } ", + Animal::Cat { + age: 5, + name: "Kate".to_string(), + }, + ), + ( + " { \"AntHive\" : [\"Bob\", \"Stuart\"] } ", + Animal::AntHive(vec!["Bob".to_string(), "Stuart".to_string()]), + ), + ], + ); + + test_parse_unusual_ok( + cx, + vec![ + ("{\"Dog\":null}", Animal::Dog), + (" { \"Dog\" : null } ", Animal::Dog), + ], + ); + + test_parse_ok( + cx, + vec![( + concat!( + "{", + " \"a\": \"Dog\",", + " \"b\": {\"Frog\":[\"Henry\", []]}", + "}" + ), + treemap!( + "a".to_string() => Animal::Dog, + "b".to_string() => Animal::Frog("Henry".to_string(), vec![]) + ), + )], + ); +} + +fn test_missing_option_field(cx: &mut FunctionContext) { + #[derive(Debug, PartialEq, Deserialize)] + struct Foo { + x: Option, + } + + let value: Foo = from_str(cx, "{}").unwrap(); + assert_eq!(value, Foo { x: None }); + + let value: Foo = from_str(cx, "{\"x\": 5}").unwrap(); + assert_eq!(value, Foo { x: Some(5) }); +} + +fn test_missing_renamed_field(cx: &mut FunctionContext) { + #[derive(Debug, PartialEq, Deserialize)] + struct Foo { + #[serde(rename = "y")] + x: Option, + } + + let value: Foo = from_str(cx, "{}").unwrap(); + assert_eq!(value, Foo { x: None }); + + let value: Foo = from_str(cx, "{\"y\": 5}").unwrap(); + assert_eq!(value, Foo { x: Some(5) }); +} + +fn test_serialize_map_with_no_len(cx: &mut FunctionContext) { + #[derive(Clone, Debug, PartialEq)] + struct MyMap(BTreeMap); + + impl ser::Serialize for MyMap + where + K: ser::Serialize + Ord, + V: ser::Serialize, + { + #[inline] + fn serialize(&self, serializer: S) -> Result + where + S: ser::Serializer, + { + let mut map = serializer.serialize_map(None)?; + for (k, v) in &self.0 { + ser::SerializeMap::serialize_entry(&mut map, k, v)?; + } + ser::SerializeMap::end(map) + } + } + + struct Visitor { + marker: PhantomData>, + } + + impl<'de, K, V> de::Visitor<'de> for Visitor + where + K: de::Deserialize<'de> + Eq + Ord, + V: de::Deserialize<'de>, + { + type Value = MyMap; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("map") + } + + #[inline] + fn visit_unit(self) -> Result, E> + where + E: de::Error, + { + Ok(MyMap(BTreeMap::new())) + } + + #[inline] + fn visit_map(self, mut visitor: Visitor) -> Result, Visitor::Error> + where + Visitor: de::MapAccess<'de>, + { + let mut values = BTreeMap::new(); + + while let Some((key, value)) = visitor.next_entry()? { + values.insert(key, value); + } + + Ok(MyMap(values)) + } + } + + impl<'de, K, V> de::Deserialize<'de> for MyMap + where + K: de::Deserialize<'de> + Eq + Ord, + V: de::Deserialize<'de>, + { + fn deserialize(deserializer: D) -> Result, D::Error> + where + D: de::Deserializer<'de>, + { + deserializer.deserialize_map(Visitor { + marker: PhantomData, + }) + } + } + + let mut map = BTreeMap::new(); + map.insert("a".to_owned(), MyMap(BTreeMap::new())); + map.insert("b".to_owned(), MyMap(BTreeMap::new())); + let map: MyMap<_, MyMap> = MyMap(map); + + test_encode_ok(cx, &[(map, "{\"a\":{},\"b\":{}}")]); +} + +fn test_serialize_rejects_bool_keys(cx: &mut FunctionContext) { + let map = treemap!( + true => 2, + false => 4 + ); + + let r = cx.try_catch(|cx| neon::serialize::(cx, &map)); + assert!(r.is_err()); +} + +fn test_serialize_rejects_adt_keys(cx: &mut FunctionContext) { + let map = treemap!( + Some("a") => 2, + Some("b") => 4, + None => 6 + ); + + let r = cx.try_catch(|cx| neon::serialize::(cx, &map)); + assert!(r.is_err()); +} + +fn test_bytes_ser(cx: &mut FunctionContext) { + let buf = vec![]; + let bytes = Bytes::new(&buf); + let v = neon::serialize::(cx, &bytes).unwrap(); + + assert_eq!(&buf, v.as_slice(cx)); + + let buf = vec![1, 2, 3]; + let bytes = Bytes::new(&buf); + let v = neon::serialize::(cx, &bytes).unwrap(); + + assert_eq!(&buf, v.as_slice(cx)); +} + +fn test_byte_buf_ser(cx: &mut FunctionContext) { + let bytes = ByteBuf::new(); + let v = neon::serialize::(cx, &bytes).unwrap(); + + assert_eq!(&bytes, v.as_slice(cx)); + + let bytes = ByteBuf::from(vec![1, 2, 3]); + let v = neon::serialize::(cx, &bytes).unwrap(); + + assert_eq!(&bytes, v.as_slice(cx)); +} + +fn test_byte_buf_de(cx: &mut FunctionContext) { + let bytes = ByteBuf::new(); + let buf = cx.array_buffer(0).unwrap(); + let v = neon::deserialize::(cx, buf).unwrap(); + assert_eq!(v, bytes); + + let bytes = ByteBuf::from(vec![1, 2, 3]); + let buf = JsArrayBuffer::from_slice(cx, &bytes).unwrap(); + + let v = neon::deserialize::(cx, buf).unwrap(); + assert_eq!(v, bytes); +} + +fn test_array_view_de(cx: &mut FunctionContext) { + let bytes = ByteBuf::new(); + let buf = JsUint8Array::from_slice(cx, &bytes).unwrap(); + let v = neon::deserialize::(cx, buf).unwrap(); + assert_eq!(v, bytes); + + let bytes = ByteBuf::from(vec![1, 2, 3]); + let buf = JsUint8Array::from_slice(cx, &bytes).unwrap(); + + let v = neon::deserialize::(cx, buf).unwrap(); + assert_eq!(v, bytes); +} + +fn test_byte_buf_de_multiple(cx: &mut FunctionContext) { + let a = ByteBuf::from(b"ab\nc".to_vec()); + let b = ByteBuf::from(b"cd\ne".to_vec()); + let left = vec![a, b]; + let v = neon::serialize(cx, &left).unwrap(); + let right = neon::deserialize::, JsValue, _>(cx, v).unwrap(); + + assert_eq!(left, right); +} + +fn test_deny_float_key(cx: &mut FunctionContext) { + #[derive(Eq, PartialEq, Ord, PartialOrd)] + struct Float; + impl Serialize for Float { + fn serialize(&self, serializer: S) -> Result + where + S: ser::Serializer, + { + serializer.serialize_f32(1.0) + } + } + + // map with float key + let map = treemap!(Float => "x"); + let r = cx.try_catch(|cx| neon::serialize::(cx, &map)); + assert!(r.is_err()); +} + +fn test_effectively_string_keys(cx: &mut FunctionContext) { + #[derive(Eq, PartialEq, Ord, PartialOrd, Debug, Clone, Serialize, Deserialize)] + enum Enum { + One, + Two, + } + let map = treemap! { + Enum::One => 1, + Enum::Two => 2 + }; + let expected = r#"{"One":1,"Two":2}"#; + test_encode_ok(cx, &[(map.clone(), expected)]); + test_parse_ok(cx, vec![(expected, map)]); + + #[derive(Eq, PartialEq, Ord, PartialOrd, Debug, Clone, Serialize, Deserialize)] + struct Wrapper(String); + let map = treemap! { + Wrapper("zero".to_owned()) => 0, + Wrapper("one".to_owned()) => 1 + }; + let expected = r#"{"one":1,"zero":0}"#; + test_encode_ok(cx, &[(map.clone(), expected)]); + test_parse_ok(cx, vec![(expected, map)]); +} + +// Note: This is `Issue #220` in the `serde/serde-json` repository +fn issue_220(cx: &mut FunctionContext) { + #[derive(Debug, PartialEq, Eq, Deserialize, Serialize)] + enum E { + V(u8), + } + + assert_eq!(from_str::(cx, r#"{"V": 0}"#).unwrap(), E::V(0)); +} + +// Test that numeric handling is consistent across neon and serde_json implementations +fn test_numbers(cx: &mut FunctionContext) { + macro_rules! serialize { + ($typ:ty, $value:expr) => {{ + let value: $typ = $value; + let neon: Handle = neon::serialize(cx, &value).unwrap(); + let json = neon::serialize::(cx, &(value,)) + .unwrap() + .get::(cx, 0) + .unwrap(); + + assert_eq!(neon.value(cx), json.value(cx)); + }}; + } + + macro_rules! serialize_fails { + ($typ:ty, $value:expr) => {{ + let value: $typ = $value; + let neon = cx.try_catch(|cx| neon::serialize::(cx, &value)); + let json = cx.try_catch(|cx| neon::serialize::(cx, &(value,))); + + assert!(neon.is_err()); + assert!(json.is_err()); + }}; + } + + macro_rules! deserialize { + ($typ:ty, $value:expr) => {{ + let value = cx.number($value); + let arr = cx.empty_array(); + + arr.set(cx, 0, value).unwrap(); + + let neon = neon::deserialize::<$typ, _, _>(cx, value).unwrap(); + let (json,) = neon::deserialize::<($typ,), _, _>(cx, arr).unwrap(); + + assert_eq!(neon, json); + }}; + } + + macro_rules! deserialize_fails { + ($typ:ty, $value:expr) => {{ + let value = cx.number($value); + let arr = cx.empty_array(); + + arr.set(cx, 0, value).unwrap(); + + let neon = cx.try_catch(|cx| neon::deserialize::<$typ, _, _>(cx, value)); + let json = cx.try_catch(|cx| neon::deserialize::<($typ,), _, _>(cx, arr)); + + assert!(neon.is_err()); + assert!(json.is_err()); + }}; + } + + // Simple float tests + serialize!(f64, -1.1); + serialize!(f64, 0.0); + serialize!(f64, 1.1); + serialize!(f64, 0.1 * 0.2); + serialize!(f64, -0.1 * 0.2); + + // Out of range + serialize_fails!(u64, MAX_SAFE_INTEGER + 2); + serialize_fails!(i64, MAX_SAFE_INTEGER as i64 + 2); + serialize_fails!(i64, MIN_SAFE_INTEGER - 2); + serialize_fails!(u128, MAX_SAFE_INTEGER as u128 + 2); + serialize_fails!(i128, MAX_SAFE_INTEGER as i128 + 2); + serialize_fails!(i128, MIN_SAFE_INTEGER as i128 - 2); + + // Simple float tests + deserialize!(f64, -1.1); + deserialize!(f64, 0.0); + deserialize!(f64, 1.1); + deserialize!(f64, 0.1 * 0.2); + deserialize!(f64, -0.1 * 0.2); + + // Out of range + deserialize_fails!(u8, -1.0); + deserialize_fails!(u8, f64::MAX); + deserialize_fails!(u16, -1.0); + deserialize_fails!(u16, f64::MAX); + deserialize_fails!(u32, -1.0); + deserialize_fails!(u32, f64::MAX); + deserialize_fails!(u64, -1.0); + deserialize_fails!(u64, f64::MAX); + deserialize_fails!(u128, -1.0); + deserialize_fails!(u128, f64::MAX); + + deserialize_fails!(i8, f64::MIN); + deserialize_fails!(i8, f64::MAX); + deserialize_fails!(i16, f64::MIN); + deserialize_fails!(i16, f64::MAX); + deserialize_fails!(i32, f64::MIN); + deserialize_fails!(i32, f64::MAX); + deserialize_fails!(i64, f64::MIN); + deserialize_fails!(i64, f64::MAX); + deserialize_fails!(i128, f64::MIN); + deserialize_fails!(i128, f64::MAX); + + // Unexpected truncate + deserialize_fails!(u8, 1.1); + deserialize_fails!(u16, 1.1); + deserialize_fails!(u32, 1.1); + deserialize_fails!(u64, 1.1); + deserialize_fails!(u128, 1.1); + deserialize_fails!(i8, 1.1); + deserialize_fails!(i16, 1.1); + deserialize_fails!(i32, 1.1); + deserialize_fails!(i64, 1.1); + deserialize_fails!(i128, 1.1); +} + +pub fn build_suite(mut cx: FunctionContext) -> JsResult { + let o = cx.empty_object(); + + export(&mut cx, &o, test_write_null)?; + export(&mut cx, &o, test_write_u64)?; + export(&mut cx, &o, test_write_i64)?; + export(&mut cx, &o, test_write_f64)?; + export(&mut cx, &o, test_encode_nonfinite_float_yields_null)?; + export(&mut cx, &o, test_write_str)?; + export(&mut cx, &o, test_write_bool)?; + export(&mut cx, &o, test_write_char)?; + export(&mut cx, &o, test_write_list)?; + export(&mut cx, &o, test_write_object)?; + export(&mut cx, &o, test_write_tuple)?; + export(&mut cx, &o, test_write_enum)?; + export(&mut cx, &o, test_write_option)?; + export(&mut cx, &o, test_write_newtype_struct)?; + export(&mut cx, &o, test_deserialize_number_to_untagged_enum)?; + export(&mut cx, &o, test_parse_null)?; + export(&mut cx, &o, test_parse_bool)?; + export(&mut cx, &o, test_parse_char)?; + export(&mut cx, &o, test_parse_i64)?; + export(&mut cx, &o, test_parse_u64)?; + export(&mut cx, &o, test_parse_f64)?; + export(&mut cx, &o, test_value_as_f64)?; + export(&mut cx, &o, test_roundtrip_f64)?; + export(&mut cx, &o, test_roundtrip_f32)?; + export(&mut cx, &o, test_serialize_char)?; + export(&mut cx, &o, test_parse_number)?; + export(&mut cx, &o, test_parse_string)?; + export(&mut cx, &o, test_parse_list)?; + export(&mut cx, &o, test_parse_object)?; + export(&mut cx, &o, test_parse_struct)?; + export(&mut cx, &o, test_parse_option)?; + export(&mut cx, &o, test_parse_enum)?; + export(&mut cx, &o, test_missing_option_field)?; + export(&mut cx, &o, test_missing_renamed_field)?; + export(&mut cx, &o, test_serialize_map_with_no_len)?; + export(&mut cx, &o, test_serialize_rejects_bool_keys)?; + export(&mut cx, &o, test_serialize_rejects_adt_keys)?; + export(&mut cx, &o, test_bytes_ser)?; + export(&mut cx, &o, test_byte_buf_ser)?; + export(&mut cx, &o, test_byte_buf_de)?; + export(&mut cx, &o, test_array_view_de)?; + export(&mut cx, &o, test_byte_buf_de_multiple)?; + export(&mut cx, &o, test_deny_float_key)?; + export(&mut cx, &o, test_effectively_string_keys)?; + export(&mut cx, &o, issue_220)?; + export(&mut cx, &o, test_numbers)?; + + Ok(o) +} diff --git a/test/napi/src/lib.rs b/test/napi/src/lib.rs index c2bb89204..30a603e2d 100644 --- a/test/napi/src/lib.rs +++ b/test/napi/src/lib.rs @@ -15,6 +15,7 @@ mod js { pub mod futures; pub mod numbers; pub mod objects; + pub mod serde; pub mod strings; pub mod threads; pub mod typedarrays; @@ -319,6 +320,7 @@ fn main(mut cx: ModuleContext) -> NeonResult<()> { cx.export_function("call_and_catch", call_and_catch)?; cx.export_function("get_number_or_default", get_number_or_default)?; cx.export_function("is_construct", is_construct)?; + cx.export_function("deserialize_greet", deserialize_greet)?; cx.export_function("caller_with_drop_callback", caller_with_drop_callback)?; cx.export_function("count_called", { @@ -393,5 +395,8 @@ fn main(mut cx: ModuleContext) -> NeonResult<()> { cx.export_function("lazy_async_add", js::futures::lazy_async_add)?; cx.export_function("lazy_async_sum", js::futures::lazy_async_sum)?; + // Serde + cx.export_function("build_serde_test_suite", js::serde::build_suite)?; + Ok(()) }