Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Protobuf validation #130

Merged
merged 13 commits into from
Oct 27, 2023
562 changes: 432 additions & 130 deletions Cargo.lock

Large diffs are not rendered by default.

31 changes: 18 additions & 13 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,49 +14,51 @@ license = "Apache-2.0"
[[bin]]
name = "buffrs"
path = "src/main.rs"
required-features = ["build", "git"]
required-features = ["build", "git", "validation"]

[[test]]
name = "e2e"
path = "tests/lib.rs"
test = true


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

[dependencies]
async-recursion = "1.0.5"
async-trait = "0.1"
anyhow = { version = "1.0", optional = true }
bytes = "1.0"
clap = { version = "4.3", features = ["cargo", "derive"] }
diff-struct = { version = "0.5.3", optional = true }
flate2 = "1"
futures = "0.3"
git2 = { version = "0.18.1", optional = true }
hex = "0.4.3"
home = "0.5.5"
human-panic = "1"
miette = { version = "5.10.0", features = ["fancy"] }
protobuf = { version = "3.3.0", optional = true }
protobuf-parse = { version = "3.3.0", optional = true }
protoc-bin-vendored = { version = "3.0.0", optional = true }
reqwest = { version = "0.11", features = ["rustls-tls-native-roots"], default-features = false }
ring = "0.16"
semver = { version = "1", features = ["serde"] }
serde = { version = "1", features = ["derive"] }
serde_typename = "0.1"
semver = { version = "1", features = ["serde"] }
tar = "0.4"
thiserror = "1.0.49"
tokio = { version = "1", features = ["fs", "rt", "macros", "process", "io-std", "tracing"] }
toml = "0.8.0"
tonic-build = { version = "0.10.0", optional = true }
tracing = "0.1"
tracing-subscriber = "0.3"
url = { version = "2.4", features = ["serde"] }
walkdir = "2"
async-recursion = "1.0.5"
thiserror = "1.0.49"
miette = { version = "5.10.0", features = ["fancy"] }

[dependencies.reqwest]
version = "0.11"
features = ["rustls-tls-native-roots"]
default-features = false

[dev-dependencies]
assert_cmd = "2.0"
Expand All @@ -66,3 +68,6 @@ 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"
16 changes: 16 additions & 0 deletions src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,22 @@ pub async fn uninstall() -> miette::Result<()> {
PackageStore::current().await?.clear().await
}

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

let violations = store.validate(&manifest).await?;

violations
.into_iter()
.map(miette::Report::new)
.for_each(|r| println!("{r:?}"));

Ok(())
}

/// Generate bindings for a given language
#[cfg(feature = "build")]
pub async fn generate(language: Language, out_dir: PathBuf) -> miette::Result<()> {
Expand Down
3 changes: 3 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,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
11 changes: 9 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use std::path::PathBuf;
use buffrs::command;
use buffrs::generator::Language;
use buffrs::manifest::Manifest;
use buffrs::package::PackageName;
use buffrs::package::{PackageName, PackageStore};
use buffrs::registry::RegistryUri;
use buffrs::{manifest::MANIFEST_FILE, package::PackageType};
use clap::{Parser, Subcommand};
Expand Down Expand Up @@ -48,6 +48,9 @@ enum Command {
package: Option<PackageName>,
},

/// Check rule violations for this package.
Lint,

/// Adds dependencies to a manifest file
Add {
/// Artifactory url (e.g. https://<domain>/artifactory)
Expand Down Expand Up @@ -206,12 +209,16 @@ async fn main() -> miette::Result<()> {
.wrap_err(miette!(
"failed to publish `{package}` to `{registry}:{repository}`",
)),
Command::Lint => command::lint().await.wrap_err(miette!(
"failed to lint protocol buffers in `{}`",
PackageStore::PROTO_PATH
)),
Command::Install => command::install()
.await
.wrap_err(miette!("failed to install dependencies for `{package}`")),
Command::Uninstall => command::uninstall()
.await
.wrap_err(miette!("failed to install dependencies for `{package}`")),
.wrap_err(miette!("failed to uninstall dependencies for `{package}`")),
Command::Generate { language, out_dir } => command::generate(language, out_dir)
.await
.wrap_err(miette!("failed to generate {language} language bindings")),
Expand Down
18 changes: 18 additions & 0 deletions src/package/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,24 @@ impl PackageStore {
Ok(manifest)
}

/// Validate this package
#[cfg(feature = "validation")]
pub async fn validate(
xfbs marked this conversation as resolved.
Show resolved Hide resolved
&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::Validator::new(&pkg_path, &manifest.package.name);

for file in &source_files {
parser.input(file);
}

parser.validate()
}

/// Packages a release from the local file system state
pub async fn release(&self, manifest: Manifest) -> miette::Result<Package> {
ensure!(
Expand Down
57 changes: 57 additions & 0 deletions src/validation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// 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.
mod data;
/// Rules for protocol buffer definitions.
mod rules;
/// Serde utilities.
pub(crate) mod serde;

mod parse;
#[cfg(test)]
mod tests;
xfbs marked this conversation as resolved.
Show resolved Hide resolved
mod violation;

use self::parse::*;

pub use self::violation::*;

use miette::IntoDiagnostic;
use std::path::Path;

pub struct Validator {
parser: parse::Parser,
package: String,
}

impl Validator {
/// Create new parser with a given root path.
pub fn new(root: &Path, package: &str) -> Self {
xfbs marked this conversation as resolved.
Show resolved Hide resolved
xfbs marked this conversation as resolved.
Show resolved Hide resolved
Self {
parser: parse::Parser::new(root),
package: package.into(),
}
}

pub fn input(&mut self, file: &Path) {
xfbs marked this conversation as resolved.
Show resolved Hide resolved
self.parser.input(file);
}

pub fn validate(self) -> miette::Result<Violations> {
let parsed = self.parser.parse().into_diagnostic()?;
let mut rule_set = rules::package_rules(&self.package);
xfbs marked this conversation as resolved.
Show resolved Hide resolved
Ok(parsed.check(&mut rule_set))
}
}
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,
};
xfbs marked this conversation as resolved.
Show resolved Hide resolved

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

xfbs marked this conversation as resolved.
Show resolved Hide resolved
pub use self::{entity::*, message::*, package::*, packages::*, r#enum::*, service::*};
56 changes: 56 additions & 0 deletions src/validation/data/entity.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// 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 super::*;
xfbs marked this conversation as resolved.
Show resolved Hide resolved

/// Entity that can be defined in a protocol buffer file.
#[derive(Serialize, Deserialize, Clone, Debug, 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 From<Enum> for Entity {
fn from(entity: Enum) -> Self {
Self::Enum(entity)
}
}

impl From<Service> for Entity {
fn from(entity: Service) -> Self {
Self::Service(entity)
}
}

impl From<Message> for Entity {
fn from(entity: Message) -> Self {
Self::Message(entity)
}
}

impl Entity {
/// Check [`Entity`] against [`RuleSet`] for [`Violations`].
pub fn check(&self, _rules: &mut RuleSet) -> Violations {
xfbs marked this conversation as resolved.
Show resolved Hide resolved
Violations::default()
}
}
Loading