├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── appveyor.yml ├── ci ├── before_deploy.ps1 ├── before_deploy.sh ├── install.sh └── script.sh ├── etc ├── example.jsd └── example.json └── src ├── compiler.rs ├── formatter.rs ├── main.rs └── parser.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | **/*.rs.bk 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Based on the "trust" template v0.1.2 2 | # https://github.com/japaric/trust/tree/v0.1.2 3 | 4 | dist: bionic 5 | language: rust 6 | services: docker 7 | sudo: required 8 | 9 | # TODO Rust builds on stable by default, this can be 10 | # overridden on a case by case basis down below. 11 | 12 | env: 13 | global: 14 | # TODO Update this to match the name of your project. 15 | - CRATE_NAME=chema 16 | 17 | matrix: 18 | # TODO These are all the build jobs. Adjust as necessary. Comment out what you 19 | # don't need 20 | include: 21 | # Android 22 | # - env: TARGET=aarch64-linux-android DISABLE_TESTS=1 23 | # - env: TARGET=arm-linux-androideabi DISABLE_TESTS=1 24 | # - env: TARGET=armv7-linux-androideabi DISABLE_TESTS=1 25 | # - env: TARGET=i686-linux-android DISABLE_TESTS=1 26 | # - env: TARGET=x86_64-linux-android DISABLE_TESTS=1 27 | 28 | # iOS 29 | # - env: TARGET=aarch64-apple-ios DISABLE_TESTS=1 30 | # os: osx 31 | # - env: TARGET=armv7-apple-ios DISABLE_TESTS=1 32 | # os: osx 33 | # - env: TARGET=armv7s-apple-ios DISABLE_TESTS=1 34 | # os: osx 35 | # - env: TARGET=i386-apple-ios DISABLE_TESTS=1 36 | # os: osx 37 | # - env: TARGET=x86_64-apple-ios DISABLE_TESTS=1 38 | # os: osx 39 | 40 | # Linux 41 | - env: TARGET=aarch64-unknown-linux-gnu 42 | - env: TARGET=arm-unknown-linux-gnueabi 43 | - env: TARGET=armv7-unknown-linux-gnueabihf 44 | - env: TARGET=i686-unknown-linux-gnu 45 | - env: TARGET=i686-unknown-linux-musl 46 | # - env: TARGET=mips-unknown-linux-gnu 47 | # - env: TARGET=mips64-unknown-linux-gnuabi64 48 | # - env: TARGET=mips64el-unknown-linux-gnuabi64 49 | # - env: TARGET=mipsel-unknown-linux-gnu 50 | # - env: TARGET=powerpc-unknown-linux-gnu 51 | # - env: TARGET=powerpc64-unknown-linux-gnu 52 | # - env: TARGET=powerpc64le-unknown-linux-gnu 53 | # - env: TARGET=s390x-unknown-linux-gnu DISABLE_TESTS=1 54 | - env: TARGET=x86_64-unknown-linux-gnu 55 | - env: TARGET=x86_64-unknown-linux-musl 56 | 57 | # OSX 58 | - env: TARGET=i686-apple-darwin 59 | os: osx 60 | - env: TARGET=x86_64-apple-darwin 61 | os: osx 62 | 63 | # *BSD 64 | - env: TARGET=i686-unknown-freebsd DISABLE_TESTS=1 65 | - env: TARGET=x86_64-unknown-freebsd DISABLE_TESTS=1 66 | - env: TARGET=x86_64-unknown-netbsd DISABLE_TESTS=1 67 | 68 | # Windows 69 | # - env: TARGET=x86_64-pc-windows-gnu 70 | 71 | # Bare metal 72 | # These targets don't support std and as such are likely not suitable for 73 | # most crates. 74 | # - env: TARGET=thumbv6m-none-eabi 75 | # - env: TARGET=thumbv7em-none-eabi 76 | # - env: TARGET=thumbv7em-none-eabihf 77 | # - env: TARGET=thumbv7m-none-eabi 78 | 79 | # Testing other channels 80 | - env: TARGET=x86_64-unknown-linux-gnu 81 | rust: nightly 82 | - env: TARGET=x86_64-apple-darwin 83 | os: osx 84 | rust: nightly 85 | 86 | before_install: 87 | - set -e 88 | - rustup self update 89 | 90 | install: 91 | - sh ci/install.sh 92 | - source ~/.cargo/env || true 93 | 94 | script: 95 | - bash ci/script.sh 96 | 97 | after_script: set +e 98 | 99 | before_deploy: 100 | - sh ci/before_deploy.sh 101 | 102 | deploy: 103 | # TODO update `api_key.secure` 104 | # - Create a `public_repo` GitHub token. Go to: https://github.com/settings/tokens/new 105 | # - Encrypt it: `travis encrypt 0123456789012345678901234567890123456789 106 | # - Paste the output down here 107 | api_key: 108 | secure: "plR86aca4DYP+oB0lKAij+wUqGefuR6Hzfk7iqCprDNLkzBLtmG12+UX0bZ/xsYXYSaLjpTNGS4ynNYNLAO1+DUvK6P/NiUGgjbbqGcolb7p/oW7Ac/YIhjmEyDdD6U488l3e/zDh7dFL0K2yZWnEohfTg43fAcsbK2ZXrRPXLBR1kjOFu5KW6WeEkyVgnbDm9w34wqjbyraLNJYZDMMw9edtwfVoVv2o0fuyOFdcFueDZ+IYfeGHmi7Z6VcuzwPiHMf0d70b5WBA2Lwcotnm6ivMBiczvaz2kKjxsZWHNK8x/EYhiYadX8X0T+d5WhS8fjnywQtApQfxITUIxUhd+tOdXXjfs7LQsouSol87gO02MvFaRqsFwiF3St2jaxnPIa7d4JYzpPoGN3TxjQwnlat8EcTVfujMkwv+Z8111Yi79xcZBa3CVzBb9RYaBn4ode/Ft9kT/tY2RZTjiEMd5q6fIRD6BbtOY8wm1KZGCxplTIUEnoD9sFn7Y5qsvHu1WxC4/o6qlLzDoJ9udM0KxFsuCg+REDp+dZ4Lx51K8ruTeKcG0B+TLZqZraztVzzeX8olLfciva4FVfEL6k1R87b4t5uHc5sKBSQO4q1wjoAMVGxRqF+dEaebF7LXO8MZF18UJecSEIBj7u2jBcbV5XovkQFITMa0jjrLcRaT3M=" 109 | file_glob: true 110 | file: $CRATE_NAME-$TRAVIS_TAG-$TARGET.* 111 | on: 112 | # TODO Here you can pick which targets will generate binary releases 113 | # In this example, there are some targets that are tested using the stable 114 | # and nightly channels. This condition makes sure there is only one release 115 | # for such targets and that's generated using the stable channel 116 | condition: $TRAVIS_RUST_VERSION = stable 117 | tags: true 118 | provider: releases 119 | skip_cleanup: true 120 | 121 | cache: cargo 122 | before_cache: 123 | # Travis can't cache files that are not readable by "others" 124 | - chmod -R a+r $HOME/.cargo 125 | 126 | branches: 127 | only: 128 | # release tags 129 | - /^v\d+\.\d+\.\d+.*$/ 130 | - master 131 | 132 | notifications: 133 | email: 134 | on_success: never 135 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.0.9 2 | ## New Feature 3 | 4 | * Improve error messages 5 | * New syntax: optional field namely `struct { field?: type }` 6 | 7 | ## Internal 8 | 9 | * update combine to 4 10 | 11 | # 0.0.8 12 | ## Bug Fix 13 | 14 | * code generation of `nullable` with reference types are fixed 15 | 16 | ## Internal 17 | 18 | * migrated code base to Rust 2018 Edition 19 | 20 | 21 | # 0.0.7 22 | ## New Feature 23 | 24 | * support `TYPE where PRED` synax 25 | + it looks like `string where 1 <= length && length <= 128 && it =~ /[a-z0-9!"#$%&'()=~|@]+/` 26 | 27 | # 0.0.6 28 | ## New Feature 29 | 30 | * support `url("schema_url")` syntax 31 | * support `--path-prefix` flag 32 | 33 | ## Internal 34 | 35 | * update dependencies 36 | 37 | # 0.0.5 38 | 39 | ??? 40 | 41 | # 0.0.4 42 | ## New Feature 43 | 44 | * support format types 45 | 46 | # 0.0.3 47 | ## New Feature 48 | 49 | * support string constant type 50 | 51 | # 0.0.2 52 | ## Bug Fix 53 | 54 | * fix a bug of multiple comment handling 55 | 56 | # 0.0.1 57 | initial release 58 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "aho-corasick" 5 | version = "0.7.15" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" 8 | dependencies = [ 9 | "memchr", 10 | ] 11 | 12 | [[package]] 13 | name = "ansi_term" 14 | version = "0.11.0" 15 | source = "registry+https://github.com/rust-lang/crates.io-index" 16 | checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" 17 | dependencies = [ 18 | "winapi", 19 | ] 20 | 21 | [[package]] 22 | name = "atty" 23 | version = "0.2.14" 24 | source = "registry+https://github.com/rust-lang/crates.io-index" 25 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 26 | dependencies = [ 27 | "hermit-abi", 28 | "libc", 29 | "winapi", 30 | ] 31 | 32 | [[package]] 33 | name = "bitflags" 34 | version = "1.2.1" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 37 | 38 | [[package]] 39 | name = "bytes" 40 | version = "0.5.6" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" 43 | 44 | [[package]] 45 | name = "cfg-if" 46 | version = "0.1.10" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 49 | 50 | [[package]] 51 | name = "chema" 52 | version = "0.0.9" 53 | dependencies = [ 54 | "combine", 55 | "env_logger", 56 | "lazy_static", 57 | "log", 58 | "regex", 59 | "serde", 60 | "serde_json", 61 | "serde_yaml", 62 | "structopt", 63 | "structopt-derive", 64 | ] 65 | 66 | [[package]] 67 | name = "clap" 68 | version = "2.33.3" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" 71 | dependencies = [ 72 | "ansi_term", 73 | "atty", 74 | "bitflags", 75 | "strsim", 76 | "textwrap", 77 | "unicode-width", 78 | "vec_map", 79 | ] 80 | 81 | [[package]] 82 | name = "combine" 83 | version = "4.3.2" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "2809f67365382d65fd2b6d9c22577231b954ed27400efeafbe687bda75abcc0b" 86 | dependencies = [ 87 | "bytes", 88 | "memchr", 89 | "pin-project-lite", 90 | ] 91 | 92 | [[package]] 93 | name = "dtoa" 94 | version = "0.4.6" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "134951f4028bdadb9b84baf4232681efbf277da25144b9b0ad65df75946c422b" 97 | 98 | [[package]] 99 | name = "env_logger" 100 | version = "0.6.2" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "aafcde04e90a5226a6443b7aabdb016ba2f8307c847d524724bd9b346dd1a2d3" 103 | dependencies = [ 104 | "atty", 105 | "humantime", 106 | "log", 107 | "regex", 108 | "termcolor", 109 | ] 110 | 111 | [[package]] 112 | name = "heck" 113 | version = "0.3.1" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" 116 | dependencies = [ 117 | "unicode-segmentation", 118 | ] 119 | 120 | [[package]] 121 | name = "hermit-abi" 122 | version = "0.1.17" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "5aca5565f760fb5b220e499d72710ed156fdb74e631659e99377d9ebfbd13ae8" 125 | dependencies = [ 126 | "libc", 127 | ] 128 | 129 | [[package]] 130 | name = "humantime" 131 | version = "1.3.0" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" 134 | dependencies = [ 135 | "quick-error", 136 | ] 137 | 138 | [[package]] 139 | name = "itoa" 140 | version = "0.4.6" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" 143 | 144 | [[package]] 145 | name = "lazy_static" 146 | version = "1.4.0" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 149 | 150 | [[package]] 151 | name = "libc" 152 | version = "0.2.80" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614" 155 | 156 | [[package]] 157 | name = "linked-hash-map" 158 | version = "0.5.3" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" 161 | 162 | [[package]] 163 | name = "log" 164 | version = "0.4.11" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" 167 | dependencies = [ 168 | "cfg-if", 169 | ] 170 | 171 | [[package]] 172 | name = "memchr" 173 | version = "2.3.4" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525" 176 | 177 | [[package]] 178 | name = "pin-project-lite" 179 | version = "0.1.11" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "c917123afa01924fc84bb20c4c03f004d9c38e5127e3c039bbf7f4b9c76a2f6b" 182 | 183 | [[package]] 184 | name = "proc-macro2" 185 | version = "0.4.30" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" 188 | dependencies = [ 189 | "unicode-xid", 190 | ] 191 | 192 | [[package]] 193 | name = "quick-error" 194 | version = "1.2.3" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" 197 | 198 | [[package]] 199 | name = "quote" 200 | version = "0.6.13" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" 203 | dependencies = [ 204 | "proc-macro2", 205 | ] 206 | 207 | [[package]] 208 | name = "regex" 209 | version = "1.4.2" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "38cf2c13ed4745de91a5eb834e11c00bcc3709e773173b2ce4c56c9fbde04b9c" 212 | dependencies = [ 213 | "aho-corasick", 214 | "memchr", 215 | "regex-syntax", 216 | "thread_local", 217 | ] 218 | 219 | [[package]] 220 | name = "regex-syntax" 221 | version = "0.6.21" 222 | source = "registry+https://github.com/rust-lang/crates.io-index" 223 | checksum = "3b181ba2dcf07aaccad5448e8ead58db5b742cf85dfe035e2227f137a539a189" 224 | 225 | [[package]] 226 | name = "ryu" 227 | version = "1.0.5" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" 230 | 231 | [[package]] 232 | name = "serde" 233 | version = "1.0.117" 234 | source = "registry+https://github.com/rust-lang/crates.io-index" 235 | checksum = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a" 236 | 237 | [[package]] 238 | name = "serde_json" 239 | version = "1.0.59" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "dcac07dbffa1c65e7f816ab9eba78eb142c6d44410f4eeba1e26e4f5dfa56b95" 242 | dependencies = [ 243 | "itoa", 244 | "ryu", 245 | "serde", 246 | ] 247 | 248 | [[package]] 249 | name = "serde_yaml" 250 | version = "0.8.14" 251 | source = "registry+https://github.com/rust-lang/crates.io-index" 252 | checksum = "f7baae0a99f1a324984bcdc5f0718384c1f69775f1c7eec8b859b71b443e3fd7" 253 | dependencies = [ 254 | "dtoa", 255 | "linked-hash-map", 256 | "serde", 257 | "yaml-rust", 258 | ] 259 | 260 | [[package]] 261 | name = "strsim" 262 | version = "0.8.0" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 265 | 266 | [[package]] 267 | name = "structopt" 268 | version = "0.2.18" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "16c2cdbf9cc375f15d1b4141bc48aeef444806655cd0e904207edc8d68d86ed7" 271 | dependencies = [ 272 | "clap", 273 | "structopt-derive", 274 | ] 275 | 276 | [[package]] 277 | name = "structopt-derive" 278 | version = "0.2.18" 279 | source = "registry+https://github.com/rust-lang/crates.io-index" 280 | checksum = "53010261a84b37689f9ed7d395165029f9cc7abb9f56bbfe86bee2597ed25107" 281 | dependencies = [ 282 | "heck", 283 | "proc-macro2", 284 | "quote", 285 | "syn", 286 | ] 287 | 288 | [[package]] 289 | name = "syn" 290 | version = "0.15.44" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" 293 | dependencies = [ 294 | "proc-macro2", 295 | "quote", 296 | "unicode-xid", 297 | ] 298 | 299 | [[package]] 300 | name = "termcolor" 301 | version = "1.1.0" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" 304 | dependencies = [ 305 | "winapi-util", 306 | ] 307 | 308 | [[package]] 309 | name = "textwrap" 310 | version = "0.11.0" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 313 | dependencies = [ 314 | "unicode-width", 315 | ] 316 | 317 | [[package]] 318 | name = "thread_local" 319 | version = "1.0.1" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" 322 | dependencies = [ 323 | "lazy_static", 324 | ] 325 | 326 | [[package]] 327 | name = "unicode-segmentation" 328 | version = "1.6.0" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" 331 | 332 | [[package]] 333 | name = "unicode-width" 334 | version = "0.1.8" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" 337 | 338 | [[package]] 339 | name = "unicode-xid" 340 | version = "0.1.0" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" 343 | 344 | [[package]] 345 | name = "vec_map" 346 | version = "0.8.2" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 349 | 350 | [[package]] 351 | name = "winapi" 352 | version = "0.3.9" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 355 | dependencies = [ 356 | "winapi-i686-pc-windows-gnu", 357 | "winapi-x86_64-pc-windows-gnu", 358 | ] 359 | 360 | [[package]] 361 | name = "winapi-i686-pc-windows-gnu" 362 | version = "0.4.0" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 365 | 366 | [[package]] 367 | name = "winapi-util" 368 | version = "0.1.5" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 371 | dependencies = [ 372 | "winapi", 373 | ] 374 | 375 | [[package]] 376 | name = "winapi-x86_64-pc-windows-gnu" 377 | version = "0.4.0" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 380 | 381 | [[package]] 382 | name = "yaml-rust" 383 | version = "0.4.4" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "39f0c922f1a334134dc2f7a8b67dc5d25f0735263feec974345ff706bcf20b0d" 386 | dependencies = [ 387 | "linked-hash-map", 388 | ] 389 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["Sunrin SHIMURA (keen) <3han5chou7@gmail.com>"] 3 | name = "chema" 4 | version = "0.0.9" 5 | license = "Apache-2.0/MIT" 6 | description = "An external DSL for JSON Schema" 7 | readme = "README.md" 8 | repository = "https://github.com/KeenS/chema" 9 | keywords = ["json-schema", "swagger", "DSL", "cli"] 10 | categories = ["development-tools"] 11 | edition="2018" 12 | 13 | [dependencies] 14 | combine = "4" 15 | env_logger = "0.6.0" 16 | lazy_static = "1.0" 17 | log = "0.4.0" 18 | regex = "1.0" 19 | serde = "1.0" 20 | serde_json = "1.0" 21 | serde_yaml = "0.8.0" 22 | structopt = "0.2.0" 23 | structopt-derive = "0.2.0" 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache 2.0 2 | 3 | Copyright 2017 κeen 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | 18 | MIT 19 | Copyright 2017 κeen 20 | 21 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 22 | 23 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/KeenS/chema.svg?branch=master)](https://travis-ci.org/KeenS/chema) 2 | [![Build status](https://ci.appveyor.com/api/projects/status/3o96tvfb0wv597ud/branch/master?svg=true)](https://ci.appveyor.com/project/KeenS/chema/branch/master) 3 | 4 | [![chema at crates.io](https://img.shields.io/crates/v/chema.svg)](https://crates.io/crates/chema) 5 | 6 | # Chema 7 | 8 | Generate JSON Schema from a lightweight DSL. 9 | 10 | This is originally intended to generate a `definitions` section of swagger specifications. 11 | 12 | # Install 13 | 14 | Download a binary from https://github.com/KeenS/chema/releases 15 | or if you have setup `cargo`, use `cargo install like below. 16 | 17 | ``` 18 | $ cargo install chema 19 | ``` 20 | 21 | # Usage 22 | 23 | ``` 24 | chema 0.0.8 25 | Sunrin SHIMURA (keen) <3han5chou7@gmail.com> 26 | An external DSL for JSON Schema 27 | USAGE: 28 | chema [FLAGS] [OPTIONS] 29 | FLAGS: 30 | -h, --help Prints help information 31 | --no-swagger don't use swagger specific notation 32 | --pack if pack the output 33 | -V, --version Prints version information 34 | OPTIONS: 35 | --format output format (json|yaml) [default: json] 36 | --path-prefix path prefix of paths [default: /definitions] 37 | ARGS: 38 | input file 39 | ``` 40 | 41 | # Syntax 42 | 43 | ``` 44 | TOP = ITEMS 45 | ITEMS = ITEM+ 46 | ITEM = TYPEDEF 47 | 48 | TYPEDEF = "type" IDENT "=" TYPE ";" 49 | 50 | TYPE = "null" | "boolean" | "object" | "number" | "string" | "integer" 51 | | IDENT | "[" TYPE "]" | STRUCT | ENUM | TYPE "?" 52 | | "format" "(" STRING ")" | "url" "(" STRING ")" 53 | | TYPE "&" TYPE | TYPE "|" TYPE 54 | | TYPE "where" PRED 55 | | "(" TYPE ")" | STRING 56 | 57 | STRUCT = "struct" "{" (FIELD ",")+ "}" 58 | FIELD = IDENT "?"? ":" TYPE 59 | 60 | ENUM = "enum" "{" (VARIANT",")+ "}" 61 | VARIANT = STRING 62 | 63 | PRED = UNUMBER "<=" "length" | "length" <= UNUMBER 64 | | "format" "=" STRING | "it" "=~" REGEX 65 | | PRED && PRED 66 | 67 | IDENT = [a-zA-Z_][a-zA-Z0-9_]* 68 | STRIING = "\"" ([^"\\]|\\.)* "\"" 69 | REGEX = "/" ([^/\\]|\\.)* "/" 70 | UNUMBER = [0-9]+ 71 | 72 | COMMENT = "//" any "\n" | "/*" any "*/" 73 | DOC_COMMENT = "/**" any "*/" 74 | ``` 75 | 76 | # Example 77 | 78 | See [etc](etc). `*.jsd`s are the sources and `*.jsons` are the generated files. 79 | 80 | # Supported Platforms 81 | 82 | UNIX-like system will be supported. 83 | Ubuntu LTS is the major target. 84 | Windows support is best effort and may not work . 85 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # Based on the "trust" template v0.1.2 2 | # https://github.com/japaric/trust/tree/v0.1.2 3 | 4 | environment: 5 | global: 6 | # TODO This is the Rust channel that build jobs will use by default but can be 7 | # overridden on a case by case basis down below 8 | RUST_VERSION: stable 9 | 10 | # TODO Update this to match the name of your project. 11 | CRATE_NAME: chema 12 | 13 | # TODO These are all the build jobs. Adjust as necessary. Comment out what you 14 | # don't need 15 | matrix: 16 | # MinGW 17 | - TARGET: i686-pc-windows-gnu 18 | - TARGET: x86_64-pc-windows-gnu 19 | 20 | # MSVC 21 | - TARGET: i686-pc-windows-msvc 22 | - TARGET: x86_64-pc-windows-msvc 23 | 24 | # Testing other channels 25 | - TARGET: x86_64-pc-windows-gnu 26 | RUST_VERSION: nightly 27 | - TARGET: x86_64-pc-windows-msvc 28 | RUST_VERSION: nightly 29 | 30 | install: 31 | - ps: >- 32 | If ($Env:TARGET -eq 'x86_64-pc-windows-gnu') { 33 | $Env:PATH += ';C:\msys64\mingw64\bin' 34 | } ElseIf ($Env:TARGET -eq 'i686-pc-windows-gnu') { 35 | $Env:PATH += ';C:\msys64\mingw32\bin' 36 | } 37 | - curl -sSf -o rustup-init.exe https://win.rustup.rs/ 38 | - rustup-init.exe -y --default-host %TARGET% --default-toolchain %RUST_VERSION% 39 | - set PATH=%PATH%;C:\Users\appveyor\.cargo\bin 40 | - rustc -Vv 41 | - cargo -V 42 | 43 | # TODO This is the "test phase", tweak it as you see fit 44 | test_script: 45 | # we don't run the "test phase" when doing deploys 46 | - if [%APPVEYOR_REPO_TAG%]==[false] ( 47 | cargo build --target %TARGET% && 48 | cargo test --target %TARGET% && 49 | cargo run --target %TARGET% etc\example.jsd 50 | ) 51 | 52 | before_deploy: 53 | # TODO Update this to build the artifacts that matter to you 54 | - cargo build --target %TARGET% --release 55 | - ps: ci\before_deploy.ps1 56 | 57 | deploy: 58 | artifact: /.*\.zip/ 59 | # TODO update `auth_token.secure` 60 | # - Create a `public_repo` GitHub token. Go to: https://github.com/settings/tokens/new 61 | # - Encrypt it. Go to https://ci.appveyor.com/tools/encrypt 62 | # - Paste the output down here 63 | auth_token: 64 | secure: lTR5YHLdbEBUFRTU1M1l6uIf9ckmTsXhDP3vRMt19aVgtBZ08LfHWBfSaaPngAVR 65 | description: '' 66 | on: 67 | # TODO Here you can pick which targets will generate binary releases 68 | # In this example, there are some targets that are tested using the stable 69 | # and nightly channels. This condition makes sure there is only one release 70 | # for such targets and that's generated using the stable channel 71 | RUST_VERSION: stable 72 | appveyor_repo_tag: true 73 | provider: GitHub 74 | 75 | cache: 76 | - C:\Users\appveyor\.cargo\registry 77 | - target 78 | 79 | branches: 80 | only: 81 | # Release tags 82 | - /^v\d+\.\d+\.\d+.*$/ 83 | - master 84 | 85 | notifications: 86 | - provider: Email 87 | on_build_success: false 88 | 89 | # Building is done in the test phase, so we disable Appveyor's build phase. 90 | build: false 91 | -------------------------------------------------------------------------------- /ci/before_deploy.ps1: -------------------------------------------------------------------------------- 1 | # This script takes care of packaging the build artifacts that will go in the 2 | # release zipfile 3 | 4 | $SRC_DIR = $PWD.Path 5 | $STAGE = [System.Guid]::NewGuid().ToString() 6 | 7 | Set-Location $ENV:Temp 8 | New-Item -Type Directory -Name $STAGE 9 | Set-Location $STAGE 10 | 11 | $ZIP = "$SRC_DIR\$($Env:CRATE_NAME)-$($Env:APPVEYOR_REPO_TAG_NAME)-$($Env:TARGET).zip" 12 | 13 | # TODO Update this to package the right artifacts 14 | Copy-Item "$SRC_DIR\target\$($Env:TARGET)\release\chema.exe" '.\' 15 | 16 | 7z a "$ZIP" * 17 | 18 | Push-AppveyorArtifact "$ZIP" 19 | 20 | Remove-Item *.* -Force 21 | Set-Location .. 22 | Remove-Item $STAGE 23 | Set-Location $SRC_DIR 24 | -------------------------------------------------------------------------------- /ci/before_deploy.sh: -------------------------------------------------------------------------------- 1 | # This script takes care of building your crate and packaging it for release 2 | 3 | set -ex 4 | 5 | main() { 6 | local src=$(pwd) \ 7 | stage= 8 | 9 | case $TRAVIS_OS_NAME in 10 | linux) 11 | stage=$(mktemp -d) 12 | ;; 13 | osx) 14 | stage=$(mktemp -d -t tmp) 15 | ;; 16 | esac 17 | 18 | test -f Cargo.lock || cargo generate-lockfile 19 | 20 | # TODO Update this to build the artifacts that matter to you 21 | cross build --target $TARGET --release 22 | 23 | # TODO Update this to package the right artifacts 24 | cp target/$TARGET/release/chema $stage/ 25 | 26 | cd $stage 27 | tar czf $src/$CRATE_NAME-$TRAVIS_TAG-$TARGET.tar.gz * 28 | cd $src 29 | 30 | rm -rf $stage 31 | } 32 | 33 | main 34 | -------------------------------------------------------------------------------- /ci/install.sh: -------------------------------------------------------------------------------- 1 | set -ex 2 | 3 | main() { 4 | local target= 5 | if [ $TRAVIS_OS_NAME = linux ]; then 6 | target=x86_64-unknown-linux-musl 7 | sort=sort 8 | else 9 | target=x86_64-apple-darwin 10 | sort=gsort # for `sort --sort-version`, from brew's coreutils. 11 | fi 12 | 13 | # Builds for iOS are done on OSX, but require the specific target to be 14 | # installed. 15 | case $TARGET in 16 | aarch64-apple-ios) 17 | rustup target install aarch64-apple-ios 18 | ;; 19 | armv7-apple-ios) 20 | rustup target install armv7-apple-ios 21 | ;; 22 | armv7s-apple-ios) 23 | rustup target install armv7s-apple-ios 24 | ;; 25 | i386-apple-ios) 26 | rustup target install i386-apple-ios 27 | ;; 28 | x86_64-apple-ios) 29 | rustup target install x86_64-apple-ios 30 | ;; 31 | esac 32 | 33 | # This fetches latest stable release 34 | local tag=$(git ls-remote --tags --refs --exit-code https://github.com/japaric/cross \ 35 | | cut -d/ -f3 \ 36 | | grep -E '^v[0.1.0-9.]+$' \ 37 | | $sort --version-sort \ 38 | | tail -n1) 39 | curl -LSfs https://japaric.github.io/trust/install.sh | \ 40 | sh -s -- \ 41 | --force \ 42 | --git japaric/cross \ 43 | --tag $tag \ 44 | --target $target 45 | } 46 | 47 | main 48 | -------------------------------------------------------------------------------- /ci/script.sh: -------------------------------------------------------------------------------- 1 | # This script takes care of testing your crate 2 | 3 | set -ex 4 | 5 | # TODO This is the "test phase", tweak it as you see fit 6 | main() { 7 | cross build --target $TARGET 8 | # cross build --target $TARGET --release 9 | 10 | if [ ! -z $DISABLE_TESTS ]; then 11 | return 12 | fi 13 | 14 | cross test --target $TARGET 15 | # cross test --target $TARGET --release 16 | 17 | cross run --target $TARGET etc/example.jsd 18 | # cross run --target $TARGET --release 19 | } 20 | 21 | # we don't run the "test phase" when doing deploys 22 | if [ -z $TRAVIS_TAG ]; then 23 | main 24 | fi 25 | -------------------------------------------------------------------------------- /etc/example.jsd: -------------------------------------------------------------------------------- 1 | /** id type */ 2 | type id = integer; 3 | 4 | /** rotation angle */ 5 | type angle = integer where 0<= it && it < 360 && it = 90 * n; 6 | 7 | /** @title User */ 8 | type user = struct { 9 | /** unique id of the user */ 10 | id: id, 11 | name: string?, 12 | type: enum {"admin", "writer", "reader"}, 13 | SNSs: [string], 14 | }; 15 | /** 16 | * @title Group 17 | * this expresses a group of users 18 | */ 19 | type group = struct { 20 | id: id, 21 | admin: user?, 22 | members: [user], 23 | // this is short hand of `string where format = "date-time"` 24 | created_at: format("date-time"), 25 | }; 26 | 27 | type password = string where 1 <= length && length <= 128 && it =~ /[a-z0-9!"#$%&'()=~|@]+/; 28 | 29 | type error = struct { 30 | code: string, 31 | message?: string, 32 | }; 33 | 34 | type not_found = error & struct { 35 | code: "not_found", 36 | }; 37 | -------------------------------------------------------------------------------- /etc/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "angle": { 3 | "description": "rotation angle", 4 | "exclusiveMaximum": 360, 5 | "minimum": 0, 6 | "multipleOf": 90, 7 | "type": "integer" 8 | }, 9 | "error": { 10 | "properties": { 11 | "code": { 12 | "type": "string" 13 | }, 14 | "message": { 15 | "type": "string" 16 | } 17 | }, 18 | "required": [ 19 | "code" 20 | ], 21 | "type": "object" 22 | }, 23 | "group": { 24 | "description": "this expresses a group of users", 25 | "properties": { 26 | "admin": { 27 | "anyOf": [ 28 | { 29 | "$ref": "#/definitions/user" 30 | } 31 | ], 32 | "nullable": true 33 | }, 34 | "created_at": { 35 | "format": "date-time", 36 | "type": "string" 37 | }, 38 | "id": { 39 | "$ref": "#/definitions/id" 40 | }, 41 | "members": { 42 | "items": { 43 | "$ref": "#/definitions/user" 44 | }, 45 | "type": "array" 46 | } 47 | }, 48 | "required": [ 49 | "id", 50 | "admin", 51 | "members", 52 | "created_at" 53 | ], 54 | "title": "Group", 55 | "type": "object" 56 | }, 57 | "id": { 58 | "description": "id type", 59 | "type": "integer" 60 | }, 61 | "not_found": { 62 | "allOf": [ 63 | { 64 | "$ref": "#/definitions/error" 65 | }, 66 | { 67 | "properties": { 68 | "code": { 69 | "constant": "not_found" 70 | } 71 | }, 72 | "required": [ 73 | "code" 74 | ], 75 | "type": "object" 76 | } 77 | ] 78 | }, 79 | "password": { 80 | "maxLength": 128, 81 | "minLength": 1, 82 | "pattern": "[a-z0-9!\"#$%&'()=~|@]+", 83 | "type": "string" 84 | }, 85 | "user": { 86 | "properties": { 87 | "SNSs": { 88 | "items": { 89 | "type": "string" 90 | }, 91 | "type": "array" 92 | }, 93 | "id": { 94 | "$ref": "#/definitions/id", 95 | "description": "unique id of the user" 96 | }, 97 | "name": { 98 | "nullable": true, 99 | "type": "string" 100 | }, 101 | "type": { 102 | "enum": [ 103 | "admin", 104 | "writer", 105 | "reader" 106 | ], 107 | "type": "string" 108 | } 109 | }, 110 | "required": [ 111 | "id", 112 | "name", 113 | "type", 114 | "SNSs" 115 | ], 116 | "title": "User", 117 | "type": "object" 118 | } 119 | } -------------------------------------------------------------------------------- /src/compiler.rs: -------------------------------------------------------------------------------- 1 | use crate::parser::*; 2 | use crate::Config; 3 | 4 | use serde_json::{json, Map, Value}; 5 | 6 | fn insert_if(mut map: Map, k: &str, v: Option) -> Map { 7 | if let Some(v) = v { 8 | map.insert(k.into(), Value::String(v)); 9 | } 10 | map 11 | } 12 | 13 | pub fn compile(config: &Config, ast: AST) -> Map { 14 | ast.0 15 | .into_iter() 16 | .map(|Item::TypeDef(td)| { 17 | let type_ = compile_type(config, td.t.type_); 18 | let type_ = insert_if(type_, "description", td.meta.doc); 19 | let type_ = insert_if(type_, "title", td.meta.title); 20 | (td.t.ident.0, Value::Object(type_)) 21 | }) 22 | .collect() 23 | } 24 | 25 | fn compile_type(config: &Config, ty: Type) -> Map { 26 | use self::Type::*; 27 | let kvs = match ty { 28 | Null => vec![("type".to_string(), Value::String("null".into()))], 29 | Boolean => vec![("type".to_string(), Value::String("boolean".into()))], 30 | Object => vec![("type".to_string(), Value::String("object".into()))], 31 | Number => vec![("type".to_string(), Value::String("number".into()))], 32 | String => vec![("type".to_string(), Value::String("string".into()))], 33 | Integer => vec![("type".to_string(), Value::String("integer".into()))], 34 | Ref(url) => vec![("$ref".to_string(), Value::String(url))], 35 | Ident(crate::parser::Ident(i)) => vec![( 36 | "$ref".to_string(), 37 | Value::String(format!("#{}/{}", &config.path_prefix, i)), 38 | )], 39 | Const(c) => { 40 | use crate::parser::Const::*; 41 | match c { 42 | String(s) => vec![("constant".to_string(), Value::String(s))], 43 | } 44 | } 45 | Array(ty) => vec![ 46 | ("type".to_string(), Value::String("array".into())), 47 | ( 48 | "items".to_string(), 49 | Value::Object(compile_type(config, *ty)), 50 | ), 51 | ], 52 | Struct(Annot { 53 | t: crate::parser::Struct { fields }, 54 | meta, 55 | }) => { 56 | let required = collect_requied(&fields) 57 | .into_iter() 58 | .map(|id| Value::String(id.0)) 59 | .collect(); 60 | let properties = fields 61 | .into_iter() 62 | .map(|f| { 63 | ( 64 | f.t.ident.0, 65 | Value::Object(insert_if( 66 | compile_type(config, f.t.type_), 67 | "description", 68 | f.meta.doc, 69 | )), 70 | ) 71 | }) 72 | .collect(); 73 | let mut vec = vec![ 74 | ("type".to_string(), Value::String("object".to_string())), 75 | ("properties".to_string(), Value::Object(properties)), 76 | ("required".to_string(), Value::Array(required)), 77 | ]; 78 | if let Some(title) = meta.title { 79 | vec.push(("title".to_string(), Value::String(title))); 80 | } 81 | if let Some(doc) = meta.doc { 82 | vec.push(("description".to_string(), Value::String(doc))) 83 | } 84 | 85 | vec 86 | } 87 | Enum(Annot { 88 | t: crate::parser::Enum { variants }, 89 | meta, 90 | }) => { 91 | let variants = variants.into_iter().map(|v| Value::String(v.0)).collect(); 92 | let mut vec = vec![ 93 | ("type".to_string(), Value::String("string".into())), 94 | ("enum".to_string(), Value::Array(variants)), 95 | ]; 96 | if let Some(title) = meta.title { 97 | vec.push(("title".to_string(), Value::String(title))); 98 | } 99 | if let Some(doc) = meta.doc { 100 | vec.push(("description".to_string(), Value::String(doc))) 101 | } 102 | vec 103 | } 104 | Option(ty) => { 105 | if config.no_swagger { 106 | let map = compile_type(config, *ty); 107 | let null = compile_type(config, Type::Null); 108 | vec![("oneOf".into(), Value::Array(vec![map.into(), null.into()]))] 109 | } else { 110 | let mut map = if matches!(&*ty, Ident(_) | Ref(_)) { 111 | // See https://github.com/OAI/OpenAPI-Specification/issues/1368#issuecomment-580103688 112 | vec![("anyOf".to_string(), json!([compile_type(config, *ty)]))] 113 | .into_iter() 114 | .collect::>() 115 | } else { 116 | compile_type(config, *ty) 117 | }; 118 | // Swagger only 119 | map.insert("nullable".into(), Value::Bool(true)); 120 | map.into_iter().collect() 121 | } 122 | } 123 | And(tys) => { 124 | let tys = tys 125 | .into_iter() 126 | .map(|ty| compile_type(config, ty)) 127 | .map(Value::Object) 128 | .collect::>(); 129 | vec![("allOf".into(), Value::Array(tys))] 130 | } 131 | Or(tys) => { 132 | let tys = tys 133 | .into_iter() 134 | .map(|ty| compile_type(config, ty)) 135 | .map(Value::Object) 136 | .collect::>(); 137 | vec![("anyOf".into(), Value::Array(tys))] 138 | } 139 | Where(ty, preds) => preds 140 | .into_iter() 141 | .fold(compile_type(config, *ty), |mut m, pred| { 142 | m.extend(compile_pred(config, pred)); 143 | m 144 | }) 145 | .into_iter() 146 | .collect::>(), 147 | }; 148 | kvs.into_iter().collect() 149 | } 150 | 151 | fn compile_pred(_config: &Config, pred: Pred) -> Map { 152 | let kvs = match pred { 153 | Pred::MinLength(n) => vec![("minLength".to_string(), Value::Number(n.into()))], 154 | Pred::MaxLength(n) => vec![("maxLength".to_string(), Value::Number(n.into()))], 155 | Pred::MinSize(n) => vec![("minimum".to_string(), Value::Number(n.into()))], 156 | Pred::MaxSize(n) => vec![("maximum".to_string(), Value::Number(n.into()))], 157 | Pred::ExclusiveMinSize(n) => { 158 | vec![("exclusiveMinimum".to_string(), Value::Number(n.into()))] 159 | } 160 | Pred::ExclusiveMaxSize(n) => { 161 | vec![("exclusiveMaximum".to_string(), Value::Number(n.into()))] 162 | } 163 | Pred::MultipleOf(n) => { 164 | vec![(("multipleOf").to_string(), Value::Number(n.into()))] 165 | } 166 | Pred::Format(format) => vec![("format".to_string(), Value::String(format.into()))], 167 | Pred::Match(regex) => vec![("pattern".to_string(), Value::String(regex.into()))], 168 | }; 169 | kvs.into_iter().collect() 170 | } 171 | 172 | fn collect_requied(fs: &Vec>) -> Vec { 173 | fs.iter() 174 | .filter_map(|f| { 175 | if f.t.is_optional { 176 | None 177 | } else { 178 | Some(f.t.ident.clone()) 179 | } 180 | }) 181 | .collect() 182 | } 183 | -------------------------------------------------------------------------------- /src/formatter.rs: -------------------------------------------------------------------------------- 1 | use serde_json::{self, Map, Value}; 2 | use serde_yaml; 3 | use crate::{Config, Format}; 4 | 5 | use std::io::stdout; 6 | 7 | pub fn format(config: &Config, schema: Map) { 8 | let out = stdout(); 9 | 10 | match config.format { 11 | Format::Yaml => serde_yaml::to_writer(out, &schema).expect("write yaml to the output"), 12 | Format::Json => { 13 | let res = if config.pack { 14 | serde_json::to_writer(out, &schema) 15 | } else { 16 | serde_json::to_writer_pretty(out, &schema) 17 | }; 18 | res.expect("write json to the output") 19 | } 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "128"] 2 | #[macro_use] 3 | extern crate combine; 4 | 5 | pub mod compiler; 6 | pub mod formatter; 7 | pub mod parser; 8 | 9 | use log::debug; 10 | use std::fs::File; 11 | use std::io::{BufReader, Read}; 12 | use std::str::FromStr; 13 | use structopt::clap::{Error, ErrorKind}; 14 | use structopt::StructOpt; 15 | 16 | #[derive(Debug, Clone)] 17 | pub enum Format { 18 | Json, 19 | Yaml, 20 | } 21 | 22 | impl FromStr for Format { 23 | type Err = Error; 24 | fn from_str(s: &str) -> Result { 25 | match s { 26 | "json" => Ok(Format::Json), 27 | "yaml" => Ok(Format::Yaml), 28 | _ => Err(Error { 29 | message: "yaml|json".into(), 30 | kind: ErrorKind::InvalidValue, 31 | info: None, 32 | }), 33 | } 34 | } 35 | } 36 | 37 | #[derive(Debug, Clone, StructOpt)] 38 | pub struct Config { 39 | #[structopt( 40 | long = "path-prefix", 41 | help = "path prefix of paths", 42 | default_value = "/definitions" 43 | )] 44 | pub path_prefix: String, 45 | #[structopt(long = "no-swagger", help = "don't use swagger specific notation")] 46 | pub no_swagger: bool, 47 | #[structopt( 48 | long = "format", 49 | help = "output format (json|yaml)", 50 | default_value = "json" 51 | )] 52 | pub format: Format, 53 | #[structopt(long = "pack", help = "if pack the output")] 54 | pub pack: bool, 55 | #[structopt(help = "input file")] 56 | pub input: String, 57 | } 58 | 59 | fn main() { 60 | let config = Config::from_args(); 61 | debug!("CLI options: {:?}", config); 62 | let file = File::open(&config.input).expect("file exits"); 63 | let mut br = BufReader::new(file); 64 | let mut input = String::new(); 65 | br.read_to_string(&mut input).expect("read succeed"); 66 | 67 | let ast = match parser::parse(&config, &input) { 68 | Ok(ast) => ast, 69 | Err(e) => { 70 | use std::process::exit; 71 | eprintln!("A syntax error found"); 72 | eprintln!("{}", e); 73 | exit(-1) 74 | } 75 | }; 76 | let schema = compiler::compile(&config, ast); 77 | formatter::format(&config, schema); 78 | } 79 | -------------------------------------------------------------------------------- /src/parser.rs: -------------------------------------------------------------------------------- 1 | //! TOP = ITEMS 2 | //! ITEMS = ITEM+ 3 | //! ITEM = TYPEDEF 4 | //! 5 | //! TYPEDEF = "type" IDENT ""=" TYPE ";" 6 | //! 7 | //! TYPE = "null" | "boolean" | "object" | "number" | "string" | "integer" 8 | //! | IDENT | "[" TYPE "]" | STRUCT | ENUM | TYPE "?" 9 | //! | "format" "(" STRING ")" | "url" "(" STRING ")" 10 | //! | TYPE "&" TYPE | TYPE "|" TYPE 11 | //! | TYPE "where" PRED 12 | //! | "(" TYPE ")" | STRING 13 | //! 14 | //! STRUCT = "struct" "{" (FIELD ",")+ "}" 15 | //! FIELD = IDENT "?"? ":" TYPE 16 | //! 17 | //! ENUM = "enum" "{" (VARIANT",")+ "}" 18 | //! VARIANT = STRING 19 | //! 20 | //! PRED = UNUMBER "<=" "length" | "length" <= UNUMBER 21 | //! | UNUMBER "<" "it" | "it" < UNUMBER 22 | //! | UNUMBER "<=" "it" | "it" <= UNUMBER 23 | //! | "it" "=" UNUMBER "*" "n" 24 | //! | "format" "=" STRING | "it" "=~" REGEX 25 | //! | PRED && PRED 26 | //! 27 | //! IDENT = [a-zA-Z_][a-zA-Z0-9_]* 28 | //! STRIING = "\"" ([^"\\]|\\.)* "\"" 29 | //! REGEX = "/" ([^/\\]|\\.)* "/" 30 | //! UNUMBER = [0-9]+ 31 | //! 32 | //! COMMENT = "//" any "\n" | "/*" any "*/" 33 | //! DOC_COMMENT = "/**" any "*/" 34 | 35 | use crate::Config; 36 | 37 | use combine::parser::char::{char, digit, newline, spaces, string}; 38 | use combine::parser::combinator::{from_str, recognize}; 39 | use combine::stream::position::{SourcePosition, Stream as PositionedInput}; 40 | use combine::{ 41 | any, attempt, between, many, many1, not_followed_by, optional, satisfy, sep_by1, sep_end_by1, 42 | skip_many, 43 | }; 44 | use combine::{easy, EasyParser, ParseError, Parser, Stream}; 45 | use lazy_static::lazy_static; 46 | use regex::Regex; 47 | use std::collections::BTreeMap; 48 | 49 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] 50 | pub struct Metadata { 51 | pub title: Option, 52 | pub doc: Option, 53 | } 54 | 55 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 56 | pub struct Annot { 57 | pub t: T, 58 | pub meta: Metadata, 59 | } 60 | 61 | impl Annot { 62 | #[allow(dead_code)] 63 | fn new(t: T) -> Self { 64 | Self { 65 | t, 66 | meta: Default::default(), 67 | } 68 | } 69 | } 70 | 71 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 72 | pub struct AST(pub Vec); 73 | 74 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 75 | pub enum Item { 76 | TypeDef(Annot), 77 | } 78 | 79 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 80 | pub struct TypeDef { 81 | pub ident: Ident, 82 | pub type_: Type, 83 | } 84 | 85 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 86 | pub struct Struct { 87 | pub fields: Vec>, 88 | } 89 | 90 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 91 | pub struct Field { 92 | pub ident: Ident, 93 | pub is_optional: bool, 94 | pub type_: Type, 95 | } 96 | 97 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 98 | pub struct Enum { 99 | pub variants: Vec, 100 | } 101 | 102 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 103 | pub struct Variant(pub String); 104 | 105 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 106 | pub enum Type { 107 | Null, 108 | Boolean, 109 | Object, 110 | Number, 111 | String, 112 | Integer, 113 | Ref(String), 114 | Ident(Ident), 115 | Const(Const), 116 | Array(Box), 117 | Struct(Annot), 118 | Enum(Annot), 119 | Option(Box), 120 | And(Vec), 121 | Or(Vec), 122 | Where(Box, Vec), 123 | } 124 | 125 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 126 | pub enum Pred { 127 | MinLength(usize), 128 | MaxLength(usize), 129 | MinSize(usize), 130 | MaxSize(usize), 131 | ExclusiveMinSize(usize), 132 | ExclusiveMaxSize(usize), 133 | MultipleOf(usize), 134 | Format(String), 135 | Match(String), 136 | } 137 | 138 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 139 | pub enum Const { 140 | String(String), 141 | } 142 | 143 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 144 | pub struct Ident(pub String); 145 | 146 | pub fn parse<'cfg, 'a>( 147 | _: &'cfg Config, 148 | input: &'a str, 149 | ) -> Result>> { 150 | let input = PositionedInput::new(input); 151 | ast().easy_parse(input).map(|r| r.0) 152 | } 153 | 154 | fn ast<'a, I>() -> impl Parser + 'a 155 | where 156 | I: Stream + 'a, 157 | I::Error: ParseError, 158 | { 159 | blank().with(sep_end_by1(item(), blank())).map(AST) 160 | } 161 | 162 | fn item<'a, I>() -> impl Parser + 'a 163 | where 164 | I: Stream + 'a, 165 | I::Error: ParseError, 166 | { 167 | typedef().map(Item::TypeDef) 168 | } 169 | 170 | fn typedef<'a, I>() -> impl Parser> + 'a 171 | where 172 | I: Stream + 'a, 173 | I::Error: ParseError, 174 | { 175 | let typedef_ = struct_parser! { 176 | TypeDef { 177 | _: string("type").skip(blank()), 178 | ident: ident().skip(blank()), 179 | _: char('=').skip(blank()), 180 | type_: type_().skip(blank()), 181 | _: char(';').message("typedef must end with ';'"), 182 | } 183 | }; 184 | with_annot(typedef_) 185 | } 186 | 187 | fn struct_<'a, I>() -> impl Parser> + 'a 188 | where 189 | I: Stream + 'a, 190 | I::Error: ParseError, 191 | { 192 | let field = struct_parser! { 193 | Field { 194 | ident: ident().skip(blank()), 195 | is_optional: optional(char('?')).map(|o| o.is_some()), 196 | _: char(':').skip(blank()), 197 | type_: type_() 198 | } 199 | }; 200 | let field = with_annot(field); 201 | 202 | let struct__ = struct_parser! { 203 | Struct { 204 | _: string("struct").skip(blank()), 205 | fields: between(char('{').skip(blank()), 206 | char('}'), 207 | sep_end_by1(field.skip(blank()), char(',').skip(blank()))), 208 | } 209 | }; 210 | with_annot(struct__) 211 | } 212 | 213 | fn enum_<'a, I>() -> impl Parser> + 'a 214 | where 215 | I: Stream + 'a, 216 | I::Error: ParseError, 217 | { 218 | let variant = str_().message("enum variants must be strings").map(Variant); 219 | 220 | let enum__ = struct_parser! { 221 | Enum { 222 | _: string("enum").skip(blank()), 223 | variants: between(char('{').skip(blank()), 224 | char('}'), 225 | sep_end_by1(variant.skip(blank()), char(',').skip(blank()))) 226 | } 227 | }; 228 | with_annot(enum__) 229 | } 230 | 231 | fn type0<'a, I>() -> impl Parser + 'a 232 | where 233 | I: Stream + 'a, 234 | I::Error: ParseError, 235 | { 236 | choice!( 237 | attempt(string("null").map(|_| Type::Null)), 238 | attempt(string("boolean").map(|_| Type::Boolean)), 239 | attempt(string("object").map(|_| Type::Object)), 240 | attempt(string("number").map(|_| Type::Number)), 241 | attempt(string("string").map(|_| Type::String)), 242 | attempt(string("integer").map(|_| Type::Integer)), 243 | attempt( 244 | between( 245 | string("format") 246 | .skip(blank()) 247 | .skip(string("(")) 248 | .skip(blank()), 249 | string(")"), 250 | str_() 251 | ) 252 | .map(|f| Type::Where(Box::new(Type::String), vec![Pred::Format(f)])) 253 | ), 254 | attempt( 255 | between( 256 | string("ref").skip(blank()).skip(string("(")).skip(blank()), 257 | string(")"), 258 | str_() 259 | ) 260 | .map(Type::Ref) 261 | ), 262 | attempt(str_().map(|s| Type::Const(Const::String(s)))), 263 | attempt( 264 | (char('[').skip(blank()), type_(), blank().with(char(']'))) 265 | .map(|(_, ty, _)| Type::Array(Box::new(ty))) 266 | ), 267 | attempt((char('(').skip(blank()), type_(), blank().with(char(')'))).map(|(_, ty, _)| ty)), 268 | attempt(struct_().map(Type::Struct)), 269 | attempt(enum_().map(Type::Enum)), 270 | ident().map(Type::Ident) 271 | ) 272 | } 273 | 274 | fn type1<'a, I>() -> impl Parser + 'a 275 | where 276 | I: Stream + 'a, 277 | I::Error: ParseError, 278 | { 279 | (type0(), optional(char('?'))).map(|(ty, q)| { 280 | if q.is_some() { 281 | Type::Option(Box::new(ty)) 282 | } else { 283 | ty 284 | } 285 | }) 286 | } 287 | 288 | fn type2<'a, I>() -> impl Parser + 'a 289 | where 290 | I: Stream + 'a, 291 | I::Error: ParseError, 292 | { 293 | ( 294 | type1().skip(blank()), 295 | optional((string("where").skip(blank()), preds())), 296 | ) 297 | .map(|(ty, preds)| match preds { 298 | Some((_, preds)) => Type::Where(Box::new(ty), preds), 299 | None => ty, 300 | }) 301 | } 302 | 303 | fn type3<'a, I>() -> impl Parser + 'a 304 | where 305 | I: Stream + 'a, 306 | I::Error: ParseError, 307 | { 308 | fn shift(item: Type, mut vec: Vec) -> Vec { 309 | vec.insert(0, item); 310 | vec 311 | } 312 | 313 | choice!( 314 | attempt( 315 | ( 316 | type2().skip(blank()).skip(string("&").skip(blank())), 317 | sep_by1(type2().skip(blank()), string("&").skip(blank())) 318 | ) 319 | .map(|(ty, tys)| Type::And(shift(ty, tys))) 320 | ), 321 | attempt( 322 | ( 323 | type2().skip(blank()).skip(string("|").skip(blank())), 324 | sep_by1(type2().skip(blank()), string("|").skip(blank())) 325 | ) 326 | .map(|(ty, tys)| Type::Or(shift(ty, tys))) 327 | ), 328 | type2() 329 | ) 330 | } 331 | 332 | parser! { 333 | fn type_[I]()(I) -> Type 334 | where [I: Stream] 335 | { 336 | opaque!(type3()) 337 | } 338 | 339 | } 340 | 341 | fn preds<'a, I>() -> impl Parser> + 'a 342 | where 343 | I: Stream + 'a, 344 | I::Error: ParseError, 345 | { 346 | sep_by1(pred().skip(blank()), attempt(string("&&").skip(blank()))) 347 | } 348 | 349 | fn pred<'a, I>() -> impl Parser + 'a 350 | where 351 | I: Stream + 'a, 352 | I::Error: ParseError, 353 | { 354 | choice!( 355 | attempt( 356 | ( 357 | string("format"), 358 | blank().skip(string("=")).skip(blank()), 359 | str_() 360 | ) 361 | .map(|(_, _, s)| Pred::Format(s.to_string())) 362 | ), 363 | attempt( 364 | ( 365 | string("it"), 366 | blank().skip(string("=~")).skip(blank()), 367 | regex() 368 | ) 369 | .map(|(_, _, s)| Pred::Match(s.to_string())) 370 | ), 371 | attempt( 372 | ( 373 | string("length"), 374 | blank().skip(string("<=")).skip(blank()), 375 | number() 376 | ) 377 | .map(|(_, _, n)| Pred::MaxLength(n)) 378 | ), 379 | attempt( 380 | ( 381 | number(), 382 | blank().skip(string("<=")).skip(blank()), 383 | string("length") 384 | ) 385 | .map(|(n, _, _)| Pred::MinLength(n)) 386 | ), 387 | attempt( 388 | ( 389 | string("it"), 390 | blank().skip(string("<")).skip(blank()), 391 | number() 392 | ) 393 | .map(|(_, _, n)| Pred::ExclusiveMaxSize(n)) 394 | ), 395 | attempt( 396 | ( 397 | number(), 398 | blank().skip(string("<")).skip(blank()), 399 | string("it") 400 | ) 401 | .map(|(n, _, _)| Pred::ExclusiveMinSize(n)) 402 | ), 403 | attempt( 404 | ( 405 | string("it"), 406 | blank().skip(string("<=")).skip(blank()), 407 | number() 408 | ) 409 | .map(|(_, _, n)| Pred::MaxSize(n)) 410 | ), 411 | attempt( 412 | ( 413 | number(), 414 | blank().skip(string("<=")).skip(blank()), 415 | string("it") 416 | ) 417 | .map(|(n, _, _)| Pred::MinSize(n)) 418 | ), 419 | attempt( 420 | ( 421 | string("it"), 422 | blank().skip(string("=")).skip(blank()), 423 | number().skip(blank()), 424 | string("*").skip(blank()), 425 | string("n") 426 | ) 427 | .map(|(_, _, n, _, _)| Pred::MultipleOf(n)) 428 | ) 429 | ) 430 | } 431 | 432 | fn ident<'a, I>() -> impl Parser 433 | where 434 | I: Stream + 'a, 435 | I::Error: ParseError, 436 | { 437 | recognize( 438 | satisfy(|c: char| c.is_alphabetic() || "_".contains(c)).with(skip_many(satisfy( 439 | |c: char| c.is_alphanumeric() || "_".contains(c), 440 | ))), 441 | ) 442 | .message("ident") 443 | .map(|s: String| Ident(s)) 444 | } 445 | 446 | fn regex<'a, I>() -> impl Parser 447 | where 448 | I: Stream + 'a, 449 | I::Error: ParseError, 450 | { 451 | between( 452 | char('/'), 453 | char('/'), 454 | many(char('\\').with(any()).or(satisfy(|c: char| c != '/'))), 455 | ) 456 | .message("regex literal") 457 | } 458 | 459 | fn str_<'a, I>() -> impl Parser 460 | where 461 | I: Stream + 'a, 462 | I::Error: ParseError, 463 | { 464 | between( 465 | char('"'), 466 | char('"'), 467 | many(char('\\').with(any()).or(satisfy(|c: char| c != '"'))), 468 | ) 469 | .message("string literal") 470 | } 471 | 472 | fn number<'a, I>() -> impl Parser 473 | where 474 | I: Stream + 'a, 475 | I::Error: ParseError, 476 | { 477 | from_str(many1::(digit())) 478 | } 479 | 480 | fn blank<'a, I>() -> impl Parser 481 | where 482 | I: Stream + 'a, 483 | I::Error: ParseError, 484 | { 485 | spaces().skip(skip_many(attempt(comment().skip(spaces())))) 486 | } 487 | 488 | fn comment<'a, I>() -> impl Parser 489 | where 490 | I: Stream + 'a, 491 | I::Error: ParseError, 492 | { 493 | let slasla = between(string("//"), newline(), skip_many(satisfy(|c| c != '\n'))) 494 | .map(|_| ()) 495 | .message("comment"); 496 | let slaaster = between( 497 | string("/*").skip(not_followed_by(char('*'))), 498 | string("*/"), 499 | skip_many(satisfy(|c| c != '*').or(attempt(char('*').skip(not_followed_by(char('/')))))), 500 | ); 501 | attempt(slasla).or(slaaster) 502 | } 503 | 504 | fn with_annot<'a, P, I>(p: P) -> impl Parser> + 'a 505 | where 506 | I: Stream + 'a, 507 | P: Parser + 'a, 508 | I::Error: ParseError, 509 | { 510 | optional(doc_comments().skip(blank())) 511 | .and(p) 512 | .map(|(data, t)| { 513 | let meta = if let Some(mut data) = data { 514 | Metadata { 515 | doc: data.remove("desc"), 516 | title: data.remove("title"), 517 | } 518 | } else { 519 | Metadata { 520 | doc: None, 521 | title: None, 522 | } 523 | }; 524 | Annot { t, meta } 525 | }) 526 | } 527 | 528 | fn leading_aster(s: &str) -> &str { 529 | lazy_static! { 530 | static ref RE: Regex = Regex::new("^\\s*\\*").unwrap(); 531 | } 532 | 533 | match RE.find(s) { 534 | None => s, 535 | Some(mtch) => &s[mtch.end()..], 536 | } 537 | } 538 | 539 | enum Line<'a> { 540 | Plain(&'a str), 541 | Attribute(&'a str, &'a str), 542 | } 543 | 544 | fn attribute_line(s: &str) -> Line<'_> { 545 | lazy_static! { 546 | static ref RE: Regex = Regex::new("^\\s*@(\\w+)\\s+(.*)").unwrap(); 547 | } 548 | match RE.captures(s) { 549 | None => Line::Plain(s), 550 | Some(caps) => Line::Attribute(caps.get(1).unwrap().as_str(), caps.get(2).unwrap().as_str()), 551 | } 552 | } 553 | 554 | // to avoid type loop, manually define the function and return boxed type 555 | fn doc_comments<'a, I>() -> impl Parser> 556 | where 557 | I: Stream + 'a, 558 | I::Error: ParseError, 559 | { 560 | fn process_docs(s: String) -> BTreeMap { 561 | let mut desc = Vec::new(); 562 | let mut attrs = Vec::new(); 563 | let lines = s.trim().lines().map(leading_aster).map(attribute_line); 564 | for line in lines { 565 | match line { 566 | Line::Plain(s) => desc.push(s), 567 | Line::Attribute(k, v) => attrs.push((k.to_string(), v.to_string())), 568 | } 569 | } 570 | let desc = desc.join("\n").trim().to_string(); 571 | if !desc.is_empty() { 572 | attrs.push(("desc".into(), desc)); 573 | } 574 | attrs.into_iter().collect() 575 | } 576 | 577 | between( 578 | string("/**"), 579 | string("*/"), 580 | recognize(skip_many( 581 | satisfy(|c| c != '*').or(attempt(char('*').skip(not_followed_by(char('/'))))), 582 | )), 583 | ) 584 | .map(process_docs) 585 | } 586 | 587 | #[cfg(test)] 588 | mod test { 589 | use super::*; 590 | 591 | macro_rules! assert_parsed { 592 | ($parser: expr, $input: expr, $expected: expr) => { 593 | assert_parsed!($parser, $input, $expected, "") 594 | }; 595 | ($parser: expr, $input: expr, $expected: expr, $rest: expr) => { 596 | assert_eq!( 597 | $parser.easy_parse($input).map(|(t1, t2)| (t1, t2)), 598 | Ok(($expected, $rest)) 599 | ) 600 | }; 601 | } 602 | 603 | macro_rules! assert_parsed_partial { 604 | ($parser: expr, $input: expr, $expected: expr) => { 605 | assert_eq!($parser.easy_parse($input).map(|t| t.0), Ok($expected)) 606 | }; 607 | } 608 | 609 | macro_rules! assert_parse_fail { 610 | ($parser: expr, $input: expr) => { 611 | assert!($parser.parse($input).is_err()) 612 | }; 613 | } 614 | 615 | #[test] 616 | fn test_blank() { 617 | assert_parsed!( 618 | blank(), 619 | " 620 | 621 | ", 622 | () 623 | ); 624 | 625 | assert_parsed!( 626 | blank(), 627 | r#" 628 | // this is comment 629 | // continued 630 | "#, 631 | () 632 | ); 633 | 634 | { 635 | let input = "/// doc comments aren't blank"; 636 | assert_parsed!(blank(), input, (), input); 637 | } 638 | 639 | assert_parsed!(blank(), "/* comment */", ()); 640 | assert_parsed!(blank(), "/* comment * comment */", ()); 641 | assert_parsed!(blank(), "/* comment * / comment */", ()); 642 | assert_parsed!(blank(), "/* comment /**/", ()); 643 | } 644 | 645 | #[test] 646 | fn test_doc_comments() { 647 | assert_parsed!( 648 | doc_comments(), 649 | r#"/**single line*/"#, 650 | vec![("desc".into(), "single line".into())] 651 | .into_iter() 652 | .collect() 653 | ); 654 | 655 | assert_parsed_partial!( 656 | doc_comments(), 657 | r#"/** doc */ other data"#, 658 | vec![("desc".into(), "doc".into())].into_iter().collect() 659 | ); 660 | 661 | assert_parsed!( 662 | doc_comments(), 663 | r#"/** multiple 664 | line */"#, 665 | vec![("desc".into(), "multiple\nline".into())] 666 | .into_iter() 667 | .collect() 668 | ); 669 | 670 | assert_parsed!( 671 | doc_comments(), 672 | r#"/** multiple 673 | * line 674 | */"#, 675 | vec![("desc".into(), "multiple\n line".into())] 676 | .into_iter() 677 | .collect() 678 | ); 679 | 680 | assert_parsed!( 681 | doc_comments(), 682 | r#"/** multiple 683 | * line 684 | * @title Title 685 | */"#, 686 | vec![ 687 | ("desc".into(), "multiple\n line".into()), 688 | ("title".into(), "Title".into()), 689 | ] 690 | .into_iter() 691 | .collect() 692 | ); 693 | 694 | assert_parsed!( 695 | doc_comments(), 696 | r#"/** multiple 697 | * line 698 | * @title Title 699 | * @attr unknown attribute 700 | */"#, 701 | (vec![ 702 | ("desc".into(), "multiple\n line".into()), 703 | ("title".into(), "Title".into()), 704 | ("attr".into(), "unknown attribute".into()), 705 | ] 706 | .into_iter() 707 | .collect()) 708 | ); 709 | } 710 | 711 | #[test] 712 | fn test_ident() { 713 | assert_parsed!(ident(), "ident1", Ident("ident1".into())); 714 | 715 | assert_parsed!(ident(), "ident1 ", Ident("ident1".into()), " "); 716 | 717 | assert_parsed!(ident(), "_ident1_ ", Ident("_ident1_".into()), " "); 718 | 719 | assert_parse_fail!(ident(), "0ident1"); 720 | } 721 | 722 | #[test] 723 | fn test_str_() { 724 | assert_parsed!(str_(), r#""""#, "".into()); 725 | assert_parsed!(str_(), r#""abc""#, "abc".into()); 726 | assert_parsed!(str_(), r#""abc\"def\"\\""#, "abc\"def\"\\".into()); 727 | } 728 | 729 | #[test] 730 | fn test_struct() { 731 | assert_parsed!( 732 | struct_(), 733 | "struct {id: integer, name: string}", 734 | Annot::new(Struct { 735 | fields: vec![ 736 | Annot::new(Field { 737 | is_optional: false, 738 | ident: Ident("id".into()), 739 | type_: Type::Integer, 740 | }), 741 | Annot::new(Field { 742 | is_optional: false, 743 | ident: Ident("name".into()), 744 | type_: Type::String, 745 | }), 746 | ], 747 | }) 748 | ); 749 | 750 | assert_parsed!( 751 | struct_(), 752 | "struct {id: integer, name: string,}", 753 | Annot::new(Struct { 754 | fields: vec![ 755 | Annot::new(Field { 756 | is_optional: false, 757 | ident: Ident("id".into()), 758 | type_: Type::Integer, 759 | }), 760 | Annot::new(Field { 761 | is_optional: false, 762 | ident: Ident("name".into()), 763 | type_: Type::String, 764 | }), 765 | ], 766 | }) 767 | ); 768 | 769 | assert_parsed!( 770 | struct_(), 771 | "struct {id?: integer}", 772 | Annot::new(Struct { 773 | fields: vec![Annot::new(Field { 774 | is_optional: true, 775 | ident: Ident("id".into()), 776 | type_: Type::Integer, 777 | }),], 778 | }) 779 | ); 780 | 781 | assert_parsed!( 782 | struct_(), 783 | "/** doc */ 784 | struct { 785 | /** doc */ 786 | id: integer, 787 | name: string, 788 | }", 789 | Annot { 790 | t: Struct { 791 | fields: vec![ 792 | Annot { 793 | t: Field { 794 | is_optional: false, 795 | ident: Ident("id".into()), 796 | type_: Type::Integer, 797 | }, 798 | meta: Metadata { 799 | doc: Some("doc".into()), 800 | title: None, 801 | }, 802 | }, 803 | Annot::new(Field { 804 | is_optional: false, 805 | ident: Ident("name".into()), 806 | type_: Type::String, 807 | }), 808 | ], 809 | }, 810 | meta: Metadata { 811 | doc: Some("doc".into()), 812 | title: None, 813 | }, 814 | } 815 | ); 816 | 817 | assert_parsed!( 818 | struct_(), 819 | "/** @title User */struct {id: integer, name: string}", 820 | Annot { 821 | t: (Struct { 822 | fields: vec![ 823 | Annot::new(Field { 824 | is_optional: false, 825 | ident: Ident("id".into()), 826 | type_: Type::Integer, 827 | }), 828 | Annot::new(Field { 829 | is_optional: false, 830 | ident: Ident("name".into()), 831 | type_: Type::String, 832 | }), 833 | ], 834 | }), 835 | meta: Metadata { 836 | doc: None, 837 | title: Some("User".into()), 838 | }, 839 | } 840 | ); 841 | 842 | assert_parse_fail!(struct_(), "struct {}"); 843 | assert_parse_fail!(struct_(), "struct \"User\" {}"); 844 | } 845 | 846 | #[test] 847 | fn test_enum() { 848 | assert_parsed!( 849 | enum_(), 850 | "enum { \"OK\", \"NG\"}", 851 | Annot::new(Enum { 852 | variants: vec![Variant("OK".into()), Variant("NG".into())], 853 | }) 854 | ); 855 | 856 | assert_parsed!( 857 | enum_(), 858 | "enum { \"OK\", \"NG\",}", 859 | Annot::new(Enum { 860 | variants: vec![Variant("OK".into()), Variant("NG".into())], 861 | }) 862 | ); 863 | assert_parsed!( 864 | enum_(), 865 | "/** doc */ 866 | enum { \"OK\", \"NG\",}", 867 | Annot { 868 | t: (Enum { 869 | variants: vec![Variant("OK".into()), Variant("NG".into())], 870 | }), 871 | meta: Metadata { 872 | doc: Some("doc".into()), 873 | title: None, 874 | }, 875 | } 876 | ); 877 | 878 | assert_parsed!( 879 | enum_(), 880 | "/** @title Result*/ enum { \"OK\", \"NG\"}", 881 | Annot { 882 | t: (Enum { 883 | variants: vec![Variant("OK".into()), Variant("NG".into())], 884 | }), 885 | meta: Metadata { 886 | doc: None, 887 | title: Some("Result".to_string()), 888 | }, 889 | } 890 | ); 891 | 892 | assert_parse_fail!(enum_(), "enum {}"); 893 | } 894 | 895 | #[test] 896 | fn test_type_null() { 897 | assert_parsed!(type_(), "null", Type::Null); 898 | } 899 | 900 | #[test] 901 | fn test_type_boolean() { 902 | assert_parsed!(type_(), "boolean", Type::Boolean); 903 | } 904 | 905 | #[test] 906 | fn test_type_object() { 907 | assert_parsed!(type_(), "object", Type::Object); 908 | } 909 | 910 | #[test] 911 | fn test_type_number() { 912 | assert_parsed!(type_(), "number", Type::Number); 913 | } 914 | 915 | #[test] 916 | fn test_type_string() { 917 | assert_parsed!(type_(), "string", Type::String); 918 | } 919 | 920 | #[test] 921 | fn test_type_integer() { 922 | assert_parsed!(type_(), "integer", Type::Integer); 923 | } 924 | 925 | #[test] 926 | fn test_type_format() { 927 | assert_parsed!( 928 | type_(), 929 | r#"format("date-time")"#, 930 | Type::Where( 931 | Box::new(Type::String), 932 | vec![Pred::Format("date-time".to_string())] 933 | ) 934 | ); 935 | } 936 | 937 | #[test] 938 | fn test_type_ref() { 939 | assert_parsed!( 940 | type_(), 941 | r#"ref("http://json-schema.org/draft-07/hyper-schema")"#, 942 | Type::Ref("http://json-schema.org/draft-07/hyper-schema".to_string()) 943 | ); 944 | } 945 | 946 | #[test] 947 | fn test_type_ident() { 948 | assert_parsed!(type_(), "user", Type::Ident(Ident("user".into()))); 949 | } 950 | 951 | #[test] 952 | fn test_type_const() { 953 | assert_parsed!( 954 | type_(), 955 | r#""const literal""#, 956 | Type::Const(Const::String("const literal".into())) 957 | ); 958 | } 959 | 960 | #[test] 961 | fn test_type_array() { 962 | assert_parsed!(type_(), "[string]", Type::Array(Box::new(Type::String))); 963 | } 964 | 965 | #[test] 966 | fn test_type_struct() { 967 | assert_parsed!( 968 | type_(), 969 | "struct {id: integer, name: string}", 970 | Type::Struct(Annot::new(Struct { 971 | fields: vec![ 972 | Annot::new(Field { 973 | is_optional: false, 974 | ident: Ident("id".into()), 975 | type_: Type::Integer, 976 | }), 977 | Annot::new(Field { 978 | is_optional: false, 979 | ident: Ident("name".into()), 980 | type_: Type::String, 981 | }), 982 | ], 983 | })) 984 | ); 985 | } 986 | 987 | #[test] 988 | fn test_type_enum() { 989 | assert_parsed!( 990 | type_(), 991 | "enum { \"OK\", \"NG\"}", 992 | Type::Enum(Annot::new(Enum { 993 | variants: vec![Variant("OK".into()), Variant("NG".into())], 994 | })) 995 | ); 996 | } 997 | 998 | #[test] 999 | fn test_type_optional() { 1000 | assert_parsed!(type_(), "integer?", Type::Option(Box::new(Type::Integer))); 1001 | } 1002 | 1003 | #[test] 1004 | fn test_type_where() { 1005 | assert_parsed!( 1006 | type_(), 1007 | r#"string where format = "email" && 1 <= length &&length<=100 && it =~ /[a-z]+@[a-z.]+/"#, 1008 | Type::Where( 1009 | Box::new(Type::String), 1010 | vec![ 1011 | Pred::Format("email".into()), 1012 | Pred::MinLength(1), 1013 | Pred::MaxLength(100), 1014 | Pred::Match("[a-z]+@[a-z.]+".into()) 1015 | ] 1016 | ) 1017 | ); 1018 | } 1019 | 1020 | #[test] 1021 | fn test_type_where_number() { 1022 | assert_parsed!( 1023 | type_(), 1024 | r#"integer where 1 <= it &&it<100 && it = 5* n"#, 1025 | Type::Where( 1026 | Box::new(Type::Integer), 1027 | vec![ 1028 | Pred::MinSize(1), 1029 | Pred::ExclusiveMaxSize(100), 1030 | Pred::MultipleOf(5), 1031 | ] 1032 | ) 1033 | ); 1034 | } 1035 | 1036 | #[test] 1037 | fn test_type_or() { 1038 | assert_parsed!( 1039 | type_(), 1040 | "integer | string", 1041 | Type::Or(vec![Type::Integer, Type::String]) 1042 | ); 1043 | } 1044 | 1045 | #[test] 1046 | fn test_type_and() { 1047 | assert_parsed!( 1048 | type_(), 1049 | "struct {id: integer} & struct {name: string}", 1050 | Type::And(vec![ 1051 | Type::Struct(Annot::new(Struct { 1052 | fields: vec![Annot::new(Field { 1053 | is_optional: false, 1054 | ident: Ident("id".into()), 1055 | type_: Type::Integer, 1056 | }),], 1057 | })), 1058 | Type::Struct(Annot::new(Struct { 1059 | fields: vec![Annot::new(Field { 1060 | is_optional: false, 1061 | ident: Ident("name".into()), 1062 | type_: Type::String, 1063 | }),], 1064 | })), 1065 | ]) 1066 | ); 1067 | } 1068 | 1069 | #[test] 1070 | fn test_type_paren() { 1071 | assert_parsed!(type_(), "(integer)", Type::Integer); 1072 | } 1073 | 1074 | #[test] 1075 | fn test_type() { 1076 | assert_parsed!( 1077 | type_(), 1078 | "[[string]]", 1079 | Type::Array(Box::new(Type::Array(Box::new(Type::String)))) 1080 | ); 1081 | assert_parsed!( 1082 | type_(), 1083 | "[integer?]?", 1084 | Type::Option(Box::new(Type::Array(Box::new(Type::Option(Box::new( 1085 | Type::Integer 1086 | )))),)) 1087 | ); 1088 | 1089 | assert_parsed!( 1090 | type_(), 1091 | r#"string? where 1 <= length & string where format="email""#, 1092 | Type::And(vec![ 1093 | Type::Where( 1094 | Box::new(Type::Option(Box::new(Type::String))), 1095 | vec![Pred::MinLength(1)] 1096 | ), 1097 | Type::Where(Box::new(Type::String), vec![Pred::Format("email".into())]) 1098 | ]) 1099 | ); 1100 | 1101 | assert_parsed!( 1102 | type_(), 1103 | "(integer | string | null) & sometype", 1104 | Type::And(vec![ 1105 | Type::Or(vec![Type::Integer, Type::String, Type::Null]), 1106 | Type::Ident(Ident("sometype".into())), 1107 | ]) 1108 | ); 1109 | 1110 | assert_parsed!( 1111 | type_(), 1112 | r#"struct { code: "not_json", message: string}"#, 1113 | Type::Struct(Annot::new(Struct { 1114 | fields: vec![ 1115 | Annot::new(Field { 1116 | is_optional: false, 1117 | ident: Ident("code".into()), 1118 | type_: Type::Const(Const::String("not_json".into())), 1119 | }), 1120 | Annot::new(Field { 1121 | is_optional: false, 1122 | ident: Ident("message".into()), 1123 | type_: Type::String, 1124 | }), 1125 | ], 1126 | })) 1127 | ); 1128 | } 1129 | 1130 | #[test] 1131 | fn test_typedef() { 1132 | assert_parsed!( 1133 | typedef(), 1134 | "type id = integer;", 1135 | Annot::new(TypeDef { 1136 | ident: Ident("id".into()), 1137 | type_: Type::Integer, 1138 | }) 1139 | ); 1140 | 1141 | assert_parsed!( 1142 | typedef(), 1143 | "/** @title User */ type user = struct {id: id, name: string};", 1144 | Annot { 1145 | t: TypeDef { 1146 | ident: Ident("user".into()), 1147 | type_: Type::Struct(Annot::new(Struct { 1148 | fields: vec![ 1149 | Annot::new(Field { 1150 | is_optional: false, 1151 | ident: Ident("id".into()), 1152 | type_: Type::Ident(Ident("id".into())), 1153 | }), 1154 | Annot::new(Field { 1155 | is_optional: false, 1156 | ident: Ident("name".into()), 1157 | type_: Type::String, 1158 | }), 1159 | ], 1160 | })), 1161 | }, 1162 | meta: Metadata { 1163 | doc: None, 1164 | title: Some("User".into()), 1165 | }, 1166 | } 1167 | ); 1168 | 1169 | assert_parsed!( 1170 | typedef(), 1171 | r#"/** This is User 1172 | * @title User */ 1173 | type user = struct { 1174 | /** This is id */ 1175 | id: id, 1176 | // comment is ignored 1177 | name: string, 1178 | };"#, 1179 | Annot { 1180 | t: TypeDef { 1181 | ident: Ident("user".into()), 1182 | type_: Type::Struct(Annot::new(Struct { 1183 | fields: vec![ 1184 | Annot { 1185 | t: Field { 1186 | is_optional: false, 1187 | ident: Ident("id".into()), 1188 | type_: Type::Ident(Ident("id".into())), 1189 | }, 1190 | meta: Metadata { 1191 | doc: Some("This is id".into()), 1192 | title: None, 1193 | }, 1194 | }, 1195 | Annot::new(Field { 1196 | is_optional: false, 1197 | ident: Ident("name".into()), 1198 | type_: Type::String, 1199 | }), 1200 | ], 1201 | })), 1202 | }, 1203 | meta: Metadata { 1204 | doc: Some("This is User".into()), 1205 | title: Some("User".into()), 1206 | }, 1207 | } 1208 | ); 1209 | 1210 | assert_parsed!( 1211 | typedef(), 1212 | r#"/** @title Person */ 1213 | type person = struct { 1214 | id : struct { value: integer } 1215 | };"#, 1216 | Annot { 1217 | t: TypeDef { 1218 | ident: Ident("person".into()), 1219 | type_: Type::Struct(Annot::new(Struct { 1220 | fields: vec![Annot::new(Field { 1221 | is_optional: false, 1222 | ident: Ident("id".into()), 1223 | type_: Type::Struct(Annot::new(Struct { 1224 | fields: vec![Annot::new(Field { 1225 | is_optional: false, 1226 | ident: Ident("value".into()), 1227 | type_: Type::Integer, 1228 | }),], 1229 | })), 1230 | }),], 1231 | })), 1232 | }, 1233 | meta: Metadata { 1234 | doc: None, 1235 | title: Some("Person".into()), 1236 | }, 1237 | } 1238 | ); 1239 | 1240 | assert_parsed!( 1241 | typedef(), 1242 | r#"/** @title Person */ 1243 | type person = struct { 1244 | name: string, 1245 | sex: enum { "M", "F" } 1246 | };"#, 1247 | Annot { 1248 | t: TypeDef { 1249 | ident: Ident("person".into()), 1250 | type_: Type::Struct(Annot::new(Struct { 1251 | fields: vec![ 1252 | Annot::new(Field { 1253 | is_optional: false, 1254 | ident: Ident("name".into()), 1255 | type_: Type::String, 1256 | }), 1257 | Annot::new(Field { 1258 | is_optional: false, 1259 | ident: Ident("sex".into()), 1260 | type_: Type::Enum(Annot::new(Enum { 1261 | variants: vec![Variant("M".into()), Variant("F".into())], 1262 | })), 1263 | }), 1264 | ], 1265 | })), 1266 | }, 1267 | meta: Metadata { 1268 | doc: None, 1269 | title: Some("Person".into()), 1270 | }, 1271 | } 1272 | ); 1273 | } 1274 | 1275 | #[test] 1276 | fn test_ast() { 1277 | assert_parsed!( 1278 | ast(), 1279 | r#" 1280 | type id = integer; 1281 | /** @title User */ 1282 | type user = struct {id: id, name: string}; 1283 | 1284 | "#, 1285 | AST(vec![ 1286 | Item::TypeDef(Annot::new(TypeDef { 1287 | ident: Ident("id".into()), 1288 | type_: Type::Integer, 1289 | })), 1290 | Item::TypeDef(Annot { 1291 | t: (TypeDef { 1292 | ident: Ident("user".into()), 1293 | type_: Type::Struct(Annot::new(Struct { 1294 | fields: vec![ 1295 | Annot::new(Field { 1296 | is_optional: false, 1297 | ident: Ident("id".into()), 1298 | type_: Type::Ident(Ident("id".into())), 1299 | }), 1300 | Annot::new(Field { 1301 | is_optional: false, 1302 | ident: Ident("name".into()), 1303 | type_: Type::String, 1304 | }), 1305 | ], 1306 | })), 1307 | }), 1308 | meta: Metadata { 1309 | doc: None, 1310 | title: Some("User".into()), 1311 | }, 1312 | }), 1313 | ]) 1314 | ); 1315 | } 1316 | } 1317 | --------------------------------------------------------------------------------