Skip to content

Commit

Permalink
wip: implement protoc-gen-prost-validate
Browse files Browse the repository at this point in the history
Signed-off-by: Adphi <[email protected]>
  • Loading branch information
Adphi committed Oct 11, 2024
1 parent a75c8ba commit fae0fe0
Show file tree
Hide file tree
Showing 11 changed files with 652 additions and 52 deletions.
357 changes: 336 additions & 21 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions example/build-with-buf/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ pbjson = "0.7"
pbjson-types = "0.7"
serde = "1.0"
tonic = { version = "0.12", features = ["gzip"] }
prost-validate = { path = "../../../prost-validate" }

[workspace]
25 changes: 17 additions & 8 deletions example/build-with-buf/buf.gen.yaml
Original file line number Diff line number Diff line change
@@ -1,27 +1,36 @@
version: v1
plugins:
- plugin: prost
#path: ../../target/debug/protoc-gen-prost
# path: ../../target/debug/protoc-gen-prost
out: src/gen
opt:
# - bytes=.
- compile_well_known_types
- extern_path=.google.protobuf=::pbjson_types
# - bytes=.
# - compile_well_known_types
# - extern_path=.google.protobuf=::pbjson_types
- file_descriptor_set
- enable_type_names
- plugin: prost-serde
#path: ../../target/debug/protoc-gen-prost-serde
path: ../../target/debug/protoc-gen-prost-serde
out: src/gen
- plugin: tonic
#path: ../../target/debug/protoc-gen-tonic
# path: ../../target/debug/protoc-gen-tonic
out: src/gen
opt:
- compile_well_known_types
- extern_path=.google.protobuf=::pbjson_types
# - extern_path=.google.protobuf=::pbjson_types
- plugin: prost-crate
#path: ../../target/debug/protoc-gen-prost-crate
# path: ../../target/debug/protoc-gen-prost-crate
out: .
strategy: all
opt:
- include_file=src/gen/mod.rs
- gen_crate
- plugin: prost-validate
# path: ../../target/debug/protoc-gen-prost-validate
out: src/gen
strategy: all
opt:
# - bytes=.
# - compile_well_known_types
# - extern_path=.google.protobuf=::pbjson_types
- enable_type_names
8 changes: 8 additions & 0 deletions example/build-with-buf/proto/buf.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Generated by buf. DO NOT EDIT.
version: v1
deps:
- remote: buf.build
owner: envoyproxy
repository: protoc-gen-validate
commit: daf171c6cdb54629b5f51e345a79e4dd
digest: shake256:4ae167d7eed10da5f83a3f5df8c670d249170f11b1f2fd19afda06be2cff4d47dcc95e9e4a15151ecc8ce2d3d3614caf9a04d3ad82fb768a3870dedfa9455f36
2 changes: 2 additions & 0 deletions example/build-with-buf/proto/buf.yaml
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
version: v1
deps:
- buf.build/envoyproxy/protoc-gen-validate
4 changes: 3 additions & 1 deletion example/build-with-buf/proto/example/ExampleRequest.proto
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ syntax = "proto3";

package example;

import "validate/validate.proto";

message ExampleRequest {
string example = 1;
string example = 1 [(validate.rules).string = {min_len: 1, max_len: 10}];
}
13 changes: 12 additions & 1 deletion protoc-gen-prost-validate/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
[package]
name = "protoc-gen-prost-validate"
version = "0.0.1"
authors = ["Marcus Griep <[email protected]>"]
authors = ["Marcus Griep <[email protected]>", "Adphi <[email protected]>"]
description = "Coming soon…"
license = "Apache-2.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
prost-build.workspace = true
prost-types.workspace = true
prost.workspace = true
protoc-gen-prost = { version = "0.4.0", path = "../protoc-gen-prost" }
prost-validate-build = { version = "0.2.1" }
prost-validate-derive-core = { version = "0.2.1" }
prost-validate-types = { version = "0.2.1" }
syn = "2.0.75"
proc-macro2 = "1.0.86"
prost-reflect = "0.14.2"
prettyplease = "0.2.9"
166 changes: 166 additions & 0 deletions protoc-gen-prost-validate/src/generator.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
use proc_macro2::TokenStream;
use prost::Message;
use prost_build::Module;
use prost_reflect::DescriptorPool;
use prost_types::compiler::code_generator_response::File;
use prost_validate_build::Builder;
use protoc_gen_prost::{Error, Generator, ModuleRequest, ModuleRequestSet, Result};
use std::collections::HashMap;
use syn::__private::ToTokens;

pub struct ProstValidateGenerator {
builder: Builder,
config: prost_build::Config,
insert_include: bool,
}

impl Generator for ProstValidateGenerator {
fn generate(&mut self, module_request_set: &ModuleRequestSet) -> Result {
let file_contents = self.generate_prost(module_request_set)?;
Ok(module_request_set
.requests()
.filter_map(|(module, request)| {
self.generate_one(module, request, &file_contents)
.transpose()
})
.collect::<std::result::Result<Vec<_>, Error>>()?
.iter()
.flatten()
.cloned()
.collect())
}
}

