1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
//! A prost toolkit to build protobuf with serde support.
//!
//! Usually when we define our protobuf messages, we hope some of the generated
//! data structures have good serde support. Fortunately `serde-build` has that
//! capability - you can create a config with `prost_build::Config::new()` and
//! and then set proper attributes for type or field. For exmaple, you can add
//! serde support for this structure by using:
//!
//! ```ignore
//! config.type_attribute("package.RequestPing", "#[derive(serde::Serialize, serde::Deserialize)]");
//! config.type_attribute("package.RequestPing", "#[serde(default)]");
//! ```
//!
//! and you will get this generated code:
//!
//! ```ignore
//! #[derive(serde::Serialize, serde::Deserialize)]
//! #[serde(default)]
//! #[derive(Clone, PartialEq, ::prost::Message)]
//! pub struct RequestPing {
//! #[prost(string, tag = "1")]
//! pub ping: ::prost::alloc::string::String,
//! }
//! ```
//!
//! This crate helps to simplify this build script by using a predefined build configuration.
//!
//! # Getting started
//!
//! First of all, you shall create a JSON file which contains configuration for the builder. You can
//! get a copy of a default JSON file from: [default_build_config.json](https://raw.githubusercontent.com/tyrchen/prost-helper/master/prost-serde/default_build_config.json). See an example of [build_config.json](https://raw.githubusercontent.com/tyrchen/prost-helper/master/prost-serde/examples/build_config.json).
//! Please add the proto files, proto includes, output dir, and the data structure or field you want
//! to add the desired attributes.
//! Then you could use it in:
//!
//! ```ignore
//! use prost_serde::build_with_serde;
//!
//! fn main() {
//! let json = include_str!("../examples/build_config.json");
//! build_with_serde(json);
//! }
//! ```
use serde::{Deserialize, Serialize};
use std::{fs, process::Command};
#[derive(Deserialize, Serialize, Debug, Default)]
#[serde(default)]
pub struct BuildConfig {
/// protobuf include dirs
pub includes: Vec<String>,
/// protobuf files
pub files: Vec<String>,
/// dir for generated code, defaults to Cargo OUT_DIR, else the current dir
pub output: Option<String>,
/// build options for serde support
pub opts: Vec<BuildOption>,
}
#[derive(Deserialize, Serialize, Debug, Default)]
#[serde(default)]
pub struct BuildOption {
/// scope of the attribute
pub scope: String,
/// description of the option
pub description: String,
/// extra attribute to put on generated data structure, for example: `#[derive(Serialize, Deserialize)]`
pub attr: String,
/// a list of paths you want to add the attribute
pub paths: Vec<String>,
}
/// Build the protobuf files with the build opts provided by a JSON string.
///
/// Normally you should put the json file in your crate, and load it with `include_str!`,
/// then pass it to this build function.
pub fn build_with_serde(json: &str) -> BuildConfig {
let build_config: BuildConfig = serde_json::from_str(json).unwrap();
// For the output directory, use the specified one, or fallback to the
// OUT_DIR env variable provided by Cargo if it exists (it should!), else
// fallback to the current directory.
let output_dir: String = match &build_config.output {
None => {
let default_output_dir = std::env::var("OUT_DIR");
match default_output_dir {
Err(_) => String::new(),
Ok(cargo_out_dir) => cargo_out_dir,
}
}
Some(specified_output) => specified_output.to_owned(),
};
let mut config = prost_build::Config::new();
for opt in build_config.opts.iter() {
match opt.scope.as_ref() {
"bytes" => {
config.bytes(&opt.paths);
continue;
}
"btree_map" => {
config.btree_map(&opt.paths);
continue;
}
_ => (),
};
for path in opt.paths.iter() {
match opt.scope.as_str() {
"type" => config.type_attribute(path, opt.attr.as_str()),
"field" => config.field_attribute(path, opt.attr.as_str()),
v => panic!("Not supported type: {}", v),
};
}
}
fs::create_dir_all(&output_dir).unwrap();
config.out_dir(&output_dir);
config
.compile_protos(&build_config.files, &build_config.includes)
.unwrap_or_else(|e| panic!("Failed to compile proto files. Error: {:?}", e));
Command::new("cargo")
.args(&["fmt"])
.status()
.expect("cargo fmt failed");
build_config
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generate_serde_supported_code() {
let json = include_str!("../examples/build_config.json");
build_with_serde(json);
}
}