Skip to content

Commit

Permalink
Adds validation support to buffrs.
Browse files Browse the repository at this point in the history
For the time being, this will only be used to build informative lints.
  • Loading branch information
xfbs committed Oct 25, 2023
1 parent 069eca3 commit 4bc736d
Show file tree
Hide file tree
Showing 31 changed files with 1,947 additions and 41 deletions.
346 changes: 308 additions & 38 deletions Cargo.lock

Large diffs are not rendered by default.

15 changes: 14 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ path = "tests/lib.rs"
test = true

[features]
default = ["build", "git"]
default = ["build", "git", "validation"]
build = ["tonic-build", "protoc-bin-vendored"]
validation = ["dep:protobuf", "dep:protobuf-parse", "dep:diff-struct"]
git = ["git2"]

[dependencies]
Expand Down Expand Up @@ -52,6 +53,11 @@ walkdir = "2"
async-recursion = "1.0.5"
thiserror = "1.0.49"
miette = { version = "5.10.0", features = ["fancy"] }
protobuf = { version = "3.3.0", optional = true }
protobuf-parse = { version = "3.3.0", optional = true }
diff-struct = { version = "0.5.3", optional = true }
derive_more = "0.99.17"
anyhow = "1.0.75"

[dependencies.reqwest]
version = "0.11"
Expand All @@ -66,6 +72,13 @@ predicates = "3.0"
pretty_assertions = "1.4"
ring = "0.16.20"
hex = "0.4.3"
similar-asserts = "1.5.0"
serde_json = { version = "1.0.107" }
paste = "1.0.14"

[[test]]
name = "validation"
required-features = ["validation"]

[workspace]
members = [".", "registry"]
14 changes: 14 additions & 0 deletions src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,20 @@ impl Context {
Lockfile::from_iter(locked.into_iter()).write().await
}

/// Parses current package and validates rules.
#[cfg(feature = "validation")]
pub async fn lint(self: Arc<Self>) -> miette::Result<()> {
let manifest = Manifest::read().await?;
let violations = self.store().validate(&manifest).await?;

for violation in violations.into_iter() {
let report = miette::Report::new(violation);
println!("{report:?}");
}

Ok(())
}

