diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 521bec2d..b11a57c1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,7 +33,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: test - args: --verbose --workspace + args: --verbose --workspace --all-features - name: Rustfmt Check uses: actions-rs/cargo@v1 with: diff --git a/Cargo.lock b/Cargo.lock index bf195eb5..44178d38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "aho-corasick" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -38,7 +53,7 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c98233c6673d8601ab23e77eb38f999c51100d46c5703b17288c57fddf3a1ffe" dependencies = [ - "bstr", + "bstr 0.2.17", "doc-comment", "predicates", "predicates-core", @@ -92,6 +107,15 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bstr" version = "0.2.17" @@ -103,6 +127,16 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "bstr" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a246e68bb43f6cd9db24bea052a53e40405417c5fb372e3d1a8a7f770a564ef5" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "bumpalo" version = "3.11.0" @@ -139,19 +173,40 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.22" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" dependencies = [ + "android-tzdata", "iana-time-zone", "js-sys", - "num-integer", "num-traits", - "time", "wasm-bindgen", "winapi", ] +[[package]] +name = "chrono-tz" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58549f1842da3080ce63002102d5bc954c7bc843d4f47818e642abdc36253552" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db058d493fb2f65f41861bfed7e3fe6335264a9f0f92710cab5bdf01fef09069" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + [[package]] name = "clap" version = "4.0.18" @@ -196,12 +251,46 @@ dependencies = [ "winapi", ] +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation-sys" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +[[package]] +name = "cpufeatures" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e69e28e9f7f77debdedbaafa2866e1de9ba56df55a8bd7cfc724c25a09987c" +dependencies = [ + "libc", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[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 = "cxx" version = "1.0.78" @@ -257,12 +346,28 @@ dependencies = [ "syn 1.0.102", ] +[[package]] +name = "deunicode" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "850878694b7933ca4c9569d30a34b55031b9b139ee1fc7b94a527c4ef960d690" + [[package]] name = "difflib" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "doc-comment" version = "0.3.3" @@ -305,6 +410,12 @@ dependencies = [ "instant", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "futures" version = "0.3.28" @@ -361,7 +472,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.18", + "syn 2.0.39", ] [[package]] @@ -400,6 +511,61 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "globset" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc" +dependencies = [ + "aho-corasick 0.7.19", + "bstr 1.5.0", + "fnv", + "log", + "regex", +] + +[[package]] +name = "globwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +dependencies = [ + "bitflags", + "ignore", + "walkdir", +] + +[[package]] +name = "half" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc52e53916c08643f1b56ec082790d1e86a32e58dc5268f897f313fbae7b4872" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -421,6 +587,15 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + [[package]] name = "iana-time-zone" version = "0.1.51" @@ -445,6 +620,23 @@ dependencies = [ "cxx-build", ] +[[package]] +name = "ignore" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492" +dependencies = [ + "globset", + "lazy_static", + "log", + "memchr", + "regex", + "same-file", + "thread_local", + "walkdir", + "winapi-util", +] + [[package]] name = "indexmap" version = "1.9.2" @@ -484,31 +676,16 @@ dependencies = [ "base64 0.21.2", "clap", "colored", - "ion-rs 0.18.1", + "convert_case", + "ion-rs", "ion-schema", + "matches", "memmap", "rstest", + "serde", "serde_json", "tempfile", -] - -[[package]] -name = "ion-rs" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe8594aa5cfda30225ba647805592f5b155a5f63c5c815491cf57e61c64ee589" -dependencies = [ - "arrayvec", - "base64 0.12.3", - "bigdecimal", - "bytes", - "chrono", - "delegate", - "nom", - "num-bigint 0.4.3", - "num-integer", - "num-traits", - "smallvec", + "tera", "thiserror", ] @@ -534,12 +711,12 @@ dependencies = [ [[package]] name = "ion-schema" -version = "0.7.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67b8eded634bb9e026d0f1402e7faa387d98dac8e2977209295fa6309fa7b20" +checksum = "3c270634d42a3369c81e1f04087a62809407db9722667a484786b3844365204f" dependencies = [ - "chrono", - "ion-rs 0.17.0", + "half", + "ion-rs", "num-bigint 0.3.3", "num-traits", "regex", @@ -591,6 +768,12 @@ version = "0.2.146" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f92be4933c13fd498862a9e02a3055f8a8d9c039ce33db97306fd5a6caa7f29b" +[[package]] +name = "libm" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" + [[package]] name = "link-cplusplus" version = "1.0.7" @@ -615,6 +798,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + [[package]] name = "memchr" version = "2.5.0" @@ -700,6 +889,104 @@ version = "6.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3baf96e39c5359d2eb0dd6ccb42c62b91d9678aa68160d261b9e0ccbf9e9dea9" +[[package]] +name = "parse-zoneinfo" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" +dependencies = [ + "regex", +] + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "pest" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16833386b02953ca926d19f64af613b9bf742c48dcd5e09b32fbfc9740bf84e2" +dependencies = [ + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7763190f9406839f99e5197afee8c9e759969f7dbfa40ad3b8dbee8757b745b5" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "249061b22e99973da1f5f5f1410284419e283bb60b79255bf5f42a94b66a2e00" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "pest_meta" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "457c310cfc9cf3f22bc58901cc7f0d3410ac5d6298e432a4f9a6138565cb6df6" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", + "uncased", +] + [[package]] name = "pin-project-lite" version = "0.2.9" @@ -712,6 +999,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "predicates" version = "2.1.1" @@ -741,22 +1034,52 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.60" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.28" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "redox_syscall" version = "0.3.5" @@ -768,11 +1091,11 @@ dependencies = [ [[package]] name = "regex" -version = "1.6.0" +version = "1.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" dependencies = [ - "aho-corasick", + "aho-corasick 1.0.2", "memchr", "regex-syntax", ] @@ -785,9 +1108,9 @@ checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" [[package]] name = "regex-syntax" -version = "0.6.27" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" +checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" [[package]] name = "rstest" @@ -844,6 +1167,15 @@ version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +[[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 = "scratch" version = "1.0.2" @@ -858,9 +1190,23 @@ checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" [[package]] name = "serde" -version = "1.0.154" +version = "1.0.164" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cdd151213925e7f1ab45a9bbfb129316bd00799784b174b7cc7bcd16961c49e" +checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] [[package]] name = "serde_json" @@ -874,6 +1220,23 @@ dependencies = [ "serde", ] +[[package]] +name = "sha2" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "siphasher" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" + [[package]] name = "slab" version = "0.4.8" @@ -883,6 +1246,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "slug" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3bc762e6a4b6c6fcaade73e77f9ebc6991b676f88bb2358bddb56560f073373" +dependencies = [ + "deunicode", +] + [[package]] name = "smallvec" version = "1.10.0" @@ -908,9 +1280,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.18" +version = "2.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" dependencies = [ "proc-macro2", "quote", @@ -930,6 +1302,29 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "tera" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ab29bb4f3e256ae6ad5c3e2775aa1f8829f2c0c101fc407bfd3a6df15c60c5" +dependencies = [ + "chrono", + "chrono-tz", + "globwalk", + "humansize", + "lazy_static", + "percent-encoding", + "pest", + "pest_derive", + "rand", + "regex", + "serde", + "serde_json", + "slug", + "thread_local", + "unic-segment", +] + [[package]] name = "termcolor" version = "1.1.3" @@ -947,33 +1342,102 @@ checksum = "507e9898683b6c43a9aa55b64259b721b52ba226e0f3779137e50ad114a4c90b" [[package]] name = "thiserror" -version = "1.0.37" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.37" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", - "syn 1.0.102", + "syn 2.0.39", ] [[package]] -name = "time" -version = "0.1.44" +name = "thread_local" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" dependencies = [ - "libc", - "wasi", - "winapi", + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "ucd-trie" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" + +[[package]] +name = "uncased" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b9bc53168a4be7402ab86c3aad243a84dd7381d09be0eddc81280c1da95ca68" +dependencies = [ + "version_check", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" +dependencies = [ + "unic-ucd-segment", +] + +[[package]] +name = "unic-ucd-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", ] [[package]] @@ -982,12 +1446,24 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + [[package]] name = "unicode-width" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + [[package]] name = "wait-timeout" version = "0.2.0" @@ -997,11 +1473,21 @@ dependencies = [ "libc", ] +[[package]] +name = "walkdir" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" +version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" diff --git a/Cargo.toml b/Cargo.toml index 065febd2..a4b14733 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,15 +18,24 @@ colored = "2.0.0" ion-rs = "0.18.1" memmap = "0.7.0" tempfile = "3.2.0" -ion-schema = "0.7.0" +ion-schema = "0.10.0" +serde = { version = "1.0.163", features = ["derive"] } serde_json = { version = "1.0.81", features = [ "arbitrary_precision", "preserve_order" ] } base64 = "0.21.1" +tera = { version = "1.18.1", optional = true } +convert_case = { version = "0.6.0", optional = true } +matches = "0.1.10" +thiserror = "1.0.50" [dev-dependencies] rstest = "~0.17.0" assert_cmd = "~1.0.5" tempfile = "~3.5.0" +[features] +default = [] +beta-subcommands = ["dep:tera", "dep:convert_case"] + [[bin]] name = "ion" test = true diff --git a/src/bin/ion/commands/beta/generate/context.rs b/src/bin/ion/commands/beta/generate/context.rs new file mode 100644 index 00000000..97fd7e18 --- /dev/null +++ b/src/bin/ion/commands/beta/generate/context.rs @@ -0,0 +1,43 @@ +use serde::Serialize; +use std::fmt::{Display, Formatter}; + +/// Represents a context that will be used for code generation +pub struct CodeGenContext { + // Initially the data_model field is set to None. + // Once an ISL type definition is mapped to a data model this will have Some value. + pub(crate) data_model: Option, +} + +impl CodeGenContext { + pub fn new() -> Self { + Self { data_model: None } + } + + pub fn with_data_model(&mut self, data_model: DataModel) { + self.data_model = Some(data_model); + } +} + +/// Represents a data model type that can be used to determine which templates can be used for code generation. +#[derive(Debug, Clone, PartialEq, Serialize)] +pub enum DataModel { + Value, // a struct with a scalar value (used for `type` constraint) + // TODO: Make Sequence parameterized over data type. + // add a data type for sequence here that can be used to read elements for that data type. + Sequence, // a struct with a sequence/collection value (used for `element` constraint) + Struct, +} + +impl Display for DataModel { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + DataModel::Value => "single value struct", + DataModel::Sequence => "sequence value struct", + DataModel::Struct => "struct", + } + ) + } +} diff --git a/src/bin/ion/commands/beta/generate/generator.rs b/src/bin/ion/commands/beta/generate/generator.rs new file mode 100644 index 00000000..183c9f2a --- /dev/null +++ b/src/bin/ion/commands/beta/generate/generator.rs @@ -0,0 +1,457 @@ +use crate::commands::beta::generate::context::{CodeGenContext, DataModel}; +use crate::commands::beta::generate::result::{invalid_data_model_error, CodeGenResult}; +use crate::commands::beta::generate::utils::{Field, Import, Language}; +use crate::commands::beta::generate::utils::{IonSchemaType, Template}; +use convert_case::{Case, Casing}; +use ion_schema::isl::isl_constraint::{IslConstraint, IslConstraintValue}; +use ion_schema::isl::isl_type::IslType; +use ion_schema::isl::isl_type_reference::IslTypeRef; +use ion_schema::isl::IslSchema; +use std::collections::HashMap; +use std::fs::File; +use std::io::Write; +use std::path::Path; +use tera::{Context, Tera}; + +// TODO: generator can store language and output path as it doesn't change during code generation process +pub(crate) struct CodeGenerator<'a> { + // Represents the templating engine - tera + // more information: https://docs.rs/tera/latest/tera/ + pub(crate) tera: Tera, + language: Language, + output: &'a Path, + // Represents a counter for naming anonymous type definitions + pub(crate) anonymous_type_counter: usize, +} + +impl<'a> CodeGenerator<'a> { + pub fn new(language: Language, output: &'a Path) -> Self { + Self { + language, + output, + anonymous_type_counter: 0, + tera: Tera::new("src/bin/ion/commands/beta/generate/templates/**/*.templ").unwrap(), + } + } + + /// Returns true if its a built in type otherwise returns false + pub fn is_built_in_type(&self, name: &str) -> bool { + match self.language { + Language::Rust => { + matches!(name, "i64" | "String" | "bool" | "Vec" | "f64") + } + Language::Java => { + matches!(name, "int" | "String" | "boolean" | "byte[]" | "float") + } + } + } + + /// Represents a [tera] filter that converts given tera string value to [upper camel case]. + /// Returns error if the given value is not a string. + /// + /// For more information: + /// + /// [tera]: + /// [upper camel case]: + pub fn upper_camel( + value: &tera::Value, + _map: &HashMap, + ) -> Result { + Ok(tera::Value::String( + value + .as_str() + .ok_or(tera::Error::msg("Required string for this filter"))? + .to_case(Case::UpperCamel), + )) + } + + /// Generates code for given Ion Schema + pub fn generate(&mut self, schema: IslSchema) -> CodeGenResult<()> { + // this will be used for Rust to create mod.rs which lists all the generated modules + let mut modules = vec![]; + let mut module_context = tera::Context::new(); + + // Register a tera filter that can be used to convert a string to upper camel case + self.tera.register_filter("upper_camel", Self::upper_camel); + + for isl_type in schema.types() { + self.generate_data_model(&mut modules, isl_type)?; + } + + if self.language == Language::Rust { + module_context.insert("modules", &modules); + let rendered = self.tera.render("rust/mod.templ", &module_context)?; + let mut file = File::create(self.output.join("mod.rs"))?; + file.write_all(rendered.as_bytes())?; + } + + Ok(()) + } + + /// Generates data model based on given ISL type definition + fn generate_data_model( + &mut self, + modules: &mut Vec, + isl_type: &IslType, + ) -> CodeGenResult<()> { + let data_model_name = match isl_type.name().clone() { + None => { + format!("AnonymousType{}", self.anonymous_type_counter) + } + Some(name) => name, + }; + + let mut context = Context::new(); + let mut tera_fields = vec![]; + let mut imports: Vec = vec![]; + let mut code_gen_context = CodeGenContext::new(); + + // Set the target kind name of the data model (i.e. enum/class) + context.insert( + "target_kind_name", + &data_model_name.to_case(Case::UpperCamel), + ); + + let constraints = isl_type.constraints(); + for constraint in constraints { + self.map_constraint_to_data_model( + modules, + &mut tera_fields, + &mut imports, + constraint, + &mut code_gen_context, + )?; + } + + // add imports for the template + context.insert("imports", &imports); + + // generate read and write APIs for the data model + self.generate_read_api(&mut context, &mut tera_fields, &mut code_gen_context)?; + self.generate_write_api(&mut context, &mut tera_fields, &mut code_gen_context); + modules.push(self.language.file_name(&data_model_name)); + + // Render or generate file for the template with the given context + let template: &Template = &code_gen_context.data_model.as_ref().try_into()?; + let rendered = self + .tera + .render( + &format!("{}/{}.templ", &self.language, template.name(&self.language)), + &context, + ) + .unwrap(); + let mut file = File::create(self.output.join(format!( + "{}.{}", + self.language.file_name(&data_model_name), + self.language.file_extension() + )))?; + file.write_all(rendered.as_bytes())?; + Ok(()) + } + + /// Maps the given constraint value to a data model + fn map_constraint_to_data_model( + &mut self, + modules: &mut Vec, + tera_fields: &mut Vec, + imports: &mut Vec, + constraint: &IslConstraint, + code_gen_context: &mut CodeGenContext, + ) -> CodeGenResult<()> { + match constraint.constraint() { + IslConstraintValue::Element(isl_type, _) => { + self.verify_data_model_consistency(DataModel::Sequence, code_gen_context)?; + self.generate_struct_field( + tera_fields, + isl_type, + modules, + "value", + imports, + code_gen_context, + )?; + } + IslConstraintValue::Fields(fields, _content_closed) => { + self.verify_data_model_consistency(DataModel::Struct, code_gen_context)?; + for (name, value) in fields.iter() { + self.generate_struct_field( + tera_fields, + value.type_reference(), + modules, + name, + imports, + code_gen_context, + )?; + } + } + IslConstraintValue::Type(isl_type) => { + self.verify_data_model_consistency(DataModel::Value, code_gen_context)?; + self.generate_struct_field( + tera_fields, + isl_type, + modules, + "value", + imports, + code_gen_context, + )?; + } + _ => {} + } + Ok(()) + } + + /// Verify that the current data model is same as previously determined data model + /// This is referring to data model determined with each constraint that is verifies + /// that all the constraints map to a single data model and not different data models. + /// e.g. + /// ``` + /// type::{ + /// name: foo, + /// type: string, + /// fields:{ + /// source: String, + /// destination: String + /// } + /// } + /// ``` + /// For the above schema, both `fields` and `type` constraints map to different data models + /// respectively Struct(with given fields `source` and `destination`) and Value(with a single field that has String data type). + fn verify_data_model_consistency( + &mut self, + current_data_model: DataModel, + code_gen_context: &mut CodeGenContext, + ) -> CodeGenResult<()> { + if let Some(data_model) = &code_gen_context.data_model { + if data_model != ¤t_data_model { + return invalid_data_model_error(format!("Can not determine abstract data type as current constraint {} conflicts with prior constraints for {}.", current_data_model, data_model)); + } + } else { + code_gen_context.with_data_model(current_data_model); + } + Ok(()) + } + + /// Generates a struct field based on field name and value(data type) + fn generate_struct_field( + &mut self, + tera_fields: &mut Vec, + isl_type_ref: &IslTypeRef, + modules: &mut Vec, + field_name: &str, + imports: &mut Vec, + code_gen_context: &mut CodeGenContext, + ) -> CodeGenResult<()> { + let value = self.generate_field_value(isl_type_ref, modules, imports, code_gen_context)?; + + tera_fields.push(Field { + name: { + match self.language { + Language::Rust => field_name.to_case(Case::Snake), + Language::Java => field_name.to_case(Case::Camel), + } + }, + value, + }); + Ok(()) + } + + /// Generates field value in a struct which represents a data type in codegen's programming language + fn generate_field_value( + &mut self, + isl_type_ref: &IslTypeRef, + modules: &mut Vec, + imports: &mut Vec, + code_gen_context: &mut CodeGenContext, + ) -> CodeGenResult { + Ok(match isl_type_ref { + IslTypeRef::Named(name, _) => { + if !self.is_built_in_type(name) { + imports.push(Import { + module_name: name.to_case(Case::Snake), + type_name: name.to_case(Case::UpperCamel), + }); + } + let schema_type: IonSchemaType = name.into(); + self.generate_sequence_field_value( + schema_type.target_type(&self.language).to_string(), + code_gen_context, + ) + } + IslTypeRef::TypeImport(_, _) => { + unimplemented!("Imports in schema are not supported yet!"); + } + IslTypeRef::Anonymous(type_def, _) => { + self.anonymous_type_counter += 1; + self.generate_data_model(modules, type_def)?; + let name = format!("AnonymousType{}", self.anonymous_type_counter); + imports.push(Import { + module_name: name.to_case(Case::Snake), + type_name: name.to_case(Case::UpperCamel), + }); + self.generate_sequence_field_value(name, code_gen_context) + } + }) + } + + /// Generates an appropriately typed sequence in the target programming language to use as a field value + pub fn generate_sequence_field_value( + &mut self, + name: String, + code_gen_context: &mut CodeGenContext, + ) -> String { + if code_gen_context.data_model == Some(DataModel::Sequence) { + return match self.language { + Language::Rust => { + format!("Vec<{}>", name) + } + Language::Java => { + format!("ArrayList<{}>", name) + } + }; + } + name + } + + /// Generates Generates a read API for an abstract data type. + /// This adds statements for reading each the Ion value(s) that collectively represent the given abstract data type. + // TODO: add support for Java + fn generate_read_api( + &mut self, + context: &mut Context, + tera_fields: &mut Vec, + code_gen_context: &mut CodeGenContext, + ) -> CodeGenResult<()> { + let mut read_statements = vec![]; + + if code_gen_context.data_model == Some(DataModel::Struct) + || code_gen_context.data_model == Some(DataModel::Value) + || code_gen_context.data_model == Some(DataModel::Sequence) + { + context.insert("fields", &tera_fields); + if let Some(data_model) = &code_gen_context.data_model { + context.insert("data_model", data_model); + } else { + return invalid_data_model_error( + "Can not determine data model, constraints are mapping not mapping to a data model.", + ); + } + + for tera_field in tera_fields { + if !self.is_built_in_type(&tera_field.value) { + if code_gen_context.data_model == Some(DataModel::Sequence) { + read_statements.push(format!( + "\"{}\" => {{ data_model.{} =", + &tera_field.name, &tera_field.name, + )); + read_statements.push( + r#"{ + let mut values = vec![]; + reader.step_in()?; + while reader.next()? != StreamItem::Nothing {"# + .to_string(), + ); + let sequence_type = &tera_field.value.replace("Vec<", "").replace('>', ""); + if !self.is_built_in_type(sequence_type) { + read_statements.push(format!( + "values.push({}::read_from(reader)?)", + sequence_type + )); + } else { + read_statements.push(format!( + "values.push(reader.read_{}()?)", + sequence_type.to_lowercase() + )); + } + + read_statements.push( + r#"} + values }}"# + .to_string(), + ); + } else if code_gen_context.data_model == Some(DataModel::Value) { + context.insert( + "read_statement", + &format!("{}::read_from(reader)?", &tera_field.value,), + ); + } else { + read_statements.push(format!( + "\"{}\" => {{ data_model.{} = {}::read_from(reader)?;}}", + &tera_field.name, &tera_field.name, &tera_field.value, + )); + } + } else { + if code_gen_context.data_model == Some(DataModel::Value) { + context.insert( + "read_statement", + &format!("reader.read_{}()?", &tera_field.value.to_lowercase(),), + ); + } + read_statements.push(format!( + "\"{}\" => {{ data_model.{} = reader.read_{}()?;}}", + &tera_field.name, + &tera_field.name, + &tera_field.value.to_lowercase() + )); + } + } + } + context.insert("statements", &read_statements); + Ok(()) + } + + /// Generates write API for a data model + /// This adds statements for writing data model as Ion value that will be used by data model templates + // TODO: add support for Java + fn generate_write_api( + &mut self, + context: &mut Context, + tera_fields: &mut Vec, + code_gen_context: &mut CodeGenContext, + ) { + let mut write_statements = Vec::new(); + if code_gen_context.data_model == Some(DataModel::Value) { + for tera_field in tera_fields { + if !self.is_built_in_type(&tera_field.value) { + write_statements.push(format!("self.{}.write_to(writer)?;", &tera_field.name,)); + } else { + write_statements.push(format!( + "writer.write_{}(self.value)?;", + &tera_field.value.to_lowercase(), + )); + } + } + } else if code_gen_context.data_model == Some(DataModel::Struct) { + write_statements.push("writer.step_in(IonType::Struct)?;".to_string()); + for tera_field in tera_fields { + write_statements.push(format!("writer.set_field_name(\"{}\");", &tera_field.name)); + + if !self.is_built_in_type(&tera_field.value) { + write_statements.push(format!("self.{}.write_to(writer)?;", &tera_field.name,)); + } else { + write_statements.push(format!( + "writer.write_{}(self.{})?;", + &tera_field.value.to_lowercase(), + &tera_field.name + )); + } + } + write_statements.push("writer.step_out()?;".to_string()); + } else if code_gen_context.data_model == Some(DataModel::Sequence) { + write_statements.push("writer.step_in(IonType::List)?;".to_string()); + for tera_field in tera_fields { + let sequence_type = &tera_field.value.replace("Vec<", "").replace('>', ""); + write_statements.push("for value in self.value {".to_string()); + if !self.is_built_in_type(sequence_type) { + write_statements.push("value.write_to(writer)?;".to_string()); + } else { + write_statements.push(format!( + "writer.write_{}(value)?;", + &sequence_type.to_lowercase(), + )); + } + write_statements.push("}".to_string()); + } + write_statements.push("writer.step_out()?;".to_string()); + } + context.insert("write_statements", &write_statements); + } +} diff --git a/src/bin/ion/commands/beta/generate/mod.rs b/src/bin/ion/commands/beta/generate/mod.rs new file mode 100644 index 00000000..0abb0e78 --- /dev/null +++ b/src/bin/ion/commands/beta/generate/mod.rs @@ -0,0 +1,111 @@ +mod context; +mod generator; +mod result; +mod utils; + +use crate::commands::beta::generate::generator::CodeGenerator; +use crate::commands::beta::generate::utils::Language; +use crate::commands::IonCliCommand; +use anyhow::Result; +use clap::{Arg, ArgAction, ArgMatches, Command}; +use ion_schema::authority::{DocumentAuthority, FileSystemDocumentAuthority}; +use ion_schema::system::SchemaSystem; +use std::fs; +use std::path::{Path, PathBuf}; + +pub struct GenerateCommand; + +impl IonCliCommand for GenerateCommand { + fn name(&self) -> &'static str { + "generate" + } + + fn about(&self) -> &'static str { + "Generates code using given schema file." + } + + fn configure_args(&self, command: Command) -> Command { + command + .arg( + Arg::new("output") + .long("output") + .short('o') + .help("Output directory [default: current directory]"), + ) + .arg( + Arg::new("schema") + .long("schema") + .required(true) + .short('s') + .help("Schema file"), + ) + .arg( + Arg::new("language") + .long("language") + .short('l') + .required(true) + .value_parser(["java", "rust"]) + .help("Programming language for the generated code"), + ) + .arg( + // Directory(s) that will be used as authority(s) for schema system + Arg::new("directory") + .long("directory") + .short('d') + // If this appears more than once, collect all values + .action(ArgAction::Append) + .value_name("DIRECTORY") + .required(true) + .help("One or more directories that will be searched for the requested schema"), + ) + } + + fn run(&self, _command_path: &mut Vec, args: &ArgMatches) -> Result<()> { + // Extract programming language for code generation + let language: Language = args.get_one::("language").unwrap().as_str().into(); + + // Extract output path information where the generated code will be saved + // Create a module `ion_data_model` for storing all the generated code in the output directory + let binding = match args.get_one::("output") { + Some(output_path) => PathBuf::from(output_path).join("ion_data_model"), + None => PathBuf::from("./ion_data_model"), + }; + + let output = binding.as_path(); + + // Extract the user provided document authorities/ directories + let authorities: Vec<&String> = args.get_many("directory").unwrap().collect(); + + // Extract schema file provided by user + let schema_id = args.get_one::("schema").unwrap(); + + // Set up document authorities vector + let mut document_authorities: Vec> = vec![]; + + for authority in authorities { + document_authorities.push(Box::new(FileSystemDocumentAuthority::new(Path::new( + authority, + )))) + } + + // Create a new schema system from given document authorities + let mut schema_system = SchemaSystem::new(document_authorities); + + let schema = schema_system.load_isl_schema(schema_id).unwrap(); + + // clean the target output directory if it already exists, before generating new code + if output.exists() { + fs::remove_dir_all(output).unwrap(); + } + fs::create_dir_all(output).unwrap(); + + println!("Started generating code..."); + + // generate code based on schema and programming language + CodeGenerator::new(language, output).generate(schema)?; + + println!("Code generation complete successfully!"); + println!("Path to generated code: {}", output.display()); + Ok(()) + } +} diff --git a/src/bin/ion/commands/beta/generate/result.rs b/src/bin/ion/commands/beta/generate/result.rs new file mode 100644 index 00000000..5c338103 --- /dev/null +++ b/src/bin/ion/commands/beta/generate/result.rs @@ -0,0 +1,35 @@ +use ion_schema::result::IonSchemaError; +use thiserror::Error; + +/// Represents code generation result +pub type CodeGenResult = Result; + +/// Represents an error found during code generation +#[derive(Debug, Error)] +pub enum CodeGenError { + #[error("{source:?}")] + IonSchemaError { + #[from] + source: IonSchemaError, + }, + #[error("{source:?}")] + IoError { + #[from] + source: std::io::Error, + }, + #[error("{source:?}")] + TeraError { + #[from] + source: tera::Error, + }, + #[error("{description}")] + InvalidDataModel { description: String }, +} + +/// A convenience method for creating an CodeGen containing an CodeGenError::InvalidDataModel +/// with the provided description text. +pub fn invalid_data_model_error>(description: S) -> CodeGenResult { + Err(CodeGenError::InvalidDataModel { + description: description.as_ref().to_string(), + }) +} diff --git a/src/bin/ion/commands/beta/generate/templates/java/class.templ b/src/bin/ion/commands/beta/generate/templates/java/class.templ new file mode 100644 index 00000000..588bf80b --- /dev/null +++ b/src/bin/ion/commands/beta/generate/templates/java/class.templ @@ -0,0 +1,19 @@ +{% if import %} +import ion_data_model.{{ import_type }}; +{% endif %} +public final class {{ target_kind_name }} { +{% for field in fields -%} + private final {{ field.value }} {{ field.name }}; +{% endfor %} + + public {{ target_kind_name }}({% for field in fields -%}{{ field.value }} {{ field.name }},{% endfor %}) { + {% for field in fields -%} + this.{{ field.name }} = {{ field.name }}; + {% endfor %} + } + + {% for field in fields -%}public {{ field.value }} get{% filter upper_camel %}{{ field.name }}{% endfilter %}() { + return this.{{ field.name }}; + } + {% endfor %} +} diff --git a/src/bin/ion/commands/beta/generate/templates/rust/mod.templ b/src/bin/ion/commands/beta/generate/templates/rust/mod.templ new file mode 100644 index 00000000..0c127ee6 --- /dev/null +++ b/src/bin/ion/commands/beta/generate/templates/rust/mod.templ @@ -0,0 +1,3 @@ +{% for module in modules -%} +pub mod {{ module }}; +{% endfor %} diff --git a/src/bin/ion/commands/beta/generate/templates/rust/struct.templ b/src/bin/ion/commands/beta/generate/templates/rust/struct.templ new file mode 100644 index 00000000..e49560b6 --- /dev/null +++ b/src/bin/ion/commands/beta/generate/templates/rust/struct.templ @@ -0,0 +1,60 @@ +use ion_rs::{IonResult, IonReader, Reader, StreamItem}; +{% for import in imports %} +use crate::ion_data_model::{{ import.module_name }}::{{ import.type_name }}; +{% endfor %} + +#[derive(Debug, Clone, Default)] +pub struct {{ target_kind_name }} { +{% for field in fields -%} + {{ field.name | indent(first = true) }}: {{ field.value }}, +{% endfor %} +} + +impl {{ target_kind_name }} { + pub fn new({% for field in fields -%}{{ field.name }}: {{ field.value }},{% endfor %}) -> Self { + Self { + {% for field in fields -%} + {{ field.name }}, + {% endfor %} + } + } + + + {% for field in fields -%}pub fn {{ field.name }}(&self) -> &{{ field.value }} { + &self.{{ field.name }} + } + {% endfor %} + + {% if statements %} + pub fn read_from(reader: &mut Reader) -> IonResult { + reader.next()?; + {% if data_model == "UnitStruct"%} + let mut data_model = {{ target_kind_name }}::default(); + data_model.value = {{ read_statement }}; + {% else %} + {% if data_model == "Struct"%}reader.step_in()?;{% endif %} + let mut data_model = {{ target_kind_name }}::default(); + while reader.next()? != StreamItem::Nothing { + if let Some(field_name) = reader.field_name()?.text() { + match field_name { + + {% for statement in statements -%} + {{ statement }} + {% endfor %} + _ => {} + } + } + } + {% if data_model == "Struct"%}reader.step_out()?;{% endif %} + {% endif %} + Ok(data_model) + } + {% endif %} + + pub fn write_to(&self, writer: &mut W) -> IonResult<()> { + {% for statement in write_statements -%} + {{ statement }} + {% endfor %} + Ok(()) + } +} diff --git a/src/bin/ion/commands/beta/generate/utils.rs b/src/bin/ion/commands/beta/generate/utils.rs new file mode 100644 index 00000000..8e53157c --- /dev/null +++ b/src/bin/ion/commands/beta/generate/utils.rs @@ -0,0 +1,177 @@ +use crate::commands::beta::generate::context::DataModel; +use crate::commands::beta::generate::result::{invalid_data_model_error, CodeGenError}; +use convert_case::{Case, Casing}; +use serde::Serialize; +use std::fmt::{Display, Formatter}; + +/// Represents a field that will be added to generated data model. +/// This will be used by the template engine to fill properties of a struct/classs. +#[derive(Serialize)] +pub struct Field { + pub(crate) name: String, + pub(crate) value: String, +} + +/// Represents an import statement in a module file. +/// This will be used by template engine to fill import statements of a type definition. +#[derive(Serialize)] +pub struct Import { + pub(crate) module_name: String, + pub(crate) type_name: String, +} + +/// Represent the programming language for code generation. +#[derive(Debug, Clone, PartialEq)] +pub enum Language { + Rust, + Java, +} + +impl Language { + pub fn file_extension(&self) -> &str { + match self { + Language::Rust => "rs", + Language::Java => "java", + } + } + + pub fn file_name(&self, name: &str) -> String { + match self { + Language::Rust => name.to_case(Case::Snake), + Language::Java => name.to_case(Case::UpperCamel), + } + } +} + +impl From<&str> for Language { + fn from(value: &str) -> Self { + match value { + "java" => Language::Java, + "rust" => Language::Rust, + _ => unreachable!("Unsupported programming language: {}, this tool only supports Java and Rust code generation.", value) + } + } +} + +impl Display for Language { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Language::Rust => { + write!(f, "rust") + } + Language::Java => { + write!(f, "java") + } + } + } +} + +/// Represents all the supported templates for code generation. +/// These templates will be used by [tera] templating engine to render the generated code with appropriate context value. +/// _Note: These template variants are based on Rust programming language. +/// [Template::name] provides an appropriate template file name based on given programming language._ +/// +/// [tera]: +pub enum Template { + Struct, // Represents a template for a Rust struct or Java class +} + +impl Template { + /// Returns a string that represent the template file name based on given programming language. + pub fn name(&self, language: &Language) -> &str { + match language { + Language::Rust => "struct", + Language::Java => "class", + } + } +} + +impl TryFrom> for Template { + type Error = CodeGenError; + + fn try_from(value: Option<&DataModel>) -> Result { + match value { + Some(DataModel::Value) | Some(DataModel::Sequence) | Some(DataModel::Struct) => { + Ok(Template::Struct) + } + None => invalid_data_model_error( + "Can not get a template without determining data model first.", + ), + } + } +} + +/// Represents an Ion schema type which could either be one of the [built-int types] or a user defined type. +/// +/// [built-in types]: `` +// TODO: Add enum variants for missing built-in ISL types. +pub enum IonSchemaType { + Int, + String, + Symbol, + Float, + Bool, + Blob, + Clob, + SchemaDefined(String), // A user defined schema type +} + +impl IonSchemaType { + /// Maps the given ISL type name to a target type + pub fn target_type(&self, language: &Language) -> &str { + use IonSchemaType::*; + use Language::*; + match (self, language) { + (Int, Rust) => "i64", + (Int, Java) => "int", + (String | Symbol, _) => "String", + (Float, Rust) => "f64", + (Float, Java) => "double", + (Bool, Rust) => "bool", + (Bool, Java) => "boolean", + (Blob | Clob, Rust) => "Vec", + (Blob | Clob, Java) => "byte[]", + (SchemaDefined(name), _) => name, + } + } +} + +impl From<&str> for IonSchemaType { + fn from(value: &str) -> Self { + use IonSchemaType::*; + match value { + "int" => Int, + "string" => String, + "symbol" => Symbol, + "float" => Float, + "bool" => Bool, + "blob" => Blob, + "clob" => Clob, + _ if &value[..1] == "$" => { + unimplemented!("Built in types with nulls are not supported yet!") + } + "number" | "text" | "lob" | "document" | "nothing" => { + unimplemented!("Complex types are not supported yet!") + } + "decimal" | "timestamp" => { + unimplemented!("Decimal, Number and Timestamp aren't support yet!") + } + "list" | "struct" | "sexp" => { + unimplemented!("Generic containers aren't supported yet!") + } + _ => SchemaDefined(value.to_case(Case::UpperCamel)), + } + } +} + +impl From for IonSchemaType { + fn from(value: String) -> Self { + value.as_str().into() + } +} + +impl From<&String> for IonSchemaType { + fn from(value: &String) -> Self { + value.as_str().into() + } +} diff --git a/src/bin/ion/commands/beta/mod.rs b/src/bin/ion/commands/beta/mod.rs index 8063a004..86b82e1f 100644 --- a/src/bin/ion/commands/beta/mod.rs +++ b/src/bin/ion/commands/beta/mod.rs @@ -1,5 +1,8 @@ pub mod count; pub mod from; + +#[cfg(feature = "beta-subcommands")] +pub mod generate; pub mod head; pub mod inspect; pub mod primitive; @@ -9,6 +12,8 @@ pub mod to; use crate::commands::beta::count::CountCommand; use crate::commands::beta::from::FromNamespace; +#[cfg(feature = "beta-subcommands")] +use crate::commands::beta::generate::GenerateCommand; use crate::commands::beta::head::HeadCommand; use crate::commands::beta::inspect::InspectCommand; use crate::commands::beta::primitive::PrimitiveCommand; @@ -38,6 +43,8 @@ impl IonCliCommand for BetaNamespace { Box::new(FromNamespace), Box::new(ToNamespace), Box::new(SymtabNamespace), + #[cfg(feature = "beta-subcommands")] + Box::new(GenerateCommand), ] } } diff --git a/src/bin/ion/commands/beta/schema/load.rs b/src/bin/ion/commands/beta/schema/load.rs index 76aeecfb..a6a50b2f 100644 --- a/src/bin/ion/commands/beta/schema/load.rs +++ b/src/bin/ion/commands/beta/schema/load.rs @@ -13,8 +13,8 @@ impl IonCliCommand for LoadCommand { } fn about(&self) -> &'static str { - r#"Loads an Ion Schema file indicated by a user-provided schema ID and outputs a result message.\ - Shows an error message if any invalid schema syntax was found during the load process."# + r"Loads an Ion Schema file indicated by a user-provided schema ID and outputs a result message.\ + Shows an error message if any invalid schema syntax was found during the load process." } fn configure_args(&self, command: Command) -> Command { diff --git a/src/bin/ion/commands/beta/schema/validate.rs b/src/bin/ion/commands/beta/schema/validate.rs index 73441052..5e35e6de 100644 --- a/src/bin/ion/commands/beta/schema/validate.rs +++ b/src/bin/ion/commands/beta/schema/validate.rs @@ -1,6 +1,7 @@ use crate::commands::IonCliCommand; use anyhow::{Context, Result}; use clap::{Arg, ArgAction, ArgMatches, Command}; +use ion_rs::element::writer::TextKind; use ion_schema::authority::{DocumentAuthority, FileSystemDocumentAuthority}; use ion_schema::external::ion_rs::element::reader::ElementReader; use ion_schema::external::ion_rs::element::writer::ElementWriter; @@ -105,7 +106,7 @@ impl IonCliCommand for ValidateCommand { // create a text writer to make the output let mut output = vec![]; - let mut writer = TextWriterBuilder::new().build(&mut output)?; + let mut writer = TextWriterBuilder::new(TextKind::Pretty).build(&mut output)?; // validate owned_elements according to type_ref for owned_element in owned_elements { @@ -140,7 +141,7 @@ impl IonCliCommand for ValidateCommand { // release of ion-rs. fn element_to_string(element: &Element) -> IonResult { let mut buffer = Vec::new(); - let mut text_writer = TextWriterBuilder::new().build(&mut buffer)?; + let mut text_writer = TextWriterBuilder::new(TextKind::Pretty).build(&mut buffer)?; text_writer.write_element(element)?; text_writer.flush()?; Ok(from_utf8(text_writer.output().as_slice()) diff --git a/tests/cli.rs b/tests/cli.rs index 6f446f18..41d5128f 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -2,6 +2,7 @@ use anyhow::Result; use assert_cmd::Command; use ion_rs::element::Element; use rstest::*; +use std::fs; use std::fs::File; use std::io::{Read, Write}; use std::time::Duration; @@ -144,6 +145,7 @@ fn run_it>( Ok(()) } +#[cfg(feature = "beta-subcommands")] #[rstest] #[case(0, "")] #[case(2, "{foo: bar, abc: [123, 456]}\n{foo: baz, abc: [42.0, 4.3e1]}")] @@ -188,3 +190,228 @@ fn test_write_all_values(#[case] number: i32, #[case] expected_output: &str) -> assert_eq!(stdout.trim_end(), expected_output); Ok(()) } + +#[cfg(feature = "beta-subcommands")] +#[rstest] +#[case( + "simple_struct", + r#" + type::{ + name: simple_struct, + fields: { + name: string, + id: int, + }, + } + "#, + &["id: i64", "name: String"], + &["pub fn name(&self) -> &String {", "pub fn id(&self) -> &i64 {"] +)] +#[case( + "value_struct", + r#" + type::{ + name: value_struct, + type: int // this will be a field in struct + } + "#, + &["value: i64"], + &["pub fn value(&self) -> &i64 {"] +)] +#[case( + "sequence_struct", + r#" + type::{ + name: sequence_struct, + element: string // this will be a sequence field in struct + } + "#, + &["value: Vec"], + &["pub fn value(&self) -> &Vec {"] +)] +#[case( + "struct_with_reference_field", + r#" + type::{ + name: struct_with_reference_field, + fields: { + reference: other_type + } + } + + type::{ + name: other_type, + type: int + } + "#, + &["reference: OtherType"], + &["pub fn reference(&self) -> &OtherType {"] +)] +#[case( + "struct_with_anonymous_type", + r#" + type::{ + name: struct_with_anonymous_type, + fields: { + anonymous_type: { type: int } + } + } + "#, + &["anonymous_type: AnonymousType1"], + &["pub fn anonymous_type(&self) -> &AnonymousType1 {"] +)] +/// Calls ion-cli beta generate with different schema file. Pass the test if the return value contains the expected properties and accessors. +fn test_code_generation_in_rust( + #[case] test_name: &str, + #[case] test_schema: &str, + #[case] expected_properties: &[&str], + #[case] expected_accessors: &[&str], +) -> Result<()> { + let mut cmd = Command::cargo_bin("ion")?; + let temp_dir = TempDir::new()?; + let input_schema_path = temp_dir.path().join("test_schema.isl"); + let mut input_schema_file = File::create(&input_schema_path)?; + input_schema_file.write(test_schema.as_bytes())?; + input_schema_file.flush()?; + cmd.args([ + "beta", + "generate", + "--schema", + "test_schema.isl", + "--output", + temp_dir.path().to_str().unwrap(), + "--language", + "rust", + "--directory", + temp_dir.path().to_str().unwrap(), + ]); + let command_assert = cmd.assert(); + let output_file_path = temp_dir + .path() + .join("ion_data_model") + .join(format!("{}.rs", test_name)); + command_assert.success(); + let contents = + fs::read_to_string(output_file_path).expect("Should have been able to read the file"); + for expected_property in expected_properties { + assert!(contents.contains(expected_property)); + } + for expected_accessor in expected_accessors { + assert!(contents.contains(expected_accessor)); + } + // verify that it generates read-write APIs + assert!(contents.contains("pub fn read_from(reader: &mut Reader) -> IonResult {")); + assert!(contents + .contains("pub fn write_to(&self, writer: &mut W) -> IonResult<()> {")); + Ok(()) +} + +#[cfg(feature = "beta-subcommands")] +#[rstest] +#[case( + "SimpleStruct", + r#" + type::{ + name: simple_struct, + fields: { + name: string, + id: int, + } + } + "#, + &["private final int id;", "private final String name;"], + &["public String getName() {", "public int getId() {"] +)] +#[case( + "ValueStruct", + r#" + type::{ + name: value_struct, + type: int // this will be a field in struct + } + "#, + &["private final int value;"], + &["public int getValue() {"] +)] +#[case( + "SequenceStruct", + r#" + type::{ + name: sequence_struct, + element: string // this will be a sequence field in struct + } + "#, + &["private final ArrayList value;"], + &["public ArrayList getValue() {"] +)] +#[case( + "StructWithReferenceField", + r#" + type::{ + name: struct_with_reference_field, + fields: { + reference: other_type + } + } + + type::{ + name: other_type, + type: int + } + "#, + &["private final OtherType reference;"], + &["public OtherType getReference() {"] +)] +#[case( + "StructWithAnonymousType", + r#" + type::{ + name: struct_with_anonymous_type, + fields: { + anonymous_type: { type: int } + } + } + "#, + &["private final AnonymousType1 anonymousType;"], + &["public AnonymousType1 getAnonymousType() {"] +)] +/// Calls ion-cli beta generate with different schema file. Pass the test if the return value contains the expected properties and accessors. +fn test_code_generation_in_java( + #[case] test_name: &str, + #[case] test_schema: &str, + #[case] expected_properties: &[&str], + #[case] expected_accessors: &[&str], +) -> Result<()> { + let mut cmd = Command::cargo_bin("ion")?; + let temp_dir = TempDir::new()?; + let input_schema_path = temp_dir.path().join("test_schema.isl"); + let mut input_schema_file = File::create(&input_schema_path)?; + input_schema_file.write(test_schema.as_bytes())?; + input_schema_file.flush()?; + cmd.args([ + "beta", + "generate", + "--schema", + "test_schema.isl", + "--output", + temp_dir.path().to_str().unwrap(), + "--language", + "java", + "--directory", + temp_dir.path().to_str().unwrap(), + ]); + let command_assert = cmd.assert(); + let output_file_path = temp_dir + .path() + .join("ion_data_model") + .join(format!("{}.java", test_name)); + command_assert.success(); + let contents = fs::read_to_string(output_file_path).expect("Can not read generated code file."); + for expected_property in expected_properties { + assert!(contents.contains(expected_property)); + } + for expected_accessor in expected_accessors { + assert!(contents.contains(expected_accessor)); + } + Ok(()) +}