Skip to content

Commit

Permalink
feat: add init command (#154)
Browse files Browse the repository at this point in the history
  • Loading branch information
camshaft authored Jan 21, 2025
1 parent 2e299d0 commit c5bb847
Show file tree
Hide file tree
Showing 39 changed files with 598 additions and 35 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

### Bug Fixes

* remove reduntant borrows ([#89](https://github.com/awslabs/duvet/issues/89)) ([0cfc8ce](https://github.com/awslabs/duvet/commit/0cfc8ce88a8a5183a68581fd5824498dbe4e376a))
* remove redundant borrows ([#89](https://github.com/awslabs/duvet/issues/89)) ([0cfc8ce](https://github.com/awslabs/duvet/commit/0cfc8ce88a8a5183a68581fd5824498dbe4e376a))
* handle duplicate markdown section names ([#94](https://github.com/awslabs/duvet/issues/94)) ([5d31dd2](https://github.com/awslabs/duvet/commit/5d31dd21c05f5998b8a4e6c66e18552688a3e788))

## 0.1.1 (2022-10-07)
Expand Down
7 changes: 1 addition & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
[workspace]
members = [
"duvet",
"duvet-core",
"duvet-macros",
"xtask",
]
members = ["duvet", "duvet-core", "duvet-macros", "xtask"]
resolver = "2"

[profile.bench]
Expand Down
69 changes: 46 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,56 @@
## Duvet
# Duvet

A code quality tool to help bound correctness.
By starting from a specification Duvet extracts every RFC 2119 requirement.
Duvet can then use this information to report on a code base.
Duvet can then report on every requirement,
where it is honored in source,
as well as how that source is tested.
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:

## Support
This tool is still in beta.
Interfaces should be considered unstable and may change before the 1.0.0 release.
> 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)
## Test
First run `make` in the main `duvet` directory to generate the necessary files.
```
cargo test
```
## Quick Start

Before getting started, Duvet requires a [rust toolchain](https://www.rust-lang.org/tools/install).

1. Install command

```console
$ cargo install duvet --locked
```

2. Initialize repository

In this example, we are using Rust. However, Duvet can be used with any language.

```console
$ duvet init --lang-rust --specification https://www.rfc-editor.org/rfc/rfc2324
```

3. Add a implementation comment in the project

## Build
```rust
// src/lib.rs

If there are any changes to the JS
it will also need to be built.
In the `www` directory run `make build`
//= https://www.rfc-editor.org/rfc/rfc2324#section-2.1.1
//# A coffee pot server MUST accept both the BREW and POST method
//# equivalently.
```

## Install
4. Generate a report

```console
$ duvet report
```

## Development

### Building

```console
$ cargo xtask build
```

### Testing

```console
$ cargo xtask test
```
cargo +stable install --force --path .
````

## Security

Expand All @@ -35,4 +59,3 @@ See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more inform
## License

This project is licensed under the Apache-2.0 License.
2 changes: 2 additions & 0 deletions _typos.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[files]
extend-exclude = ["*.snap", "integration/snapshots/**", "specs/**"]
3 changes: 3 additions & 0 deletions duvet-core/src/diff.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

use console::{style, Style};
use similar::{udiff::UnifiedHunkHeader, Algorithm, ChangeTag, TextDiff};
use std::{
Expand Down
1 change: 1 addition & 0 deletions duvet/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ edition = "2021"
license = "Apache-2.0"
repository = "https://github.com/awslabs/duvet"
include = ["/src/**/*.rs", "/www/public"]
default-run = "duvet"

[dependencies]
clap = { version = "4", features = ["derive"] }
Expand Down
2 changes: 2 additions & 0 deletions duvet/src/config/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ use serde::{de, Deserialize};

pub mod v0_4_0;

pub static DEFAULT: &str = "https://awslabs.github.io/duvet/config/v0.4.0.json";

#[derive(Debug, Deserialize)]
#[serde(tag = "$schema", deny_unknown_fields)]
pub enum Schema {
Expand Down
249 changes: 249 additions & 0 deletions duvet/src/init.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

use crate::Result;
use clap::Parser;
use duvet_core::{progress, vfs as fs};
use std::{fs::File, io::Write, path::Path};

static GITIGNORE: &str = r#"
reports/
"#;

#[derive(Debug, Parser)]
pub struct Init {
#[clap(long)]
/// Include the given specification in the configuration
specification: Vec<String>,

#[clap(flatten)]
languages: Languages,
}

impl Init {
pub async fn exec(&self) -> Result {
let mut languages = self.languages;

if languages.is_empty() {
languages.detect().await;
}

let dir = Path::new(".duvet");

std::fs::create_dir_all(dir)?;

macro_rules! put {
($path:expr, $writer:expr) => {{
let path = dir.join($path);
let progress = progress!("Writing {}", path.display());
if path.exists() {
progress!(progress, "Skipping {} - already exists", path.display());
} else {
let mut out = File::create_new(&path)?;
let result: Result = ($writer)(&mut out);
result?;
out.flush()?;
drop(out);

progress!(progress, "Wrote {}", path.display());
}
}};
}

put!("config.toml", |out: &mut File| {
writeln!(out, "'$schema' = {:?}", crate::config::schema::DEFAULT)?;
writeln!(out)?;

languages.write(out)?;

if self.specification.is_empty() {
writeln!(out, "# Include required specifications here")?;
writeln!(out, "# [[specification]]")?;
writeln!(
out,
"# source = {:?}",
"https://www.rfc-editor.org/rfc/rfc2324"
)?;
} else {
for spec in &self.specification {
writeln!(out, "[[specification]]")?;
writeln!(out, "source = {:?}", spec)?;
}
}

writeln!(out, "[report.html]")?;
writeln!(out, "enabled = true")?;
// TODO detect git repo
writeln!(out)?;

writeln!(
out,
"# Enable snapshots to prevent requirement coverage regressions"
)?;
writeln!(out, "[report.snapshot]")?;
writeln!(out, "enabled = true")?;

Ok(())
});

put!(".gitignore", |out: &mut File| {
write!(out, "{}", GITIGNORE.trim_start())?;
Ok(())
});

Ok(())
}
}

#[derive(Copy, Clone, Debug, Default, PartialEq, Parser)]
struct Languages {
/// Include rules for the C language
#[clap(long = "lang-c")]
c: bool,
/// Include rules for the Go language
#[clap(long = "lang-go")]
go: bool,
/// Include rules for the Java language
#[clap(long = "lang-java")]
java: bool,
/// Include rules for the JavaScript language
#[clap(long = "lang-javascript")]
javascript: bool,
/// Include rules for the Python language
#[clap(long = "lang-python")]
python: bool,
/// Include rules for the TypeScript language
#[clap(long = "lang-typescript")]
typescript: bool,
/// Include rules for the Ruby language
#[clap(long = "lang-ruby")]
ruby: bool,
/// Include rules for the Rust language
#[clap(long = "lang-rust")]
rust: bool,
}

impl Languages {
fn is_empty(&self) -> bool {
Self::default().eq(self)
}

fn write<O: Write>(&self, out: &mut O) -> Result {
if self.is_empty() {
writeln!(out, "# Specify source code patterns here")?;
writeln!(out, "# [[source]]")?;
writeln!(out, "# pattern = {:?}", "src/**/*.rs")?;
writeln!(out)?;
return Ok(());
}

macro_rules! write {
($lang:ident) => {
if self.$lang {
lang::$lang(out)?;
}
};
}

write!(c);
write!(go);
write!(java);
write!(javascript);
write!(python);
write!(typescript);
write!(ruby);
write!(rust);

Ok(())
}

async fn detect(&mut self) {
async fn check(path: &str) -> bool {
fs::read_metadata(path).await.is_ok()
}

if check("CMakeLists.txt").await {
self.c = true;
}

if check("go.mod").await {
self.go = true;
}

if check("pom.xml").await || check("build.gradle").await || check("build.gradle.kts").await
{
self.java = true;
}

if check("package.json").await {
self.javascript = true;
}

if check("requirements.txt").await
|| check("pyproject.toml").await
|| check("setup.py").await
{
self.python = true;
}

if check("tsconfig.json").await {
self.typescript = true;
}

if check("Gemfile").await {
self.ruby = true;
}

if check("Cargo.toml").await {
self.rust = true;
}
}
}

mod lang {
use super::*;

macro_rules! lang {
($name:ident, $pattern:expr $(, $extra:expr)?) => {
pub fn $name<O: Write>(out: &mut O) -> Result {
writeln!(out, "[[source]]")?;
writeln!(out, "pattern = {:?}", $pattern)?;
$(
writeln!(out, "{}", $extra)?;
)?
writeln!(out)?;
Ok(())
}
};
}

lang!(
c,
"src/**/*.c",
format_args!(
"comment-style = {{ meta = {:?}, content = {:?} }}",
"*=", "*#"
)
);
lang!(go, "src/**/*.go");
lang!(java, "src/**/*.java");
lang!(javascript, "src/**/*.js");
lang!(
python,
"src/**/*.py",
format_args!(
"comment-style = {{ meta = {:?}, content = {:?} }}",
"##=", "##%"
)
);
lang!(typescript, "src/**/*.ts");
lang!(
ruby,
"lib/**/*.rb",
format_args!(
"comment-style = {{ meta = {:?}, content = {:?} }}",
"##=", "##%"
)
);
lang!(rust, "src/**/*.rs");
}
Loading

0 comments on commit c5bb847

Please sign in to comment.