/// Uninstalls dependencies
pub async fn uninstall(self: Arc<Self>) -> miette::Result<()> {
PackageStore::current().await?.clear().await
Expand Down
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ pub mod package;
pub mod registry;
/// Resolve package dependencies.
pub mod resolver;
/// Validation for buffrs packages.
#[cfg(feature = "validation")]
pub mod validation;

/// Cargo build integration for buffrs
///
Expand Down
23 changes: 23 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,21 @@ enum Command {
package: Option<PackageName>,
},

/// Check rule violations for this package.
Lint {
/// Allow these rules to be violated.
#[clap(long, short)]
allow: Vec<String>,

/// Treat these rule violations as errors.
#[clap(long, short)]
deny: Vec<String>,

/// Treat these rule violations as warnings.
#[clap(long, short)]
warn: Vec<String>,
},

/// Adds dependencies to a manifest file
Add {
/// Artifactory url (e.g. https://<domain>/artifactory)
Expand Down Expand Up @@ -196,6 +211,14 @@ async fn main() -> miette::Result<()> {
.publish(registry, repository, allow_dirty, dry_run)
.await
.wrap_err(miette!("publish command failed")),
Command::Lint {
allow: _,
warn: _,
deny: _,
} => context
.lint()
.await
.wrap_err(miette!("lint command failed")),
Command::Install => context
.install()
.await
Expand Down
23 changes: 21 additions & 2 deletions src/package.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,24 @@ impl PackageStore {
.wrap_err(miette!("failed to resolve package {package}"))
}

/// Validate this package
#[cfg(feature = "validation")]
pub async fn validate(
&self,
manifest: &Manifest,
) -> miette::Result<crate::validation::Violations> {
let pkg_path = self.proto_path();
let source_files = self.collect(&pkg_path).await;

let mut parser = crate::validation::Parser::new(&pkg_path);
for file in &source_files {
parser.input(file);
}
let parsed = parser.parse().into_diagnostic()?;
let mut rule_set = crate::validation::rules::package_rules(&manifest.package.name);
Ok(parsed.check(&mut rule_set))
}

/// Packages a release from the local file system state
pub async fn release(&self, manifest: Manifest) -> miette::Result<Package> {
ensure!(
Expand All @@ -198,9 +216,10 @@ impl PackageStore {
}

let pkg_path = self.proto_path();
let mut entries = BTreeMap::new();
let source_files = self.collect(&pkg_path).await;

for entry in self.collect(&pkg_path).await {
let mut entries = BTreeMap::new();
for entry in &source_files {
let path = entry.strip_prefix(&pkg_path).into_diagnostic()?;
let contents = tokio::fs::read(&entry).await.unwrap();
entries.insert(path.into(), contents.into());
Expand Down
34 changes: 34 additions & 0 deletions src/validation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright 2023 Helsing GmbH
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/// Parsed protocol buffer definitions.
pub mod data;

/// Rules for protocol buffer definitions.
pub mod rules;

mod parse;

/// Serde utilities.
pub(crate) mod serde;

mod violation;

/// Rules for checking package differences.
pub mod diff;

pub use self::{
parse::{ParseError, Parser},
violation::*,
};
41 changes: 41 additions & 0 deletions src/validation/data.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright 2023 Helsing GmbH
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::{
collections::{btree_map::Entry, BTreeMap},
path::PathBuf,
};

use diff::Diff;
use miette::Diagnostic;
use protobuf::descriptor::{
field_descriptor_proto::{Label as FieldDescriptorLabel, Type as FieldDescriptorType},
*,
};
use serde::{Deserialize, Serialize};
use thiserror::Error;

use crate::validation::{
rules::{Rule, RuleSet},
Violations,
};

mod entity;
mod r#enum;
mod message;
mod package;
mod packages;
mod service;

pub use self::{entity::*, message::*, package::*, packages::*, r#enum::*, service::*};
24 changes: 24 additions & 0 deletions src/validation/data/entity.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use super::*;

/// Entity that can be defined in a protocol buffer file.
#[derive(Serialize, Deserialize, Clone, Debug, derive_more::From, PartialEq, Eq, Diff)]
#[serde(tag = "kind", rename_all = "snake_case")]
#[diff(attr(
#[derive(Debug)]
#[allow(missing_docs)]
))]
pub enum Entity {
/// Enumeration.
Enum(Enum),
/// Service definition.
Service(Service),
/// Message definition.
Message(Message),
}

impl Entity {
/// Check [`Entity`] against [`RuleSet`] for [`Violations`].
pub fn check(&self, _rules: &mut RuleSet) -> Violations {
Violations::default()
}
}
64 changes: 64 additions & 0 deletions src/validation/data/enum.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use super::*;

/// Error parsing package.
#[derive(Error, Debug, Diagnostic)]
#[allow(missing_docs)]
pub enum EnumError {
#[error("missing value number")]
ValueNumberMissing,

#[error("missing value name")]
ValueNameMissing,
}

/// Enumeration definition.
#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq, Eq, Diff)]
#[diff(attr(
#[derive(Debug)]
#[allow(missing_docs)]
))]
pub struct Enum {
/// Variants of this enum.
#[serde(deserialize_with = "crate::validation::serde::de_int_key")]
pub values: BTreeMap<i32, EnumValue>,
}

impl Enum {
/// Attempt to create new from [`EnumDescriptorProto`].
pub fn new(descriptor: &EnumDescriptorProto) -> Result<Self, EnumError> {
let mut entity = Self::default();

for value in &descriptor.value {
entity.add(value)?;
}

Ok(entity)
}

/// Add an [`EnumValue`] to this enum definition.
pub fn add(&mut self, value: &EnumValueDescriptorProto) -> Result<(), EnumError> {
let number = value.number.ok_or(EnumError::ValueNumberMissing)?;
self.values.insert(number, EnumValue::new(value)?);
Ok(())
}
}

/// Single value for an [`Enum`].
#[derive(Serialize, Deserialize, Default, Clone, Debug, PartialEq, Eq, Diff)]
#[diff(attr(
#[derive(Debug)]
#[allow(missing_docs)]
))]
pub struct EnumValue {
/// Name of this enum value.
pub name: String,
}

impl EnumValue {
/// Attempt to create new from [`EnumValueDescriptorProto`].
pub fn new(descriptor: &EnumValueDescriptorProto) -> Result<Self, EnumError> {
Ok(Self {
name: descriptor.name.clone().ok_or(EnumError::ValueNameMissing)?,
})
}
}
Loading

0 comments on commit 4bc736d

Please sign in to comment.