├── .duvet ├── snapshot.txt ├── .gitignore └── config.toml ├── duvet ├── README.md ├── src │ ├── text │ │ ├── snapshots │ │ │ ├── duvet__text__find__tests__empty.snap │ │ │ ├── duvet__text__find__tests__end.snap │ │ │ ├── duvet__text__find__tests__start.snap │ │ │ ├── duvet__text__find__tests__end_2.snap │ │ │ ├── duvet__text__find__tests__middle.snap │ │ │ ├── duvet__text__find__tests__middle_2.snap │ │ │ ├── duvet__text__find__tests__start_2.snap │ │ │ ├── duvet__text__find__tests__hyphenated_haystack.snap │ │ │ ├── duvet__text__find__tests__hyphenated_needle.snap │ │ │ ├── duvet__text__find__tests__punctuation_test.snap │ │ │ └── duvet__text__find__tests__ws_difference.snap │ │ ├── whitespace.rs │ │ └── view.rs │ ├── comment │ │ ├── snapshots │ │ │ ├── duvet__comment__tokenizer__tests__empty.snap │ │ │ ├── duvet__comment__tests__content_without_meta.snap │ │ │ ├── duvet__comment__tests__meta_without_content.snap │ │ │ ├── duvet__comment__tokenizer__tests__only_unnamed.snap │ │ │ ├── duvet__comment__tokenizer__tests__configured.snap │ │ │ ├── duvet__comment__tokenizer__tests__duplicate_meta.snap │ │ │ ├── duvet__comment__tokenizer__tests__basic.snap │ │ │ ├── duvet__comment__tests__missing_new_line.snap │ │ │ ├── duvet__comment__tests__type_citation.snap │ │ │ ├── duvet__comment__tests__type_test.snap │ │ │ ├── duvet__comment__tests__type_todo.snap │ │ │ └── duvet__comment__tests__type_exception.snap │ │ └── tests.rs │ ├── specification │ │ ├── ietf │ │ │ ├── snapshots.tar.gz │ │ │ ├── break_filter.rs │ │ │ └── parser.rs │ │ ├── markdown │ │ │ ├── snapshots │ │ │ │ ├── duvet__specification__markdown__tests__simple__tokens.snap │ │ │ │ ├── duvet__specification__markdown__tests__multi_line_header__tokens.snap │ │ │ │ ├── duvet__specification__markdown__tests__multi_line_header_strong_heading_attrs__tokens.snap │ │ │ │ ├── duvet__specification__markdown__tests__multi_line_header_link_heading_attrs__tokens.snap │ │ │ │ ├── duvet__specification__markdown__tests__simple__tree.snap │ │ │ │ ├── duvet__specification__markdown__tests__multi_line_header__tree.snap │ │ │ │ ├── duvet__specification__markdown__tests__multi_line_header_strong_heading_attrs__tree.snap │ │ │ │ ├── duvet__specification__markdown__tests__multi_line_header_link_heading_attrs__tree.snap │ │ │ │ ├── duvet__specification__markdown__tests__duplicate_sections__tokens.snap │ │ │ │ ├── duvet__specification__markdown__tests__duplicate_sections__tree.snap │ │ │ │ ├── duvet__specification__markdown__tests__heading_attributes__tree.snap │ │ │ │ ├── duvet__specification__markdown__tests__heading_attributes__tokens.snap │ │ │ │ ├── duvet__specification__markdown__tests__multiple__tokens.snap │ │ │ │ ├── duvet__specification__markdown__tests__multiple__tree.snap │ │ │ │ ├── duvet__specification__markdown__tests__simple_tokens.snap │ │ │ │ ├── duvet__specification__markdown__tests__list_example_tokens.snap │ │ │ │ ├── duvet__specification__markdown__tests__list_example__tree.snap │ │ │ │ └── duvet__specification__markdown__tests__list_example__tokens.snap │ │ │ ├── break_filter.rs │ │ │ ├── tests.rs │ │ │ └── parser.rs │ │ ├── ietf.rs │ │ └── markdown.rs │ ├── text.rs │ ├── snapshots │ │ ├── duvet__tests__invalid_section.snap │ │ ├── duvet__tests__invalid_quote.snap │ │ ├── duvet__tests__inner_whitespace.snap │ │ └── duvet__tests__markdown_report.snap │ ├── source.rs │ ├── lib.rs │ ├── extract │ │ ├── tests.rs │ │ └── snapshots │ │ │ └── duvet__extract__tests__esdk_streaming.snap │ ├── main.rs │ ├── comment.rs │ ├── report │ │ ├── html.rs │ │ ├── ci.rs │ │ ├── stats.rs │ │ └── status.rs │ └── config.rs ├── www │ ├── Makefile │ ├── src │ │ ├── link.js │ │ ├── index.js │ │ └── App.js │ ├── public │ │ └── index.html │ ├── .gitignore │ └── package.json └── Cargo.toml ├── .clippy.toml ├── duvet-core ├── .gitignore ├── tests │ ├── line_count │ │ ├── c.txt │ │ ├── a.txt │ │ ├── manifest.txt │ │ └── b.txt │ ├── macros.rs │ └── line_count.rs ├── src │ ├── macro_support.rs │ ├── artifact.rs │ ├── testing.rs │ ├── lib.rs │ ├── env.rs │ ├── dir.rs │ ├── hash.rs │ ├── progress.rs │ ├── contents.rs │ ├── http.rs │ ├── diff.rs │ ├── glob.rs │ ├── vfs.rs │ └── path.rs ├── README.md └── Cargo.toml ├── CODEOWNERS ├── guide ├── .gitignore ├── src │ ├── config.md │ ├── SUMMARY.md │ ├── specifications.md │ ├── introduction.md │ ├── example-config.toml │ ├── reports.md │ └── annotations.md └── book.toml ├── .cargo └── config.toml ├── NOTICE ├── _typos.toml ├── rust-toolchain ├── .gitignore ├── .rustfmt.toml ├── .gitattributes ├── integration ├── s2n-tls.toml ├── aws-encryption-sdk-dafny.toml ├── s2n-quic.toml ├── snapshots │ ├── h3_json.snap │ ├── init-c_json.snap │ ├── init-java_json.snap │ ├── init-rust_json.snap │ ├── s2n-tls_json.snap │ ├── init-python_json.snap │ ├── s2n-quic_json.snap │ ├── init-quick-start_json.snap │ ├── report-markdown_json.snap │ ├── report-ignore-whitespace_json.snap │ ├── aws-encryption-sdk-dafny_json.snap │ ├── extract-compound-requirement_json.snap │ ├── extract-duplicate-requirement_json.snap │ ├── report-relative-spec-path_json.snap │ ├── extract-compound-mixed-requirement_json.snap │ ├── aws-database-encryption-sdk-dynamodb_json.snap │ ├── aws-cryptographic-material-providers-library_json.snap │ ├── init-c.snap │ ├── init-java.snap │ ├── init-rust.snap │ ├── init-python.snap │ ├── report-ignore-whitespace.snap │ ├── extract-duplicate-requirement.snap │ ├── extract-compound-mixed-requirement.snap │ ├── extract-compound-requirement.snap │ ├── report-markdown.snap │ ├── report-relative-spec-path.snap │ ├── report-missing-section_stderr.snap │ ├── report-invalid-quote_stderr.snap │ ├── report-snapshot-missing_stderr.snap │ ├── report-snapshot-out-of-date_stderr.snap │ └── init-quick-start.snap ├── aws-database-encryption-sdk-dynamodb.toml ├── aws-cryptographic-material-providers-library.toml ├── h3.toml ├── init-quick-start.toml ├── extract-compound-requirement.toml ├── extract-compound-mixed-requirement.toml ├── init-rust.toml ├── init-c.toml ├── init-python.toml ├── init-java.toml ├── report-invalid-quote.toml ├── report-missing-section.toml ├── report-snapshot-missing.toml ├── report-ignore-whitespace.toml ├── extract-duplicate-requirement.toml ├── report-markdown.toml ├── report-snapshot-out-of-date.toml └── report-relative-spec-path.toml ├── CODE_OF_CONDUCT.md ├── duvet-macros ├── README.md └── Cargo.toml ├── Cargo.toml ├── xtask ├── src │ ├── changelog.rs │ ├── main.rs │ ├── publish.rs │ ├── build.rs │ ├── args.rs │ ├── checks.rs │ └── guide.rs └── Cargo.toml ├── action.yml ├── .github ├── dependabot.yml ├── workflows │ ├── dependencies.yml │ ├── guide.yml │ └── ci.yml └── config │ └── cargo-deny.toml ├── README.md ├── CHANGELOG.md └── CONTRIBUTING.md /.duvet/snapshot.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /duvet/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /.clippy.toml: -------------------------------------------------------------------------------- 1 | msrv = "1.85.0" 2 | -------------------------------------------------------------------------------- /.duvet/.gitignore: -------------------------------------------------------------------------------- 1 | reports/ 2 | -------------------------------------------------------------------------------- /duvet-core/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @awslabs/duvet-owners 2 | -------------------------------------------------------------------------------- /guide/.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | command/ 3 | -------------------------------------------------------------------------------- /duvet-core/tests/line_count/c.txt: -------------------------------------------------------------------------------- 1 | short 2 | file 3 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | xtask = "run --package xtask --" 3 | -------------------------------------------------------------------------------- /duvet-core/tests/line_count/a.txt: -------------------------------------------------------------------------------- 1 | this 2 | is 3 | a 4 | file 5 | -------------------------------------------------------------------------------- /duvet-core/tests/line_count/manifest.txt: -------------------------------------------------------------------------------- 1 | a.txt 2 | b.txt 3 | c.txt 4 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | -------------------------------------------------------------------------------- /duvet-core/tests/line_count/b.txt: -------------------------------------------------------------------------------- 1 | this 2 | is 3 | another 4 | longer 5 | file 6 | -------------------------------------------------------------------------------- /_typos.toml: -------------------------------------------------------------------------------- 1 | [files] 2 | extend-exclude = ["*.snap", "integration/snapshots/**", "specs/**"] 3 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.85.0" 3 | components = [ "rustc", "clippy", "rustfmt" ] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | Cargo.lock 4 | target 5 | .history 6 | *.snap.new 7 | /duvet/src/specification/ietf/snapshots 8 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | format_macro_matchers = true 3 | imports_granularity = "Crate" 4 | use_field_init_shorthand = true 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | integration/snapshots/*_json.snap filter=lfs diff=lfs merge=lfs -text 2 | **/snapshots.tar.gz filter=lfs diff=lfs merge=lfs 3 | -------------------------------------------------------------------------------- /integration/s2n-tls.toml: -------------------------------------------------------------------------------- 1 | source = { repo = "https://github.com/aws/s2n-tls", version = "v1.5.5" } 2 | cmd = ["bash compliance/generate_report.sh"] 3 | -------------------------------------------------------------------------------- /duvet/src/text/snapshots/duvet__text__find__tests__empty.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/text/find.rs 3 | expression: "find(\"\", \"\")" 4 | --- 5 | None 6 | -------------------------------------------------------------------------------- /duvet/src/comment/snapshots/duvet__comment__tokenizer__tests__empty.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/comment/tokenizer.rs 3 | expression: tokens 4 | --- 5 | [] 6 | -------------------------------------------------------------------------------- /integration/aws-encryption-sdk-dafny.toml: -------------------------------------------------------------------------------- 1 | source = { repo = "https://github.com/aws/aws-encryption-sdk-dafny", version = "v4.1.0" } 2 | cmd = ["make duvet"] 3 | -------------------------------------------------------------------------------- /integration/s2n-quic.toml: -------------------------------------------------------------------------------- 1 | source = { repo = "https://github.com/aws/s2n-quic", version = "005f9461612df4f882b91844fa978a5ee603e9f8" } 2 | cmd = ["bash scripts/compliance"] 3 | -------------------------------------------------------------------------------- /integration/snapshots/h3_json.snap: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:04e431bdbf269446424d927cfe7fd0758df69c9eb05190971193d2d3d3c7174f 3 | size 679794 4 | -------------------------------------------------------------------------------- /integration/snapshots/init-c_json.snap: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:017ff9e279ee86a2a64fefcb72d5f47f614d261fd059463449e066cf3bbd5ee1 3 | size 25629 4 | -------------------------------------------------------------------------------- /integration/snapshots/init-java_json.snap: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:60727deb2e3ced49182c072a9ccf5191a2400b7d1ee949f5434720c4d34bb8f2 3 | size 25655 4 | -------------------------------------------------------------------------------- /integration/snapshots/init-rust_json.snap: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:1ebdd04a504e1c6120641b68e8fa0426cbb316edd4d28d4b4820b1b8d78fff1e 3 | size 25641 4 | -------------------------------------------------------------------------------- /integration/snapshots/s2n-tls_json.snap: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:9cd60cf24f3177f84d866b8206a1703897a22646b0536a9c237d26b03818ccf4 3 | size 3272000 4 | -------------------------------------------------------------------------------- /integration/aws-database-encryption-sdk-dynamodb.toml: -------------------------------------------------------------------------------- 1 | source = { repo = "https://github.com/aws/aws-database-encryption-sdk-dynamodb", version = "v3.7.0" } 2 | cmd = ["make duvet"] 3 | -------------------------------------------------------------------------------- /integration/snapshots/init-python_json.snap: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:f405615de471becd49fdb82363fc00a7d3c8f15edf0a84b81cfdb194d47b42c3 3 | size 25655 4 | -------------------------------------------------------------------------------- /integration/snapshots/s2n-quic_json.snap: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:51e43062ba43379dd13a47e195a8094faaaba394d4f3e04bf00da08f7761e286 3 | size 6286445 4 | -------------------------------------------------------------------------------- /duvet/src/specification/ietf/snapshots.tar.gz: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:2cdfabea8c45af7b7741717788e5dba44fcf10aacd2b6e25468009cb902d966f 3 | size 135902532 4 | -------------------------------------------------------------------------------- /integration/snapshots/init-quick-start_json.snap: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:7c814969aff20660fbd261127ba5cb2f5ea471176b4a6fe276d4617d2147ddc4 3 | size 60608 4 | -------------------------------------------------------------------------------- /integration/snapshots/report-markdown_json.snap: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:dc6a07cab206b14689926a7695a54c239e95a99184889ccfdad74a62550e0103 3 | size 26555 4 | -------------------------------------------------------------------------------- /integration/snapshots/report-ignore-whitespace_json.snap: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:80f549c042ebd0221160d7e921471ebce54300be410fc2d25f7df7a98462e02b 3 | size 26148 4 | -------------------------------------------------------------------------------- /integration/aws-cryptographic-material-providers-library.toml: -------------------------------------------------------------------------------- 1 | source = { repo = "https://github.com/aws/aws-cryptographic-material-providers-library", version = "v1.7.0" } 2 | cmd = ["make duvet"] 3 | -------------------------------------------------------------------------------- /integration/snapshots/aws-encryption-sdk-dafny_json.snap: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:2a20c886afe92e87a135fe58500f329bd6aa8f223ab7fce0b1cc52be7011fe67 3 | size 1250014 4 | -------------------------------------------------------------------------------- /integration/snapshots/extract-compound-requirement_json.snap: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:cd0858f9ea41c87d81c253f75b32002d2e253c0864cec88988465713b0a00551 3 | size 26040 4 | -------------------------------------------------------------------------------- /integration/snapshots/extract-duplicate-requirement_json.snap: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:7ee7c2d2ffcf8d5b08285174eba23c792ca1a3454034058cbdb4acfb846bbad1 3 | size 26212 4 | -------------------------------------------------------------------------------- /integration/snapshots/report-relative-spec-path_json.snap: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:870bd29b572c7a83f851241c9695bfc08a7ed435bad984877f1e068f2b598df0 3 | size 26392 4 | -------------------------------------------------------------------------------- /integration/h3.toml: -------------------------------------------------------------------------------- 1 | source = { repo = "https://github.com/hyperium/h3", version = "c6b92cbca902a62850b72269384fcbc32d30cb96" } 2 | cmd = ["bash ci/compliance/extract.sh", "bash ci/compliance/report.sh"] 3 | -------------------------------------------------------------------------------- /integration/snapshots/extract-compound-mixed-requirement_json.snap: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:6623d51392681c7d319b98d20c72bccc85074351d3ee66fd99daa2f973384da3 3 | size 26014 4 | -------------------------------------------------------------------------------- /duvet-core/src/macro_support.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | pub use ::tokio; 5 | pub use once_cell::sync::OnceCell; 6 | -------------------------------------------------------------------------------- /integration/snapshots/aws-database-encryption-sdk-dynamodb_json.snap: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:8ae0c4411ac861558e1dc3401052a92e51f8d3b1de303da7abeb5b48e7758d3e 3 | size 1165056 4 | -------------------------------------------------------------------------------- /integration/snapshots/aws-cryptographic-material-providers-library_json.snap: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:689da86e0a60f61a921b4ee3167d72d8d1edba6924336431e359d3f8e19cc455 3 | size 1851925 4 | -------------------------------------------------------------------------------- /integration/snapshots/init-c.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: xtask/src/tests.rs 3 | expression: snapshot 4 | --- 5 | SPECIFICATION: [C](my-spec.md) 6 | SECTION: [C](#c) 7 | TEXT[implementation]: C SHOULD be auto-detected. 8 | -------------------------------------------------------------------------------- /guide/src/config.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | Configuration files are written in the [TOML format](https://toml.io/). The following is a quick overview of all settings: 4 | 5 | ```toml 6 | {{#include example-config.toml}} 7 | ``` 8 | -------------------------------------------------------------------------------- /integration/snapshots/init-java.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: xtask/src/tests.rs 3 | expression: snapshot 4 | --- 5 | SPECIFICATION: [Java](my-spec.md) 6 | SECTION: [Java](#java) 7 | TEXT[implementation]: Java SHOULD be auto-detected. 8 | -------------------------------------------------------------------------------- /integration/snapshots/init-rust.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: xtask/src/tests.rs 3 | expression: snapshot 4 | --- 5 | SPECIFICATION: [Rust](my-spec.md) 6 | SECTION: [Rust](#rust) 7 | TEXT[implementation]: Rust SHOULD be auto-detected. 8 | -------------------------------------------------------------------------------- /integration/snapshots/init-python.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: xtask/src/tests.rs 3 | expression: snapshot 4 | --- 5 | SPECIFICATION: [Python](my-spec.md) 6 | SECTION: [Python](#python) 7 | TEXT[implementation]: Python SHOULD be auto-detected. 8 | -------------------------------------------------------------------------------- /duvet/src/text/snapshots/duvet__text__find__tests__end.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/text/find.rs 3 | expression: "find(\"d\", \"a b c d\")" 4 | --- 5 | Some( 6 | ( 7 | 6..7, 8 | Exact, 9 | "d", 10 | ), 11 | ) 12 | -------------------------------------------------------------------------------- /duvet/src/text/snapshots/duvet__text__find__tests__start.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/text/find.rs 3 | expression: "find(\"a\", \"a b c d\")" 4 | --- 5 | Some( 6 | ( 7 | 0..1, 8 | Exact, 9 | "a", 10 | ), 11 | ) 12 | -------------------------------------------------------------------------------- /duvet/src/comment/snapshots/duvet__comment__tests__content_without_meta.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/comment/tests.rs 3 | expression: "parse(\"//=,//#\", r#\"\n //# This is some content without meta\n \"#)" 4 | --- 5 | ( 6 | {}, 7 | [], 8 | ) 9 | -------------------------------------------------------------------------------- /duvet/src/text.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | pub mod find; 5 | pub mod view; 6 | pub mod whitespace; 7 | 8 | pub use find::find; 9 | pub use view::view; 10 | -------------------------------------------------------------------------------- /duvet/src/text/snapshots/duvet__text__find__tests__end_2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/text/find.rs 3 | expression: "find(\"c d\", \"a b c d\")" 4 | --- 5 | Some( 6 | ( 7 | 4..7, 8 | Exact, 9 | "c d", 10 | ), 11 | ) 12 | -------------------------------------------------------------------------------- /duvet/src/text/snapshots/duvet__text__find__tests__middle.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/text/find.rs 3 | expression: "find(\"b\", \"a b c d\")" 4 | --- 5 | Some( 6 | ( 7 | 2..3, 8 | Exact, 9 | "b", 10 | ), 11 | ) 12 | -------------------------------------------------------------------------------- /duvet/src/text/snapshots/duvet__text__find__tests__middle_2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/text/find.rs 3 | expression: "find(\"b c\", \"a b c d\")" 4 | --- 5 | Some( 6 | ( 7 | 2..5, 8 | Exact, 9 | "b c", 10 | ), 11 | ) 12 | -------------------------------------------------------------------------------- /duvet/src/text/snapshots/duvet__text__find__tests__start_2.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/text/find.rs 3 | expression: "find(\"a b\", \"a b c d\")" 4 | --- 5 | Some( 6 | ( 7 | 0..3, 8 | Exact, 9 | "a b", 10 | ), 11 | ) 12 | -------------------------------------------------------------------------------- /integration/snapshots/report-ignore-whitespace.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: xtask/src/tests.rs 3 | expression: snapshot 4 | --- 5 | SPECIFICATION: [Testing](my-spec.md) 6 | SECTION: [Testing](#testing) 7 | TEXT[!SHOULD,implementation]: This SHOULD ignore whitespace. 8 | -------------------------------------------------------------------------------- /integration/snapshots/extract-duplicate-requirement.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: xtask/src/tests.rs 3 | expression: snapshot 4 | snapshot_kind: text 5 | --- 6 | SPECIFICATION: [Testing](my-spec.md) 7 | SECTION: [Testing](#testing) 8 | TEXT[!MUST,implementation]: This MUST deduplicate the requirement. 9 | -------------------------------------------------------------------------------- /duvet/www/Makefile: -------------------------------------------------------------------------------- 1 | public/script.js: node_modules $(wildcard src/*.js) 2 | @rm -rf build 3 | @npm run build 4 | @awk 1 build/static/js/*.js > public/script.js 5 | 6 | node_modules: 7 | @npm install 8 | 9 | dev: node_modules src/result.test.json 10 | @npm start 11 | 12 | .PHONY: dev 13 | -------------------------------------------------------------------------------- /duvet/src/comment/snapshots/duvet__comment__tests__meta_without_content.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/comment/tests.rs 3 | expression: "parse(\"//=,//#\", r#\"\n //= type=todo\n \"#)" 4 | --- 5 | ( 6 | {}, 7 | [ 8 | "comment is missing source specification", 9 | ], 10 | ) 11 | -------------------------------------------------------------------------------- /integration/snapshots/extract-compound-mixed-requirement.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: xtask/src/tests.rs 3 | expression: snapshot 4 | --- 5 | SPECIFICATION: [Testing](my-spec.md) 6 | SECTION: [Testing](#testing) 7 | TEXT[!MUST]: This SHOULD support compound requirements and it MUST deduplicate them for now. 8 | -------------------------------------------------------------------------------- /duvet/src/text/snapshots/duvet__text__find__tests__hyphenated_haystack.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/text/find.rs 3 | expression: "find(\"this is a new-line\", \"this is a new-\\nline\")" 4 | --- 5 | Some( 6 | ( 7 | 0..19, 8 | Exact, 9 | "this is a new-\nline", 10 | ), 11 | ) 12 | -------------------------------------------------------------------------------- /duvet/src/text/snapshots/duvet__text__find__tests__hyphenated_needle.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/text/find.rs 3 | expression: "find(\"this is a new-\\nline\", \"this is a new-line\")" 4 | --- 5 | Some( 6 | ( 7 | 0..18, 8 | Exact, 9 | "this is a new-line", 10 | ), 11 | ) 12 | -------------------------------------------------------------------------------- /integration/snapshots/extract-compound-requirement.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: xtask/src/tests.rs 3 | expression: snapshot 4 | snapshot_kind: text 5 | --- 6 | SPECIFICATION: [Testing](my-spec.md) 7 | SECTION: [Testing](#testing) 8 | TEXT[!SHOULD]: This SHOULD support compound requirements and it SHOULD deduplicate them for now. 9 | -------------------------------------------------------------------------------- /integration/snapshots/report-markdown.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: xtask/src/tests.rs 3 | expression: snapshot 4 | --- 5 | SPECIFICATION: [My spec](my-spec.md) 6 | SECTION: [Testing](#testing) 7 | TEXT[!MUST,implementation]: This quote MUST work 8 | TEXT[implementation]: * with 9 | TEXT[implementation]: * bullets 10 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /integration/snapshots/report-relative-spec-path.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: xtask/src/tests.rs 3 | assertion_line: 302 4 | expression: snapshot 5 | --- 6 | SPECIFICATION: [Test Specification](../specifications/spec.md) 7 | SECTION: [Section 1](#section-1) 8 | TEXT[!MUST,implementation]: This is a test implementation requirement that MUST be annotated. 9 | -------------------------------------------------------------------------------- /duvet/src/text/snapshots/duvet__text__find__tests__punctuation_test.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/text/find.rs 3 | expression: "find(\" Second Sentence. \",\n\" First sentence. Second Sentence. Third Sentence. \")" 4 | --- 5 | Some( 6 | ( 7 | 22..38, 8 | Exact, 9 | "Second Sentence.", 10 | ), 11 | ) 12 | -------------------------------------------------------------------------------- /duvet/src/snapshots/duvet__tests__invalid_section.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/tests.rs 3 | expression: error 4 | --- 5 | × missing section "foo" in my-spec.md 6 | ╭─[src/my-code.rs:2:5] 7 | 1 │ 8 | 2 │ //= my-spec.md#foo 9 | · ───────┬────── 10 | · ╰── referenced here 11 | 3 │ //# This quote MUST NOT work 12 | ╰──── 13 | -------------------------------------------------------------------------------- /duvet/src/comment/snapshots/duvet__comment__tokenizer__tests__only_unnamed.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/comment/tokenizer.rs 3 | expression: tokens 4 | --- 5 | [ 6 | UnnamedMeta { 7 | value: "this is meta", 8 | line: 1, 9 | }, 10 | UnnamedMeta { 11 | value: "this is other meta", 12 | line: 2, 13 | }, 14 | ] 15 | -------------------------------------------------------------------------------- /guide/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Introduction](./introduction.md) 4 | - [Configuration](./config.md) 5 | - [Specifications](./specifications.md) 6 | - [Annotations](./annotations.md) 7 | - [Reports](./reports.md) 8 | - [Commands]() 9 | - [init](./command/init.md) 10 | - [extract](./command/extract.md) 11 | - [report](./command/report.md) 12 | -------------------------------------------------------------------------------- /duvet/src/comment/snapshots/duvet__comment__tokenizer__tests__configured.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/comment/tokenizer.rs 3 | expression: tokens 4 | --- 5 | [ 6 | Meta { 7 | key: "meta", 8 | value: "goes here", 9 | line: 2, 10 | }, 11 | Content { 12 | value: "content goes here", 13 | line: 3, 14 | }, 15 | ] 16 | -------------------------------------------------------------------------------- /duvet/src/comment/snapshots/duvet__comment__tokenizer__tests__duplicate_meta.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/comment/tokenizer.rs 3 | expression: tokens 4 | --- 5 | [ 6 | Meta { 7 | key: "meta", 8 | value: "1", 9 | line: 1, 10 | }, 11 | Meta { 12 | key: "meta", 13 | value: "2", 14 | line: 2, 15 | }, 16 | ] 17 | -------------------------------------------------------------------------------- /duvet/www/src/link.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Link as A } from "react-router-dom"; 5 | import B from "@material-ui/core/Link"; 6 | 7 | export function Link(props) { 8 | if (props.href) return ; 9 | 10 | return ; 11 | } 12 | -------------------------------------------------------------------------------- /duvet/src/snapshots/duvet__tests__invalid_quote.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/tests.rs 3 | expression: error 4 | --- 5 | × could not find text in section "section" of my-spec.md 6 | ╭─[src/my-code.rs:3:5] 7 | 2 │ //= my-spec.md#section 8 | 3 │ //# Here is missing text 9 | · ──────────┬───────── 10 | · ╰── text here 11 | 4 │ 12 | ╰──── 13 | -------------------------------------------------------------------------------- /duvet/www/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | React App 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /.duvet/config.toml: -------------------------------------------------------------------------------- 1 | '$schema' = "https://awslabs.github.io/duvet/config/v0.4.0.json" 2 | 3 | [[source]] 4 | pattern = "duvet/**/*.rs" 5 | 6 | [report.html] 7 | enabled = true 8 | issue-link = "https://github.com/awslabs/duvet/issues" 9 | blob-link = "https://github.com/awslabs/duvet/blob/${{ GITHUB_SHA || 'main' }}" 10 | 11 | [report.json] 12 | enabled = true 13 | 14 | [report.snapshot] 15 | enabled = true 16 | -------------------------------------------------------------------------------- /duvet-core/README.md: -------------------------------------------------------------------------------- 1 | # duvet-core 2 | 3 | This is an internal crate used by [duvet](https://github.com/awslabs/duvet). The API is not currently stable and should not be used directly. 4 | 5 | ## License 6 | 7 | This project is licensed under the [Apache-2.0 License][license-url]. 8 | 9 | [license-badge]: https://img.shields.io/badge/license-apache-blue.svg 10 | [license-url]: https://aws.amazon.com/apache-2-0/ 11 | -------------------------------------------------------------------------------- /integration/init-quick-start.toml: -------------------------------------------------------------------------------- 1 | source = { local = true } 2 | cmd = [ 3 | "duvet init --lang-rust --specification https://www.rfc-editor.org/rfc/rfc2324", 4 | "duvet report", 5 | ] 6 | 7 | [[file]] 8 | path = "src/lib.rs" 9 | contents = """ 10 | //= https://www.rfc-editor.org/rfc/rfc2324#section-2.1.1 11 | //# A coffee pot server MUST accept both the BREW and POST method 12 | //# equivalently. 13 | """ 14 | -------------------------------------------------------------------------------- /duvet-macros/README.md: -------------------------------------------------------------------------------- 1 | # duvet-macros 2 | 3 | This is an internal crate used by [duvet](https://github.com/awslabs/duvet). The API is not currently stable and should not be used directly. 4 | 5 | ## License 6 | 7 | This project is licensed under the [Apache-2.0 License][license-url]. 8 | 9 | [license-badge]: https://img.shields.io/badge/license-apache-blue.svg 10 | [license-url]: https://aws.amazon.com/apache-2-0/ 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["duvet", "duvet-core", "duvet-macros", "xtask"] 3 | resolver = "2" 4 | 5 | [profile.bench] 6 | lto = true 7 | codegen-units = 1 8 | incremental = false 9 | # improve flamegraph information 10 | debug = true 11 | 12 | [profile.fuzz] 13 | inherits = "dev" 14 | opt-level = 3 15 | incremental = false 16 | codegen-units = 1 17 | 18 | [profile.release-debug] 19 | inherits = "dev" 20 | opt-level = 3 21 | -------------------------------------------------------------------------------- /duvet/src/text/snapshots/duvet__text__find__tests__ws_difference.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/text/find.rs 3 | expression: "find(\" this should ignore whitespace differences\",\n\" this should ignore whitespace differences\")" 4 | --- 5 | Some( 6 | ( 7 | 9..85, 8 | Exact, 9 | "this should ignore whitespace differences", 10 | ), 11 | ) 12 | -------------------------------------------------------------------------------- /integration/extract-compound-requirement.toml: -------------------------------------------------------------------------------- 1 | source = { local = true } 2 | cmd = ["duvet report"] 3 | 4 | [[file]] 5 | path = ".duvet/config.toml" 6 | contents = """ 7 | '$schema' = "https://awslabs.github.io/duvet/config/v0.4.0.json" 8 | 9 | [[specification]] 10 | source = "my-spec.md" 11 | """ 12 | 13 | [[file]] 14 | path = "my-spec.md" 15 | contents = """ 16 | # Testing 17 | 18 | This SHOULD support compound requirements and it SHOULD deduplicate them for now. 19 | """ 20 | -------------------------------------------------------------------------------- /integration/extract-compound-mixed-requirement.toml: -------------------------------------------------------------------------------- 1 | source = { local = true } 2 | cmd = ["duvet report"] 3 | 4 | [[file]] 5 | path = ".duvet/config.toml" 6 | contents = """ 7 | '$schema' = "https://awslabs.github.io/duvet/config/v0.4.0.json" 8 | 9 | [[specification]] 10 | source = "my-spec.md" 11 | """ 12 | 13 | [[file]] 14 | path = "my-spec.md" 15 | contents = """ 16 | # Testing 17 | 18 | This SHOULD support compound requirements and it MUST deduplicate them for now. 19 | """ 20 | -------------------------------------------------------------------------------- /integration/init-rust.toml: -------------------------------------------------------------------------------- 1 | source = { local = true } 2 | cmd = ["duvet init", "duvet report"] 3 | 4 | [[file]] 5 | path = "Cargo.toml" 6 | contents = """ 7 | [package] 8 | name = "testing" 9 | version = "0.1.0" 10 | """ 11 | 12 | [[file]] 13 | path = "src/lib.rs" 14 | contents = """ 15 | //= my-spec.md#rust 16 | //# Rust SHOULD be auto-detected. 17 | """ 18 | 19 | [[file]] 20 | path = "my-spec.md" 21 | contents = """ 22 | # Rust 23 | 24 | Rust SHOULD be auto-detected. 25 | """ 26 | -------------------------------------------------------------------------------- /duvet-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "duvet-macros" 3 | version = "0.4.1" 4 | description = "Internal crate used by duvet" 5 | authors = ["Cameron Bytheway "] 6 | edition = "2021" 7 | license = "Apache-2.0" 8 | repository = "https://github.com/awslabs/duvet" 9 | rust-version = "1.85" 10 | 11 | [lib] 12 | proc-macro = true 13 | 14 | [dependencies] 15 | darling = "0.14" 16 | proc-macro2 = "1" 17 | quote = "1" 18 | syn = { version = "1", features = ["full"] } 19 | -------------------------------------------------------------------------------- /integration/init-c.toml: -------------------------------------------------------------------------------- 1 | source = { local = true } 2 | cmd = ["duvet init", "duvet report"] 3 | 4 | [[file]] 5 | path = "CMakeLists.txt" 6 | contents = """ 7 | cmake_minimum_required (VERSION 3.9) 8 | project (testing C) 9 | """ 10 | 11 | [[file]] 12 | path = "src/testing.c" 13 | contents = """ 14 | /** 15 | *= my-spec.md#c 16 | *# C SHOULD be auto-detected. 17 | */ 18 | """ 19 | 20 | [[file]] 21 | path = "my-spec.md" 22 | contents = """ 23 | # C 24 | 25 | C SHOULD be auto-detected. 26 | """ 27 | -------------------------------------------------------------------------------- /integration/init-python.toml: -------------------------------------------------------------------------------- 1 | source = { local = true } 2 | cmd = ["duvet init", "duvet report"] 3 | 4 | [[file]] 5 | path = "pyproject.toml" 6 | contents = """ 7 | [project] 8 | name = "testing" 9 | version = "0.1.0" 10 | """ 11 | 12 | [[file]] 13 | path = "src/testing.py" 14 | contents = """ 15 | ##= my-spec.md#python 16 | ##% Python SHOULD be auto-detected. 17 | """ 18 | 19 | [[file]] 20 | path = "my-spec.md" 21 | contents = """ 22 | # Python 23 | 24 | Python SHOULD be auto-detected. 25 | """ 26 | -------------------------------------------------------------------------------- /duvet/www/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | /src/result.test.json 11 | 12 | # production 13 | /build 14 | package-lock.json 15 | 16 | # misc 17 | .DS_Store 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | public/script.js 28 | -------------------------------------------------------------------------------- /guide/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Cameron Bytheway"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "Duvet" 7 | 8 | [build] 9 | build-dir = "build" 10 | 11 | [output.html] 12 | copy-fonts = true 13 | git-repository-url = "https://github.com/awslabs/duvet" 14 | edit-url-template = "https://github.com/awslabs/duvet/edit/main/book/{path}" 15 | 16 | [output.html.playground] 17 | editable = false 18 | copyable = true 19 | copy-js = true 20 | line-numbers = false 21 | runnable = false 22 | -------------------------------------------------------------------------------- /guide/src/specifications.md: -------------------------------------------------------------------------------- 1 | # Specifications 2 | 3 | Duvet currently supports two specification formats: IETF and Markdown. Specifications using either of these formats will be scanned for requirements using the [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) key words (e.g. `MUST`, `SHOULD`, `MAY`, etc.) and track completion of these requirements. If a specification does not use these key words, or has additional requirements, then [requirement files](./annotations.md#spec) can be provided in the [configuration](./config.md). 4 | -------------------------------------------------------------------------------- /integration/init-java.toml: -------------------------------------------------------------------------------- 1 | source = { local = true } 2 | cmd = ["duvet init", "duvet report"] 3 | 4 | [[file]] 5 | path = "build.gradle" 6 | contents = """ 7 | plugins { 8 | id 'java' 9 | } 10 | 11 | group 'com.example' 12 | version '1.0-SNAPSHOT' 13 | """ 14 | 15 | [[file]] 16 | path = "src/main/HelloDuvet.java" 17 | contents = """ 18 | //= my-spec.md#java 19 | //# Java SHOULD be auto-detected. 20 | """ 21 | 22 | [[file]] 23 | path = "my-spec.md" 24 | contents = """ 25 | # Java 26 | 27 | Java SHOULD be auto-detected. 28 | """ 29 | -------------------------------------------------------------------------------- /xtask/src/changelog.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use crate::Result; 5 | use clap::Parser; 6 | use xshell::{cmd, Shell}; 7 | 8 | #[derive(Debug, Default, Parser)] 9 | pub struct Changelog {} 10 | 11 | impl Changelog { 12 | pub fn run(&self, sh: &Shell) -> Result { 13 | cmd!( 14 | sh, 15 | "npx conventional-changelog-cli -p conventionalcommits -i CHANGELOG.md -s" 16 | ) 17 | .run()?; 18 | 19 | Ok(()) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /integration/report-invalid-quote.toml: -------------------------------------------------------------------------------- 1 | source = { local = true } 2 | cmd = ["duvet report"] 3 | 4 | [[file]] 5 | path = ".duvet/config.toml" 6 | contents = """ 7 | '$schema' = "https://awslabs.github.io/duvet/config/v0.4.0.json" 8 | 9 | [[source]] 10 | pattern = "src/my-code.rs" 11 | 12 | [[specification]] 13 | source = "my-spec.md" 14 | """ 15 | 16 | [[file]] 17 | path = "src/my-code.rs" 18 | contents = """ 19 | //= my-spec.md#section 20 | //# Here is missing text 21 | """ 22 | 23 | [[file]] 24 | path = "my-spec.md" 25 | contents = """ 26 | # Section 27 | 28 | here is a spec 29 | """ 30 | -------------------------------------------------------------------------------- /integration/report-missing-section.toml: -------------------------------------------------------------------------------- 1 | source = { local = true } 2 | cmd = ["duvet report"] 3 | 4 | [[file]] 5 | path = ".duvet/config.toml" 6 | contents = """ 7 | '$schema' = "https://awslabs.github.io/duvet/config/v0.4.0.json" 8 | 9 | [[source]] 10 | pattern = "src/my-code.rs" 11 | 12 | [[specification]] 13 | source = "my-spec.md" 14 | """ 15 | 16 | [[file]] 17 | path = "src/my-code.rs" 18 | contents = """ 19 | //= my-spec.md#foo 20 | //# This quote MUST NOT work 21 | """ 22 | 23 | [[file]] 24 | path = "my-spec.md" 25 | contents = """ 26 | # Section 27 | 28 | here is a spec 29 | """ 30 | -------------------------------------------------------------------------------- /duvet/src/snapshots/duvet__tests__inner_whitespace.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/tests.rs 3 | expression: "out[\"specifications\"][\"my-spec.md\"]" 4 | --- 5 | { 6 | "format": "markdown", 7 | "requirements": [], 8 | "sections": [ 9 | { 10 | "id": "testing", 11 | "lines": [ 12 | [ 13 | [ 14 | [ 15 | 0 16 | ], 17 | 16, 18 | "This SHOULD ignore whitespace." 19 | ] 20 | ] 21 | ], 22 | "title": "Testing" 23 | } 24 | ], 25 | "title": "Testing" 26 | } 27 | -------------------------------------------------------------------------------- /xtask/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xtask" 3 | version = "0.0.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | anyhow = "1" 9 | clap = { version = "4", features = ["derive"] } 10 | insta = { version = "1", features = ["json"] } 11 | serde = { version = "1", features = ["derive"] } 12 | serde_json = "1" 13 | toml = "0.9" 14 | xshell = "0.2" 15 | 16 | [lints.rust.unexpected_cfgs] 17 | level = "warn" 18 | check-cfg = [ 19 | # xshell uses this `cfg` to make rust analyzer highlight the `cmd!` macro arguments 20 | 'cfg(trick_rust_analyzer_into_highlighting_interpolated_bits)', 21 | ] 22 | -------------------------------------------------------------------------------- /xtask/src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use clap::Parser; 5 | use xshell::Shell; 6 | 7 | type Error = anyhow::Error; 8 | type Result = core::result::Result; 9 | 10 | mod args; 11 | mod build; 12 | mod changelog; 13 | mod checks; 14 | mod guide; 15 | mod publish; 16 | mod tests; 17 | 18 | fn main() { 19 | let sh = Shell::new().unwrap(); 20 | if let Err(err) = args::Args::parse().run(&sh) { 21 | eprintln!("{err:?}"); 22 | std::process::exit(1); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: Duvet 2 | description: 'Installs Duvet in the GitHub Actions environment' 3 | inputs: 4 | version: 5 | description: 'Version of Duvet to install' 6 | default: 0.3.0 7 | required: false 8 | runs: 9 | using: "composite" 10 | steps: 11 | - name: Install rust toolchain 12 | id: toolchain 13 | shell: bash 14 | run: | 15 | rustup toolchain install stable 16 | rustup override set stable 17 | 18 | - name: Install Duvet 19 | uses: camshaft/install@v1 20 | with: 21 | crate: duvet 22 | version: ${{ inputs.version }} 23 | 24 | -------------------------------------------------------------------------------- /duvet/src/comment/snapshots/duvet__comment__tokenizer__tests__basic.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/comment/tokenizer.rs 3 | expression: tokens 4 | --- 5 | [ 6 | UnnamedMeta { 7 | value: "thing goes here", 8 | line: 1, 9 | }, 10 | Meta { 11 | key: "meta", 12 | value: "foo", 13 | line: 2, 14 | }, 15 | Meta { 16 | key: "meta2", 17 | value: "bar", 18 | line: 3, 19 | }, 20 | Content { 21 | value: "content goes", 22 | line: 4, 23 | }, 24 | Content { 25 | value: "here", 26 | line: 5, 27 | }, 28 | ] 29 | -------------------------------------------------------------------------------- /integration/report-snapshot-missing.toml: -------------------------------------------------------------------------------- 1 | source = { local = true } 2 | cmd = ["duvet report --ci"] 3 | 4 | [[file]] 5 | path = ".duvet/config.toml" 6 | contents = """ 7 | '$schema' = "https://awslabs.github.io/duvet/config/v0.4.0.json" 8 | 9 | [[source]] 10 | pattern = "src/my-code.rs" 11 | 12 | [[specification]] 13 | source = "my-spec.md" 14 | 15 | [report.snapshot] 16 | enabled = true 17 | """ 18 | 19 | [[file]] 20 | path = "src/my-code.rs" 21 | contents = """ 22 | //= my-spec.md#section 23 | //# here is a spec 24 | """ 25 | 26 | [[file]] 27 | path = "my-spec.md" 28 | contents = """ 29 | # Section 30 | 31 | here is a spec 32 | """ 33 | -------------------------------------------------------------------------------- /integration/report-ignore-whitespace.toml: -------------------------------------------------------------------------------- 1 | source = { local = true } 2 | cmd = ["duvet report"] 3 | 4 | [[file]] 5 | path = ".duvet/config.toml" 6 | contents = """ 7 | '$schema' = "https://awslabs.github.io/duvet/config/v0.4.0.json" 8 | 9 | [[source]] 10 | pattern = "src/my-code.rs" 11 | 12 | [[specification]] 13 | source = "my-spec.md" 14 | """ 15 | 16 | [[file]] 17 | path = "src/my-code.rs" 18 | contents = """ 19 | //= my-spec.md#testing 20 | //# This SHOULD ignore whitespace. 21 | """ 22 | 23 | [[file]] 24 | path = "my-spec.md" 25 | contents = """ 26 | # Testing 27 | 28 | This SHOULD ignore whitespace. 29 | """ 30 | -------------------------------------------------------------------------------- /integration/extract-duplicate-requirement.toml: -------------------------------------------------------------------------------- 1 | source = { local = true } 2 | cmd = ["duvet report"] 3 | 4 | [[file]] 5 | path = ".duvet/config.toml" 6 | contents = """ 7 | '$schema' = "https://awslabs.github.io/duvet/config/v0.4.0.json" 8 | 9 | [[source]] 10 | pattern = "src/my-code.rs" 11 | 12 | [[specification]] 13 | source = "my-spec.md" 14 | """ 15 | 16 | [[file]] 17 | path = "src/my-code.rs" 18 | contents = """ 19 | //= my-spec.md#testing 20 | //# This MUST deduplicate the requirement. 21 | """ 22 | 23 | [[file]] 24 | path = "my-spec.md" 25 | contents = """ 26 | # Testing 27 | 28 | This MUST deduplicate the requirement. 29 | 30 | This MUST deduplicate the requirement. 31 | """ 32 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | # Maintain dependencies for GitHub Actions 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: "daily" 13 | 14 | # Maintain dependencies for cargo 15 | - package-ecosystem: "cargo" 16 | directory: "/" 17 | schedule: 18 | interval: "daily" 19 | -------------------------------------------------------------------------------- /integration/report-markdown.toml: -------------------------------------------------------------------------------- 1 | source = { local = true } 2 | cmd = ["duvet report"] 3 | 4 | [[file]] 5 | path = ".duvet/config.toml" 6 | contents = """ 7 | '$schema' = "https://awslabs.github.io/duvet/config/v0.4.0.json" 8 | 9 | [[source]] 10 | pattern = "src/my-code.rs" 11 | 12 | [[specification]] 13 | source = "my-spec.md" 14 | """ 15 | 16 | [[file]] 17 | path = "src/my-code.rs" 18 | contents = """ 19 | //= my-spec.md#testing 20 | //# This quote MUST work 21 | //# * with 22 | //# * bullets 23 | """ 24 | 25 | [[file]] 26 | path = "my-spec.md" 27 | contents = """ 28 | # My spec 29 | 30 | here is a spec 31 | 32 | ## Testing 33 | 34 | This quote MUST work 35 | * with 36 | * bullets 37 | """ 38 | -------------------------------------------------------------------------------- /xtask/src/publish.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use crate::Result; 5 | use clap::Parser; 6 | use xshell::{cmd, Shell}; 7 | 8 | #[derive(Debug, Default, Parser)] 9 | pub struct Publish {} 10 | 11 | impl Publish { 12 | pub fn run(&self, sh: &Shell) -> Result { 13 | crate::build::Build::default().run(sh)?; 14 | 15 | cmd!(sh, "git diff --exit-code").run()?; 16 | 17 | for pkg in ["duvet-macros", "duvet-core", "duvet"] { 18 | let _dir = sh.push_dir(pkg); 19 | cmd!(sh, "cargo publish --allow-dirty").run()?; 20 | } 21 | 22 | Ok(()) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /duvet/src/specification/markdown/snapshots/duvet__specification__markdown__tests__simple__tokens.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/specification/markdown/tests.rs 3 | expression: "super :: tokens(& contents).collect :: < Vec < _ >> ()" 4 | --- 5 | [ 6 | Content { 7 | value: "", 8 | line: 1, 9 | }, 10 | Section { 11 | id: None, 12 | title: "This is a test", 13 | level: 1, 14 | line: 2, 15 | }, 16 | Content { 17 | value: "", 18 | line: 3, 19 | }, 20 | Content { 21 | value: "Content goes here. Another", 22 | line: 4, 23 | }, 24 | Content { 25 | value: "sentence here.", 26 | line: 5, 27 | }, 28 | ] 29 | -------------------------------------------------------------------------------- /duvet/www/src/index.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { default as React, useEffect } from "react"; 5 | import ReactDOM from "react-dom"; 6 | import { HashRouter, useLocation } from "react-router-dom"; 7 | import App from "./App"; 8 | 9 | function ScrollToTop() { 10 | const { pathname } = useLocation(); 11 | 12 | useEffect(() => { 13 | window.scrollTo(0, 0); 14 | }, [pathname]); 15 | 16 | return null; 17 | } 18 | 19 | ReactDOM.render( 20 | 21 | 22 | 23 | 24 | 25 | , 26 | document.getElementById("root") 27 | ); 28 | -------------------------------------------------------------------------------- /duvet/src/specification/markdown/snapshots/duvet__specification__markdown__tests__multi_line_header__tokens.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/specification/markdown/tests.rs 3 | expression: "super :: tokens(& contents).collect :: < Vec < _ >> ()" 4 | --- 5 | [ 6 | Content { 7 | value: "", 8 | line: 1, 9 | }, 10 | Section { 11 | id: None, 12 | title: "Foo *bar\nbaz*", 13 | level: 1, 14 | line: 2, 15 | }, 16 | Content { 17 | value: "", 18 | line: 5, 19 | }, 20 | Content { 21 | value: "Content goes here. Another", 22 | line: 6, 23 | }, 24 | Content { 25 | value: "sentence here.", 26 | line: 7, 27 | }, 28 | ] 29 | -------------------------------------------------------------------------------- /duvet/src/specification/markdown/break_filter.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use super::tokenizer::Token; 5 | 6 | /// Filters out duplicate breaks 7 | pub fn break_filter>(tokens: T) -> impl Iterator { 8 | let mut state = false; 9 | tokens.filter(move |token| { 10 | let prev_break = core::mem::take(&mut state); 11 | 12 | match token { 13 | Token::Section { .. } | Token::Content { .. } => true, 14 | Token::Break { .. } => { 15 | state = true; 16 | 17 | // dedup breaks 18 | !prev_break 19 | } 20 | } 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /integration/report-snapshot-out-of-date.toml: -------------------------------------------------------------------------------- 1 | source = { local = true } 2 | cmd = ["duvet report --ci"] 3 | 4 | [[file]] 5 | path = ".duvet/config.toml" 6 | contents = """ 7 | '$schema' = "https://awslabs.github.io/duvet/config/v0.4.0.json" 8 | 9 | [[source]] 10 | pattern = "src/my-code.rs" 11 | 12 | [[specification]] 13 | source = "my-spec.md" 14 | 15 | [report.snapshot] 16 | enabled = true 17 | """ 18 | 19 | [[file]] 20 | path = ".duvet/snapshot.txt" 21 | contents = """ 22 | SPECIFICATION: [Section](my-spec.md) 23 | """ 24 | 25 | [[file]] 26 | path = "src/my-code.rs" 27 | contents = """ 28 | //= my-spec.md#section 29 | //# here is a spec 30 | """ 31 | 32 | [[file]] 33 | path = "my-spec.md" 34 | contents = """ 35 | # Section 36 | 37 | here is a spec 38 | """ 39 | -------------------------------------------------------------------------------- /duvet/src/specification/markdown/snapshots/duvet__specification__markdown__tests__multi_line_header_strong_heading_attrs__tokens.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/specification/markdown/tests.rs 3 | expression: "super :: tokens(& contents).collect :: < Vec < _ >> ()" 4 | --- 5 | [ 6 | Content { 7 | value: "", 8 | line: 1, 9 | }, 10 | Section { 11 | id: Some( 12 | "blah", 13 | ), 14 | title: "Foo **bar\nbaz**", 15 | level: 1, 16 | line: 2, 17 | }, 18 | Content { 19 | value: "", 20 | line: 5, 21 | }, 22 | Content { 23 | value: "Content goes here. Another", 24 | line: 6, 25 | }, 26 | Content { 27 | value: "sentence here.", 28 | line: 7, 29 | }, 30 | ] 31 | -------------------------------------------------------------------------------- /duvet/src/specification/markdown/snapshots/duvet__specification__markdown__tests__multi_line_header_link_heading_attrs__tokens.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/specification/markdown/tests.rs 3 | expression: "super :: tokens(& contents).collect :: < Vec < _ >> ()" 4 | --- 5 | [ 6 | Content { 7 | value: "", 8 | line: 1, 9 | }, 10 | Section { 11 | id: Some( 12 | "blah", 13 | ), 14 | title: "Foo **bar\nbaz** [I'm link](http://something)", 15 | level: 1, 16 | line: 2, 17 | }, 18 | Content { 19 | value: "", 20 | line: 5, 21 | }, 22 | Content { 23 | value: "Content goes here. Another", 24 | line: 6, 25 | }, 26 | Content { 27 | value: "sentence here.", 28 | line: 7, 29 | }, 30 | ] 31 | -------------------------------------------------------------------------------- /duvet-core/src/artifact.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use std::path::Path; 5 | 6 | /// Synchronizes a value to the file system 7 | /// 8 | /// When the `CI` environment variable is set, this method asserts the value matches 9 | /// what is on disk. 10 | pub fn sync(path: impl AsRef, value: impl AsRef) { 11 | let path = path.as_ref(); 12 | let value = value.as_ref(); 13 | if std::env::var("CI").is_err() { 14 | if let Some(parent) = path.parent() { 15 | std::fs::create_dir_all(parent).unwrap(); 16 | } 17 | std::fs::write(path, value).unwrap(); 18 | return; 19 | } 20 | 21 | let actual = std::fs::read_to_string(path).unwrap(); 22 | assert_eq!(actual, value); 23 | } 24 | -------------------------------------------------------------------------------- /integration/snapshots/report-missing-section_stderr.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: xtask/src/tests.rs 3 | assertion_line: 284 4 | expression: stderr 5 | --- 6 | $ duvet report 7 | EXIT: Some(1) 8 | Extracting requirements 9 | Extracted requirements from 1 specifications 10 | Scanning sources 11 | Scanned 1 sources 12 | Parsing annotations 13 | Parsed 1 annotations 14 | Loading specifications 15 | Loaded 1 specifications 16 | Mapping sections 17 | Mapped 1 sections 18 | Matching references 19 | × × missing section "foo" in my-spec.md 20 | │ ╭─[src/my-code.rs:1:5] 21 | │ 1 │ //= my-spec.md#foo 22 | │ · ───────┬────── 23 | │ · ╰── referenced here 24 | │ 2 │ //# This quote MUST NOT work 25 | │ ╰──── 26 | │ 27 | │ 28 | ╰─▶ encountered 1 errors 29 | -------------------------------------------------------------------------------- /integration/snapshots/report-invalid-quote_stderr.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: xtask/src/tests.rs 3 | assertion_line: 284 4 | expression: stderr 5 | --- 6 | $ duvet report 7 | EXIT: Some(1) 8 | Extracting requirements 9 | Extracted requirements from 1 specifications 10 | Scanning sources 11 | Scanned 1 sources 12 | Parsing annotations 13 | Parsed 1 annotations 14 | Loading specifications 15 | Loaded 1 specifications 16 | Mapping sections 17 | Mapped 1 sections 18 | Matching references 19 | × × could not find text in section "section" of my-spec.md 20 | │ ╭─[src/my-code.rs:2:5] 21 | │ 1 │ //= my-spec.md#section 22 | │ 2 │ //# Here is missing text 23 | │ · ──────────┬───────── 24 | │ · ╰── text here 25 | │ ╰──── 26 | │ 27 | │ 28 | ╰─▶ encountered 1 errors 29 | -------------------------------------------------------------------------------- /duvet/src/specification/markdown/snapshots/duvet__specification__markdown__tests__simple__tree.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/specification/markdown/tests.rs 3 | expression: "super :: parse(& contents)" 4 | --- 5 | Ok( 6 | Specification { 7 | title: Some( 8 | "This is a test", 9 | ), 10 | sections: [ 11 | Section { 12 | id: "this-is-a-test", 13 | title: "This is a test", 14 | full_title: "This is a test", 15 | lines: [ 16 | Str( 17 | "Content goes here. Another", 18 | ), 19 | Str( 20 | "sentence here.", 21 | ), 22 | ], 23 | }, 24 | ], 25 | format: Markdown, 26 | }, 27 | ) 28 | -------------------------------------------------------------------------------- /.github/workflows/dependencies.yml: -------------------------------------------------------------------------------- 1 | name: dependencies 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - '**/Cargo.toml' 9 | - '**/Cargo.lock' 10 | - '.github/workflows/dependencies.yml' 11 | 12 | pull_request: 13 | branches: 14 | - main 15 | paths: 16 | - '**/Cargo.toml' 17 | - '**/Cargo.lock' 18 | - '.github/workflows/dependencies.yml' 19 | 20 | schedule: 21 | # run every morning at 10am Pacific Time 22 | - cron: '0 17 * * *' 23 | 24 | jobs: 25 | deny: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v6 29 | 30 | - name: "Remove rust-toolchain" 31 | run: rm rust-toolchain 32 | 33 | - uses: EmbarkStudios/cargo-deny-action@v2 34 | with: 35 | command: check --config .github/config/cargo-deny.toml 36 | -------------------------------------------------------------------------------- /duvet/src/specification/markdown/snapshots/duvet__specification__markdown__tests__multi_line_header__tree.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/specification/markdown/tests.rs 3 | expression: "super :: parse(& contents)" 4 | --- 5 | Ok( 6 | Specification { 7 | title: Some( 8 | "Foo *bar baz*", 9 | ), 10 | sections: [ 11 | Section { 12 | id: "foo-bar-baz", 13 | title: "Foo *bar baz*", 14 | full_title: "Foo *bar\nbaz*", 15 | lines: [ 16 | Str( 17 | "Content goes here. Another", 18 | ), 19 | Str( 20 | "sentence here.", 21 | ), 22 | ], 23 | }, 24 | ], 25 | format: Markdown, 26 | }, 27 | ) 28 | -------------------------------------------------------------------------------- /integration/snapshots/report-snapshot-missing_stderr.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: xtask/src/tests.rs 3 | assertion_line: 284 4 | expression: stderr 5 | --- 6 | $ duvet report --ci 7 | EXIT: Some(1) 8 | Extracting requirements 9 | Extracted requirements from 1 specifications 10 | Scanning sources 11 | Scanned 1 sources 12 | Parsing annotations 13 | Parsed 1 annotations 14 | Loading specifications 15 | Loaded 1 specifications 16 | Mapping sections 17 | Mapped 1 sections 18 | Matching references 19 | Matched 1 references 20 | Sorting references 21 | Sorted 1 references 22 | Writing .duvet/reports/report.html 23 | Wrote .duvet/reports/report.html 24 | Checking .duvet/snapshot.txt 25 | × .duvet/snapshot.txt 26 | ╰─▶ Could not read report snapshot. This is required to enforce CI checks. 27 | -------------------------------------------------------------------------------- /duvet/src/specification/markdown/snapshots/duvet__specification__markdown__tests__multi_line_header_strong_heading_attrs__tree.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/specification/markdown/tests.rs 3 | expression: "super :: parse(& contents)" 4 | --- 5 | Ok( 6 | Specification { 7 | title: Some( 8 | "Foo **bar baz**", 9 | ), 10 | sections: [ 11 | Section { 12 | id: "blah", 13 | title: "Foo **bar baz**", 14 | full_title: "Foo **bar\nbaz**", 15 | lines: [ 16 | Str( 17 | "Content goes here. Another", 18 | ), 19 | Str( 20 | "sentence here.", 21 | ), 22 | ], 23 | }, 24 | ], 25 | format: Markdown, 26 | }, 27 | ) 28 | -------------------------------------------------------------------------------- /integration/report-relative-spec-path.toml: -------------------------------------------------------------------------------- 1 | source = { local = true } 2 | cmd = ["duvet report"] 3 | cwd = "project/subdir" 4 | 5 | [[file]] 6 | path = "project/subdir/.duvet/config.toml" 7 | contents = """ 8 | '$schema' = "https://awslabs.github.io/duvet/config/v0.4.0.json" 9 | 10 | [[source]] 11 | pattern = "src/**/*.rs" 12 | 13 | [[specification]] 14 | source = "../specifications/spec.md" 15 | 16 | [report.html] 17 | enabled = true 18 | 19 | [report.snapshot] 20 | enabled = true 21 | """ 22 | 23 | [[file]] 24 | path = "project/subdir/src/lib.rs" 25 | contents = """ 26 | //= ../specifications/spec.md#section-1 27 | //# This is a test implementation requirement that MUST be annotated. 28 | pub fn test() {} 29 | """ 30 | 31 | [[file]] 32 | path = "project/specifications/spec.md" 33 | contents = """ 34 | # Test Specification 35 | 36 | ## Section 1 37 | 38 | This is a test implementation requirement that MUST be annotated. 39 | """ 40 | -------------------------------------------------------------------------------- /duvet/src/comment/snapshots/duvet__comment__tests__missing_new_line.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/comment/tests.rs 3 | expression: "parse(\"//@=,//@#\",\nr#\"\n //@= https://example.com/spec.txt\n //@# Here is my citation\"#)" 4 | --- 5 | ( 6 | { 7 | Annotation { 8 | source: "file.rs", 9 | anno_line: 2, 10 | original_target: "https://example.com/spec.txt", 11 | original_text: "https://example.com/spec.txt\n //@# Here is my citation", 12 | original_quote: "Here is my citation", 13 | anno: Citation, 14 | target: "https://example.com/spec.txt", 15 | quote: "Here is my citation", 16 | comment: "", 17 | manifest_dir: "[CWD]", 18 | level: Auto, 19 | format: Auto, 20 | tracking_issue: "", 21 | feature: "", 22 | tags: {}, 23 | }, 24 | }, 25 | [], 26 | ) 27 | -------------------------------------------------------------------------------- /duvet/src/comment/snapshots/duvet__comment__tests__type_citation.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/comment/tests.rs 3 | expression: "parse(\"//@=,//@#\",\nr#\"\n //@= https://example.com/spec.txt\n //@# Here is my citation\n \"#)" 4 | --- 5 | ( 6 | { 7 | Annotation { 8 | source: "file.rs", 9 | anno_line: 2, 10 | original_target: "https://example.com/spec.txt", 11 | original_text: "https://example.com/spec.txt\n //@# Here is my citation", 12 | original_quote: "Here is my citation", 13 | anno: Citation, 14 | target: "https://example.com/spec.txt", 15 | quote: "Here is my citation", 16 | comment: "", 17 | manifest_dir: "[CWD]", 18 | level: Auto, 19 | format: Auto, 20 | tracking_issue: "", 21 | feature: "", 22 | tags: {}, 23 | }, 24 | }, 25 | [], 26 | ) 27 | -------------------------------------------------------------------------------- /duvet/src/specification/markdown/snapshots/duvet__specification__markdown__tests__multi_line_header_link_heading_attrs__tree.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/specification/markdown/tests.rs 3 | expression: "super :: parse(& contents)" 4 | --- 5 | Ok( 6 | Specification { 7 | title: Some( 8 | "Foo **bar baz** [I'm link](http://something)", 9 | ), 10 | sections: [ 11 | Section { 12 | id: "blah", 13 | title: "Foo **bar baz** [I'm link](http://something)", 14 | full_title: "Foo **bar\nbaz** [I'm link](http://something)", 15 | lines: [ 16 | Str( 17 | "Content goes here. Another", 18 | ), 19 | Str( 20 | "sentence here.", 21 | ), 22 | ], 23 | }, 24 | ], 25 | format: Markdown, 26 | }, 27 | ) 28 | -------------------------------------------------------------------------------- /duvet/src/comment/snapshots/duvet__comment__tests__type_test.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/comment/tests.rs 3 | expression: "parse(\"//@=,//@#\",\nr#\"\n //@= https://example.com/spec.txt\n //@= type=test\n //@# Here is my citation\n \"#)" 4 | --- 5 | ( 6 | { 7 | Annotation { 8 | source: "file.rs", 9 | anno_line: 2, 10 | original_target: "https://example.com/spec.txt", 11 | original_text: "https://example.com/spec.txt\n //@= type=test\n //@# Here is my citation", 12 | original_quote: "Here is my citation", 13 | anno: Test, 14 | target: "https://example.com/spec.txt", 15 | quote: "Here is my citation", 16 | comment: "", 17 | manifest_dir: "[CWD]", 18 | level: Auto, 19 | format: Auto, 20 | tracking_issue: "", 21 | feature: "", 22 | tags: {}, 23 | }, 24 | }, 25 | [], 26 | ) 27 | -------------------------------------------------------------------------------- /duvet/src/specification/markdown/snapshots/duvet__specification__markdown__tests__duplicate_sections__tokens.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/specification/markdown/tests.rs 3 | expression: "super :: tokens(& contents).collect :: < Vec < _ >> ()" 4 | --- 5 | [ 6 | Content { 7 | value: "", 8 | line: 1, 9 | }, 10 | Section { 11 | id: None, 12 | title: "Duplicate header", 13 | level: 1, 14 | line: 2, 15 | }, 16 | Content { 17 | value: "", 18 | line: 3, 19 | }, 20 | Content { 21 | value: "testing 123", 22 | line: 4, 23 | }, 24 | Content { 25 | value: "", 26 | line: 5, 27 | }, 28 | Section { 29 | id: None, 30 | title: "Duplicate header", 31 | level: 2, 32 | line: 6, 33 | }, 34 | Content { 35 | value: "", 36 | line: 7, 37 | }, 38 | Content { 39 | value: "other test", 40 | line: 8, 41 | }, 42 | ] 43 | -------------------------------------------------------------------------------- /duvet-core/src/testing.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | pub fn init_tracing() { 5 | use std::sync::Once; 6 | 7 | static TRACING: Once = Once::new(); 8 | 9 | // make sure this only gets initialized once 10 | TRACING.call_once(|| { 11 | let format = tracing_subscriber::fmt::format() 12 | //.with_level(false) // don't include levels in formatted output 13 | //.with_ansi(false) 14 | .compact(); // Use a less verbose output format. 15 | 16 | let env_filter = tracing_subscriber::EnvFilter::builder() 17 | .with_default_directive(tracing::Level::DEBUG.into()) 18 | .with_env_var("DUVET_LOG") 19 | .from_env() 20 | .unwrap(); 21 | 22 | tracing_subscriber::fmt() 23 | .with_env_filter(env_filter) 24 | .event_format(format) 25 | .with_test_writer() 26 | .init(); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /duvet/src/snapshots/duvet__tests__markdown_report.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/tests.rs 3 | expression: "out[\"specifications\"][\"my-spec.md\"]" 4 | --- 5 | { 6 | "format": "markdown", 7 | "requirements": [], 8 | "sections": [ 9 | { 10 | "id": "my-spec", 11 | "lines": [ 12 | "here is a spec" 13 | ], 14 | "title": "My spec" 15 | }, 16 | { 17 | "id": "testing", 18 | "lines": [ 19 | [ 20 | [ 21 | [ 22 | 0 23 | ], 24 | 16, 25 | "This quote MUST work" 26 | ] 27 | ], 28 | [ 29 | [ 30 | [ 31 | 0 32 | ], 33 | 16, 34 | "* with" 35 | ] 36 | ], 37 | [ 38 | [ 39 | [ 40 | 0 41 | ], 42 | 16, 43 | "* bullets" 44 | ] 45 | ] 46 | ], 47 | "title": "Testing" 48 | } 49 | ], 50 | "title": "My spec" 51 | } 52 | -------------------------------------------------------------------------------- /integration/snapshots/report-snapshot-out-of-date_stderr.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: xtask/src/tests.rs 3 | assertion_line: 284 4 | expression: stderr 5 | --- 6 | $ duvet report --ci 7 | EXIT: Some(1) 8 | Extracting requirements 9 | Extracted requirements from 1 specifications 10 | Scanning sources 11 | Scanned 1 sources 12 | Parsing annotations 13 | Parsed 1 annotations 14 | Loading specifications 15 | Loaded 1 specifications 16 | Mapping sections 17 | Mapped 1 sections 18 | Matching references 19 | Matched 1 references 20 | Sorting references 21 | Sorted 1 references 22 | Writing .duvet/reports/report.html 23 | Wrote .duvet/reports/report.html 24 | Checking .duvet/snapshot.txt 25 | 26 | Differences detected in .duvet/snapshot.txt: 27 | 28 | @@ -1 +1,3 @@ 29 | SPECIFICATION: [Section](my-spec.md) 30 | + SECTION: [Section](#section) 31 | + TEXT[implementation]: here is a spec 32 | × .duvet/snapshot.txt 33 | ╰─▶ Report snapshot does not match with CI mode enabled. 34 | -------------------------------------------------------------------------------- /xtask/src/build.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use crate::Result; 5 | use clap::Parser; 6 | use std::path::PathBuf; 7 | use xshell::{cmd, Shell}; 8 | 9 | #[derive(Debug, Default, Parser)] 10 | pub struct Build { 11 | #[clap(long, default_value = "dev")] 12 | pub profile: String, 13 | } 14 | 15 | impl Build { 16 | pub fn run(&self, sh: &Shell) -> Result { 17 | { 18 | let _dir = sh.push_dir("duvet/www"); 19 | cmd!(sh, "make").run()?; 20 | } 21 | 22 | let args = vec!["--profile".to_string(), self.profile.clone()]; 23 | 24 | cmd!(sh, "cargo build -p duvet {args...}").run()?; 25 | 26 | let path = if self.profile == "dev" { 27 | sh.current_dir().join("target/debug/duvet") 28 | } else { 29 | sh.current_dir() 30 | .join("target") 31 | .join(&self.profile) 32 | .join("duvet") 33 | }; 34 | 35 | Ok(path) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /duvet/src/specification/markdown/snapshots/duvet__specification__markdown__tests__duplicate_sections__tree.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/specification/markdown/tests.rs 3 | expression: "super :: parse(& contents)" 4 | --- 5 | Ok( 6 | Specification { 7 | title: Some( 8 | "Duplicate header", 9 | ), 10 | sections: [ 11 | Section { 12 | id: "duplicate-header", 13 | title: "Duplicate header", 14 | full_title: "Duplicate header", 15 | lines: [ 16 | Str( 17 | "testing 123", 18 | ), 19 | ], 20 | }, 21 | Section { 22 | id: "duplicate-header-1", 23 | title: "Duplicate header", 24 | full_title: "Duplicate header", 25 | lines: [ 26 | Str( 27 | "other test", 28 | ), 29 | ], 30 | }, 31 | ], 32 | format: Markdown, 33 | }, 34 | ) 35 | -------------------------------------------------------------------------------- /duvet-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | #[macro_export] 5 | macro_rules! ensure { 6 | ($cond:expr) => { 7 | ensure!($cond, ()); 8 | }; 9 | ($cond:expr, $otherwise:expr) => { 10 | if !($cond) { 11 | return $otherwise; 12 | } 13 | }; 14 | } 15 | 16 | #[cfg(any(test, feature = "testing"))] 17 | pub mod testing; 18 | 19 | #[cfg(any(test, feature = "testing"))] 20 | pub mod artifact; 21 | mod cache; 22 | pub mod contents; 23 | pub mod diagnostic; 24 | #[cfg(feature = "diff")] 25 | pub mod diff; 26 | pub mod dir; 27 | pub mod env; 28 | pub mod file; 29 | pub mod glob; 30 | pub mod hash; 31 | #[cfg(feature = "http")] 32 | pub mod http; 33 | pub mod path; 34 | pub mod progress; 35 | mod query; 36 | pub mod vfs; 37 | 38 | #[doc(hidden)] 39 | pub mod macro_support; 40 | 41 | pub use ::console; 42 | pub use cache::Cache; 43 | pub use duvet_macros::*; 44 | pub use query::Query; 45 | 46 | pub type Result = core::result::Result; 47 | -------------------------------------------------------------------------------- /duvet/src/comment/snapshots/duvet__comment__tests__type_todo.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/comment/tests.rs 3 | expression: "parse(\"//@=,//@#\",\nr#\"\n //@= https://example.com/spec.txt\n //@= type=todo\n //@= feature=cool-things\n //@= tracking-issue=123\n //@# Here is my citation\n \"#)" 4 | --- 5 | ( 6 | { 7 | Annotation { 8 | source: "file.rs", 9 | anno_line: 2, 10 | original_target: "https://example.com/spec.txt", 11 | original_text: "https://example.com/spec.txt\n //@= type=todo\n //@= feature=cool-things\n //@= tracking-issue=123\n //@# Here is my citation", 12 | original_quote: "Here is my citation", 13 | anno: Todo, 14 | target: "https://example.com/spec.txt", 15 | quote: "Here is my citation", 16 | comment: "", 17 | manifest_dir: "[CWD]", 18 | level: Auto, 19 | format: Auto, 20 | tracking_issue: "123", 21 | feature: "cool-things", 22 | tags: {}, 23 | }, 24 | }, 25 | [], 26 | ) 27 | -------------------------------------------------------------------------------- /duvet/src/source.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use crate::{ 5 | annotation::{AnnotationSet, AnnotationType}, 6 | comment, Error, 7 | }; 8 | use duvet_core::path::Path; 9 | 10 | pub mod toml; 11 | 12 | #[derive(Clone, Debug, PartialEq, PartialOrd, Ord, Eq, Hash)] 13 | pub enum SourceFile { 14 | Text { 15 | pattern: comment::Pattern, 16 | default_type: AnnotationType, 17 | path: Path, 18 | }, 19 | Toml(Path), 20 | } 21 | 22 | impl SourceFile { 23 | pub async fn annotations(&self) -> (AnnotationSet, Vec) { 24 | match self { 25 | Self::Text { 26 | pattern, 27 | default_type, 28 | path, 29 | } => match duvet_core::vfs::read_string(path).await { 30 | Ok(text) => comment::extract(&text, pattern, *default_type), 31 | Err(err) => (Default::default(), vec![err]), 32 | }, 33 | Self::Toml(file) => toml::load(file).await, 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /duvet/src/comment/snapshots/duvet__comment__tests__type_exception.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/comment/tests.rs 3 | expression: "parse(\"//@=,//@#\",\nr#\"\n //@= https://example.com/spec.txt\n //@= type=exception\n //@= reason=This isn't possible currently\n //@# Here is my citation\n \"#)" 4 | --- 5 | ( 6 | { 7 | Annotation { 8 | source: "file.rs", 9 | anno_line: 2, 10 | original_target: "https://example.com/spec.txt", 11 | original_text: "https://example.com/spec.txt\n //@= type=exception\n //@= reason=This isn't possible currently\n //@# Here is my citation", 12 | original_quote: "Here is my citation", 13 | anno: Exception, 14 | target: "https://example.com/spec.txt", 15 | quote: "Here is my citation", 16 | comment: "This isn't possible currently", 17 | manifest_dir: "[CWD]", 18 | level: Auto, 19 | format: Auto, 20 | tracking_issue: "", 21 | feature: "", 22 | tags: {}, 23 | }, 24 | }, 25 | [], 26 | ) 27 | -------------------------------------------------------------------------------- /duvet-core/src/env.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use crate::{diagnostic::IntoDiagnostic, path::Path, Result}; 5 | use core::cell::RefCell; 6 | use once_cell::sync::Lazy; 7 | use std::sync::Arc; 8 | 9 | static GLOBAL_ARGS: Lazy> = Lazy::new(|| std::env::args().collect()); 10 | static GLOBAL_DIR: Lazy> = 11 | Lazy::new(|| std::env::current_dir().map(|v| v.into()).into_diagnostic()); 12 | 13 | thread_local! { 14 | static ARGS: RefCell> = RefCell::new(GLOBAL_ARGS.clone()); 15 | static DIR: RefCell> = RefCell::new(GLOBAL_DIR.clone()); 16 | } 17 | 18 | pub fn args() -> Arc<[String]> { 19 | ARGS.with(|current| current.borrow().clone()) 20 | } 21 | 22 | pub fn set_args(args: Arc<[String]>) { 23 | ARGS.with(|current| *current.borrow_mut() = args); 24 | } 25 | 26 | pub fn current_dir() -> Result { 27 | DIR.with(|current| current.borrow().clone()) 28 | } 29 | 30 | pub fn set_current_dir(dir: Path) { 31 | DIR.with(|current| *current.borrow_mut() = Ok(dir)); 32 | } 33 | -------------------------------------------------------------------------------- /.github/config/cargo-deny.toml: -------------------------------------------------------------------------------- 1 | [advisories] 2 | yanked = "deny" 3 | 4 | [bans] 5 | skip-tree = [ 6 | # all of these are going to be just test dependencies 7 | { name = "insta" }, 8 | ] 9 | 10 | [sources] 11 | unknown-registry = "deny" 12 | unknown-git = "deny" 13 | 14 | [licenses] 15 | confidence-threshold = 0.9 16 | # ignore licenses for private crates 17 | private = { ignore = true } 18 | allow = [ 19 | "Apache-2.0", 20 | "BSD-2-Clause", 21 | "BSD-3-Clause", 22 | "CC0-1.0", 23 | "ISC", 24 | "MIT", 25 | "OpenSSL", 26 | "Unicode-DFS-2016", 27 | "Zlib", 28 | "Unicode-3.0", 29 | ] 30 | 31 | [[licenses.clarify]] 32 | name = "ring" 33 | expression = "MIT AND ISC AND OpenSSL" 34 | license-files = [ 35 | { path = "LICENSE", hash = 0xbd0eed23 }, 36 | ] 37 | 38 | [[licenses.clarify]] 39 | name = "webpki" 40 | expression = "ISC" 41 | license-files = [ 42 | { path = "LICENSE", hash = 0x001c7e6c }, 43 | ] 44 | 45 | [[licenses.clarify]] 46 | name = "encoding_rs" 47 | version = "*" 48 | expression = "(Apache-2.0 OR MIT) AND BSD-3-Clause" 49 | license-files = [ 50 | { path = "COPYRIGHT", hash = 0x39f8ad31 } 51 | ] 52 | -------------------------------------------------------------------------------- /duvet/www/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "duvet-report", 3 | "version": "0.2.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "4", 7 | "@mui/x-data-grid": "4", 8 | "@material-ui/icons": "4", 9 | "clsx": "1", 10 | "copy-to-clipboard": "3", 11 | "react": "17", 12 | "react-dom": "17", 13 | "react-router-dom": "5" 14 | }, 15 | "devDependencies": { 16 | "@testing-library/jest-dom": "5", 17 | "@testing-library/react": "11", 18 | "@testing-library/user-event": "12", 19 | "react-scripts": "5" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test", 25 | "eject": "react-scripts eject" 26 | }, 27 | "eslintConfig": { 28 | "extends": [ 29 | "react-app", 30 | "react-app/jest" 31 | ] 32 | }, 33 | "browserslist": { 34 | "production": [ 35 | ">0.2%", 36 | "not dead", 37 | "not op_mini all" 38 | ], 39 | "development": [ 40 | "last 1 chrome version", 41 | "last 1 firefox version", 42 | "last 1 safari version" 43 | ] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /duvet/src/specification/ietf.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use super::{Format, Line, Section, Specification}; 5 | use crate::Result; 6 | use duvet_core::file::SourceFile; 7 | 8 | pub mod break_filter; 9 | pub mod parser; 10 | pub mod tokenizer; 11 | 12 | #[cfg(test)] 13 | mod tests; 14 | 15 | pub fn parse(contents: &SourceFile) -> Result { 16 | let tokens = tokenizer::tokens(contents); 17 | let tokens = break_filter::break_filter(tokens); 18 | let parser = parser::parse(tokens); 19 | 20 | let sections = parser 21 | .map(|section| { 22 | let id = section.id.to_string(); 23 | 24 | let section = Section { 25 | title: section.title.to_string(), 26 | id: id.clone(), 27 | full_title: section.title, 28 | lines: section.lines.into_iter().map(Line::Str).collect(), 29 | }; 30 | 31 | (id, section) 32 | }) 33 | .collect(); 34 | 35 | Ok(Specification { 36 | title: None, 37 | sections, 38 | format: Format::Ietf, 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /duvet-core/tests/macros.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use duvet_core::{query, Query}; 5 | 6 | #[query] 7 | async fn add(a: Query, b: Query) -> u64 { 8 | let a = *a.get().await; 9 | let b = *b.get().await; 10 | a + b 11 | } 12 | 13 | #[query(cache)] 14 | async fn args() -> Vec { 15 | std::env::args().collect() 16 | } 17 | 18 | #[query(delegate)] 19 | async fn delegate() -> Vec { 20 | args() 21 | } 22 | 23 | #[query(cache)] 24 | async fn add_cache(a: Query, b: Query) -> u64 { 25 | let a = *a.get().await; 26 | let b = *b.get().await; 27 | a + b 28 | } 29 | 30 | #[query(cache)] 31 | async fn mixed_cache(a: Query, b: u64) -> u64 { 32 | let a = *a.get().await; 33 | a + b 34 | } 35 | 36 | #[query(cache)] 37 | async fn ignored_cache(a: Query, b: Query, #[skip] log: bool) -> u64 { 38 | let a = *a.get().await; 39 | let b = *b.get().await; 40 | let value = a + b; 41 | 42 | if log { 43 | dbg!(value); 44 | } 45 | 46 | value 47 | } 48 | 49 | #[query(cache, delegate)] 50 | async fn cache_delegate(a: Query) -> Vec { 51 | if a.await { 52 | args() 53 | } else { 54 | delegate() 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /duvet/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use clap::Parser; 5 | use std::sync::Arc; 6 | 7 | mod annotation; 8 | mod comment; 9 | mod config; 10 | mod extract; 11 | mod init; 12 | mod project; 13 | mod reference; 14 | mod report; 15 | mod source; 16 | mod specification; 17 | mod target; 18 | mod text; 19 | 20 | pub use duvet_core::{diagnostic::Error, Result}; 21 | 22 | #[allow(clippy::large_enum_variant)] 23 | #[derive(Debug, Parser)] 24 | pub enum Arguments { 25 | /// Initializes a duvet project 26 | Init(init::Init), 27 | /// Extracts requirements out of a specification 28 | Extract(extract::Extract), 29 | /// Generates reports for the project 30 | Report(report::Report), 31 | } 32 | 33 | #[duvet_core::query(cache)] 34 | pub async fn arguments() -> Arc { 35 | Arc::new(Arguments::parse()) 36 | } 37 | 38 | impl Arguments { 39 | pub async fn exec(&self) -> Result { 40 | match self { 41 | Self::Init(args) => args.exec().await, 42 | Self::Extract(args) => args.exec().await, 43 | Self::Report(args) => args.exec().await, 44 | } 45 | } 46 | } 47 | 48 | pub async fn run() -> Result { 49 | arguments().await.exec().await?; 50 | Ok(()) 51 | } 52 | -------------------------------------------------------------------------------- /duvet/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "duvet" 3 | version = "0.4.1" 4 | description = "A requirements traceability tool" 5 | authors = [ 6 | "Cameron Bytheway ", 7 | "Ryan Emery ", 8 | ] 9 | edition = "2021" 10 | license = "Apache-2.0" 11 | repository = "https://github.com/awslabs/duvet" 12 | include = ["/src/**/*.rs", "/www/public"] 13 | default-run = "duvet" 14 | rust-version = "1.85" 15 | 16 | [dependencies] 17 | clap = { version = "4", features = ["derive"] } 18 | duvet-core = { version = "0.4", path = "../duvet-core" } 19 | futures = { version = "0.3" } 20 | glob = "0.3" 21 | lazy_static = "1" 22 | mimalloc = { version = "0.1", default-features = false } 23 | once_cell = "1" 24 | pulldown-cmark = { version = "0.13", default-features = false } 25 | regex = "1" 26 | serde = { version = "1", features = ["derive"] } 27 | serde_spanned = "1.0" 28 | slug = { version = "0.1" } 29 | tokio = { version = "1", features = ["macros", "rt-multi-thread"] } 30 | tracing = "0.1" 31 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 32 | triple_accel = "0.4" 33 | url = "2" 34 | v_jsonescape = "0.7" 35 | 36 | [dev-dependencies] 37 | bolero = "0.13" 38 | duvet-core = { version = "0.4", path = "../duvet-core", features = ["testing"] } 39 | insta = { version = "1", features = ["filters", "json"] } 40 | schemars = "1.1" 41 | serde_json = "1" 42 | -------------------------------------------------------------------------------- /duvet-core/src/dir.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use crate::{glob::Glob, path::Path}; 5 | use futures::Stream; 6 | use std::sync::Arc; 7 | 8 | pub mod walk; 9 | 10 | #[derive(Clone)] 11 | pub struct Directory { 12 | pub(crate) path: Path, 13 | pub(crate) contents: Arc<[Path]>, 14 | } 15 | 16 | impl Directory { 17 | pub fn iter(&self) -> impl Iterator { 18 | self.contents.iter() 19 | } 20 | 21 | pub fn walk(&self) -> impl Stream { 22 | walk::dir(self.path.clone()) 23 | } 24 | 25 | pub fn glob(&self, include: Glob, ignore: Glob) -> impl Stream { 26 | walk::glob(self.path.clone(), include, ignore) 27 | } 28 | } 29 | 30 | impl IntoIterator for Directory { 31 | type Item = Path; 32 | type IntoIter = DirIter; 33 | 34 | fn into_iter(self) -> Self::IntoIter { 35 | DirIter { 36 | contents: self.contents, 37 | index: 0, 38 | } 39 | } 40 | } 41 | 42 | pub struct DirIter { 43 | contents: Arc<[Path]>, 44 | index: usize, 45 | } 46 | 47 | impl Iterator for DirIter { 48 | type Item = Path; 49 | 50 | fn next(&mut self) -> Option { 51 | let index = self.index; 52 | self.index += 1; 53 | self.contents.get(index).cloned() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /guide/src/introduction.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Duvet is a tool that establishes a bidirectional link between implementation and specification. This practice is called [requirements traceability](https://en.wikipedia.org/wiki/Requirements_traceability), which is defined as: 4 | 5 | > the ability to describe and follow the life of a requirement in both a forwards and backwards direction (i.e., from its origins, through its development and specification, to its subsequent deployment and use, and through periods of ongoing refinement and iteration in any of these phases) 6 | 7 | ## Quick Start 8 | 9 | Before getting started, Duvet requires a [rust toolchain](https://www.rust-lang.org/tools/install). 10 | 11 | 1. Install command 12 | 13 | ```console 14 | $ cargo install duvet --locked 15 | ``` 16 | 17 | 2. Initialize repository 18 | 19 | In this example, we are using Rust. However, Duvet can be used with any language. 20 | 21 | ```console 22 | $ duvet init --lang-rust --specification https://www.rfc-editor.org/rfc/rfc2324 23 | ``` 24 | 25 | 3. Add a implementation comment in the project 26 | 27 | ```rust 28 | // src/lib.rs 29 | 30 | //= https://www.rfc-editor.org/rfc/rfc2324#section-2.1.1 31 | //# A coffee pot server MUST accept both the BREW and POST method 32 | //# equivalently. 33 | ``` 34 | 35 | 4. Generate a report 36 | 37 | ```console 38 | $ duvet report 39 | ``` 40 | -------------------------------------------------------------------------------- /duvet/src/specification/markdown/snapshots/duvet__specification__markdown__tests__heading_attributes__tree.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/specification/markdown/tests.rs 3 | expression: "super :: parse(& contents)" 4 | --- 5 | Ok( 6 | Specification { 7 | title: Some( 8 | "Heading with ID", 9 | ), 10 | sections: [ 11 | Section { 12 | id: "custom-id", 13 | title: "Heading with ID", 14 | full_title: "Heading with ID", 15 | lines: [ 16 | Str( 17 | "Content under heading with custom ID.", 18 | ), 19 | ], 20 | }, 21 | Section { 22 | id: "another-id", 23 | title: "Another heading", 24 | full_title: "Another heading", 25 | lines: [ 26 | Str( 27 | "More content here.", 28 | ), 29 | ], 30 | }, 31 | Section { 32 | id: "regular-heading", 33 | title: "Regular heading", 34 | full_title: "Regular heading", 35 | lines: [ 36 | Str( 37 | "This heading doesn't have a custom ID.", 38 | ), 39 | ], 40 | }, 41 | ], 42 | format: Markdown, 43 | }, 44 | ) 45 | -------------------------------------------------------------------------------- /duvet-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "duvet-core" 3 | version = "0.4.1" 4 | description = "Internal crate used by duvet" 5 | authors = ["Cameron Bytheway "] 6 | edition = "2021" 7 | license = "Apache-2.0" 8 | repository = "https://github.com/awslabs/duvet" 9 | rust-version = "1.85" 10 | 11 | [features] 12 | default = ["diff", "http"] 13 | diff = ["dep:similar"] 14 | http = ["dep:http", "reqwest"] 15 | testing = ["tracing-subscriber"] 16 | 17 | [dependencies] 18 | blake3 = "1" 19 | bytes = "1" 20 | console = "0.16" 21 | duvet-macros = { version = "0.4", path = "../duvet-macros" } 22 | futures = { version = "0.3", default-features = false } 23 | rustc-hash = "2.1" 24 | globset = "0.4" 25 | http = { version = "1", optional = true } 26 | miette = { version = "7", features = ["fancy"] } 27 | once_cell = "1" 28 | reqwest = { version = "0.12", optional = true, features = ["native-tls"] } 29 | serde = { version = "1", features = ["derive", "rc"] } 30 | serde_json = "1" 31 | similar = { version = "2.6", features = ["inline"], optional = true } 32 | tokio = { version = "1", features = ["fs", "sync"] } 33 | tokio-util = "0.7" 34 | toml_edit = { version = "0.23", features = ["parse", "serde"] } 35 | tracing = "0.1" 36 | tracing-subscriber = { version = "0.3", features = [ 37 | "env-filter", 38 | ], optional = true } 39 | 40 | [dev-dependencies] 41 | tokio = { version = "1", features = ["full"] } 42 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 43 | -------------------------------------------------------------------------------- /duvet/src/specification/markdown/snapshots/duvet__specification__markdown__tests__heading_attributes__tokens.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/specification/markdown/tests.rs 3 | expression: "super :: tokens(& contents).collect :: < Vec < _ >> ()" 4 | --- 5 | [ 6 | Content { 7 | value: "", 8 | line: 1, 9 | }, 10 | Section { 11 | id: Some( 12 | "custom-id", 13 | ), 14 | title: "Heading with ID", 15 | level: 1, 16 | line: 2, 17 | }, 18 | Content { 19 | value: "", 20 | line: 3, 21 | }, 22 | Content { 23 | value: "Content under heading with custom ID.", 24 | line: 4, 25 | }, 26 | Content { 27 | value: "", 28 | line: 5, 29 | }, 30 | Section { 31 | id: Some( 32 | "another-id", 33 | ), 34 | title: "Another heading", 35 | level: 2, 36 | line: 6, 37 | }, 38 | Content { 39 | value: "", 40 | line: 7, 41 | }, 42 | Content { 43 | value: "More content here.", 44 | line: 8, 45 | }, 46 | Content { 47 | value: "", 48 | line: 9, 49 | }, 50 | Section { 51 | id: None, 52 | title: "Regular heading", 53 | level: 1, 54 | line: 10, 55 | }, 56 | Content { 57 | value: "", 58 | line: 11, 59 | }, 60 | Content { 61 | value: "This heading doesn't have a custom ID.", 62 | line: 12, 63 | }, 64 | ] 65 | -------------------------------------------------------------------------------- /duvet/src/extract/tests.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use super::*; 5 | 6 | macro_rules! snapshot_test { 7 | ($name:ident) => { 8 | snapshot_test!($name, ".txt"); 9 | }; 10 | ($name:ident, $ext:expr) => { 11 | #[test] 12 | fn $name() { 13 | let contents = include_str!(concat!( 14 | env!("CARGO_MANIFEST_DIR"), 15 | "/../specs/", 16 | stringify!($name), 17 | $ext, 18 | )); 19 | let path = concat!(stringify!($name), $ext); 20 | let contents = duvet_core::file::SourceFile::new(path, contents).unwrap(); 21 | 22 | let spec = Format::Auto.parse(&contents).unwrap(); 23 | let sections = extract_sections(&spec); 24 | 25 | let results: Vec<_> = sections 26 | .iter() 27 | .flat_map(|(section, features)| { 28 | let id = &*section.id; 29 | features.iter().map(move |feature| (id, feature)) 30 | }) 31 | .collect(); 32 | 33 | insta::assert_debug_snapshot!(stringify!($name), results); 34 | } 35 | }; 36 | } 37 | 38 | snapshot_test!(rfc9000); 39 | snapshot_test!(rfc9001); 40 | snapshot_test!(rfc9114); 41 | snapshot_test!(esdk_client, ".md"); 42 | snapshot_test!(esdk_decrypt, ".md"); 43 | snapshot_test!(esdk_encrypt, ".md"); 44 | snapshot_test!(esdk_streaming, ".md"); 45 | -------------------------------------------------------------------------------- /duvet/src/specification/ietf/break_filter.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use super::tokenizer::{Break, Token}; 5 | 6 | /// Filters out duplicate breaks, headers, and headers misclassified as contents 7 | pub fn break_filter>(tokens: T) -> impl Iterator { 8 | let mut break_ty = None; 9 | tokens.filter(move |token| { 10 | let prev_break = core::mem::take(&mut break_ty); 11 | 12 | match token { 13 | Token::Section { .. } | Token::Appendix { .. } | Token::NamedSection { .. } => {} 14 | Token::Break { ty, .. } => { 15 | break_ty = Some(*ty); 16 | 17 | // dedupe breaks 18 | if prev_break.is_some() { 19 | return false; 20 | } 21 | } 22 | Token::Content { .. } => { 23 | // if we previously had a page break then ignore the next line - it's a header that 24 | // didn't tokenize correctly 25 | if matches!(prev_break, Some(Break::Page)) { 26 | break_ty = Some(Break::Line); 27 | return false; 28 | } 29 | } 30 | Token::Header { value: _, line: _ } => { 31 | // set up a break since we skipped a line 32 | break_ty = Some(Break::Line); 33 | return false; 34 | } 35 | } 36 | 37 | true 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /duvet/src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | #[global_allocator] 5 | static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc; 6 | 7 | fn main() { 8 | let format = tracing_subscriber::fmt::format().compact(); // Use a less verbose output format. 9 | 10 | let env_filter = tracing_subscriber::EnvFilter::builder() 11 | .with_default_directive(tracing::Level::ERROR.into()) 12 | .with_env_var("DUVET_LOG") 13 | .from_env() 14 | .unwrap(); 15 | 16 | tracing_subscriber::fmt() 17 | .with_env_filter(env_filter) 18 | .event_format(format) 19 | .with_test_writer() 20 | .init(); 21 | 22 | let cache = duvet_core::Cache::default(); 23 | let fs = duvet_core::vfs::fs::Fs::default(); 24 | 25 | let runtime = tokio::runtime::Builder::new_current_thread() 26 | .on_thread_start({ 27 | let cache = cache.clone(); 28 | let fs = fs.clone(); 29 | move || { 30 | cache.setup_thread(); 31 | fs.setup_thread(); 32 | } 33 | }) 34 | .enable_all() 35 | // it usually takes longer to spawn threads than complete the program so keep the max low 36 | .max_blocking_threads(8) 37 | .build() 38 | .unwrap(); 39 | 40 | runtime.block_on(async { 41 | if let Err(err) = duvet::run().await { 42 | eprintln!("{err:?}"); 43 | std::process::exit(1); 44 | } 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /xtask/src/args.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use crate::Result; 5 | use clap::Parser; 6 | use xshell::Shell; 7 | 8 | #[derive(Debug, Parser)] 9 | pub enum Args { 10 | Guide(crate::guide::Guide), 11 | Build(crate::build::Build), 12 | Changelog(crate::changelog::Changelog), 13 | Checks(crate::checks::Checks), 14 | Publish(crate::publish::Publish), 15 | Test(crate::tests::Tests), 16 | } 17 | 18 | impl Args { 19 | pub fn run(&self, sh: &Shell) -> Result { 20 | match self { 21 | Args::Guide(args) => args.run(sh), 22 | Args::Build(args) => args.run(sh).map(|_| ()), 23 | Args::Changelog(args) => args.run(sh), 24 | Args::Checks(args) => args.run(sh), 25 | Args::Publish(args) => args.run(sh), 26 | Args::Test(args) => args.run(sh), 27 | } 28 | } 29 | } 30 | 31 | pub trait FlagExt { 32 | fn is_enabled(&self, default: bool) -> bool; 33 | } 34 | 35 | impl FlagExt for Option { 36 | fn is_enabled(&self, default: bool) -> bool { 37 | match self { 38 | Some(v) => *v, 39 | None => default, 40 | } 41 | } 42 | } 43 | 44 | /// Allows for argument flexibility 45 | /// * `duvet` -> default 46 | /// * `duvet --foo` -> true 47 | /// * `duvet --foo=true` -> true 48 | /// * `duvet --foo=false` -> false 49 | impl FlagExt for Option> { 50 | fn is_enabled(&self, default: bool) -> bool { 51 | match self { 52 | Some(Some(v)) => *v, 53 | Some(None) => true, 54 | None => default, 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /duvet/src/comment.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use crate::{ 5 | annotation::{AnnotationSet, AnnotationType}, 6 | Error, Result, 7 | }; 8 | use duvet_core::{error, file::SourceFile}; 9 | use std::sync::Arc; 10 | 11 | pub mod parser; 12 | pub mod tokenizer; 13 | 14 | #[cfg(test)] 15 | mod tests; 16 | 17 | pub fn extract( 18 | file: &SourceFile, 19 | pattern: &Pattern, 20 | default_type: AnnotationType, 21 | ) -> (AnnotationSet, Vec) { 22 | let tokens = tokenizer::tokens(file, pattern); 23 | let mut parser = parser::parse(tokens, default_type); 24 | 25 | let annotations = (&mut parser).map(Arc::new).collect(); 26 | let errors = parser.errors(); 27 | 28 | (Arc::new(annotations), errors) 29 | } 30 | 31 | #[derive(Clone, Debug, PartialEq, PartialOrd, Eq, Ord, Hash)] 32 | pub struct Pattern { 33 | pub meta: Arc, 34 | pub content: Arc, 35 | } 36 | 37 | impl Default for Pattern { 38 | fn default() -> Self { 39 | Self { 40 | meta: "//=".into(), 41 | content: "//#".into(), 42 | } 43 | } 44 | } 45 | 46 | impl Pattern { 47 | pub fn from_arg(arg: &str) -> Result { 48 | let mut parts = arg.split(',').filter(|p| !p.is_empty()); 49 | let meta = parts.next().expect("should have at least one pattern"); 50 | if meta.is_empty() { 51 | return Err(error!("compliance pattern cannot be empty")); 52 | } 53 | 54 | let content = parts.next().unwrap(); 55 | 56 | let meta = meta.into(); 57 | let content = content.into(); 58 | 59 | Ok(Self { meta, content }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /duvet/src/report/html.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use super::ReportResult; 5 | use crate::Result; 6 | use duvet_core::path::Path; 7 | use std::{ 8 | fs::File, 9 | io::{BufWriter, Write}, 10 | }; 11 | 12 | #[rustfmt::skip] // it gets really confused with macros that generate macros 13 | macro_rules! writer { 14 | ($writer:ident) => { 15 | #[allow(unused_macros)] 16 | macro_rules! w { 17 | ($arg: expr) => { 18 | write!($writer, "{}", $arg)? 19 | }; 20 | } 21 | }; 22 | } 23 | 24 | pub fn report(report: &ReportResult, file: &Path) -> Result { 25 | if let Some(parent) = file.parent() { 26 | std::fs::create_dir_all(parent)?; 27 | } 28 | 29 | let mut file = BufWriter::new(File::create(file)?); 30 | 31 | report_writer(report, &mut file) 32 | } 33 | 34 | pub fn report_writer(report: &ReportResult, output: &mut Output) -> Result { 35 | writer!(output); 36 | 37 | w!("\n"); 38 | w!(""); 39 | w!(""); 40 | w!(r#""#); 41 | w!(""); 42 | w!("Compliance Coverage Report"); 43 | w!(""); 44 | 45 | w!(r#""); 48 | w!(""); 49 | w!(""); 50 | w!("
"); 51 | w!(r#""#); 57 | w!(""); 58 | w!(""); 59 | Ok(()) 60 | } 61 | -------------------------------------------------------------------------------- /duvet/src/specification/markdown/snapshots/duvet__specification__markdown__tests__multiple__tokens.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/specification/markdown/tests.rs 3 | expression: "super :: tokens(& contents).collect :: < Vec < _ >> ()" 4 | --- 5 | [ 6 | Content { 7 | value: "", 8 | line: 1, 9 | }, 10 | Section { 11 | id: None, 12 | title: "This is a test", 13 | level: 1, 14 | line: 2, 15 | }, 16 | Content { 17 | value: "", 18 | line: 3, 19 | }, 20 | Content { 21 | value: "Content goes here. Another", 22 | line: 4, 23 | }, 24 | Content { 25 | value: "sentence here.", 26 | line: 5, 27 | }, 28 | Content { 29 | value: "", 30 | line: 6, 31 | }, 32 | Section { 33 | id: None, 34 | title: "This is another test", 35 | level: 2, 36 | line: 7, 37 | }, 38 | Content { 39 | value: "", 40 | line: 8, 41 | }, 42 | Content { 43 | value: "More content goes here", 44 | line: 9, 45 | }, 46 | Content { 47 | value: "", 48 | line: 10, 49 | }, 50 | Section { 51 | id: None, 52 | title: "Nested section", 53 | level: 3, 54 | line: 11, 55 | }, 56 | Content { 57 | value: "", 58 | line: 12, 59 | }, 60 | Content { 61 | value: "Testing 123", 62 | line: 13, 63 | }, 64 | Content { 65 | value: "", 66 | line: 14, 67 | }, 68 | Section { 69 | id: None, 70 | title: "Up one", 71 | level: 2, 72 | line: 15, 73 | }, 74 | Content { 75 | value: "", 76 | line: 16, 77 | }, 78 | Content { 79 | value: "Another section", 80 | line: 17, 81 | }, 82 | ] 83 | -------------------------------------------------------------------------------- /duvet/src/specification/markdown/snapshots/duvet__specification__markdown__tests__multiple__tree.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/specification/markdown/tests.rs 3 | expression: "super :: parse(& contents)" 4 | --- 5 | Ok( 6 | Specification { 7 | title: Some( 8 | "This is a test", 9 | ), 10 | sections: [ 11 | Section { 12 | id: "this-is-a-test", 13 | title: "This is a test", 14 | full_title: "This is a test", 15 | lines: [ 16 | Str( 17 | "Content goes here. Another", 18 | ), 19 | Str( 20 | "sentence here.", 21 | ), 22 | ], 23 | }, 24 | Section { 25 | id: "this-is-another-test", 26 | title: "This is another test", 27 | full_title: "This is another test", 28 | lines: [ 29 | Str( 30 | "More content goes here", 31 | ), 32 | ], 33 | }, 34 | Section { 35 | id: "nested-section", 36 | title: "Nested section", 37 | full_title: "Nested section", 38 | lines: [ 39 | Str( 40 | "Testing 123", 41 | ), 42 | ], 43 | }, 44 | Section { 45 | id: "up-one", 46 | title: "Up one", 47 | full_title: "Up one", 48 | lines: [ 49 | Str( 50 | "Another section", 51 | ), 52 | ], 53 | }, 54 | ], 55 | format: Markdown, 56 | }, 57 | ) 58 | -------------------------------------------------------------------------------- /integration/snapshots/init-quick-start.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: xtask/src/tests.rs 3 | expression: snapshot 4 | --- 5 | SPECIFICATION: https://www.rfc-editor.org/rfc/rfc2324 6 | SECTION: [The BREW method, and the use of POST](#section-2.1.1) 7 | TEXT[!MUST,implementation]: A coffee pot server MUST accept both the BREW and POST method 8 | TEXT[!MUST,implementation]: equivalently. 9 | 10 | SECTION: [406 Not Acceptable](#section-2.3.1) 11 | TEXT[!MAY]: In HTCPCP, this response code MAY be 12 | TEXT[!MAY]: returned if the operator of the coffee pot cannot comply with the 13 | TEXT[!MAY]: Accept-Addition request. 14 | TEXT[!SHOULD]: Unless the request was a HEAD request, the 15 | TEXT[!SHOULD]: response SHOULD include an entity containing a list of available 16 | TEXT[!SHOULD]: coffee additions. 17 | 18 | SECTION: [418 I'm a teapot](#section-2.3.2) 19 | TEXT[!MAY]: The resulting entity body MAY be short and 20 | TEXT[!MAY]: stout. 21 | 22 | SECTION: [The "coffee" URI scheme](#section-3) 23 | TEXT[!MAY]: However, the use 24 | TEXT[!MAY]: of coffee-scheme in various languages MAY be interpreted as an 25 | TEXT[!MAY]: indication of the kind of coffee produced by the coffee pot. 26 | 27 | SECTION: [The "message/coffeepot" media type](#section-4) 28 | TEXT[!MUST]: The entity body of a POST or BREW request MUST be of Content-Type 29 | TEXT[!MUST]: "message/coffeepot". 30 | 31 | SECTION: [Timing Considerations](#section-5.1) 32 | TEXT[!SHOULD]: Coffee pots SHOULD use the Network Time 33 | TEXT[!SHOULD]: Protocol [NTP] to synchronize their clocks to a globally accurate 34 | TEXT[!SHOULD]: time standard. 35 | 36 | SECTION: [Crossing firewalls](#section-5.2) 37 | TEXT[!SHOULD]: Every home computer network SHOULD be protected by a firewall 38 | TEXT[!SHOULD]: from sources of heat. 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Duvet 2 | 3 | Duvet is a tool that establishes a bidirectional link between implementation and specification. This practice is called [requirements traceability](https://en.wikipedia.org/wiki/Requirements_traceability), which is defined as: 4 | 5 | > the ability to describe and follow the life of a requirement in both a forwards and backwards direction (i.e., from its origins, through its development and specification, to its subsequent deployment and use, and through periods of ongoing refinement and iteration in any of these phases) 6 | 7 | ## Quick Start 8 | 9 | Before getting started, Duvet requires a [rust toolchain](https://www.rust-lang.org/tools/install). 10 | 11 | 1. Install command 12 | 13 | ```console 14 | $ cargo install duvet --locked 15 | ``` 16 | 17 | 2. Initialize repository 18 | 19 | In this example, we are using Rust. However, Duvet can be used with any language. 20 | 21 | ```console 22 | $ duvet init --lang-rust --specification https://www.rfc-editor.org/rfc/rfc2324 23 | ``` 24 | 25 | 3. Add a implementation comment in the project 26 | 27 | ```rust 28 | // src/lib.rs 29 | 30 | //= https://www.rfc-editor.org/rfc/rfc2324#section-2.1.1 31 | //# A coffee pot server MUST accept both the BREW and POST method 32 | //# equivalently. 33 | ``` 34 | 35 | 4. Generate a report 36 | 37 | ```console 38 | $ duvet report 39 | ``` 40 | 41 | ## Development 42 | 43 | You must have `git lfs` installed. You can check this by running 44 | ```shell 45 | git lfs version 46 | ``` 47 | 48 | ### Building 49 | 50 | ```console 51 | $ cargo xtask build 52 | ``` 53 | 54 | ### Testing 55 | 56 | ```console 57 | $ cargo xtask test 58 | ``` 59 | 60 | ## Security 61 | 62 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 63 | 64 | ## License 65 | 66 | This project is licensed under the Apache-2.0 License. 67 | -------------------------------------------------------------------------------- /guide/src/example-config.toml: -------------------------------------------------------------------------------- 1 | # Specifies the version of the config 2 | '$schema' = "https://awslabs.github.io/duvet/config/v0.4.json" 3 | 4 | [[source]] 5 | pattern = "src/**/*.rs" # Lists all of the source files to scan 6 | 7 | [[source]] 8 | pattern = "test/**/*.rs" 9 | type = "test" # Sets the default annotation type 10 | 11 | [[source]] 12 | pattern = "src/**/*.py" 13 | type = "implementation" 14 | # Sets the comment style for this group 15 | comment-style = { meta = "##=", content = "##%" } 16 | 17 | # Defines a required specification 18 | [[specification]] 19 | source = "https://www.rfc-editor.org/rfc/rfc2324" # URL to the specification 20 | 21 | [[specification]] 22 | source = "https://www.rfc-editor.org/rfc/rfc9000" # URL to the specification 23 | format = "ietf" # Specifies the format 24 | 25 | [[specification]] 26 | source = "my-specification.md" # Sets the local path to a specification 27 | 28 | # Loads additional requirement files. By default it includes: 29 | # * ".duvet/requirements/**/*.toml", 30 | # * ".duvet/todos/**/*.toml", 31 | # * ".duvet/exceptions/**/*.toml", 32 | [[requirement]] 33 | pattern = ".duvet/implications/**/*.toml" 34 | 35 | [report.html] 36 | enabled = true # Enables the HTML report 37 | path = ".duvet/reports/report.html" # Sets the path to the report output 38 | issue-link = "https://github.com/awslabs/duvet/issues" # Configures issue creation links 39 | blob-link = "https://github.com/awslabs/duvet/blob/main" # Configures source file links 40 | 41 | [report.json] 42 | enabled = true # Enables the JSON report 43 | path = ".duvet/reports/report.html" # Sets the path to the report output 44 | 45 | [report.snapshot] 46 | enabled = true # Enables the snapshot report 47 | path = ".duvet/snapshot.txt" # Sets the path to the report output 48 | -------------------------------------------------------------------------------- /duvet-core/src/hash.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use core::{fmt, ops::Deref}; 5 | 6 | #[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)] 7 | #[repr(transparent)] 8 | pub struct Hash([u8; HASH_LEN]); 9 | 10 | impl fmt::Debug for Hash { 11 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 12 | write!(f, "0x")?; 13 | for byte in &self.0 { 14 | write!(f, "{byte:02x}")?; 15 | } 16 | Ok(()) 17 | } 18 | } 19 | 20 | impl Deref for Hash { 21 | type Target = [u8; HASH_LEN]; 22 | 23 | fn deref(&self) -> &Self::Target { 24 | &self.0 25 | } 26 | } 27 | 28 | impl AsRef<[u8]> for Hash { 29 | fn as_ref(&self) -> &[u8] { 30 | &self.0 31 | } 32 | } 33 | 34 | impl AsRef<[u8; HASH_LEN]> for Hash { 35 | fn as_ref(&self) -> &[u8; HASH_LEN] { 36 | &self.0 37 | } 38 | } 39 | 40 | impl From for Hash { 41 | fn from(value: blake3::Hash) -> Self { 42 | Self(value.into()) 43 | } 44 | } 45 | 46 | pub const HASH_LEN: usize = 32; 47 | 48 | #[derive(Default)] 49 | pub struct Hasher { 50 | inner: blake3::Hasher, 51 | } 52 | 53 | impl Hasher { 54 | pub fn hash>(v: T) -> Hash { 55 | use core::hash::Hasher as _; 56 | let mut hash = Self::default(); 57 | hash.write(v.as_ref()); 58 | hash.finish() 59 | } 60 | 61 | pub fn finish(&self) -> Hash { 62 | self.inner.finalize().into() 63 | } 64 | } 65 | 66 | impl core::hash::Hasher for Hasher { 67 | fn write(&mut self, bytes: &[u8]) { 68 | self.inner.update(bytes); 69 | } 70 | 71 | fn finish(&self) -> u64 { 72 | let mut out = [0; 8]; 73 | let hash = self.inner.finalize(); 74 | out.copy_from_slice(&hash.as_bytes()[..8]); 75 | u64::from_le_bytes(out) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /duvet-core/src/progress.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use console::style; 5 | use core::fmt; 6 | use once_cell::sync::Lazy; 7 | use std::time::Instant; 8 | 9 | static INTERNAL_CI: Lazy = Lazy::new(|| std::env::var("DUVET_INTERNAL_CI").is_ok()); 10 | 11 | #[macro_export] 12 | macro_rules! progress { 13 | ($progress:ident, $fmt:literal $($tt:tt)*) => { 14 | $progress.finish(format_args!($fmt $($tt)*)); 15 | }; 16 | ($fmt:literal $($tt:tt)*) => { 17 | $crate::progress::Progress::new(format_args!($fmt $($tt)*)) 18 | }; 19 | } 20 | 21 | pub struct Progress { 22 | start_time: Instant, 23 | } 24 | 25 | impl Progress { 26 | pub fn new(v: T) -> Self { 27 | let start_time = Instant::now(); 28 | let v = v.to_string(); 29 | if let Some((status, info)) = v.split_once(' ') { 30 | let status = style(status).cyan().bold(); 31 | eprintln!("{status:>12} {info}") 32 | } else { 33 | eprintln!("{v}"); 34 | } 35 | Self { start_time } 36 | } 37 | 38 | pub fn finish(self, v: T) { 39 | let total = self.total_time(); 40 | let total = style(&total).dim(); 41 | 42 | let v = v.to_string(); 43 | if let Some((status, info)) = v.split_once(' ') { 44 | let status = style(status).green().bold(); 45 | eprintln!("{status:>12} {info} {total}") 46 | } else { 47 | eprintln!("{v} {total}"); 48 | } 49 | } 50 | 51 | fn total_time(&self) -> String { 52 | if *INTERNAL_CI { 53 | return String::new(); 54 | } 55 | 56 | let total = self.start_time.elapsed(); 57 | 58 | if total.as_secs() > 0 { 59 | format!("{:.2}s", total.as_secs_f32()) 60 | } else if total.as_millis() > 0 { 61 | format!("{}ms", total.as_millis()) 62 | } else if total.as_micros() > 0 { 63 | format!("{}µs", total.as_micros()) 64 | } else { 65 | format!("{total:?}") 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /guide/src/reports.md: -------------------------------------------------------------------------------- 1 | # Reports 2 | 3 | Duvet provides a `report` command to provide insight into requirement coverage for a project. Each report has its own [configuration](./config.md). 4 | 5 | ## HTML 6 | 7 | The `html` report is enabled by default. It's rendered in a browser and makes it easy to explore all of the specifications being annotated and provides statuses for each requirement. Additionally, the specifications are highlighted with links back to the project's source code, which establishes a bidirectional link between source and specification. 8 | 9 | 10 | 11 | ## Snapshot 12 | 13 | The `snapshot` report provides a mechanism for projects to ensure requirement coverage does not change without explicit approvals. It accomplishes this by writing a simple text file to `.duvet/snapshot.txt` that can be checked against a derived snapshot in the project's CI. If the snapshot stored in the repo doesn't match the derived snapshot, we know there was an unintentional change in requirement coverage and the CI job fails. 14 | 15 | ```console 16 | $ duvet report --ci 17 | EXIT: Some(1) 18 | Extracting requirements 19 | Extracted requirements from 1 specifications 20 | Scanning sources 21 | Scanned 1 sources 22 | Parsing annotations 23 | Parsed 1 annotations 24 | Loading specifications 25 | Loaded 1 specifications 26 | Mapping sections 27 | Mapped 1 sections 28 | Matching references 29 | Matched 1 references 30 | Sorting references 31 | Sorted 1 references 32 | Writing .duvet/snapshot.txt 33 | 34 | Differences detected in .duvet/snapshot.txt: 35 | 36 | @@ -1 +1,3 @@ 37 | SPECIFICATION: [Section](my-spec.md) 38 | + SECTION: [Section](#section) 39 | + TEXT[implementation]: here is a spec 40 | 41 | × .duvet/snapshot.txt 42 | ╰─▶ Report snapshot does not match with CI mode enabled. 43 | ``` 44 | 45 | This is what is known as a "snapshot test". Note that in order for this to work, the `snapshot.txt` file needs to be checked in to the source code's version control system, which ensures that it always tracks the state of the code. 46 | -------------------------------------------------------------------------------- /duvet-core/src/contents.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use crate::hash::{Hash, Hasher, HASH_LEN}; 5 | use core::{fmt, ops::Deref}; 6 | use std::sync::Arc; 7 | 8 | #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 9 | pub struct Contents(Arc<[u8]>); 10 | 11 | impl fmt::Debug for Contents { 12 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 13 | let mut s = f.debug_struct("Contents"); 14 | 15 | s.field("hash", &self.hash()); 16 | 17 | if let Ok(contents) = core::str::from_utf8(self.data()) { 18 | s.field("contents", &contents); 19 | } else { 20 | s.field("contents", &"..."); 21 | } 22 | 23 | s.finish() 24 | } 25 | } 26 | 27 | impl Contents { 28 | pub fn hash(&self) -> &Hash { 29 | self.parts().0 30 | } 31 | 32 | pub fn data(&self) -> &[u8] { 33 | self.parts().1 34 | } 35 | 36 | fn parts(&self) -> (&Hash, &[u8]) { 37 | let ptr = self.0.as_ptr(); 38 | let len = self.0.len() - HASH_LEN; 39 | let hash = unsafe { &*(ptr.add(len) as *const Hash) }; 40 | let data = unsafe { core::slice::from_raw_parts(ptr, len) }; 41 | (hash, data) 42 | } 43 | } 44 | 45 | impl Deref for Contents { 46 | type Target = [u8]; 47 | 48 | fn deref(&self) -> &Self::Target { 49 | self.data() 50 | } 51 | } 52 | 53 | impl AsRef<[u8]> for Contents { 54 | fn as_ref(&self) -> &[u8] { 55 | self 56 | } 57 | } 58 | 59 | impl From> for Contents { 60 | fn from(mut data: Vec) -> Contents { 61 | data.extend_from_slice(&*Hasher::hash(&data)); 62 | Contents(Arc::from(data)) 63 | } 64 | } 65 | 66 | impl From for Contents { 67 | fn from(value: String) -> Self { 68 | value.into_bytes().into() 69 | } 70 | } 71 | 72 | impl From<&[u8]> for Contents { 73 | fn from(value: &[u8]) -> Self { 74 | let mut vec = Vec::with_capacity(value.len() + HASH_LEN); 75 | vec.extend_from_slice(value); 76 | vec.into() 77 | } 78 | } 79 | 80 | impl From<&str> for Contents { 81 | fn from(value: &str) -> Self { 82 | value.as_bytes().into() 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /.github/workflows/guide.yml: -------------------------------------------------------------------------------- 1 | name: guide 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["main"] 8 | 9 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 10 | permissions: 11 | contents: read 12 | pages: write 13 | id-token: write 14 | 15 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 16 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 17 | concurrency: 18 | group: "pages" 19 | cancel-in-progress: false 20 | 21 | jobs: 22 | build: 23 | runs-on: ubuntu-latest 24 | permissions: 25 | contents: write 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v6 29 | 30 | - name: Install rust toolchain 31 | id: toolchain 32 | run: | 33 | rustup toolchain install stable --profile minimal 34 | rustup override set stable 35 | 36 | - name: Install mdBook 37 | uses: camshaft/install@v1 38 | with: 39 | crate: mdbook 40 | version: "^0.4" 41 | 42 | - name: Install taplo 43 | uses: camshaft/install@v1 44 | with: 45 | crate: taplo-cli 46 | bins: "taplo" 47 | 48 | - name: Install typos 49 | uses: camshaft/install@v1 50 | with: 51 | crate: typos-cli 52 | bins: "typos" 53 | 54 | - name: Setup cache 55 | uses: camshaft/rust-cache@v1 56 | 57 | - name: Build book 58 | env: 59 | MDBOOK_OUTPUT__HTML__SITE_url: "/duvet" 60 | run: cargo xtask guide 61 | 62 | - name: Setup Pages 63 | id: pages 64 | if: github.event_name == 'push' 65 | uses: actions/configure-pages@v5 66 | 67 | - name: Upload artifact 68 | uses: actions/upload-pages-artifact@v4 69 | with: 70 | path: ./guide/build 71 | 72 | # Deployment job 73 | deploy: 74 | if: github.event_name == 'push' 75 | environment: 76 | name: github-pages 77 | url: ${{ steps.deployment.outputs.page_url }} 78 | runs-on: ubuntu-latest 79 | needs: build 80 | steps: 81 | - name: Deploy to GitHub Pages 82 | id: deployment 83 | uses: actions/deploy-pages@v4 84 | -------------------------------------------------------------------------------- /xtask/src/checks.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use crate::{args::FlagExt, Result}; 5 | use anyhow::anyhow; 6 | use clap::Parser; 7 | use std::path::Path; 8 | use xshell::{cmd, Shell}; 9 | 10 | #[derive(Debug, Parser)] 11 | pub struct Checks { 12 | #[clap(long)] 13 | enforce_warnings: Option>, 14 | 15 | #[clap(long, default_value = "nightly")] 16 | rustfmt_toolchain: String, 17 | } 18 | 19 | impl Checks { 20 | pub fn run(&self, sh: &Shell) -> Result { 21 | self.copyright(sh)?; 22 | 23 | { 24 | let toolchain = format!("+{}", self.rustfmt_toolchain); 25 | cmd!(sh, "cargo {toolchain} fmt --all -- --check").run()?; 26 | } 27 | 28 | if cmd!(sh, "which typos").quiet().run().is_err() { 29 | cmd!(sh, "cargo install --locked typos-cli").run()?; 30 | } 31 | 32 | cmd!(sh, "typos").run()?; 33 | 34 | crate::build::Build { 35 | profile: "dev".into(), 36 | } 37 | .run(sh)?; 38 | 39 | let mut clippy_args = vec![]; 40 | if self.enforce_warnings.is_enabled(true) { 41 | clippy_args.extend(["-D", "warnings"]); 42 | }; 43 | 44 | cmd!( 45 | sh, 46 | "cargo clippy --all-features --all-targets -- {clippy_args...}" 47 | ) 48 | .run()?; 49 | Ok(()) 50 | } 51 | 52 | fn copyright(&self, sh: &Shell) -> Result { 53 | let files = cmd!(sh, "git ls-tree -r --name-only HEAD").read()?; 54 | 55 | let mut is_ok = true; 56 | 57 | for file in files.lines() { 58 | let file = Path::new(file); 59 | let Some(ext) = file.extension().and_then(|v| v.to_str()) else { 60 | continue; 61 | }; 62 | if !["rs", "js"].contains(&ext) { 63 | continue; 64 | } 65 | let contents = sh.read_file(file)?; 66 | let has_copyright = contents 67 | .lines() 68 | .take(3) 69 | .any(|line| line.contains("Copyright")); 70 | 71 | if !has_copyright { 72 | eprintln!("{} missing copyright header", file.display()); 73 | is_ok = false; 74 | } 75 | } 76 | 77 | if !is_ok { 78 | return Err(anyhow!("Failed copyright check")); 79 | } 80 | 81 | Ok(()) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /duvet/src/specification/markdown.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use super::{Format, Line, Section, Specification}; 5 | use crate::Result; 6 | use duvet_core::file::SourceFile; 7 | use std::collections::{hash_map::Entry, HashMap}; 8 | 9 | pub mod break_filter; 10 | pub mod parser; 11 | pub mod tokenizer; 12 | 13 | #[cfg(test)] 14 | mod tests; 15 | 16 | pub fn parse(contents: &SourceFile) -> Result { 17 | let tokens = tokenizer::tokens(contents); 18 | let tokens = break_filter::break_filter(tokens); 19 | let parser = parser::parse(tokens); 20 | 21 | let mut spec_title = None; 22 | 23 | let mut sections = HashMap::new(); 24 | 25 | for section in parser { 26 | let title = section.title.to_string().replace('\n', " "); 27 | 28 | // set the document title if it's a H1 and we haven't set it yet 29 | if spec_title.is_none() && section.level == 1 { 30 | spec_title = Some(title.clone()); 31 | } 32 | 33 | let section = Section { 34 | title, 35 | id: section.id.to_string(), 36 | full_title: section.title, 37 | lines: section 38 | .lines 39 | .into_iter() 40 | .map(|value| { 41 | if let Some(value) = value { 42 | Line::Str(value) 43 | } else { 44 | Line::Break 45 | } 46 | }) 47 | .collect(), 48 | }; 49 | 50 | insert_section(&mut sections, section); 51 | } 52 | 53 | Ok(Specification { 54 | title: spec_title, 55 | sections, 56 | format: Format::Markdown, 57 | }) 58 | } 59 | 60 | /// Inserts the section into the document, appending a unique ID if needed 61 | fn insert_section(sections: &mut HashMap, mut section: Section) { 62 | let mut counter = 0usize; 63 | 64 | loop { 65 | let key = if counter > 0 { 66 | format!("{}-{counter}", section.id) 67 | } else { 68 | section.id.clone() 69 | }; 70 | 71 | if let Entry::Vacant(entry) = sections.entry(key) { 72 | if section.id != *entry.key() { 73 | section.id = entry.key().clone(); 74 | } 75 | entry.insert(section); 76 | break; 77 | } 78 | 79 | counter += 1; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /duvet/src/report/ci.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use super::{ReportResult, TargetReport}; 5 | use crate::{annotation::AnnotationType, Result}; 6 | use duvet_core::error; 7 | use std::collections::HashSet; 8 | 9 | pub fn report(report: &ReportResult) -> Result { 10 | report 11 | .targets 12 | .iter() 13 | .try_for_each(|(_source, report)| enforce_source(report)) 14 | } 15 | 16 | pub fn enforce_source(report: &TargetReport) -> Result { 17 | let mut cited_lines = HashSet::new(); 18 | let mut tested_lines = HashSet::new(); 19 | let mut significant_lines = HashSet::new(); 20 | 21 | // record all references to specific sections 22 | for reference in &report.references { 23 | let line = reference.line(); 24 | 25 | significant_lines.insert(line); 26 | 27 | match reference.annotation.anno { 28 | AnnotationType::Test => { 29 | tested_lines.insert(line); 30 | } 31 | AnnotationType::Citation => { 32 | cited_lines.insert(line); 33 | } 34 | AnnotationType::Exception => { 35 | // mark exceptions as fully covered 36 | tested_lines.insert(line); 37 | cited_lines.insert(line); 38 | } 39 | AnnotationType::Implication => { 40 | // mark implication as fully covered 41 | tested_lines.insert(line); 42 | cited_lines.insert(line); 43 | } 44 | AnnotationType::Spec | AnnotationType::Todo => {} 45 | } 46 | } 47 | 48 | if report.require_citations { 49 | // Significant lines are not cited. 50 | if significant_lines.difference(&cited_lines).next().is_some() { 51 | return Err(error!("Specification requirements missing citation.")); 52 | } 53 | // Citations that have no significance. 54 | if cited_lines.difference(&significant_lines).next().is_some() { 55 | return Err(error!("Citation for non-existing specification.")); 56 | } 57 | } 58 | 59 | if report.require_tests { 60 | // Cited lines without tests 61 | if cited_lines.difference(&tested_lines).next().is_some() { 62 | return Err(error!("Citation missing test.")); 63 | } 64 | 65 | // Tests without citation 66 | if cited_lines.difference(&tested_lines).next().is_some() { 67 | return Err(error!("Test for non-existing citation.")); 68 | } 69 | } 70 | 71 | Ok(()) 72 | } 73 | -------------------------------------------------------------------------------- /duvet/src/comment/tests.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use super::*; 5 | 6 | fn parse(pattern: &str, value: &str) -> (AnnotationSet, Vec) { 7 | let file = SourceFile::new("file.rs", value).unwrap(); 8 | let pattern = Pattern::from_arg(pattern).unwrap(); 9 | let (annotations, errors) = extract(&file, &pattern, Default::default()); 10 | let errors = errors.into_iter().map(|error| error.to_string()).collect(); 11 | (annotations, errors) 12 | } 13 | 14 | macro_rules! snapshot { 15 | ($name:ident, $value:expr) => { 16 | // use a different pattern so we don't register these tests as part of the duvet report 17 | snapshot!($name, "//@=,//@#", $value); 18 | }; 19 | ($name:ident, $pattern:expr, $value:expr) => { 20 | #[test] 21 | fn $name() { 22 | let mut settings = insta::Settings::clone_current(); 23 | // ignore CWD 24 | settings.add_filter( 25 | &dbg!(duvet_core::env::current_dir() 26 | .unwrap() 27 | .as_ref() 28 | .display() 29 | .to_string() 30 | .replace('/', "\\/")), 31 | "[CWD]", 32 | ); 33 | let _bound = settings.bind_to_scope(); 34 | insta::assert_debug_snapshot!(stringify!($name), parse($pattern, $value)); 35 | } 36 | }; 37 | } 38 | 39 | snapshot!( 40 | content_without_meta, 41 | r#" 42 | //@# This is some content without meta 43 | "# 44 | ); 45 | 46 | snapshot!( 47 | meta_without_content, 48 | r#" 49 | //@= type=todo 50 | "# 51 | ); 52 | 53 | snapshot!( 54 | type_citation, 55 | r#" 56 | //@= https://example.com/spec.txt 57 | //@# Here is my citation 58 | "# 59 | ); 60 | 61 | snapshot!( 62 | type_test, 63 | r#" 64 | //@= https://example.com/spec.txt 65 | //@= type=test 66 | //@# Here is my citation 67 | "# 68 | ); 69 | 70 | snapshot!( 71 | type_todo, 72 | r#" 73 | //@= https://example.com/spec.txt 74 | //@= type=todo 75 | //@= feature=cool-things 76 | //@= tracking-issue=123 77 | //@# Here is my citation 78 | "# 79 | ); 80 | 81 | snapshot!( 82 | type_exception, 83 | r#" 84 | //@= https://example.com/spec.txt 85 | //@= type=exception 86 | //@= reason=This isn't possible currently 87 | //@# Here is my citation 88 | "# 89 | ); 90 | 91 | snapshot!( 92 | missing_new_line, 93 | r#" 94 | //@= https://example.com/spec.txt 95 | //@# Here is my citation"# 96 | ); 97 | -------------------------------------------------------------------------------- /duvet/src/report/stats.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use super::Reference; 5 | use crate::annotation::{AnnotationLevel, AnnotationType}; 6 | 7 | #[derive(Clone, Copy, Debug, Default)] 8 | pub struct Statistics { 9 | pub must: AnnotationStatistics, 10 | pub should: AnnotationStatistics, 11 | pub may: AnnotationStatistics, 12 | } 13 | 14 | impl Statistics { 15 | #[allow(dead_code)] 16 | pub(super) fn record(&mut self, reference: &Reference) { 17 | match reference.annotation.level { 18 | AnnotationLevel::Auto => { 19 | // don't record auto references 20 | } 21 | AnnotationLevel::Must => { 22 | self.must.record(reference); 23 | } 24 | AnnotationLevel::Should => { 25 | self.should.record(reference); 26 | } 27 | AnnotationLevel::May => { 28 | self.may.record(reference); 29 | } 30 | } 31 | } 32 | } 33 | 34 | #[derive(Clone, Copy, Debug, Default)] 35 | pub struct AnnotationStatistics { 36 | pub total: Stat, 37 | pub citations: Stat, 38 | pub tests: Stat, 39 | pub exceptions: Stat, 40 | pub todos: Stat, 41 | pub implications: Stat, 42 | } 43 | 44 | impl AnnotationStatistics { 45 | #[allow(dead_code)] 46 | fn record(&mut self, reference: &Reference) { 47 | self.total.record(reference); 48 | match reference.annotation.anno { 49 | AnnotationType::Citation => { 50 | self.citations.record(reference); 51 | } 52 | AnnotationType::Test => { 53 | self.tests.record(reference); 54 | } 55 | AnnotationType::Exception => { 56 | self.exceptions.record(reference); 57 | } 58 | AnnotationType::Todo => { 59 | self.todos.record(reference); 60 | } 61 | AnnotationType::Implication => { 62 | self.implications.record(reference); 63 | } 64 | AnnotationType::Spec => { 65 | // do nothing, it's just a reference 66 | } 67 | } 68 | } 69 | } 70 | 71 | #[derive(Clone, Copy, Debug, Default)] 72 | pub struct Stat { 73 | pub range: u64, 74 | pub lines: u64, 75 | cursor: u64, 76 | } 77 | 78 | impl Stat { 79 | fn record(&mut self, reference: &Reference) { 80 | let start = reference.start() as u64; 81 | let end = reference.end() as u64; 82 | let len = end - start.max(self.cursor); 83 | if len > 0 { 84 | self.range += len; 85 | self.lines += 1; 86 | } 87 | self.cursor = end; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /duvet/src/specification/markdown/snapshots/duvet__specification__markdown__tests__simple_tokens.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/specification/markdown/tests.rs 3 | assertion_line: 20 4 | expression: "tokens(r#\"\n# This is a test\n\nContent goes here\n\n## This is another test\n\nMore content goes here\n\n### Nested section\n\nTesting 123\n\n## Up one\n\nAnother section\n\"#)" 5 | --- 6 | [ 7 | Line( 8 | Str { 9 | value: "", 10 | pos: 0, 11 | line: 1, 12 | }, 13 | ), 14 | Header( 15 | Str { 16 | value: "# This is a test", 17 | pos: 1, 18 | line: 2, 19 | }, 20 | ), 21 | Line( 22 | Str { 23 | value: "", 24 | pos: 18, 25 | line: 3, 26 | }, 27 | ), 28 | Line( 29 | Str { 30 | value: "Content goes here", 31 | pos: 19, 32 | line: 4, 33 | }, 34 | ), 35 | Line( 36 | Str { 37 | value: "", 38 | pos: 37, 39 | line: 5, 40 | }, 41 | ), 42 | Header( 43 | Str { 44 | value: "## This is another test", 45 | pos: 38, 46 | line: 6, 47 | }, 48 | ), 49 | Line( 50 | Str { 51 | value: "", 52 | pos: 62, 53 | line: 7, 54 | }, 55 | ), 56 | Line( 57 | Str { 58 | value: "More content goes here", 59 | pos: 63, 60 | line: 8, 61 | }, 62 | ), 63 | Line( 64 | Str { 65 | value: "", 66 | pos: 86, 67 | line: 9, 68 | }, 69 | ), 70 | Header( 71 | Str { 72 | value: "### Nested section", 73 | pos: 87, 74 | line: 10, 75 | }, 76 | ), 77 | Line( 78 | Str { 79 | value: "", 80 | pos: 106, 81 | line: 11, 82 | }, 83 | ), 84 | Line( 85 | Str { 86 | value: "Testing 123", 87 | pos: 107, 88 | line: 12, 89 | }, 90 | ), 91 | Line( 92 | Str { 93 | value: "", 94 | pos: 119, 95 | line: 13, 96 | }, 97 | ), 98 | Header( 99 | Str { 100 | value: "## Up one", 101 | pos: 120, 102 | line: 14, 103 | }, 104 | ), 105 | Line( 106 | Str { 107 | value: "", 108 | pos: 130, 109 | line: 15, 110 | }, 111 | ), 112 | Line( 113 | Str { 114 | value: "Another section", 115 | pos: 131, 116 | line: 16, 117 | }, 118 | ), 119 | ] 120 | -------------------------------------------------------------------------------- /duvet-core/src/http.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use crate::{ 5 | contents::Contents, 6 | diagnostic::IntoDiagnostic, 7 | file::{BinaryFile, SourceFile}, 8 | path::Path, 9 | Cache, Query, Result, 10 | }; 11 | use std::sync::Arc; 12 | 13 | pub use http::response::Parts; 14 | pub use reqwest::Client; 15 | 16 | fn default_headers() -> reqwest::header::HeaderMap { 17 | let mut map = reqwest::header::HeaderMap::new(); 18 | 19 | map.insert("accept", "text/plain".parse().unwrap()); 20 | 21 | map 22 | } 23 | 24 | pub fn client() -> Query { 25 | #[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)] 26 | struct Q; 27 | 28 | Cache::current().get_or_init(Q, || { 29 | Query::from( 30 | Client::builder() 31 | .user_agent(concat!( 32 | env!("CARGO_PKG_NAME"), 33 | "/", 34 | env!("CARGO_PKG_VERSION") 35 | )) 36 | .default_headers(default_headers()) 37 | .build() 38 | .unwrap(), 39 | ) 40 | }) 41 | } 42 | 43 | pub fn get_full(url: U) -> Query, Contents)>> 44 | where 45 | U: 'static + Clone + AsRef + Send + Sync, 46 | { 47 | Cache::current().get_or_init(url.as_ref().to_string(), move || { 48 | Query::new(async move { 49 | let client = client().await; 50 | 51 | let resp = client.get(url.as_ref()).send().await.into_diagnostic()?; 52 | let mut resp = resp.error_for_status().into_diagnostic()?; 53 | 54 | let mut body = vec![]; 55 | 56 | while let Some(chunk) = resp.chunk().await.into_diagnostic()? { 57 | body.extend_from_slice(&chunk); 58 | } 59 | 60 | let resp: http::Response = resp.into(); 61 | 62 | let (headers, _) = resp.into_parts(); 63 | 64 | let headers = Arc::new(headers); 65 | let body = Contents::from(body); 66 | 67 | Ok((headers, body)) 68 | }) 69 | }) 70 | } 71 | 72 | pub fn get(url: U) -> Query> 73 | where 74 | U: 'static + Clone + AsRef + Send + Sync, 75 | { 76 | Query::new(async move { 77 | let resp = get_full(url).await?; 78 | Ok(resp.1) 79 | }) 80 | } 81 | 82 | pub fn get_cached(url: U, cached_path: P) -> Query> 83 | where 84 | U: 'static + Clone + AsRef + Send + Sync, 85 | P: Into, 86 | { 87 | crate::vfs::read_file_or_create(cached_path, get(url)) 88 | } 89 | 90 | pub fn get_cached_string(url: U, cached_path: P) -> Query> 91 | where 92 | U: 'static + Clone + AsRef + Send + Sync, 93 | P: Into, 94 | { 95 | crate::vfs::read_string_or_create(cached_path, get(url)) 96 | } 97 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.4.0 (2025-01-22) 2 | 3 | ### Features 4 | 5 | * Added support for configuration files in place of command line arguments ([#152](https://github.com/awslabs/duvet/pull/152)) 6 | * New `snapshot` report output, which prevents accidental changes in requirement coverage ([#153](https://github.com/awslabs/duvet/pull/153)) 7 | * New `duvet init` command, which creates a configuration file based on the current directory ([#154](https://github.com/awslabs/duvet/pull/154)) 8 | * Detailed errors with specific line numbers about what went wrong. 9 | 10 | ### Bug Fixes 11 | 12 | * More robust parsing for both IETF and markdown specification types. 13 | 14 | ## 0.3.0 (2023-10-06) 15 | 16 | 17 | ### Features 18 | 19 | * specify path to the spec files ([#118](https://github.com/awslabs/duvet/issues/118)) ([ce9325e](https://github.com/awslabs/duvet/commit/ce9325ec7e5352f73a26d4b6a4dde34b58b06de1)) 20 | 21 | 22 | ## 0.2.0 (2022-11-16) 23 | 24 | 25 | ### Features 26 | 27 | * add basic markdown support ([#84](https://github.com/awslabs/duvet/issues/84)) ([f8ebf29](https://github.com/awslabs/duvet/commit/f8ebf298c6dca3c2a261d6a3fbc3703dd1c6703b)) 28 | 29 | 30 | ### Bug Fixes 31 | 32 | * remove redundant borrows ([#89](https://github.com/awslabs/duvet/issues/89)) ([0cfc8ce](https://github.com/awslabs/duvet/commit/0cfc8ce88a8a5183a68581fd5824498dbe4e376a)) 33 | * handle duplicate markdown section names ([#94](https://github.com/awslabs/duvet/issues/94)) ([5d31dd2](https://github.com/awslabs/duvet/commit/5d31dd21c05f5998b8a4e6c66e18552688a3e788)) 34 | 35 | ## 0.1.1 (2022-10-07) 36 | 37 | ### Features 38 | 39 | * Add type implication ([#16](https://github.com/awslabs/duvet/issues/16)) ([45bd9df](https://github.com/awslabs/duvet/commit/45bd9df437ce1788a9b81b6d4d4ff3895b205eec)) 40 | 41 | ### Bug Fixes 42 | 43 | * add word boundary assertions for extracted keywords ([#72](https://github.com/awslabs/duvet/issues/72)) ([02c9245](https://github.com/awslabs/duvet/commit/02c92452158debf1be82c702824689ab01b08aa0)) 44 | * finish pattern state machine after iterating lines ([#76](https://github.com/awslabs/duvet/issues/76)) ([7d500ff](https://github.com/awslabs/duvet/commit/7d500ffec0bdeaefb1342645965c655b5fd69eed)) 45 | * normalize quotes with indentations ([#79](https://github.com/awslabs/duvet/issues/79)) ([65835f7](https://github.com/awslabs/duvet/commit/65835f7cb45c7a84f9f43d7e348225f954a871a5)) 46 | * prefix anchors in spec links ([#68](https://github.com/awslabs/duvet/issues/68)) ([93c7875](https://github.com/awslabs/duvet/commit/93c78754f2adb88b4412030b04719c95963f73a1)) 47 | * sort Requirements table ([#82](https://github.com/awslabs/duvet/issues/82)) ([71f6152](https://github.com/awslabs/duvet/commit/71f6152dca7a8649823fcddb5a0cccbecc8b7103)) 48 | * use BTreeMap for target data ([#86](https://github.com/awslabs/duvet/issues/86)) ([2ea2336](https://github.com/awslabs/duvet/commit/2ea2336fcdd2db247046320c7f3b7b7f4a397bea)) 49 | * panic on file without trailing newline ([002fce8](https://github.com/awslabs/duvet/commit/002fce863d7620526e9500d58f9e1268b824841b)) 50 | -------------------------------------------------------------------------------- /duvet-core/src/diff.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use console::{style, Style}; 5 | use similar::{udiff::UnifiedHunkHeader, Algorithm, ChangeTag, TextDiff}; 6 | use std::{ 7 | io::{self, Write}, 8 | time::Duration, 9 | }; 10 | 11 | pub fn dump(mut o: Output, old: &str, new: &str) -> io::Result<()> { 12 | let diff = TextDiff::configure() 13 | .timeout(Duration::from_millis(200)) 14 | .algorithm(Algorithm::Patience) 15 | .diff_lines(old, new); 16 | 17 | for group in diff.grouped_ops(4).into_iter() { 18 | if group.is_empty() { 19 | continue; 20 | } 21 | 22 | // find the previous text that doesn't have an indent 23 | let line = diff.iter_changes(&group[0]).next().unwrap().value(); 24 | let scope = find_scope(old, line); 25 | 26 | let header = style(UnifiedHunkHeader::new(&group)).cyan(); 27 | 28 | if scope != line { 29 | writeln!(o, "{header} {scope}")?; 30 | } else { 31 | writeln!(o, "{header}")?; 32 | } 33 | 34 | for op in group { 35 | for change in diff.iter_inline_changes(&op) { 36 | let (marker, style) = match change.tag() { 37 | ChangeTag::Delete => ('-', Style::new().red()), 38 | ChangeTag::Insert => ('+', Style::new().green()), 39 | ChangeTag::Equal => (' ', Style::new().dim()), 40 | }; 41 | write!(o, "{}", style.apply_to(marker).dim().bold())?; 42 | for &(emphasized, value) in change.values() { 43 | if emphasized { 44 | write!(o, "{}", style.clone().underlined().bold().apply_to(value))?; 45 | } else { 46 | write!(o, "{}", style.apply_to(value))?; 47 | } 48 | } 49 | } 50 | } 51 | } 52 | 53 | Ok(()) 54 | } 55 | 56 | /// Finds the most recent non-empty line with no indentation 57 | fn find_scope<'a>(old: &'a str, mut line: &'a str) -> &'a str { 58 | let base = old.as_ptr() as usize; 59 | 60 | while line.is_empty() || line.starts_with(char::is_whitespace) { 61 | let len = (line.as_ptr() as usize).saturating_sub(base); 62 | 63 | let Some(subject) = old[..len].lines().next_back() else { 64 | break; 65 | }; 66 | 67 | line = subject; 68 | } 69 | 70 | line 71 | } 72 | 73 | #[cfg(test)] 74 | mod tests { 75 | use super::*; 76 | 77 | #[test] 78 | fn test_find_scope() { 79 | let text = r#" 80 | header 81 | foo 82 | 83 | bar 84 | 85 | baz 86 | "# 87 | .trim_start(); 88 | 89 | assert_eq!(find_scope(text, text.lines().next().unwrap()), "header"); 90 | assert_eq!(find_scope(text, text.lines().nth(1).unwrap()), "foo"); 91 | assert_eq!(find_scope(text, text.lines().last().unwrap()), "foo"); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /xtask/src/guide.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use crate::{build::Build, Result}; 5 | use clap::Parser; 6 | use std::{fs::File, io::Write, path::Path}; 7 | use xshell::{cmd, Shell}; 8 | 9 | #[derive(Debug, Default, Parser)] 10 | pub struct Guide { 11 | #[clap(long)] 12 | pub dev: bool, 13 | } 14 | 15 | impl Guide { 16 | pub fn run(&self, sh: &Shell) -> Result { 17 | let configs = sh.read_dir("config")?; 18 | 19 | let dir = Path::new("guide").canonicalize()?; 20 | let build_dir = dir.join("build"); 21 | 22 | if cmd!(sh, "which mdbook").quiet().run().is_err() { 23 | cmd!(sh, "cargo install --locked mdbook@^0.4").run()?; 24 | } 25 | 26 | if cmd!(sh, "which taplo").quiet().run().is_err() { 27 | cmd!(sh, "cargo install --locked taplo-cli@^0.9").run()?; 28 | } 29 | 30 | if cmd!(sh, "which typos").quiet().run().is_err() { 31 | cmd!(sh, "cargo install --locked typos-cli@^1").run()?; 32 | } 33 | 34 | let bin = Build { 35 | profile: "dev".into(), 36 | } 37 | .run(sh)?; 38 | 39 | let _path = sh.push_env( 40 | "PATH", 41 | format!( 42 | "{}:{}", 43 | bin.parent().unwrap().display(), 44 | std::env::var("PATH").unwrap_or_default() 45 | ), 46 | ); 47 | 48 | let command_dir = dir.join("src/command"); 49 | sh.create_dir(&command_dir)?; 50 | for command in ["init", "extract", "report"] { 51 | let output = cmd!(sh, "duvet {command} --help") 52 | .ignore_status() 53 | .output()?; 54 | let path = command_dir.join(command).with_extension("md"); 55 | let mut file = File::create(path)?; 56 | writeln!(file, "# `{command}`")?; 57 | writeln!(file)?; 58 | writeln!(file, "```console")?; 59 | file.write_all(&output.stdout)?; 60 | writeln!(file, "```")?; 61 | file.flush()?; 62 | } 63 | 64 | let files = sh.read_dir("guide/src")?; 65 | cmd!(sh, "typos {files...}").run()?; 66 | 67 | // make sure the example config matches the schema 68 | let example_config = dir.join("src/example-config.toml"); 69 | cmd!(sh, "taplo fmt {example_config}").run()?; 70 | // TODO we need to publish the spec first 71 | // cmd!(sh, "taplo lint {example_config}").run()?; 72 | 73 | let _dir = sh.push_dir(dir); 74 | if self.dev { 75 | cmd!(sh, "mdbook serve").run()?; 76 | } else { 77 | cmd!(sh, "mdbook build").run()?; 78 | } 79 | 80 | // copy over the config schemas 81 | let config_dir = build_dir.join("config"); 82 | sh.create_dir(&config_dir)?; 83 | for file in configs { 84 | sh.copy_file(file, &config_dir)?; 85 | } 86 | 87 | Ok(()) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /duvet/src/specification/markdown/snapshots/duvet__specification__markdown__tests__list_example_tokens.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/specification/markdown/tests.rs 3 | assertion_line: 41 4 | expression: "tokens(r#\"\n# List example\n\nHere is a list:\n* Item 1\n* Item 2\n * Item 2.1\n* Item 3\n * Item 3.1\n * Item 3.1.1\n * Item 3.1.2\n * Item 3.2\n\nHere is a numbered list:\n1. Item 1\n2. Item 2\n3. Item 3\n\"#)" 5 | --- 6 | [ 7 | Line( 8 | Str { 9 | value: "", 10 | pos: 0, 11 | line: 1, 12 | }, 13 | ), 14 | Header( 15 | Str { 16 | value: "# List example", 17 | pos: 1, 18 | line: 2, 19 | }, 20 | ), 21 | Line( 22 | Str { 23 | value: "", 24 | pos: 16, 25 | line: 3, 26 | }, 27 | ), 28 | Line( 29 | Str { 30 | value: "Here is a list:", 31 | pos: 17, 32 | line: 4, 33 | }, 34 | ), 35 | Line( 36 | Str { 37 | value: "* Item 1", 38 | pos: 33, 39 | line: 5, 40 | }, 41 | ), 42 | Line( 43 | Str { 44 | value: "* Item 2", 45 | pos: 42, 46 | line: 6, 47 | }, 48 | ), 49 | Line( 50 | Str { 51 | value: " * Item 2.1", 52 | pos: 51, 53 | line: 7, 54 | }, 55 | ), 56 | Line( 57 | Str { 58 | value: "* Item 3", 59 | pos: 64, 60 | line: 8, 61 | }, 62 | ), 63 | Line( 64 | Str { 65 | value: " * Item 3.1", 66 | pos: 73, 67 | line: 9, 68 | }, 69 | ), 70 | Line( 71 | Str { 72 | value: " * Item 3.1.1", 73 | pos: 86, 74 | line: 10, 75 | }, 76 | ), 77 | Line( 78 | Str { 79 | value: " * Item 3.1.2", 80 | pos: 103, 81 | line: 11, 82 | }, 83 | ), 84 | Line( 85 | Str { 86 | value: " * Item 3.2", 87 | pos: 120, 88 | line: 12, 89 | }, 90 | ), 91 | Line( 92 | Str { 93 | value: "", 94 | pos: 133, 95 | line: 13, 96 | }, 97 | ), 98 | Line( 99 | Str { 100 | value: "Here is a numbered list:", 101 | pos: 134, 102 | line: 14, 103 | }, 104 | ), 105 | Line( 106 | Str { 107 | value: "1. Item 1", 108 | pos: 159, 109 | line: 15, 110 | }, 111 | ), 112 | Line( 113 | Str { 114 | value: "2. Item 2", 115 | pos: 169, 116 | line: 16, 117 | }, 118 | ), 119 | Line( 120 | Str { 121 | value: "3. Item 3", 122 | pos: 179, 123 | line: 17, 124 | }, 125 | ), 126 | ] 127 | -------------------------------------------------------------------------------- /duvet/src/specification/markdown/tests.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use super::{parse, tokenizer::tokens}; 5 | use duvet_core::file::SourceFile; 6 | 7 | macro_rules! snapshot { 8 | ($name:ident, $contents:expr) => { 9 | mod $name { 10 | #[test] 11 | fn tokens() { 12 | let contents = super::SourceFile::new("index.md", $contents).unwrap(); 13 | insta::assert_debug_snapshot!( 14 | "tokens", 15 | super::tokens(&contents).collect::>() 16 | ); 17 | } 18 | 19 | #[test] 20 | fn tree() { 21 | let contents = super::SourceFile::new("index.md", $contents).unwrap(); 22 | insta::assert_debug_snapshot!("tree", super::parse(&contents)); 23 | } 24 | } 25 | }; 26 | } 27 | 28 | snapshot!( 29 | simple, 30 | r#" 31 | # This is a test 32 | 33 | Content goes here. Another 34 | sentence here. 35 | "# 36 | ); 37 | 38 | snapshot!( 39 | multi_line_header, 40 | r#" 41 | Foo *bar 42 | baz* 43 | ====== 44 | 45 | Content goes here. Another 46 | sentence here. 47 | "# 48 | ); 49 | 50 | snapshot!( 51 | multi_line_header_strong_heading_attrs, 52 | r#" 53 | Foo **bar 54 | baz** {#blah} 55 | ====== 56 | 57 | Content goes here. Another 58 | sentence here. 59 | "# 60 | ); 61 | 62 | snapshot!( 63 | multi_line_header_link_heading_attrs, 64 | r#" 65 | Foo **bar 66 | baz** [I'm link](http://something) {#blah} 67 | ====== 68 | 69 | Content goes here. Another 70 | sentence here. 71 | "# 72 | ); 73 | 74 | snapshot!( 75 | multiple, 76 | r#" 77 | # This is a test 78 | 79 | Content goes here. Another 80 | sentence here. 81 | 82 | ## This is another test 83 | 84 | More content goes here 85 | 86 | ### Nested section 87 | 88 | Testing 123 89 | 90 | ## Up one 91 | 92 | Another section 93 | "# 94 | ); 95 | 96 | snapshot!( 97 | list_example, 98 | r#" 99 | # List example 100 | 101 | Here is a list: 102 | * Item 1 103 | * Item 2 104 | * Item 2.1 105 | * Item 3 106 | * Item 3.1 107 | * Item 3.1.1 108 | * Item 3.1.2 109 | * Item 3.2 110 | 111 | Here is a numbered list: 112 | 1. Item 1 113 | 2. Item 2 114 | 3. Item 3 115 | 116 | Here is a list with content: 117 | * Item 118 | More content 119 | 120 | Other content 121 | 122 | * Testing 123 | 124 | Other test 125 | 126 | Testing 123 127 | 128 | * Item 129 | More content 130 | "# 131 | ); 132 | 133 | snapshot!( 134 | duplicate_sections, 135 | r#" 136 | # Duplicate header 137 | 138 | testing 123 139 | 140 | ## Duplicate header 141 | 142 | other test 143 | "# 144 | ); 145 | 146 | snapshot!( 147 | heading_attributes, 148 | r#" 149 | # Heading with ID {#custom-id} 150 | 151 | Content under heading with custom ID. 152 | 153 | ## Another heading {#another-id} 154 | 155 | More content here. 156 | 157 | # Regular heading 158 | 159 | This heading doesn't have a custom ID. 160 | "# 161 | ); 162 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /duvet-core/src/glob.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use core::fmt; 5 | use globset as g; 6 | use serde::de; 7 | use std::{str::FromStr, sync::Arc}; 8 | 9 | #[derive(Clone)] 10 | pub struct Glob { 11 | set: Arc<(g::GlobSet, Vec)>, 12 | } 13 | 14 | impl fmt::Debug for Glob { 15 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 16 | let list = &self.set.1; 17 | if list.len() == 1 { 18 | list[0].fmt(f) 19 | } else { 20 | list.fmt(f) 21 | } 22 | } 23 | } 24 | 25 | impl Glob { 26 | pub fn is_match>(&self, path: &P) -> bool { 27 | self.set.0.is_match(path) 28 | } 29 | 30 | pub fn try_from_iter, I: AsRef>( 31 | iter: T, 32 | ) -> Result { 33 | let mut builder = g::GlobSetBuilder::new(); 34 | let mut display = vec![]; 35 | for item in iter { 36 | let value = format_value(item.as_ref()); 37 | builder.add(g::Glob::new(&value)?); 38 | display.push(value); 39 | } 40 | let set = builder.build()?; 41 | let set = Arc::new((set, display)); 42 | Ok(Self { set }) 43 | } 44 | } 45 | 46 | impl FromStr for Glob { 47 | type Err = g::Error; 48 | 49 | fn from_str(value: &str) -> Result { 50 | Self::try_from_iter(core::iter::once(value)) 51 | } 52 | } 53 | 54 | impl TryFrom<&str> for Glob { 55 | type Error = g::Error; 56 | 57 | fn try_from(value: &str) -> Result { 58 | value.parse() 59 | } 60 | } 61 | 62 | impl<'de> de::Deserialize<'de> for Glob { 63 | fn deserialize(deserializer: D) -> Result 64 | where 65 | D: de::Deserializer<'de>, 66 | { 67 | deserializer.deserialize_any(StringOrList) 68 | } 69 | } 70 | 71 | struct StringOrList; 72 | 73 | impl<'de> de::Visitor<'de> for StringOrList { 74 | type Value = Glob; 75 | 76 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 77 | formatter.write_str("string or list of strings") 78 | } 79 | 80 | fn visit_str(self, value: &str) -> Result 81 | where 82 | E: de::Error, 83 | { 84 | value.parse().map_err(serde::de::Error::custom) 85 | } 86 | 87 | fn visit_seq(self, mut seq: S) -> Result 88 | where 89 | S: de::SeqAccess<'de>, 90 | { 91 | let mut builder = g::GlobSetBuilder::new(); 92 | let mut display = vec![]; 93 | while let Some(value) = seq.next_element()? { 94 | let value = format_value(value); 95 | let item = g::Glob::new(&value).map_err(serde::de::Error::custom)?; 96 | builder.add(item); 97 | display.push(value); 98 | } 99 | let set = builder.build().map_err(serde::de::Error::custom)?; 100 | let set = Arc::new((set, display)); 101 | Ok(Glob { set }) 102 | } 103 | } 104 | 105 | fn format_value(v: &str) -> String { 106 | if v.starts_with("**/") || v.starts_with('/') { 107 | v.to_string() 108 | } else { 109 | format!("**/{v}") 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /duvet/src/text/whitespace.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | pub fn normalize(value: &str) -> String { 5 | normalize_mapped::<()>(value).0 6 | } 7 | 8 | pub fn normalize_mapped(value: &str) -> (String, O) { 9 | let offset_map = O::with_capacity(value.len()); 10 | let out = String::with_capacity(value.len()); 11 | 12 | let mut mapper = Mapper { 13 | offset_map, 14 | out, 15 | buffer: None, 16 | last_end: 0, 17 | }; 18 | 19 | for (idx, c) in value.char_indices() { 20 | mapper.on_char(idx, c); 21 | } 22 | 23 | let (out, offset_map) = mapper.finish(); 24 | 25 | (out, offset_map) 26 | } 27 | 28 | pub trait OffsetMap { 29 | fn with_capacity(len: usize) -> Self; 30 | fn push(&mut self, idx: usize); 31 | } 32 | 33 | impl OffsetMap for () { 34 | #[inline] 35 | fn with_capacity(_len: usize) -> Self {} 36 | 37 | #[inline] 38 | fn push(&mut self, _idx: usize) {} 39 | } 40 | 41 | impl OffsetMap for Vec { 42 | #[inline] 43 | fn with_capacity(len: usize) -> Self { 44 | Vec::with_capacity(len + 1) 45 | } 46 | 47 | #[inline] 48 | fn push(&mut self, idx: usize) { 49 | self.push(idx); 50 | } 51 | } 52 | 53 | struct Mapper { 54 | out: String, 55 | offset_map: O, 56 | buffer: Option, 57 | last_end: usize, 58 | } 59 | 60 | impl Mapper { 61 | #[inline] 62 | fn on_char(&mut self, idx: usize, c: char) { 63 | if c.is_alphanumeric() { 64 | self.flush(); 65 | self.push(idx, c); 66 | return; 67 | } 68 | 69 | if c.is_whitespace() { 70 | if self.buffer.is_none() && !self.out.is_empty() { 71 | self.buffer = Some(Buffer { 72 | start: idx, 73 | is_ws: true, 74 | c, 75 | }); 76 | } 77 | return; 78 | } 79 | 80 | // punctuation 81 | if let Some(buffer) = self.buffer.as_ref() { 82 | if !buffer.is_ws { 83 | self.flush(); 84 | } 85 | } 86 | 87 | self.buffer = Some(Buffer { 88 | start: idx, 89 | is_ws: false, 90 | c, 91 | }); 92 | } 93 | 94 | #[inline] 95 | fn flush(&mut self) { 96 | if let Some(buffer) = self.buffer.take() { 97 | self.push(buffer.start, buffer.c); 98 | } 99 | } 100 | 101 | #[inline] 102 | fn push(&mut self, idx: usize, c: char) { 103 | self.out.push(c); 104 | let len = c.len_utf8(); 105 | for _ in 0..len { 106 | self.offset_map.push(idx); 107 | } 108 | self.last_end = idx + len; 109 | } 110 | 111 | #[inline] 112 | fn finish(mut self) -> (String, O) { 113 | if let Some(buffer) = self.buffer.take() { 114 | if !buffer.is_ws { 115 | self.push(buffer.start, buffer.c); 116 | } 117 | } 118 | self.offset_map.push(self.last_end); 119 | (self.out, self.offset_map) 120 | } 121 | } 122 | 123 | struct Buffer { 124 | start: usize, 125 | is_ws: bool, 126 | c: char, 127 | } 128 | -------------------------------------------------------------------------------- /duvet/src/text/view.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use core::ops::{Deref, Range}; 5 | use duvet_core::file::{Slice, SourceFile}; 6 | 7 | pub fn view<'a, C>(contents: C) -> View 8 | where 9 | C: IntoIterator, 10 | { 11 | View::new(contents.into_iter()) 12 | } 13 | 14 | #[derive(Debug)] 15 | pub struct View { 16 | value: String, 17 | byte_map: Vec, 18 | file: SourceFile, 19 | } 20 | 21 | impl View { 22 | pub fn new<'a, C>(contents: C) -> Self 23 | where 24 | C: Iterator, 25 | { 26 | let mut value = String::new(); 27 | let mut byte_map = vec![]; 28 | let mut file = None; 29 | let mut pushed_chunk = false; 30 | 31 | for chunk in contents { 32 | if file.is_none() { 33 | file = Some(chunk.file().clone()); 34 | } 35 | 36 | let trimmed = chunk.trim(); 37 | if trimmed.is_empty() { 38 | continue; 39 | } 40 | 41 | // check if we already pushed a chunk. if so we need to add some whitespace 42 | if core::mem::replace(&mut pushed_chunk, true) { 43 | value.push(' '); 44 | byte_map.push(usize::MAX); 45 | } 46 | 47 | value.push_str(trimmed); 48 | let range = chunk.file().substr(trimmed).unwrap().range(); 49 | byte_map.extend(range.clone()); 50 | } 51 | 52 | debug_assert_eq!(value.len(), byte_map.len()); 53 | let file = file.expect("at least one chunk"); 54 | 55 | Self { 56 | value, 57 | byte_map, 58 | file, 59 | } 60 | } 61 | 62 | pub fn ranges(&self, src: Range) -> StrRangeIter<'_> { 63 | StrRangeIter { 64 | byte_map: &self.byte_map, 65 | file: &self.file, 66 | start: src.start, 67 | end: src.end, 68 | } 69 | } 70 | } 71 | 72 | impl Deref for View { 73 | type Target = str; 74 | 75 | fn deref(&self) -> &str { 76 | &self.value 77 | } 78 | } 79 | 80 | pub struct StrRangeIter<'a> { 81 | byte_map: &'a [usize], 82 | file: &'a SourceFile, 83 | start: usize, 84 | end: usize, 85 | } 86 | 87 | impl Iterator for StrRangeIter<'_> { 88 | type Item = Slice; 89 | 90 | fn next(&mut self) -> Option { 91 | if self.start == self.end { 92 | return None; 93 | } 94 | 95 | let mut start_target = self.byte_map[self.start]; 96 | while start_target == usize::MAX { 97 | self.start += 1; 98 | if self.start == self.end { 99 | return None; 100 | } 101 | start_target = self.byte_map[self.start]; 102 | } 103 | 104 | let mut range = start_target..start_target; 105 | self.start += 1; 106 | 107 | for i in self.start..self.end { 108 | let target = self.byte_map[i]; 109 | 110 | if target == usize::MAX { 111 | break; 112 | } 113 | 114 | if range.end <= target { 115 | range.end = target + 1; 116 | self.start += 1; 117 | } else { 118 | break; 119 | } 120 | } 121 | 122 | let slice = self.file.substr_range(range).expect("missing range"); 123 | 124 | Some(slice) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /duvet-core/tests/line_count.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use duvet_core::{ 5 | diagnostic::{Error, IntoDiagnostic}, 6 | file::{Slice, SourceFile}, 7 | path::Path, 8 | query, vfs, Query, Result, 9 | }; 10 | use std::collections::BTreeMap; 11 | 12 | macro_rules! path { 13 | ($path:expr) => { 14 | Path::from(concat!(env!("CARGO_MANIFEST_DIR"), "/tests/", $path)) 15 | }; 16 | } 17 | 18 | #[query] 19 | fn manifest_path() -> Path { 20 | path!("line_count/manifest.txt") 21 | } 22 | 23 | type ManifestSource = Result; 24 | 25 | #[query(delegate)] 26 | async fn manifest_source() -> ManifestSource { 27 | vfs::read_string(manifest_path().await) 28 | } 29 | 30 | type ManifestList = Result>>; 31 | 32 | #[query] 33 | async fn manifest_parse() -> ManifestList { 34 | let manifest = manifest_source().await?; 35 | 36 | let lines = manifest.lines(); 37 | 38 | let mut out = vec![]; 39 | for line in lines { 40 | out.push(manifest.substr(line).unwrap()); 41 | } 42 | 43 | Ok(out) 44 | } 45 | 46 | type ProjectFiles = Result, Query>>>; 47 | 48 | #[query] 49 | async fn project_files() -> ProjectFiles { 50 | let files = manifest_parse().await?; 51 | let path = manifest_path().await; 52 | let dir = path.parent().unwrap(); 53 | 54 | let mut out = BTreeMap::new(); 55 | for path in files { 56 | let source_code = path.clone(); 57 | let read = vfs::read_string(dir.join(&path[..])).map_cloned(|v| async move { 58 | let s = v.map_err(move |e| source_code.error(e, "tried to open the file here"))?; 59 | Ok::<_, Error>(s) 60 | }); 61 | out.insert(path.clone(), read); 62 | } 63 | 64 | Ok(out) 65 | } 66 | 67 | #[query] 68 | async fn project_line_counts() -> Result, Query>>> { 69 | let files = project_files(); 70 | let files = files.get().await.as_ref()?; 71 | 72 | #[query] 73 | async fn line_counts(file: Query>) -> Result { 74 | let contents = file.get().await.as_ref()?; 75 | let count = contents.lines().count(); 76 | Ok(count) 77 | } 78 | 79 | let mut out = BTreeMap::new(); 80 | for (path, contents) in files { 81 | out.insert(path.clone(), line_counts(contents.clone())); 82 | } 83 | 84 | Ok(out) 85 | } 86 | 87 | #[query] 88 | async fn total_counts() -> Result { 89 | let project = project_line_counts(); 90 | let project = project.get().await.as_ref()?; 91 | 92 | let mut out = 0; 93 | 94 | for file in project.values() { 95 | if let Ok(file) = file.get().await { 96 | out += file; 97 | } 98 | } 99 | 100 | Ok(out) 101 | } 102 | 103 | #[tokio::test] 104 | async fn line_count() -> Result<()> { 105 | assert_eq!(total_counts().await?, 11); 106 | 107 | let files = project_line_counts(); 108 | let files = files.get().await.as_ref()?; 109 | 110 | let mut errors = vec![]; 111 | 112 | for (file, counts) in files.iter() { 113 | let expected = match &**file { 114 | "a.txt" => 4, 115 | "b.txt" => 5, 116 | "c.txt" => 2, 117 | _ => 0, 118 | }; 119 | 120 | match counts.get().await { 121 | Ok(actual) => { 122 | assert_eq!(expected, *actual, "in {file}"); 123 | } 124 | Err(err) => errors.push(err.clone()), 125 | } 126 | } 127 | 128 | errors.into_diagnostic()?; 129 | 130 | Ok(()) 131 | } 132 | -------------------------------------------------------------------------------- /duvet/src/specification/markdown/parser.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use super::tokenizer::Token; 5 | use core::fmt; 6 | use duvet_core::file::Slice; 7 | 8 | pub fn parse>(tokens: T) -> Parser { 9 | Parser { 10 | section: None, 11 | tokens: tokens.into_iter(), 12 | } 13 | } 14 | 15 | pub struct Parser { 16 | section: Option
, 17 | tokens: T, 18 | } 19 | 20 | pub struct Section { 21 | pub level: u8, 22 | pub id: Id, 23 | pub title: Slice, 24 | pub lines: Vec>, 25 | } 26 | 27 | pub enum Id { 28 | Fragment(Slice), 29 | Title(Slice), 30 | } 31 | 32 | impl fmt::Display for Id { 33 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 34 | match self { 35 | Id::Fragment(id) => write!(f, "{id}"), 36 | Id::Title(title) => write!(f, "{}", slug::slugify(title)), 37 | } 38 | } 39 | } 40 | 41 | impl Section { 42 | fn push(&mut self, value: Option) { 43 | // don't push an empty first line 44 | if self.lines.is_empty() && is_empty(&value) { 45 | return; 46 | } 47 | 48 | self.lines.push(value); 49 | } 50 | } 51 | 52 | impl> Parser { 53 | fn on_token(&mut self, token: Token) -> Option
{ 54 | match token { 55 | Token::Section { 56 | id, 57 | title, 58 | level, 59 | line: _, 60 | } => { 61 | let prev = self.flush(); 62 | 63 | let id = id 64 | .map(Id::Fragment) 65 | .unwrap_or_else(|| Id::Title(title.clone())); 66 | 67 | self.section = Some(Section { 68 | level, 69 | id, 70 | title, 71 | lines: vec![], 72 | }); 73 | 74 | prev 75 | } 76 | Token::Break { line: _ } => { 77 | if let Some(section) = self.section.as_mut() { 78 | // just get the line offset 79 | section.push(None); 80 | } 81 | 82 | None 83 | } 84 | Token::Content { value, line: _ } => { 85 | if let Some(section) = self.section.as_mut() { 86 | section.push(Some(value)); 87 | } 88 | 89 | None 90 | } 91 | } 92 | } 93 | 94 | fn flush(&mut self) -> Option
{ 95 | let mut section = core::mem::take(&mut self.section)?; 96 | 97 | // trim any trailing lines 98 | loop { 99 | let Some(line) = section.lines.last() else { 100 | break; 101 | }; 102 | 103 | if !is_empty(line) { 104 | break; 105 | } 106 | 107 | section.lines.pop(); 108 | } 109 | 110 | Some(section) 111 | } 112 | } 113 | 114 | impl> Iterator for Parser { 115 | type Item = Section; 116 | 117 | fn next(&mut self) -> Option { 118 | loop { 119 | let Some(token) = self.tokens.next() else { 120 | return self.flush(); 121 | }; 122 | if let Some(section) = self.on_token(token) { 123 | return Some(section); 124 | } 125 | } 126 | } 127 | } 128 | 129 | fn is_empty(v: &Option) -> bool { 130 | v.as_ref().is_none_or(|v| v.trim().is_empty()) 131 | } 132 | -------------------------------------------------------------------------------- /duvet/src/extract/snapshots/duvet__extract__tests__esdk_streaming.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: src/extract/tests.rs 3 | expression: results 4 | --- 5 | [ 6 | ( 7 | "overview", 8 | Feature { 9 | level: May, 10 | quote: [ 11 | "The AWS Encryption SDK MAY provide APIs that enable streamed [encryption](encrypt.md)", 12 | "and [decryption](decrypt.md).", 13 | ], 14 | }, 15 | ), 16 | ( 17 | "overview", 18 | Feature { 19 | level: Should, 20 | quote: [ 21 | "If an implementation requires holding the entire input in memory in order to perform the operation,", 22 | "that implementation SHOULD NOT provide an API that allows the caller to stream the operation.", 23 | ], 24 | }, 25 | ), 26 | ( 27 | "overview", 28 | Feature { 29 | level: Should, 30 | quote: [ 31 | "APIs that support streaming of the encrypt or decrypt operation SHOULD allow customers", 32 | "to be able to process arbitrarily large inputs with a finite amount of working memory.", 33 | ], 34 | }, 35 | ), 36 | ( 37 | "release", 38 | Feature { 39 | level: Must, 40 | quote: [ 41 | "The decrypt and encrypt operations specify when output bytes MUST NOT be released", 42 | "and when they SHOULD be released.", 43 | ], 44 | }, 45 | ), 46 | ( 47 | "inputs", 48 | Feature { 49 | level: Must, 50 | quote: [ 51 | "In order to support streaming, the operation MUST accept some input within a streaming framework.", 52 | ], 53 | }, 54 | ), 55 | ( 56 | "inputs", 57 | Feature { 58 | level: Must, 59 | quote: [ 60 | "- There MUST be a mechanism for input bytes to become consumable.", 61 | ], 62 | }, 63 | ), 64 | ( 65 | "inputs", 66 | Feature { 67 | level: Must, 68 | quote: [ 69 | "- There MUST be a mechanism to indicate that there are no more input bytes.", 70 | ], 71 | }, 72 | ), 73 | ( 74 | "outputs", 75 | Feature { 76 | level: Must, 77 | quote: [ 78 | "In order to support streaming, the operation MUST produce some output within a streaming framework.", 79 | ], 80 | }, 81 | ), 82 | ( 83 | "outputs", 84 | Feature { 85 | level: Must, 86 | quote: [ 87 | "- There MUST be a mechanism for output bytes to be released.", 88 | ], 89 | }, 90 | ), 91 | ( 92 | "outputs", 93 | Feature { 94 | level: Must, 95 | quote: [ 96 | "- There MUST be a mechanism to indicate that the entire output has been released.", 97 | ], 98 | }, 99 | ), 100 | ( 101 | "outputs", 102 | Feature { 103 | level: Must, 104 | quote: [ 105 | "Operations MUST NOT indicate completion or success until an end to the output has been indicated.", 106 | ], 107 | }, 108 | ), 109 | ( 110 | "behavior", 111 | Feature { 112 | level: Must, 113 | quote: [ 114 | "The behavior of the operation specifies how the operation processes consumable bytes,", 115 | "and specifies when processed bytes MUST NOT and SHOULD be released.", 116 | ], 117 | }, 118 | ), 119 | ] 120 | -------------------------------------------------------------------------------- /duvet-core/src/vfs.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use crate::{ 5 | dir::Directory, 6 | file::{BinaryFile, SourceFile}, 7 | path::Path, 8 | query::Query, 9 | Result, 10 | }; 11 | use core::cell::RefCell; 12 | use std::{fs::FileType, time::SystemTime}; 13 | 14 | pub mod fs; 15 | pub use fs::Fs; 16 | 17 | pub type OrCreate = Query>; 18 | 19 | thread_local! { 20 | static VFS: RefCell> = RefCell::new(Box::new(Fs::default())); 21 | } 22 | 23 | pub fn setup(f: F) { 24 | VFS.with(|current| *current.borrow_mut() = Box::new(f)); 25 | } 26 | 27 | #[inline] 28 | fn vfs R, R>(f: F) -> R { 29 | VFS.with(|current| { 30 | let current = current.borrow(); 31 | let current: &dyn Vfs = &**current; 32 | f(current) 33 | }) 34 | } 35 | 36 | pub trait Vfs { 37 | fn read_dir(&self, path: Path) -> Query>; 38 | fn read_file(&self, path: Path, or_create: Option) -> Query>; 39 | fn read_string(&self, path: Path, or_create: Option) -> Query>; 40 | fn read_metadata(&self, path: Path, or_create: Option) -> Query>; 41 | } 42 | 43 | pub fn read_file>(path: P) -> Query> { 44 | vfs(|fs| fs.read_file(path.into(), None)) 45 | } 46 | 47 | pub fn read_file_or_create>( 48 | path: P, 49 | or_create: OrCreate, 50 | ) -> Query> { 51 | vfs(|fs| fs.read_file(path.into(), Some(or_create))) 52 | } 53 | 54 | pub fn read_string>(path: P) -> Query> { 55 | vfs(|fs| fs.read_string(path.into(), None)) 56 | } 57 | 58 | pub fn read_string_or_create>( 59 | path: P, 60 | or_create: OrCreate, 61 | ) -> Query> { 62 | vfs(|fs| fs.read_string(path.into(), Some(or_create))) 63 | } 64 | 65 | pub fn read_dir>(path: P) -> Query> { 66 | vfs(|fs| fs.read_dir(path.into())) 67 | } 68 | 69 | pub fn read_metadata>(path: P) -> Query> { 70 | vfs(|fs| fs.read_metadata(path.into(), None)) 71 | } 72 | 73 | pub fn read_metadata_or_create>( 74 | path: P, 75 | or_create: OrCreate, 76 | ) -> Query> { 77 | vfs(|fs| fs.read_metadata(path.into(), Some(or_create))) 78 | } 79 | 80 | #[derive(Clone, Debug)] 81 | pub struct Metadata { 82 | modified_time: Result, 83 | file_type: FileType, 84 | } 85 | 86 | impl Metadata { 87 | pub fn is_dir(&self) -> bool { 88 | self.file_type.is_dir() 89 | } 90 | 91 | pub fn is_file(&self) -> bool { 92 | self.file_type.is_file() 93 | } 94 | } 95 | 96 | #[cfg(test)] 97 | mod tests { 98 | use super::*; 99 | use futures::StreamExt; 100 | 101 | #[tokio::test] 102 | async fn self_read() { 103 | let file = read_string(file!()).await; 104 | 105 | if let Ok(contents) = file { 106 | assert!(contents.contains("THIS IS A REALLY UNIQUE STRING")); 107 | } 108 | } 109 | 110 | #[tokio::test] 111 | async fn walk() { 112 | let dir = read_dir(env!("CARGO_MANIFEST_DIR")).await; 113 | 114 | if let Ok(dir) = dir { 115 | let glob = "**/*.rs".parse().unwrap(); 116 | let ignore = "__IGNORE__".parse().unwrap(); 117 | let dir = dir.glob(glob, ignore); 118 | tokio::pin!(dir); 119 | 120 | while let Some(path) = dir.next().await { 121 | dbg!(path); 122 | } 123 | } 124 | 125 | // TODO make some assertions 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /duvet-core/src/path.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use core::{cmp::Ordering, fmt}; 5 | use serde::Deserialize; 6 | use std::{ffi::OsStr, ops::Deref, path::PathBuf, sync::Arc}; 7 | 8 | #[derive(Clone, Deserialize)] 9 | #[serde(transparent)] 10 | pub struct Path { 11 | path: Arc, 12 | } 13 | 14 | impl Path { 15 | pub fn pop(&mut self) -> bool { 16 | if let Some(parent) = self.parent() { 17 | *self = parent.into(); 18 | true 19 | } else { 20 | false 21 | } 22 | } 23 | 24 | pub fn push>(&mut self, component: V) { 25 | *self = self.join(component); 26 | } 27 | 28 | pub fn join>(&self, component: V) -> Self { 29 | self.as_ref().join(component).into() 30 | } 31 | } 32 | 33 | impl PartialEq for Path { 34 | fn eq(&self, other: &Self) -> bool { 35 | self.as_ref().eq(other.as_ref()) 36 | } 37 | } 38 | 39 | impl Eq for Path {} 40 | 41 | impl PartialOrd for Path { 42 | fn partial_cmp(&self, other: &Self) -> Option { 43 | Some(self.cmp(other)) 44 | } 45 | } 46 | 47 | impl Ord for Path { 48 | fn cmp(&self, other: &Self) -> Ordering { 49 | self.as_ref().cmp(other.as_ref()) 50 | } 51 | } 52 | 53 | impl core::hash::Hash for Path { 54 | fn hash(&self, state: &mut H) { 55 | self.as_ref().hash(state) 56 | } 57 | } 58 | 59 | impl fmt::Debug for Path { 60 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 61 | self.as_ref().fmt(f) 62 | } 63 | } 64 | 65 | impl fmt::Display for Path { 66 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 67 | let path = self.as_ref(); 68 | let path = crate::env::current_dir() 69 | .ok() 70 | .and_then(|dir| path.strip_prefix(dir).ok()) 71 | .unwrap_or(path); 72 | path.display().fmt(f) 73 | } 74 | } 75 | 76 | impl Deref for Path { 77 | type Target = std::path::Path; 78 | 79 | fn deref(&self) -> &Self::Target { 80 | std::path::Path::new(&self.path) 81 | } 82 | } 83 | 84 | impl AsRef for Path { 85 | fn as_ref(&self) -> &std::path::Path { 86 | self 87 | } 88 | } 89 | 90 | impl PartialEq for Path { 91 | fn eq(&self, other: &str) -> bool { 92 | self.as_ref().eq(std::path::Path::new(other)) 93 | } 94 | } 95 | 96 | impl PartialEq for Path { 97 | fn eq(&self, other: &std::path::Path) -> bool { 98 | self.as_ref().eq(other) 99 | } 100 | } 101 | 102 | impl From for Path { 103 | fn from(path: String) -> Self { 104 | Self { 105 | path: PathBuf::from(path).into_os_string().into(), 106 | } 107 | } 108 | } 109 | 110 | impl From for Path { 111 | fn from(path: PathBuf) -> Self { 112 | Self { 113 | path: path.into_os_string().into(), 114 | } 115 | } 116 | } 117 | 118 | impl From<&PathBuf> for Path { 119 | fn from(path: &PathBuf) -> Self { 120 | path.as_path().into() 121 | } 122 | } 123 | 124 | impl From<&std::path::Path> for Path { 125 | fn from(path: &std::path::Path) -> Self { 126 | Self { 127 | path: path.as_os_str().into(), 128 | } 129 | } 130 | } 131 | 132 | impl From for PathBuf { 133 | fn from(value: Path) -> Self { 134 | PathBuf::from(&value.path) 135 | } 136 | } 137 | 138 | impl From<&Path> for Path { 139 | fn from(path: &Path) -> Self { 140 | Self { 141 | path: path.as_os_str().into(), 142 | } 143 | } 144 | } 145 | 146 | impl From<&str> for Path { 147 | fn from(path: &str) -> Self { 148 | Self { 149 | path: std::path::Path::new(path).as_os_str().into(), 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /guide/src/annotations.md: -------------------------------------------------------------------------------- 1 | # Annotations 2 | 3 | Duvet scans source code for special comments containing references to specification text. By default, the comment style is the following: 4 | 5 | ```rust 6 | //= https://www.rfc-editor.org/rfc/rfc2324#section-2.1.1 7 | //# A coffee pot server MUST accept both the BREW and POST method 8 | //# equivalently. 9 | ``` 10 | 11 | If the default comment style is not compatible with the language being used, it can be changed in the [configuration](./config.md) with the `comment-style` field. 12 | 13 | The default type of annotation is `implementation`, meaning the reference is implementing the cited text. The type of annotation can be changed with the `type` parameter. Duvet supports the following annotation types: 14 | 15 | ## `implementation` 16 | 17 | The source code is aiming to implement the cited text from the specification. This is the default annotation type. 18 | 19 | ## `test` 20 | 21 | The source code is aiming to test that the program implements the cited text correctly. 22 | 23 | ```rust 24 | //= https://www.rfc-editor.org/rfc/rfc2324#section-2.1.1 25 | //= type=test 26 | //# A coffee pot server MUST accept both the BREW and POST method 27 | //# equivalently. 28 | #[test] 29 | fn my_test() { 30 | // TODO 31 | } 32 | ``` 33 | 34 | ## `implication` 35 | 36 | The source code is both implementing and testing the cited text. This can be useful for requirements that are correct by construction. For example, let's say our specification says the following: 37 | 38 | ``` 39 | # Section 40 | 41 | The function MUST return a 64-bit integer. 42 | ``` 43 | 44 | In a strongly-typed language, this requirement is being both implemented and tested by the compiler. 45 | 46 | ```rust 47 | //= my-spec.md#section 48 | //= type=implication 49 | //# The function MUST return a 64-bit integer. 50 | fn the_function() -> u64 { 51 | 42 52 | } 53 | ``` 54 | 55 | ## `exception` 56 | 57 | The source code has defined an exception for a requirement and is explicitly choosing not to implement it. This could be for various reasons. For example, let's consider the following specification: 58 | 59 | ``` 60 | # Section 61 | 62 | Implementations MAY panic on invalid arguments. 63 | ``` 64 | 65 | In our example here, we've chosen _not_ to panic, but instead return an error. Annotations with the `exception` type can optionally provide a reason as to why the requirement is not being implemented. 66 | 67 | ```rust 68 | //= my-spec.md#section 69 | //= type=exception 70 | //= reason=We prefer to return errors that can be handled by the caller. 71 | //# Implementations MAY panic on invalid arguments. 72 | fn the_function() -> Result { 73 | // implementation here 74 | } 75 | ``` 76 | 77 | ## `todo` 78 | 79 | Some requirements may not be currently implemented but are on the product's roadmap. Such requirements can be annotated with the `todo` type to indicate this. Optionally, the annotation can provide a tracking issue for more context/updates. 80 | 81 | ```rust 82 | //= my-spec.md#section 83 | //= type=todo 84 | //= tracking-issue=1234 85 | //# Implementations SHOULD do this thing. 86 | ``` 87 | 88 | ## `spec` 89 | 90 | The `spec` annotation type provides a way to annotate additional text in a specification that does not use the key words from [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119), but is still considered as providing a requirement. 91 | 92 | ``` 93 | # Section 94 | 95 | It's really important that implementations validate untrusted input. 96 | ``` 97 | 98 | ```rust 99 | //= my-spec.md#section 100 | //= type=spec 101 | //= level=MUST 102 | //# It's really important that implementations validate untrusted input. 103 | ``` 104 | 105 | Additionally, Duvet also supports defining these requirements in `toml`: 106 | 107 | ```toml 108 | [[spec]] 109 | target = "my-spec.md#section" 110 | level = "MUST" 111 | quote = ''' 112 | It's really important that implementations validate untrusted input. 113 | ''' 114 | ``` 115 | -------------------------------------------------------------------------------- /duvet/src/specification/markdown/snapshots/duvet__specification__markdown__tests__list_example__tree.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: duvet/src/specification/markdown/tests.rs 3 | expression: "super :: parse(& contents)" 4 | --- 5 | Ok( 6 | Specification { 7 | title: Some( 8 | "List example", 9 | ), 10 | sections: [ 11 | Section { 12 | id: "list-example", 13 | title: "List example", 14 | full_title: "List example", 15 | lines: [ 16 | Str( 17 | "Here is a list:", 18 | ), 19 | Break, 20 | Str( 21 | "* Item 1", 22 | ), 23 | Break, 24 | Str( 25 | "* Item 2", 26 | ), 27 | Break, 28 | Str( 29 | " * Item 2.1", 30 | ), 31 | Break, 32 | Str( 33 | "* Item 3", 34 | ), 35 | Break, 36 | Str( 37 | " * Item 3.1", 38 | ), 39 | Break, 40 | Str( 41 | " * Item 3.1.1", 42 | ), 43 | Break, 44 | Str( 45 | " * Item 3.1.2", 46 | ), 47 | Break, 48 | Str( 49 | " * Item 3.2", 50 | ), 51 | Break, 52 | Str( 53 | "", 54 | ), 55 | Str( 56 | "Here is a numbered list:", 57 | ), 58 | Break, 59 | Str( 60 | "1. Item 1", 61 | ), 62 | Break, 63 | Str( 64 | "2. Item 2", 65 | ), 66 | Break, 67 | Str( 68 | "3. Item 3", 69 | ), 70 | Break, 71 | Str( 72 | "", 73 | ), 74 | Str( 75 | "Here is a list with content:", 76 | ), 77 | Break, 78 | Str( 79 | "* Item", 80 | ), 81 | Str( 82 | " More content", 83 | ), 84 | Str( 85 | "", 86 | ), 87 | Str( 88 | " Other content", 89 | ), 90 | Str( 91 | "", 92 | ), 93 | Break, 94 | Str( 95 | " * Testing", 96 | ), 97 | Str( 98 | "", 99 | ), 100 | Str( 101 | " Other test", 102 | ), 103 | Break, 104 | Str( 105 | "", 106 | ), 107 | Str( 108 | "Testing 123", 109 | ), 110 | Str( 111 | "", 112 | ), 113 | Break, 114 | Str( 115 | "* Item", 116 | ), 117 | Str( 118 | "More content", 119 | ), 120 | ], 121 | }, 122 | ], 123 | format: Markdown, 124 | }, 125 | ) 126 | -------------------------------------------------------------------------------- /duvet/src/config.rs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use crate::{extract::Extraction, Result}; 5 | use duvet_core::{path::Path, vfs}; 6 | use std::sync::Arc; 7 | 8 | pub mod schema; 9 | 10 | #[derive(Clone, Debug)] 11 | pub struct Config { 12 | pub sources: Vec, 13 | pub requirements: Vec, 14 | pub specifications: Vec, 15 | pub report: Report, 16 | pub requirements_path: Path, 17 | pub download_path: Path, 18 | } 19 | 20 | impl Config { 21 | pub async fn load_specifications(&self) -> Result { 22 | let download_path = &self.download_path; 23 | let requirements_path = &self.requirements_path; 24 | 25 | for spec in &self.specifications { 26 | Extraction { 27 | download_path, 28 | base_path: Some(download_path), 29 | target: spec.target.clone(), 30 | out: requirements_path, 31 | extension: "toml", 32 | // don't log to reduce noise 33 | log: false, 34 | } 35 | .exec() 36 | .await?; 37 | } 38 | 39 | Ok(self.specifications.len()) 40 | } 41 | } 42 | 43 | #[derive(Clone, Debug)] 44 | pub struct Source { 45 | pub pattern: String, 46 | pub root: Path, 47 | pub comment_style: crate::comment::Pattern, 48 | pub default_type: crate::annotation::AnnotationType, 49 | } 50 | 51 | #[derive(Clone, Debug)] 52 | pub struct Requirement { 53 | pub pattern: String, 54 | pub root: Path, 55 | } 56 | 57 | #[derive(Clone, Debug)] 58 | pub struct Report { 59 | pub html: HtmlReport, 60 | pub json: JsonReport, 61 | pub snapshot: SnapshotReport, 62 | } 63 | 64 | #[derive(Clone, Debug)] 65 | pub struct HtmlReport { 66 | pub enabled: bool, 67 | pub path: Path, 68 | pub blob_link: Option>, 69 | pub issue_link: Option>, 70 | } 71 | 72 | impl HtmlReport { 73 | pub fn path(&self) -> Option<&Path> { 74 | Some(&self.path).filter(|_| self.enabled) 75 | } 76 | } 77 | 78 | #[derive(Clone, Debug)] 79 | pub struct JsonReport { 80 | pub enabled: bool, 81 | pub path: Path, 82 | } 83 | 84 | impl JsonReport { 85 | pub fn path(&self) -> Option<&Path> { 86 | Some(&self.path).filter(|_| self.enabled) 87 | } 88 | } 89 | 90 | #[derive(Clone, Debug)] 91 | pub struct SnapshotReport { 92 | pub enabled: bool, 93 | pub path: Path, 94 | } 95 | 96 | impl SnapshotReport { 97 | pub fn path(&self) -> Option<&Path> { 98 | Some(&self.path).filter(|_| self.enabled) 99 | } 100 | } 101 | 102 | #[derive(Clone, Debug)] 103 | pub struct Specification { 104 | pub target: Arc, 105 | } 106 | 107 | pub async fn load(path: Path, root: Path) -> Result> { 108 | let file = vfs::read_string(path.clone()).await?; 109 | let schema: Arc = file.as_toml().await?; 110 | 111 | let mut sources = vec![]; 112 | let mut requirements = vec![]; 113 | let mut specifications = vec![]; 114 | 115 | schema.load_sources(&mut sources, &root)?; 116 | schema.load_requirements(&mut requirements, &root)?; 117 | schema.load_specifications(&mut specifications, &root)?; 118 | 119 | let requirements_path = schema.requirements_path(&path, &root); 120 | let download_path = schema.download_path(&path, &root); 121 | let report = schema.report(&path, &root); 122 | 123 | Ok(Arc::new(Config { 124 | sources, 125 | requirements, 126 | specifications, 127 | requirements_path, 128 | download_path, 129 | report, 130 | })) 131 | } 132 | 133 | pub async fn default_path_and_root() -> Option<(Path, Path)> { 134 | let root = duvet_core::env::current_dir().ok()?; 135 | let path = root.join(".duvet").join("config.toml"); 136 | 137 | // check to see if it exists 138 | let _ = vfs::read_metadata(&path).await.ok()?; 139 | 140 | Some((path, root)) 141 | } 142 | -------------------------------------------------------------------------------- /duvet/www/src/App.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { useState, default as React } from "react"; 5 | import { makeStyles } from "@material-ui/core/styles"; 6 | import CssBaseline from "@material-ui/core/CssBaseline"; 7 | import Container from "@material-ui/core/Container"; 8 | import { Switch, Route, useParams } from "react-router-dom"; 9 | import { Nav } from "./nav"; 10 | import { Spec, Stats } from "./spec"; 11 | import { Section } from "./section"; 12 | import { Link } from "./link"; 13 | import specifications from "./result"; 14 | import clsx from "clsx"; 15 | 16 | const drawerWidth = 400; 17 | 18 | const useStyles = makeStyles((theme) => ({ 19 | root: { 20 | display: "flex", 21 | }, 22 | appBar: { 23 | transition: theme.transitions.create(["margin", "width"], { 24 | easing: theme.transitions.easing.sharp, 25 | duration: theme.transitions.duration.leavingScreen, 26 | }), 27 | }, 28 | appBarShift: { 29 | width: `calc(100% - ${drawerWidth}px)`, 30 | marginLeft: drawerWidth, 31 | transition: theme.transitions.create(["margin", "width"], { 32 | easing: theme.transitions.easing.easeOut, 33 | duration: theme.transitions.duration.enteringScreen, 34 | }), 35 | }, 36 | menuButton: { 37 | marginRight: theme.spacing(2), 38 | }, 39 | hide: { 40 | display: "none", 41 | }, 42 | drawer: { 43 | width: drawerWidth, 44 | flexShrink: 0, 45 | }, 46 | drawerPaper: { 47 | width: drawerWidth, 48 | }, 49 | drawerHeader: { 50 | display: "flex", 51 | alignItems: "center", 52 | padding: theme.spacing(0, 1), 53 | // necessary for content to be below app bar 54 | ...theme.mixins.toolbar, 55 | justifyContent: "flex-end", 56 | }, 57 | content: { 58 | flexGrow: 1, 59 | padding: theme.spacing(3), 60 | transition: theme.transitions.create("margin", { 61 | easing: theme.transitions.easing.sharp, 62 | duration: theme.transitions.duration.leavingScreen, 63 | }), 64 | marginLeft: -drawerWidth, 65 | }, 66 | contentShift: { 67 | transition: theme.transitions.create("margin", { 68 | easing: theme.transitions.easing.easeOut, 69 | duration: theme.transitions.duration.enteringScreen, 70 | }), 71 | marginLeft: 0, 72 | }, 73 | container: { 74 | marginBottom: theme.spacing(5), 75 | }, 76 | })); 77 | 78 | function App() { 79 | const classes = useStyles(); 80 | const [open, setOpen] = useState(false); 81 | 82 | return ( 83 |
84 | 85 |