impl ProstValidateGenerator {
pub fn new(config: prost_build::Config, insert_include: bool) -> Self {
Self {
builder: Builder::new(),
config,
insert_include,
}
}

fn generate_prost(
&mut self,
module_request_set: &ModuleRequestSet,
) -> std::result::Result<HashMap<Module, String>, Error> {
// we need to generate a raw file descriptor set in order to build prost-reflect the descriptor pool
// otherwise we can't annotate the config due to missing options in the prost crates.
let file_descriptor_set_bytes = RawProtosSet {
file: module_request_set
.requests()
.map(|(_, request)| request.raw_files())
.flatten()
.map(|f| f.to_vec())
.collect::<Vec<_>>(),
}
.encode_to_vec();
let pool = DescriptorPool::decode(file_descriptor_set_bytes.as_slice())?;

self.builder.annotate(&mut self.config, &pool);
let prost_requests: Vec<_> = module_request_set
.requests()
.flat_map(|(module, request)| {
request.files().map(|proto| (module.clone(), proto.clone()))
})
.collect();

Ok(self.config.generate(prost_requests)?)
}

fn generate_one(
&mut self,
module: &Module,
request: &ModuleRequest,
file_contents: &HashMap<Module, String>,
) -> std::result::Result<Option<Vec<File>>, Error> {
let content = match file_contents.get(module) {
Some(content) => content,
None => return Ok(None),
};
let output_filename = format!("{}.validate.rs", request.proto_package_name());

let mut file_stream = TokenStream::new();
syn::parse_file(content)
.expect("failed to parse generated file")
.items
.iter()
.filter_map(filter_item)
.map(|stream| prost_validate_derive_core::derive(stream))
.for_each(|stream| stream.to_tokens(&mut file_stream));

if file_stream.is_empty() {
return Ok(None);
}

let mut res = Vec::with_capacity(2);
if self.insert_include {
// only include file if it is part of the proto generation request
res.push(
match request.append_to_file(|buf| {
buf.push_str("include!(\"");
buf.push_str(&output_filename);
buf.push_str("\");\n");
}) {
Some(file) => file,
None => return Ok(None),
},
);
}

let file = syn::parse_file(file_stream.to_string().as_str())?;
let content = format!(
"// @generated by protoc-gen-prost-validate\n{}",
prettyplease::unparse(&file).as_str()
);
res.push(File {
name: Some(output_filename.clone()),
content: Some(content),
..File::default()
});
Ok(Some(res))
}
}

fn filter_item(item: &syn::Item) -> Option<TokenStream> {
match item {
syn::Item::Struct(s) => {
if has_validator_derive(&s.attrs) {
Some(item.to_token_stream())
} else {
None
}
}
syn::Item::Enum(e) => {
if has_validator_derive(&e.attrs) {
Some(item.to_token_stream())
} else {
None
}
}
_ => None,
}
}

fn has_validator_derive(attrs: &Vec<syn::Attribute>) -> bool {
let mut has_validator = false;
for attr in attrs {
if attr.path().is_ident("derive") {
let _ = attr.parse_nested_meta(|meta| {
has_validator =
meta.path.to_token_stream().to_string() == ":: prost_validate :: Validator";
Ok(())
});
}
if has_validator {
return true;
}
}
false
}

#[derive(Clone, PartialEq, ::prost::Message)]
struct RawProtosSet {
#[prost(bytes = "vec", repeated, tag = "1")]
pub file: Vec<Vec<u8>>,
}
63 changes: 63 additions & 0 deletions protoc-gen-prost-validate/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
mod generator;

use crate::generator::ProstValidateGenerator;
use prost::Message;
use prost_types::compiler::CodeGeneratorRequest;
use protoc_gen_prost::{
Generator, InvalidParameter, ModuleRequestSet, Param, Params, ProstParameters,
};
use std::str::FromStr;

/// Parameters use to configure [`Generator`]s built into `protoc-gen-prost-validate`
///
/// [`Generator`]: crate::Generator
#[derive(Debug, Default)]
pub struct Parameters {
/// Prost parameters, used to generate [`prost_build::Config`]
pub prost: ProstParameters,

/// Whether to include the `include!` directive in the prost generated file
pub no_include: bool,
}

impl FromStr for Parameters {
type Err = InvalidParameter;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
let mut ret_val = Self::default();
for param in Params::from_protoc_plugin_opts(s)? {
if let Err(param) = ret_val.prost.try_handle_parameter(param) {
match param {
Param::Parameter {
param: "no_include",
}
| Param::Value {
param: "no_include",
value: "true",
} => ret_val.no_include = true,
Param::Value {
param: "no_include",
value: "false",
} => (),
_ => return Err(InvalidParameter::from(param)),
}
}
}

Ok(ret_val)
}
}

/// Execute the core _Prost!_ generator from a raw [`CodeGeneratorRequest`]
pub fn execute(raw_request: &[u8]) -> protoc_gen_prost::Result {
let request = CodeGeneratorRequest::decode(raw_request)?;
let params = request.parameter().parse::<Parameters>()?;
let module_request_set = ModuleRequestSet::new(
request.file_to_generate,
request.proto_file,
raw_request,
params.prost.default_package_filename(),
)?;
let files = ProstValidateGenerator::new(params.prost.to_prost_config(), !params.no_include)
.generate(&module_request_set)?;
Ok(files)
}
27 changes: 25 additions & 2 deletions protoc-gen-prost-validate/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,26 @@
fn main() {
println!("Hello, world!");
use std::{
env,
io::{self, Read, Write},
process::exit,
};

use prost::Message;
use protoc_gen_prost::GeneratorResultExt;

fn main() -> io::Result<()> {
if env::args().any(|x| x == "--version") {
println!(env!("CARGO_PKG_VERSION"));
exit(0);
}

let mut buf = Vec::new();
io::stdin().read_to_end(&mut buf)?;

let response = protoc_gen_prost_validate::execute(buf.as_slice()).unwrap_codegen_response();

buf.clear();
response.encode(&mut buf).expect("error encoding response");
io::stdout().write_all(&buf)?;

Ok(())
}
Loading

0 comments on commit fae0fe0

Please sign in to comment.