├── .github └── workflows │ └── eipv-test.yaml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── src ├── ctx.rs ├── eip.rs ├── error.rs ├── main.rs ├── runner.rs └── validators.rs └── tests ├── fixtures ├── preamble-author-email-and-handle.md ├── preamble-author-email-invalid.md ├── preamble-author-email-trailing-info.md ├── preamble-author-email-unmatched.md ├── preamble-author-handle-invalid.md ├── preamble-author-handle-trailing-info.md ├── preamble-author-handle-unmatched.md ├── preamble-category-core.md ├── preamble-category-erc.md ├── preamble-category-interface.md ├── preamble-category-invalid.md ├── preamble-category-networking.md ├── preamble-created-malformed.md ├── preamble-description-too-long.md ├── preamble-end-malformed.md ├── preamble-invalid-discussions-to.md ├── preamble-last-call-deadline-malformed.md ├── preamble-malformed-discussions-to.md ├── preamble-malformed-eip-signed-int.md ├── preamble-malformed-eip.md ├── preamble-malformed-field.md ├── preamble-missing-author.md ├── preamble-missing-category-ok.md ├── preamble-missing-category.md ├── preamble-missing-created.md ├── preamble-missing-discussions-to.md ├── preamble-missing-eip.md ├── preamble-missing-status.md ├── preamble-missing-title.md ├── preamble-missing-type.md ├── preamble-requires-malformed.md ├── preamble-requires-multiple.md ├── preamble-requires-no-whitespace.md ├── preamble-requires-out-of-order.md ├── preamble-requires-single.md ├── preamble-requires-too-much-whitespace.md ├── preamble-start-malformed.md ├── preamble-status-draft.md ├── preamble-status-final.md ├── preamble-status-invalid.md ├── preamble-status-last-call.md ├── preamble-status-living.md ├── preamble-status-review.md ├── preamble-status-stagnant.md ├── preamble-status-withdrawn.md ├── preamble-title-too-long.md ├── preamble-type-informational.md ├── preamble-type-invalid.md ├── preamble-type-meta.md ├── preamble-type-standards.md ├── preamble-unknown-field.md ├── preamble-updated-malformed.md └── valid.md ├── tests.rs └── utils.rs /.github/workflows/eipv-test.yaml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request, workflow_dispatch] 2 | 3 | name: ci 4 | 5 | jobs: 6 | check: 7 | name: Check 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions-rs/toolchain@v1 12 | with: 13 | profile: minimal 14 | toolchain: nightly 15 | override: true 16 | - uses: actions-rs/cargo@v1 17 | with: 18 | command: check 19 | 20 | test: 21 | name: Test Suite 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v2 25 | - uses: actions-rs/toolchain@v1 26 | with: 27 | profile: minimal 28 | toolchain: nightly 29 | override: true 30 | - uses: actions-rs/cargo@v1 31 | with: 32 | command: test 33 | 34 | fmt: 35 | name: Rustfmt 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v2 39 | - uses: actions-rs/toolchain@v1 40 | with: 41 | profile: minimal 42 | toolchain: stable 43 | override: true 44 | - run: rustup component add rustfmt 45 | - uses: actions-rs/cargo@v1 46 | with: 47 | command: fmt 48 | args: --all -- --check 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.7.13" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "043164d8ba5c4c3035fec9bbee8647c0261d788f3474306f93bb65901cae0e86" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anyhow" 16 | version = "1.0.31" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "85bb70cc08ec97ca5450e6eba421deeea5f172c0fc61f78b5357b2a8e8be195f" 19 | 20 | [[package]] 21 | name = "assert_cmd" 22 | version = "1.0.1" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "c88b9ca26f9c16ec830350d309397e74ee9abdfd8eb1f71cb6ecc71a3fc818da" 25 | dependencies = [ 26 | "doc-comment", 27 | "predicates", 28 | "predicates-core", 29 | "predicates-tree", 30 | "wait-timeout", 31 | ] 32 | 33 | [[package]] 34 | name = "atty" 35 | version = "0.2.14" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 38 | dependencies = [ 39 | "hermit-abi", 40 | "libc", 41 | "winapi", 42 | ] 43 | 44 | [[package]] 45 | name = "autocfg" 46 | version = "1.0.0" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" 49 | 50 | [[package]] 51 | name = "bitflags" 52 | version = "1.2.1" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" 55 | 56 | [[package]] 57 | name = "chrono" 58 | version = "0.4.13" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "c74d84029116787153e02106bf53e66828452a4b325cc8652b788b5967c0a0b6" 61 | dependencies = [ 62 | "num-integer", 63 | "num-traits", 64 | "time", 65 | ] 66 | 67 | [[package]] 68 | name = "clap" 69 | version = "3.0.0-beta.2" 70 | source = "registry+https://github.com/rust-lang/crates.io-index" 71 | checksum = "4bd1061998a501ee7d4b6d449020df3266ca3124b941ec56cf2005c3779ca142" 72 | dependencies = [ 73 | "atty", 74 | "bitflags", 75 | "clap_derive", 76 | "indexmap", 77 | "lazy_static", 78 | "os_str_bytes", 79 | "strsim", 80 | "termcolor", 81 | "textwrap", 82 | "unicode-width", 83 | "vec_map", 84 | ] 85 | 86 | [[package]] 87 | name = "clap_derive" 88 | version = "3.0.0-beta.2" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "370f715b81112975b1b69db93e0b56ea4cd4e5002ac43b2da8474106a54096a1" 91 | dependencies = [ 92 | "heck", 93 | "proc-macro-error", 94 | "proc-macro2", 95 | "quote", 96 | "syn", 97 | ] 98 | 99 | [[package]] 100 | name = "difference" 101 | version = "2.0.0" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" 104 | 105 | [[package]] 106 | name = "doc-comment" 107 | version = "0.3.3" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" 110 | 111 | [[package]] 112 | name = "eipv" 113 | version = "0.4.0" 114 | dependencies = [ 115 | "anyhow", 116 | "assert_cmd", 117 | "chrono", 118 | "clap", 119 | "predicates", 120 | "regex", 121 | "url", 122 | ] 123 | 124 | [[package]] 125 | name = "float-cmp" 126 | version = "0.6.0" 127 | source = "registry+https://github.com/rust-lang/crates.io-index" 128 | checksum = "da62c4f1b81918835a8c6a484a397775fff5953fe83529afd51b05f5c6a6617d" 129 | dependencies = [ 130 | "num-traits", 131 | ] 132 | 133 | [[package]] 134 | name = "heck" 135 | version = "0.3.1" 136 | source = "registry+https://github.com/rust-lang/crates.io-index" 137 | checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" 138 | dependencies = [ 139 | "unicode-segmentation", 140 | ] 141 | 142 | [[package]] 143 | name = "hermit-abi" 144 | version = "0.1.15" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "3deed196b6e7f9e44a2ae8d94225d80302d81208b1bb673fd21fe634645c85a9" 147 | dependencies = [ 148 | "libc", 149 | ] 150 | 151 | [[package]] 152 | name = "idna" 153 | version = "0.2.0" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" 156 | dependencies = [ 157 | "matches", 158 | "unicode-bidi", 159 | "unicode-normalization", 160 | ] 161 | 162 | [[package]] 163 | name = "indexmap" 164 | version = "1.4.0" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "c398b2b113b55809ceb9ee3e753fcbac793f1956663f3c36549c1346015c2afe" 167 | dependencies = [ 168 | "autocfg", 169 | ] 170 | 171 | [[package]] 172 | name = "lazy_static" 173 | version = "1.4.0" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 176 | 177 | [[package]] 178 | name = "libc" 179 | version = "0.2.72" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "a9f8082297d534141b30c8d39e9b1773713ab50fdbe4ff30f750d063b3bfd701" 182 | 183 | [[package]] 184 | name = "matches" 185 | version = "0.1.8" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" 188 | 189 | [[package]] 190 | name = "memchr" 191 | version = "2.3.3" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" 194 | 195 | [[package]] 196 | name = "normalize-line-endings" 197 | version = "0.3.0" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" 200 | 201 | [[package]] 202 | name = "num-integer" 203 | version = "0.1.43" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b" 206 | dependencies = [ 207 | "autocfg", 208 | "num-traits", 209 | ] 210 | 211 | [[package]] 212 | name = "num-traits" 213 | version = "0.2.12" 214 | source = "registry+https://github.com/rust-lang/crates.io-index" 215 | checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" 216 | dependencies = [ 217 | "autocfg", 218 | ] 219 | 220 | [[package]] 221 | name = "os_str_bytes" 222 | version = "2.3.1" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "06de47b848347d8c4c94219ad8ecd35eb90231704b067e67e6ae2e36ee023510" 225 | 226 | [[package]] 227 | name = "percent-encoding" 228 | version = "2.1.0" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" 231 | 232 | [[package]] 233 | name = "predicates" 234 | version = "1.0.4" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "347a1b6f0b21e636bc9872fb60b83b8e185f6f5516298b8238699f7f9a531030" 237 | dependencies = [ 238 | "difference", 239 | "float-cmp", 240 | "normalize-line-endings", 241 | "predicates-core", 242 | "regex", 243 | ] 244 | 245 | [[package]] 246 | name = "predicates-core" 247 | version = "1.0.0" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "06075c3a3e92559ff8929e7a280684489ea27fe44805174c3ebd9328dcb37178" 250 | 251 | [[package]] 252 | name = "predicates-tree" 253 | version = "1.0.0" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "8e63c4859013b38a76eca2414c64911fba30def9e3202ac461a2d22831220124" 256 | dependencies = [ 257 | "predicates-core", 258 | "treeline", 259 | ] 260 | 261 | [[package]] 262 | name = "proc-macro-error" 263 | version = "1.0.4" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 266 | dependencies = [ 267 | "proc-macro-error-attr", 268 | "proc-macro2", 269 | "quote", 270 | "syn", 271 | "version_check", 272 | ] 273 | 274 | [[package]] 275 | name = "proc-macro-error-attr" 276 | version = "1.0.4" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 279 | dependencies = [ 280 | "proc-macro2", 281 | "quote", 282 | "version_check", 283 | ] 284 | 285 | [[package]] 286 | name = "proc-macro2" 287 | version = "1.0.18" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "beae6331a816b1f65d04c45b078fd8e6c93e8071771f41b8163255bbd8d7c8fa" 290 | dependencies = [ 291 | "unicode-xid", 292 | ] 293 | 294 | [[package]] 295 | name = "quote" 296 | version = "1.0.7" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" 299 | dependencies = [ 300 | "proc-macro2", 301 | ] 302 | 303 | [[package]] 304 | name = "regex" 305 | version = "1.3.9" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "9c3780fcf44b193bc4d09f36d2a3c87b251da4a046c87795a0d35f4f927ad8e6" 308 | dependencies = [ 309 | "aho-corasick", 310 | "memchr", 311 | "regex-syntax", 312 | "thread_local", 313 | ] 314 | 315 | [[package]] 316 | name = "regex-syntax" 317 | version = "0.6.18" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" 320 | 321 | [[package]] 322 | name = "strsim" 323 | version = "0.10.0" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 326 | 327 | [[package]] 328 | name = "syn" 329 | version = "1.0.34" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "936cae2873c940d92e697597c5eee105fb570cd5689c695806f672883653349b" 332 | dependencies = [ 333 | "proc-macro2", 334 | "quote", 335 | "unicode-xid", 336 | ] 337 | 338 | [[package]] 339 | name = "termcolor" 340 | version = "1.1.0" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" 343 | dependencies = [ 344 | "winapi-util", 345 | ] 346 | 347 | [[package]] 348 | name = "textwrap" 349 | version = "0.12.1" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "203008d98caf094106cfaba70acfed15e18ed3ddb7d94e49baec153a2b462789" 352 | dependencies = [ 353 | "unicode-width", 354 | ] 355 | 356 | [[package]] 357 | name = "thread_local" 358 | version = "1.0.1" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" 361 | dependencies = [ 362 | "lazy_static", 363 | ] 364 | 365 | [[package]] 366 | name = "time" 367 | version = "0.1.43" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" 370 | dependencies = [ 371 | "libc", 372 | "winapi", 373 | ] 374 | 375 | [[package]] 376 | name = "tinyvec" 377 | version = "0.3.3" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "53953d2d3a5ad81d9f844a32f14ebb121f50b650cd59d0ee2a07cf13c617efed" 380 | 381 | [[package]] 382 | name = "treeline" 383 | version = "0.1.0" 384 | source = "registry+https://github.com/rust-lang/crates.io-index" 385 | checksum = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41" 386 | 387 | [[package]] 388 | name = "unicode-bidi" 389 | version = "0.3.4" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" 392 | dependencies = [ 393 | "matches", 394 | ] 395 | 396 | [[package]] 397 | name = "unicode-normalization" 398 | version = "0.1.13" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "6fb19cf769fa8c6a80a162df694621ebeb4dafb606470b2b2fce0be40a98a977" 401 | dependencies = [ 402 | "tinyvec", 403 | ] 404 | 405 | [[package]] 406 | name = "unicode-segmentation" 407 | version = "1.6.0" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" 410 | 411 | [[package]] 412 | name = "unicode-width" 413 | version = "0.1.8" 414 | source = "registry+https://github.com/rust-lang/crates.io-index" 415 | checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" 416 | 417 | [[package]] 418 | name = "unicode-xid" 419 | version = "0.2.1" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" 422 | 423 | [[package]] 424 | name = "url" 425 | version = "2.1.1" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "829d4a8476c35c9bf0bbce5a3b23f4106f79728039b726d292bb93bc106787cb" 428 | dependencies = [ 429 | "idna", 430 | "matches", 431 | "percent-encoding", 432 | ] 433 | 434 | [[package]] 435 | name = "vec_map" 436 | version = "0.8.2" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 439 | 440 | [[package]] 441 | name = "version_check" 442 | version = "0.9.2" 443 | source = "registry+https://github.com/rust-lang/crates.io-index" 444 | checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" 445 | 446 | [[package]] 447 | name = "wait-timeout" 448 | version = "0.2.0" 449 | source = "registry+https://github.com/rust-lang/crates.io-index" 450 | checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" 451 | dependencies = [ 452 | "libc", 453 | ] 454 | 455 | [[package]] 456 | name = "winapi" 457 | version = "0.3.9" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 460 | dependencies = [ 461 | "winapi-i686-pc-windows-gnu", 462 | "winapi-x86_64-pc-windows-gnu", 463 | ] 464 | 465 | [[package]] 466 | name = "winapi-i686-pc-windows-gnu" 467 | version = "0.4.0" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 470 | 471 | [[package]] 472 | name = "winapi-util" 473 | version = "0.1.5" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 476 | dependencies = [ 477 | "winapi", 478 | ] 479 | 480 | [[package]] 481 | name = "winapi-x86_64-pc-windows-gnu" 482 | version = "0.4.0" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 485 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "eipv" 3 | version = "0.4.0" 4 | authors = ["lightclient "] 5 | license = "MIT OR Apache-2.0" 6 | description = "Ethereum Improvement Proposal validator" 7 | repository = "https://github.com/lightclient/eipv" 8 | categories = ["command-line-utilities"] 9 | readme = "README.md" 10 | edition = "2018" 11 | 12 | [dependencies] 13 | anyhow = "1.0" 14 | clap = "=3.0.0-beta.2" 15 | chrono = "0.4" 16 | regex = "1.3" 17 | url = "2.1" 18 | 19 | [dev-dependencies] 20 | assert_cmd = "1.0" 21 | predicates = "1.0" 22 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EIP validator 2 | 3 | **Superseded by [`eipw`](https://github.com/ethereum/eipw)** 4 | 5 | [![license](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue)](https://github.com/lightclient/eipv) 6 | [![ci status](https://github.com/lightclient/eipv/workflows/ci/badge.svg)](https://github.com/lightclient/eipv/actions) 7 | 8 | An engine which ensures [Ethereum Improvement 9 | Proposals](https://eips.ethereum.org) meet certain requirements. 10 | 11 | ## Getting Started 12 | 13 | To install `eipv` and validate the EIPs repository: 14 | 15 | ```console 16 | git clone https://github.com/lightclient/eipv.git 17 | cargo install --path=eipv eipv 18 | eipv /path/to/EIPS 19 | ``` 20 | 21 | ## Requirements 22 | 23 | This tracks what `eipv` can validate. 24 | 25 | - [x] Preamble starts with `---` 26 | - [x] Preamble ends with `---` 27 | - [x] Preamble includes all required fields: 28 | - `eip` 29 | - `title` 30 | - `description` 31 | - `author` 32 | - `discussions-to` 33 | - `created` 34 | - `status` 35 | - `type` 36 | - `category` (iff `type` == "Standards Track") 37 | - [x] Preamble does not include any unknown fields 38 | - [ ] Preamble fields are properly formed: 39 | - [x] Each field is of the shape `{field}: {value}\n` 40 | - [x] `eip` is an unsigned integer 41 | - [x] `title` is a string whose length is less than 44 characters 42 | - [x] `author` is a comma-separated string of author information which has 43 | three possible shapes: 44 | - Name only: `John A. Doe` 45 | - Name and email: `John A. Doe ` 46 | - Name and Github username: `John A. Doe (@johndoe)` 47 | - [x] `discussions-to` is a URL where discussions regarding the EIP should be 48 | directed 49 | - [ ] `discussions-to` does not point to a PR 50 | - [x] `status` is one of the following string values: 51 | - `draft` 52 | - `last call` 53 | - `accepted` 54 | - `final` 55 | - `abandoned` 56 | - `rejected` 57 | - `superseded` 58 | - [x] `type` is one of the following string values: 59 | - `standards track` 60 | - `informational` 61 | - `meta` 62 | - [x] `category` is one of the following string values: 63 | - `core` 64 | - `networking` 65 | - `interface` 66 | - `erc` 67 | - [x] `last-call-deadline` is a date value 68 | - [x] `created` is a date value 69 | - [x] `updated` is a comma-separated list of date values 70 | - [x] `requires` is a comma-separated list of EIP numbers in ascending order 71 | - [x] `withdrawal-reason` is a string 72 | - [ ] EIP numbers listed as `required` exist 73 | - [ ] The EIP body includes the required sections in the following order: 74 | - `Abstract` 75 | - `Motivation` 76 | - `Specification` 77 | - `Rationale` 78 | - `Backwards Compatibility` 79 | - `Test Cases` 80 | - `Implementations` 81 | - `Security Considerations` 82 | - `Copyright Waiver` 83 | - [ ] The `Abstract` section is no longer than 200 words 84 | - [ ] The `Copyright Waiver` section contains only the following string: 85 | `Copyright and related rights waived via CC0.` 86 | - [ ] The EIP body does not include any unclosed brackets or parentheses 87 | outside of code snippets 88 | - [ ] File name is of form `eip-N.md`, where `N` coresponds to the EIP's assigned number 89 | - [ ] URLs to other EIPs are relative links 90 | - [x] No trailing whitespace in preamble 91 | -------------------------------------------------------------------------------- /src/ctx.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Error; 2 | use std::collections::HashSet; 3 | 4 | #[derive(Debug, Default)] 5 | pub struct Context { 6 | ignore: HashSet, 7 | skip: HashSet, 8 | } 9 | 10 | impl Context { 11 | pub fn skip(&mut self, s: &str) { 12 | self.skip.insert(s.to_string()); 13 | } 14 | 15 | pub fn ignore(&mut self, e: Error) { 16 | self.ignore.insert(e); 17 | } 18 | 19 | pub fn should_ignore(&self, e: &Error) -> bool { 20 | self.ignore.contains(e) 21 | } 22 | 23 | pub fn should_skip(&self, s: &str) -> bool { 24 | self.skip.contains(s) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/eip.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Error; 2 | use crate::{ctx::Context, validators}; 3 | 4 | use anyhow::{anyhow, Result}; 5 | use chrono::NaiveDate; 6 | use url::Url; 7 | 8 | #[derive(Debug)] 9 | pub struct Eip { 10 | pub preamble: Preamble, 11 | pub body: String, 12 | } 13 | 14 | impl Eip { 15 | pub fn from_str(ctx: &Context, s: &str) -> Result> { 16 | match Preamble::from_str(ctx, s) { 17 | Ok((preamble, body)) => Ok(Eip { preamble, body }), 18 | Err(e) => Err(e), 19 | } 20 | } 21 | } 22 | 23 | #[derive(Debug, Default)] 24 | pub struct Preamble { 25 | pub eip: Option>, 26 | pub title: Option>, 27 | pub description: Option>, 28 | pub author: Option>>, 29 | pub discussions_to: Option>, 30 | pub status: Option>, 31 | pub last_call_deadline: Option>, 32 | pub ty: Option>, 33 | pub category: Option>, 34 | pub created: Option>, 35 | pub updated: Option>>, 36 | pub requires: Option>>, 37 | pub withdrawal_reason: Option>, 38 | } 39 | 40 | macro_rules! insert { 41 | ($preamble: expr, $validator: expr, $t: expr) => {{ 42 | let res = $validator($t.0); 43 | 44 | match res { 45 | Ok(v) => $preamble = Some(Ok(v)), 46 | Err(e) => { 47 | $preamble = Some(Err((anyhow!("")))); 48 | 49 | if !$t.2.should_ignore(&e) { 50 | $t.1.push(e); 51 | } 52 | } 53 | } 54 | }}; 55 | } 56 | 57 | impl Preamble { 58 | pub fn from_str(ctx: &Context, s: &str) -> Result<(Self, String), Vec> { 59 | let mut preamble = Preamble::default(); 60 | let mut errors: Vec = vec![]; 61 | 62 | let (block, rest) = validators::preamble(s).map_err(|e| vec![e])?; 63 | 64 | for (i, line) in block.lines().enumerate() { 65 | let split_idx = line.find(":"); 66 | if split_idx.is_none() { 67 | errors.push(Error::MalformedField); 68 | continue; 69 | } 70 | 71 | let (mut key, mut value) = line.split_at(split_idx.unwrap()); 72 | 73 | // sanitize key 74 | if key != key.trim_start() { 75 | errors.push(Error::LeadingWhitespace); 76 | } 77 | 78 | key = key.trim_start(); 79 | 80 | if key != key.trim_end() { 81 | errors.push(Error::ExtraWhitespace); 82 | } 83 | 84 | key = key.trim_end(); 85 | 86 | // sanitize value 87 | value = value.strip_prefix(":").unwrap(); 88 | 89 | if value.len() - 1 > value.trim_start().len() { 90 | errors.push(Error::ExtraWhitespace); 91 | } else if &value[1..] != value.trim_start() { 92 | errors.push(Error::MissingSpaceAfterColon); 93 | } 94 | 95 | value = value.trim_start(); 96 | 97 | if value != value.trim_end() { 98 | errors.push(Error::TrailingWhitespace); 99 | } 100 | 101 | value = value.trim_end(); 102 | 103 | // tuple to simplify macro calls 104 | let t = (value, &mut errors, &ctx); 105 | 106 | match key { 107 | "eip" => insert!(preamble.eip, validators::eip, t), 108 | "title" => insert!(preamble.title, validators::title, t), 109 | "description" => insert!(preamble.description, validators::description, t), 110 | "author" => insert!(preamble.author, validators::author, t), 111 | "discussions-to" => insert!(preamble.discussions_to, validators::discussions_to, t), 112 | "status" => insert!(preamble.status, validators::status, t), 113 | "last-call-deadline" => { 114 | insert!( 115 | preamble.last_call_deadline, 116 | validators::last_call_deadline, 117 | t 118 | ) 119 | } 120 | "type" => insert!(preamble.ty, validators::ty, t), 121 | "category" => insert!(preamble.category, validators::category, t), 122 | "created" => insert!(preamble.created, validators::created, t), 123 | "updated" => insert!(preamble.updated, validators::updated, t), 124 | "requires" => insert!(preamble.requires, validators::requires, t), 125 | "withdrawal-reason" => { 126 | insert!(preamble.withdrawal_reason, validators::withdrawal_reason, t) 127 | } 128 | _ => errors.push(Error::UnknownPreambleField), 129 | } 130 | } 131 | 132 | if preamble.eip.is_none() && !ctx.should_ignore(&Error::MissingEipField) { 133 | errors.push(Error::MissingEipField); 134 | } 135 | 136 | if preamble.title.is_none() && !ctx.should_ignore(&Error::MissingTitleField) { 137 | errors.push(Error::MissingTitleField); 138 | } 139 | 140 | if preamble.author.is_none() && !ctx.should_ignore(&Error::MissingAuthorField) { 141 | errors.push(Error::MissingAuthorField); 142 | } 143 | 144 | if preamble.discussions_to.is_none() 145 | && !ctx.should_ignore(&Error::MissingDiscussionsToField) 146 | { 147 | errors.push(Error::MissingDiscussionsToField); 148 | } 149 | 150 | if preamble.status.is_none() && !ctx.should_ignore(&Error::MissingStatusField) { 151 | errors.push(Error::MissingStatusField); 152 | } 153 | 154 | if let Some(Ok(ty)) = preamble.ty { 155 | if ty == Type::Standards 156 | && preamble.category.is_none() 157 | && !ctx.should_ignore(&Error::MissingCategoryField) 158 | { 159 | errors.push(Error::MissingCategoryField); 160 | } 161 | } else if !ctx.should_ignore(&Error::MissingTypeField) && preamble.ty.is_none() { 162 | errors.push(Error::MissingTypeField); 163 | } 164 | 165 | match errors.is_empty() { 166 | true => Ok((preamble, rest.to_string())), 167 | false => Err(errors), 168 | } 169 | } 170 | } 171 | 172 | #[derive(Debug)] 173 | pub enum Status { 174 | Draft, 175 | Review, 176 | LastCall, 177 | Final, 178 | Stagnant, 179 | Withdrawn, 180 | Living, 181 | } 182 | 183 | impl Status { 184 | pub fn from_str(s: &str) -> std::result::Result { 185 | match s { 186 | "Draft" => Ok(Self::Draft), 187 | "Review" => Ok(Self::Review), 188 | "Last Call" => Ok(Self::LastCall), 189 | "Final" => Ok(Self::Final), 190 | "Stagnant" => Ok(Self::Stagnant), 191 | "Withdrawn" => Ok(Self::Withdrawn), 192 | "Living" => Ok(Self::Living), 193 | _ => Err(Error::UnknownStatus), 194 | } 195 | } 196 | } 197 | 198 | #[derive(Clone, Copy, Debug, PartialEq)] 199 | pub enum Type { 200 | Standards, 201 | Informational, 202 | Meta, 203 | } 204 | 205 | impl Type { 206 | pub fn from_str(s: &str) -> std::result::Result { 207 | match s { 208 | "Standards Track" => Ok(Self::Standards), 209 | "Informational" => Ok(Self::Informational), 210 | "Meta" => Ok(Self::Meta), 211 | _ => Err(Error::UnknownType), 212 | } 213 | } 214 | } 215 | 216 | #[derive(Clone, Copy, Debug)] 217 | pub enum Category { 218 | Core, 219 | Networking, 220 | Interface, 221 | Erc, 222 | } 223 | 224 | impl Category { 225 | pub fn from_str(s: &str) -> std::result::Result { 226 | match s { 227 | "Core" => Ok(Self::Core), 228 | "Networking" => Ok(Self::Networking), 229 | "Interface" => Ok(Self::Interface), 230 | "ERC" => Ok(Self::Erc), 231 | _ => Err(Error::UnknownCategory), 232 | } 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use anyhow::anyhow; 2 | 3 | pub type Result = std::result::Result; 4 | 5 | #[derive(Clone, Debug, Eq, Hash, PartialEq)] 6 | pub enum Error { 7 | // generic errors 8 | MalformedField, 9 | MissingSpaceAfterColon, 10 | LeadingWhitespace, 11 | ExtraWhitespace, 12 | TrailingWhitespace, 13 | UnknownPreambleField, 14 | 15 | // missing field 16 | MissingEipField, 17 | MissingTitleField, 18 | MissingAuthorField, 19 | MissingDiscussionsToField, 20 | MissingStatusField, 21 | MissingCategoryField, 22 | MissingTypeField, 23 | 24 | // validator level errors 25 | StartDelimiterMissing, 26 | EndDelimiterMissing, 27 | MalformedEipNumber, 28 | TitleExceedsMaxLength, 29 | DescriptionExceedsMaxLength, 30 | MalformedDiscussionsTo, 31 | UnknownStatus, 32 | UnknownType, 33 | UnknownCategory, 34 | MalformedLastCallDeadline, 35 | MalformedCreated, 36 | MalformedUpdated, 37 | MissingSpaceAfterComma, 38 | ExtraWhitespaceBeforeComma, 39 | OutOfOrderEips, 40 | UnmatchedEmailDelimiter, 41 | UnmatchedHandleDelimiter, 42 | AuthorHasEmailAndHandle, 43 | TrailingInfoAfterEmail, 44 | TrailingInfoAfterHandle, 45 | MalformedEmail, 46 | MalformedHandle, 47 | } 48 | 49 | impl Error { 50 | pub fn from_str(s: &str) -> anyhow::Result { 51 | match s { 52 | "title_max_length" => Ok(Self::TitleExceedsMaxLength), 53 | "description_max_length" => Ok(Self::TitleExceedsMaxLength), 54 | "missing_discussions_to" => Ok(Self::MissingDiscussionsToField), 55 | _ => Err(anyhow!("unknown validator")), 56 | } 57 | } 58 | 59 | pub fn human_readable(&self) -> &'static str { 60 | match &self { 61 | // preamble level errors 62 | Self::MalformedField => "malformed field", 63 | Self::MissingSpaceAfterColon => "missing a `space` between colon and value", 64 | Self::ExtraWhitespace => "extra whitespace", 65 | Self::TrailingWhitespace => "trailing whitespace", 66 | Self::LeadingWhitespace => "leading whitespace", 67 | Self::UnknownPreambleField => "unknown preamble field", 68 | 69 | // missing required fields 70 | Self::MissingEipField => "missing EIP field in preamble", 71 | Self::MissingTitleField => "missing title field in preamble", 72 | Self::MissingAuthorField => "missing author field in preamble", 73 | Self::MissingDiscussionsToField => "missing discussions-to field in preamble", 74 | Self::MissingStatusField => "missing status field in preamble", 75 | Self::MissingCategoryField => "missing category field in preamble", 76 | Self::MissingTypeField => "missing type field in preamble", 77 | 78 | // validator level errors 79 | Self::StartDelimiterMissing => "missing initial '---' in preamble", 80 | Self::EndDelimiterMissing => "missing trailing '---' in preamble", 81 | Self::MalformedEipNumber => "EIP should be an unsigned integer", 82 | Self::TitleExceedsMaxLength => "title exceeds max length of 44 characters", 83 | Self::DescriptionExceedsMaxLength => "description exceeds max length of 140 characters", 84 | Self::MalformedDiscussionsTo => "discussions-to must be a URL", 85 | Self::UnknownStatus => "unknown status", 86 | Self::UnknownType => "unknown type", 87 | Self::UnknownCategory => "unknown category", 88 | Self::MalformedLastCallDeadline => "malformed last-call-deadline date", 89 | Self::MalformedCreated => "malformed created date", 90 | Self::MalformedUpdated => "malformed updated date", 91 | Self::MissingSpaceAfterComma => { 92 | "comma-separated values must have a single space following each comma" 93 | } 94 | Self::ExtraWhitespaceBeforeComma => { 95 | "comma-separated values must not have spaces before a comma" 96 | } 97 | Self::OutOfOrderEips => "numbers must be in ascending order", 98 | Self::UnmatchedEmailDelimiter => "unmatched email delimiter", 99 | Self::UnmatchedHandleDelimiter => "unmatched handle delimiter", 100 | Self::AuthorHasEmailAndHandle => "author can't include both an email and handle", 101 | Self::TrailingInfoAfterEmail => "trailing information after email", 102 | Self::TrailingInfoAfterHandle => "trailing information after handle", 103 | Self::MalformedEmail => "malformed email", 104 | Self::MalformedHandle => "malformed handle", 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_variables)] 2 | mod ctx; 3 | mod eip; 4 | mod error; 5 | mod runner; 6 | mod validators; 7 | 8 | use clap::{App, Arg}; 9 | use runner::Runner; 10 | use std::process::exit; 11 | 12 | fn main() { 13 | let matches = App::new("eipv") 14 | .version("0.0.0") 15 | .about("Validate the structure of Ethereum Improvement Proposals") 16 | .arg( 17 | Arg::new("path") 18 | .takes_value(true) 19 | .required(true) 20 | .about("Directory of EIPs or path to a specific EIP"), 21 | ) 22 | .arg( 23 | Arg::new("ignore") 24 | .takes_value(true) 25 | .short('i') 26 | .long("ignore") 27 | .about("Run the validation suite, ignoring the specified errors."), 28 | ) 29 | .arg( 30 | Arg::new("skip") 31 | .takes_value(true) 32 | .short('s') 33 | .long("skip") 34 | .about("Skip validation of the specified files."), 35 | ) 36 | .get_matches(); 37 | 38 | let runner = Runner::new( 39 | matches.value_of("path").unwrap(), 40 | matches.value_of("ignore"), 41 | matches.value_of("skip"), 42 | ); 43 | 44 | match runner { 45 | Ok(mut r) => { 46 | r.validate(); 47 | println!("{}", r); 48 | 49 | if r.invalid() != 0 { 50 | exit(1) 51 | } 52 | } 53 | Err(e) => { 54 | println!("{}", e); 55 | exit(1) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/runner.rs: -------------------------------------------------------------------------------- 1 | use crate::ctx::Context; 2 | use crate::eip::{Category, Eip, Status, Type}; 3 | use crate::error::Error; 4 | 5 | use anyhow::Result; 6 | use std::fmt; 7 | use std::fs; 8 | 9 | #[derive(Debug, Default)] 10 | pub struct Runner<'a> { 11 | path: &'a str, 12 | ctx: Context, 13 | errors: Vec<(String, Vec)>, 14 | 15 | // validity count 16 | valid: u64, 17 | invalid: u64, 18 | 19 | // statuses count 20 | draft: u64, 21 | review: u64, 22 | last_call: u64, 23 | final_: u64, 24 | stagnant: u64, 25 | withdrawn: u64, 26 | living: u64, 27 | 28 | // types count 29 | standards: u64, 30 | meta: u64, 31 | informational: u64, 32 | 33 | // categories count 34 | core: u64, 35 | erc: u64, 36 | interface: u64, 37 | networking: u64, 38 | } 39 | 40 | impl<'a> Runner<'a> { 41 | pub fn new(path: &'a str, ignore: Option<&'a str>, skip: Option<&'a str>) -> Result { 42 | let mut ret = Self::default(); 43 | ret.path = path; 44 | 45 | if let Some(ignore) = ignore { 46 | for i in ignore.split(',') { 47 | Error::from_str(i).and_then(|v| Ok(ret.ctx.ignore(v)))?; 48 | } 49 | } 50 | 51 | if let Some(skip) = skip { 52 | for s in skip.split(',') { 53 | ret.ctx.skip(s); 54 | } 55 | } 56 | 57 | Ok(ret) 58 | } 59 | 60 | pub fn invalid(&self) -> u64 { 61 | self.invalid 62 | } 63 | 64 | pub fn validate(&mut self) { 65 | match fs::metadata(self.path) { 66 | Ok(m) => { 67 | if m.is_file() { 68 | self.validate_single(self.path) 69 | } else { 70 | let dir = fs::read_dir(self.path).expect("unable to read dir"); 71 | for entry in dir { 72 | if let Ok(entry) = entry { 73 | self.validate_single(entry.path()) 74 | } 75 | } 76 | } 77 | } 78 | Err(e) => panic!("{}", e), 79 | } 80 | } 81 | 82 | fn validate_single + Clone>(&mut self, path: P) { 83 | if !self 84 | .ctx 85 | .should_skip(path.as_ref().file_name().unwrap().to_str().unwrap()) 86 | { 87 | let res: Result> = Eip::from_str( 88 | &self.ctx, 89 | &fs::read_to_string(path.clone()) 90 | .unwrap() 91 | // normalize newlines 92 | .replace("\r\n", "\n"), 93 | ); 94 | self.count( 95 | res, 96 | path.as_ref() 97 | .file_name() 98 | .unwrap() 99 | .to_str() 100 | .unwrap() 101 | .to_string(), 102 | ); 103 | } 104 | } 105 | 106 | fn count(&mut self, res: Result>, file_name: String) { 107 | match res { 108 | Ok(eip) => { 109 | self.valid += 1; 110 | 111 | match eip.preamble.status { 112 | Some(Ok(Status::Draft)) => self.draft += 1, 113 | Some(Ok(Status::Review)) => self.review += 1, 114 | Some(Ok(Status::LastCall)) => self.last_call += 1, 115 | Some(Ok(Status::Final)) => self.final_ += 1, 116 | Some(Ok(Status::Stagnant)) => self.stagnant += 1, 117 | Some(Ok(Status::Withdrawn)) => self.withdrawn += 1, 118 | Some(Ok(Status::Living)) => self.living += 1, 119 | _ => (), 120 | } 121 | 122 | match eip.preamble.ty { 123 | Some(Ok(Type::Standards)) => self.standards += 1, 124 | Some(Ok(Type::Informational)) => self.informational += 1, 125 | Some(Ok(Type::Meta)) => self.meta += 1, 126 | _ => (), 127 | } 128 | 129 | match eip.preamble.category { 130 | Some(Ok(Category::Core)) => self.core += 1, 131 | Some(Ok(Category::Networking)) => self.networking += 1, 132 | Some(Ok(Category::Interface)) => self.interface += 1, 133 | Some(Ok(Category::Erc)) => self.erc += 1, 134 | _ => (), 135 | } 136 | } 137 | Err(e) => { 138 | self.invalid += 1; 139 | self.errors.push((file_name, e)); 140 | } 141 | } 142 | } 143 | } 144 | 145 | impl<'a> fmt::Display for Runner<'a> { 146 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 147 | for error in self.errors.iter() { 148 | let eip = error.0.clone(); 149 | for error in error.1.iter() { 150 | write!(f, "{}:\t{}\n", eip, error.human_readable())?; 151 | } 152 | } 153 | 154 | write!(f, "\n")?; 155 | write!(f, "draft: {}, review: {}, last_call: {}, final: {}, stagnant: {}, withdrawn: {}, living: {}\n", self.draft, self.review, self.last_call, self.final_, self.stagnant, self.withdrawn, self.living)?; 156 | write!(f, "valid: {}, invalid: {}", self.valid, self.invalid) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/validators.rs: -------------------------------------------------------------------------------- 1 | use crate::eip::{Category, Status, Type}; 2 | use crate::error::{Error, Result}; 3 | 4 | use chrono::NaiveDate; 5 | use regex::Regex; 6 | use url::Url; 7 | 8 | const TITLE_MAX_LEN: usize = 44; 9 | const DESCRIPTION_MAX_LEN: usize = 140; 10 | 11 | pub fn preamble(s: &str) -> Result<(&str, &str)> { 12 | match s.starts_with("---\n") { 13 | false => Err(Error::StartDelimiterMissing), 14 | true => match s[4..].find("---\n") { 15 | Some(idx) => Ok((&s[4..idx + 4], &s[idx + 4..])), 16 | None => Err(Error::EndDelimiterMissing), 17 | }, 18 | } 19 | } 20 | 21 | pub fn eip(s: &str) -> Result { 22 | Ok(s.parse::().map_err(|_| Error::MalformedEipNumber)?) 23 | } 24 | 25 | pub fn title(s: &str) -> Result { 26 | if TITLE_MAX_LEN < s.len() { 27 | return Err(Error::TitleExceedsMaxLength); 28 | } 29 | 30 | return Ok(s.to_string()); 31 | } 32 | 33 | pub fn description(s: &str) -> Result { 34 | if DESCRIPTION_MAX_LEN < s.len() { 35 | return Err(Error::DescriptionExceedsMaxLength); 36 | } 37 | 38 | return Ok(s.to_string()); 39 | } 40 | 41 | pub fn author(s: &str) -> Result> { 42 | validate_csv(s, validate_author) 43 | } 44 | 45 | pub fn discussions_to(s: &str) -> Result { 46 | Ok(Url::parse(s).map_err(|_| Error::MalformedDiscussionsTo)?) 47 | } 48 | 49 | pub fn status(s: &str) -> Result { 50 | Status::from_str(s) 51 | } 52 | 53 | pub fn last_call_deadline(s: &str) -> Result { 54 | NaiveDate::parse_from_str(s, "%Y-%m-%d").map_err(|_| Error::MalformedLastCallDeadline) 55 | } 56 | 57 | pub fn ty(s: &str) -> Result { 58 | Type::from_str(s) 59 | } 60 | 61 | pub fn category(s: &str) -> Result { 62 | Category::from_str(s) 63 | } 64 | 65 | pub fn created(s: &str) -> Result { 66 | NaiveDate::parse_from_str(s, "%Y-%m-%d").map_err(|_| Error::MalformedCreated) 67 | } 68 | pub fn updated(s: &str) -> Result> { 69 | validate_csv(s, |acc, d| { 70 | match NaiveDate::parse_from_str(d, "%Y-%m-%d").map_err(|_| Error::MalformedUpdated) { 71 | Ok(d) => { 72 | acc.push(d); 73 | Ok(()) 74 | } 75 | Err(e) => Err(e), 76 | } 77 | }) 78 | } 79 | 80 | pub fn requires(s: &str) -> Result> { 81 | validate_csv(s, validate_eip) 82 | } 83 | 84 | pub fn withdrawal_reason(s: &str) -> Result { 85 | Ok(s.to_string()) 86 | } 87 | 88 | fn validate_csv, &str) -> Result<()>>(s: &str, f: F) -> Result> { 89 | let csv: Vec<&str> = s.split(",").collect(); 90 | 91 | let mut acc = vec![]; 92 | 93 | for (i, x) in csv.iter().enumerate() { 94 | // the first element never has whitespace, so check trailing whitespace 95 | // all other elements should have only one whitespace at n[0] 96 | if (i == 0 && x.trim_start() != *x) || (i != 0 && x.len() > 2 && x.trim_start() != &x[1..]) 97 | { 98 | return Err(Error::MissingSpaceAfterComma); 99 | } 100 | 101 | if x != &x.trim_end() { 102 | return Err(Error::ExtraWhitespaceBeforeComma); 103 | } 104 | 105 | f(&mut acc, x.trim())?; 106 | } 107 | 108 | Ok(acc) 109 | } 110 | 111 | fn validate_eip(acc: &mut Vec, s: &str) -> Result<()> { 112 | match s.parse() { 113 | Ok(n) => { 114 | if acc.len() != 0 && acc[acc.len() - 1] > n { 115 | Err(Error::OutOfOrderEips) 116 | } else { 117 | acc.push(n); 118 | Ok(()) 119 | } 120 | } 121 | Err(e) => Err(Error::MalformedEipNumber), 122 | } 123 | } 124 | 125 | fn validate_author<'a>(acc: &mut Vec, s: &str) -> Result<()> { 126 | let email_start = s.find('<'); 127 | let email_end = s.find('>'); 128 | 129 | let handle_start = s.find('('); 130 | let handle_end = s.find(')'); 131 | 132 | if email_start.is_some() != email_end.is_some() { 133 | return Err(Error::UnmatchedEmailDelimiter); 134 | } 135 | 136 | if handle_start.is_some() != handle_end.is_some() { 137 | return Err(Error::UnmatchedHandleDelimiter); 138 | } 139 | 140 | if email_start.is_some() == true && handle_start.is_some() == true { 141 | return Err(Error::AuthorHasEmailAndHandle); 142 | } 143 | 144 | if email_start.is_some() { 145 | let start = email_start.unwrap(); 146 | let end = email_end.unwrap(); 147 | 148 | if end != s.len() - 1 { 149 | return Err(Error::TrailingInfoAfterEmail); 150 | } 151 | 152 | let re = Regex::new(r#"(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])"#).unwrap(); 153 | if !re.is_match(&s[start + 1..end]) { 154 | return Err(Error::MalformedEmail); 155 | } 156 | } 157 | 158 | if handle_start.is_some() { 159 | let start = handle_start.unwrap(); 160 | let end = handle_end.unwrap(); 161 | 162 | let re = Regex::new(r#"(^@[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*$)"#).unwrap(); 163 | if !re.is_match(&s[start + 1..end]) { 164 | return Err(Error::MalformedHandle); 165 | } 166 | 167 | if end != s.len() - 1 { 168 | return Err(Error::TrailingInfoAfterHandle); 169 | } 170 | } 171 | 172 | acc.push(s.to_string()); 173 | 174 | Ok(()) 175 | } 176 | 177 | #[cfg(test)] 178 | mod tests { 179 | use super::*; 180 | 181 | #[test] 182 | fn author_invalid_email() { 183 | let a = "John Doe <@handle>"; 184 | let e = validate_author(&mut vec![], a); 185 | assert_eq!(e, Err(Error::MalformedEmail)); 186 | 187 | let a = "John Doe <@handle.com>"; 188 | let e = validate_author(&mut vec![], a); 189 | assert_eq!(e, Err(Error::MalformedEmail)); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /tests/fixtures/preamble-author-email-and-handle.md: -------------------------------------------------------------------------------- 1 | --- 2 | eip: 1 3 | title: A sample proposal 4 | author: John Doe (@johndoe) 5 | discussions-to: https://example.com 6 | status: Draft 7 | type: Standards Track 8 | category: Core 9 | created: 2020-01-01 10 | updated: 2020-01-01 11 | requires: 20, 1337, 2048 12 | --- 13 | 14 | ## Abstract 15 | This is the abstract for the EIP. 16 | 17 | ## Motivation 18 | This is the motivation for the EIP. 19 | 20 | ## Specification 21 | This is the specification for the EIP. 22 | 23 | ## Rationale 24 | This is the rationale for the EIP. 25 | 26 | ## Backwards Compatibility 27 | These are the backwards compatibility concerns for the EIP. 28 | 29 | ## Test Cases 30 | These are the test cases for the EIP. 31 | 32 | ## Implementation 33 | This is the implementation for the EIP. 34 | 35 | ## Security Considerations 36 | These are the security considerations for the EIP. 37 | 38 | ## Copyright 39 | Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). 40 | -------------------------------------------------------------------------------- /tests/fixtures/preamble-author-email-invalid.md: -------------------------------------------------------------------------------- 1 | --- 2 | eip: 1 3 | title: A sample proposal 4 | author: John Doe <@example> 5 | discussions-to: https://example.com 6 | status: Draft 7 | type: Standards Track 8 | category: Core 9 | created: 2020-01-01 10 | updated: 2020-01-01 11 | requires: 20, 1337, 2048 12 | --- 13 | 14 | ## Abstract 15 | This is the abstract for the EIP. 16 | 17 | ## Motivation 18 | This is the motivation for the EIP. 19 | 20 | ## Specification 21 | This is the specification for the EIP. 22 | 23 | ## Rationale 24 | This is the rationale for the EIP. 25 | 26 | ## Backwards Compatibility 27 | These are the backwards compatibility concerns for the EIP. 28 | 29 | ## Test Cases 30 | These are the test cases for the EIP. 31 | 32 | ## Implementation 33 | This is the implementation for the EIP. 34 | 35 | ## Security Considerations 36 | These are the security considerations for the EIP. 37 | 38 | ## Copyright 39 | Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). 40 | -------------------------------------------------------------------------------- /tests/fixtures/preamble-author-email-trailing-info.md: -------------------------------------------------------------------------------- 1 | --- 2 | eip: 1 3 | title: A sample proposal 4 | author: John Doe a 5 | discussions-to: https://example.com 6 | status: Draft 7 | type: Standards Track 8 | category: Core 9 | created: 2020-01-01 10 | updated: 2020-01-01 11 | requires: 20, 1337, 2048 12 | --- 13 | 14 | ## Abstract 15 | This is the abstract for the EIP. 16 | 17 | ## Motivation 18 | This is the motivation for the EIP. 19 | 20 | ## Specification 21 | This is the specification for the EIP. 22 | 23 | ## Rationale 24 | This is the rationale for the EIP. 25 | 26 | ## Backwards Compatibility 27 | These are the backwards compatibility concerns for the EIP. 28 | 29 | ## Test Cases 30 | These are the test cases for the EIP. 31 | 32 | ## Implementation 33 | This is the implementation for the EIP. 34 | 35 | ## Security Considerations 36 | These are the security considerations for the EIP. 37 | 38 | ## Copyright 39 | Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). 40 | -------------------------------------------------------------------------------- /tests/fixtures/preamble-author-email-unmatched.md: -------------------------------------------------------------------------------- 1 | --- 2 | eip: 1 3 | title: A sample proposal 4 | author: John Doe 6 | discussions-to: https://example.com 7 | status: Draft 8 | type: Standards Track 9 | category: Core 10 | created: 2020-01-01 11 | updated: 2019-01-02, 2020-01-01 12 | requires: 20, 1337, 2048 13 | last-call-deadline: 2020-01-01 14 | withdrawal-reason: This proposal doesn't make sense anymore. 15 | --- 16 | 17 | ## Abstract 18 | This is the abstract for the EIP. 19 | 20 | ## Motivation 21 | This is the motivation for the EIP. 22 | 23 | ## Specification 24 | This is the specification for the EIP. 25 | 26 | ## Rationale 27 | This is the rationale for the EIP. 28 | 29 | ## Backwards Compatibility 30 | These are the backwards compatibility concerns for the EIP. 31 | 32 | ## Test Cases 33 | These are the test cases for the EIP. 34 | 35 | ## Implementation 36 | This is the implementation for the EIP. 37 | 38 | ## Security Considerations 39 | These are the security considerations for the EIP. 40 | 41 | ## Copyright 42 | Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). 43 | -------------------------------------------------------------------------------- /tests/tests.rs: -------------------------------------------------------------------------------- 1 | mod utils; 2 | 3 | use utils::{ 4 | test_fixture, test_fixture_exclude_output, test_fixture_valid, test_fixture_valid_custom, 5 | }; 6 | 7 | #[test] 8 | fn valid() { 9 | test_fixture_valid("valid.md"); 10 | } 11 | 12 | #[test] 13 | fn preamble_start_malformed() { 14 | test_fixture("preamble-start-malformed.md", "missing initial"); 15 | } 16 | 17 | #[test] 18 | fn preamble_end_malformed() { 19 | test_fixture("preamble-end-malformed.md", "missing trailing"); 20 | } 21 | 22 | #[test] 23 | fn preamble_has_required_fields() { 24 | test_fixture("preamble-missing-eip.md", "missing EIP"); 25 | test_fixture("preamble-missing-title.md", "missing title"); 26 | test_fixture("preamble-missing-author.md", "missing author"); 27 | test_fixture( 28 | "preamble-missing-discussions-to.md", 29 | "missing discussions-to", 30 | ); 31 | test_fixture("preamble-missing-status.md", "missing status"); 32 | test_fixture("preamble-missing-type.md", "missing type"); 33 | test_fixture("preamble-missing-category.md", "missing category"); 34 | test_fixture_valid("preamble-missing-category-ok.md"); 35 | } 36 | 37 | #[test] 38 | fn preamble_unknown_field() { 39 | test_fixture("preamble-unknown-field.md", "unknown preamble field"); 40 | } 41 | 42 | #[test] 43 | fn preamble_malformed_field() { 44 | test_fixture("preamble-malformed-field.md", "malformed field"); 45 | } 46 | 47 | #[test] 48 | fn preamble_malformed_eip() { 49 | test_fixture( 50 | "preamble-malformed-eip.md", 51 | "EIP should be an unsigned integer", 52 | ); 53 | test_fixture( 54 | "preamble-malformed-eip-signed-int.md", 55 | "EIP should be an unsigned integer", 56 | ); 57 | } 58 | 59 | #[test] 60 | fn preamble_title_too_long() { 61 | test_fixture("preamble-title-too-long.md", "exceeds max length"); 62 | } 63 | 64 | #[test] 65 | fn preamble_invalid_discussions_to() { 66 | test_fixture("preamble-invalid-discussions-to.md", "must be a URL"); 67 | } 68 | 69 | #[test] 70 | fn preamble_status() { 71 | test_fixture_valid_custom("preamble-status-draft.md", "draft: 1"); 72 | test_fixture_valid_custom("preamble-status-review.md", "review: 1"); 73 | test_fixture_valid_custom("preamble-status-last-call.md", "last_call: 1"); 74 | test_fixture_valid_custom("preamble-status-final.md", "final: 1"); 75 | test_fixture_valid_custom("preamble-status-stagnant.md", "stagnant: 1"); 76 | test_fixture_valid_custom("preamble-status-withdrawn.md", "withdrawn: 1"); 77 | test_fixture_valid_custom("preamble-status-living.md", "living: 1"); 78 | test_fixture("preamble-status-invalid.md", "unknown status"); 79 | } 80 | 81 | #[test] 82 | fn preamble_type() { 83 | // TODO: ensure type is *actually* represented properly 84 | test_fixture_valid("preamble-type-standards.md"); 85 | test_fixture_valid("preamble-type-informational.md"); 86 | test_fixture_valid("preamble-type-meta.md"); 87 | test_fixture("preamble-type-invalid.md", "unknown type"); 88 | } 89 | 90 | #[test] 91 | fn preamble_category() { 92 | // TODO: ensure category is *actually* represented properly 93 | test_fixture_valid("preamble-category-core.md"); 94 | test_fixture_valid("preamble-category-networking.md"); 95 | test_fixture_valid("preamble-category-interface.md"); 96 | test_fixture_valid("preamble-category-erc.md"); 97 | test_fixture("preamble-category-invalid.md", "unknown category"); 98 | } 99 | 100 | #[test] 101 | fn preamble_last_call_deadline() { 102 | test_fixture( 103 | "preamble-last-call-deadline-malformed.md", 104 | "malformed last-call-deadline", 105 | ); 106 | } 107 | 108 | #[test] 109 | fn preamble_created() { 110 | test_fixture("preamble-created-malformed.md", "malformed created"); 111 | } 112 | 113 | #[test] 114 | fn preamble_updated() { 115 | test_fixture("preamble-updated-malformed.md", "malformed updated"); 116 | } 117 | 118 | #[test] 119 | fn preamble_requires() { 120 | test_fixture_valid("preamble-requires-single.md"); 121 | test_fixture_valid("preamble-requires-multiple.md"); 122 | test_fixture( 123 | "preamble-requires-malformed.md", 124 | "EIP should be an unsigned integer", 125 | ); 126 | test_fixture("preamble-requires-out-of-order.md", "ascending order"); 127 | test_fixture( 128 | "preamble-requires-no-whitespace.md", 129 | "comma-separated values", 130 | ); 131 | test_fixture( 132 | "preamble-requires-too-much-whitespace.md", 133 | "comma-separated values", 134 | ); 135 | } 136 | 137 | #[test] 138 | fn preamble_author() { 139 | test_fixture( 140 | "preamble-author-email-unmatched.md", 141 | "unmatched email delimiter", 142 | ); 143 | test_fixture( 144 | "preamble-author-handle-unmatched.md", 145 | "unmatched handle delimiter", 146 | ); 147 | test_fixture( 148 | "preamble-author-email-and-handle.md", 149 | "can't include both an email and handle", 150 | ); 151 | test_fixture( 152 | "preamble-author-email-trailing-info.md", 153 | "trailing information after email", 154 | ); 155 | test_fixture( 156 | "preamble-author-handle-trailing-info.md", 157 | "trailing information after handle", 158 | ); 159 | test_fixture("preamble-author-email-invalid.md", "malformed email"); 160 | test_fixture("preamble-author-handle-invalid.md", "malformed handle"); 161 | } 162 | 163 | #[test] 164 | fn preamble_only_error_not_missing_field() { 165 | test_fixture_exclude_output("preamble-title-too-long.md", "missing"); 166 | } 167 | 168 | #[test] 169 | fn preamble_description_too_long() { 170 | test_fixture( 171 | "preamble-description-too-long.md", 172 | "description exceeds max length", 173 | ); 174 | } 175 | -------------------------------------------------------------------------------- /tests/utils.rs: -------------------------------------------------------------------------------- 1 | use assert_cmd::Command; 2 | use predicates::prelude::{predicate::str::contains, PredicateBooleanExt}; 3 | 4 | pub fn test_fixture(f: &str, output: &str) { 5 | let path = vec!["tests/fixtures", f].join("/"); 6 | Command::cargo_bin("eipv") 7 | .expect("eipv binary missing") 8 | .arg(path) 9 | .assert() 10 | .stdout(contains(output)) 11 | .stdout(contains(f).count(1)); 12 | } 13 | 14 | pub fn test_fixture_exclude_output(f: &str, not: &str) { 15 | let path = vec!["tests/fixtures", f].join("/"); 16 | Command::cargo_bin("eipv") 17 | .expect("eipv binary missing") 18 | .arg(path) 19 | .assert() 20 | .stdout(contains(not).not()); 21 | } 22 | 23 | pub fn test_fixture_valid(f: &str) { 24 | let path = vec!["tests/fixtures", f].join("/"); 25 | Command::cargo_bin("eipv") 26 | .expect("eipv binary missing") 27 | .arg(path) 28 | .assert() 29 | .stdout(contains("valid: 1, invalid: 0")); 30 | } 31 | 32 | pub fn test_fixture_valid_custom(f: &str, output: &str) { 33 | let path = vec!["tests/fixtures", f].join("/"); 34 | Command::cargo_bin("eipv") 35 | .expect("eipv binary missing") 36 | .arg(path) 37 | .assert() 38 | .stdout(contains("valid: 1, invalid: 0")) 39 | .stdout(contains(output)); 40 | } 41 | --------------------------------------------------------------------------------