blob: d27c8432a62318677069e1a65706eb8d51560000 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
use anyhow::Context;
use anyhow::Result;
use anyhow::anyhow;
use anyhow::bail;
use prost::Message;
use prost_types::DescriptorProto;
use prost_types::FieldDescriptorProto;
use prost_types::FileDescriptorProto;
use prost_types::FileDescriptorSet;
use prost_types::MethodDescriptorProto;
const ASF_HEADER: &str = r#"// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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.
"#;
pub fn generate(workspace_dir: PathBuf) -> Result<()> {
let service_dir = workspace_dir.join("core/services/gcs-grpc");
let proto_dir = service_dir.join("proto");
let output_dir = service_dir.join("src/generated");
let descriptor_path = output_dir.join("descriptor.bin");
fs::create_dir_all(&output_dir)?;
remove_generated_files(&output_dir)?;
tonic_prost_build::configure()
.build_server(false)
.build_client(true)
.bytes(".")
.out_dir(&output_dir)
.file_descriptor_set_path(&descriptor_path)
.compile_protos(
&[proto_dir.join("google/storage/v2/storage.proto")],
std::slice::from_ref(&proto_dir),
)
.context("failed to generate the GCS gRPC client")?;
let descriptor = fs::read(&descriptor_path)?;
fs::remove_file(&descriptor_path)?;
let upstream_descriptor = fs::read(proto_dir.join("upstream-descriptor.bin"))?;
validate_compatibility(&descriptor, &upstream_descriptor)?;
for entry in fs::read_dir(&output_dir)? {
let path = entry?.path();
if path.file_name().and_then(|v| v.to_str()) == Some("mod.rs")
|| path.extension().and_then(|v| v.to_str()) != Some("rs")
{
continue;
}
let generated = fs::read_to_string(&path)?;
if generated.starts_with(ASF_HEADER) {
continue;
}
fs::write(&path, format!("{ASF_HEADER}{generated}"))?;
}
Ok(())
}
fn remove_generated_files(output_dir: &Path) -> Result<()> {
for entry in fs::read_dir(output_dir)? {
let path = entry?.path();
if path.file_name().and_then(|v| v.to_str()) != Some("mod.rs")
&& path.extension().and_then(|v| v.to_str()) == Some("rs")
{
fs::remove_file(path)?;
}
}
Ok(())
}
fn validate_compatibility(descriptor: &[u8], upstream_descriptor: &[u8]) -> Result<()> {
let descriptor = FileDescriptorSet::decode(descriptor)?;
let upstream_descriptor = FileDescriptorSet::decode(upstream_descriptor)?;
let file = storage_file(&descriptor)?;
let upstream_file = storage_file(&upstream_descriptor)?;
let service = file
.service
.iter()
.find(|service| service.name.as_deref() == Some("Storage"))
.ok_or_else(|| anyhow!("generated descriptor does not define Storage"))?;
let upstream_service = upstream_file
.service
.iter()
.find(|service| service.name.as_deref() == Some("Storage"))
.ok_or_else(|| anyhow!("upstream descriptor does not define Storage"))?;
const RETAINED_METHODS: [&str; 9] = [
"CancelResumableWrite",
"DeleteObject",
"GetObject",
"ListObjects",
"QueryWriteStatus",
"ReadObject",
"RewriteObject",
"StartResumableWrite",
"WriteObject",
];
let mut method_names = service
.method
.iter()
.map(|method| method.name.as_deref().unwrap_or_default())
.collect::<Vec<_>>();
method_names.sort_unstable();
if method_names != RETAINED_METHODS {
bail!("generated Storage methods do not match the retained method set");
}
for method in &service.method {
let name = method.name.as_deref().unwrap_or_default();
let upstream_method = upstream_service
.method
.iter()
.find(|candidate| candidate.name.as_deref() == Some(name))
.ok_or_else(|| anyhow!("upstream Storage does not define {name}"))?;
validate_method(method, upstream_method)?;
}
let package = file.package.as_deref().unwrap_or_default();
let upstream_package = upstream_file.package.as_deref().unwrap_or_default();
if package != upstream_package {
bail!("generated package {package} does not match upstream package {upstream_package}");
}
let messages = collect_messages(file);
let upstream_messages = collect_messages(upstream_file);
for (name, message) in messages {
let upstream_message = upstream_messages
.get(&name)
.ok_or_else(|| anyhow!("upstream descriptor does not define {name}"))?;
validate_message(&name, message, upstream_message)?;
}
Ok(())
}
fn storage_file(descriptor: &FileDescriptorSet) -> Result<&FileDescriptorProto> {
descriptor
.file
.iter()
.find(|file| file.name.as_deref() == Some("google/storage/v2/storage.proto"))
.ok_or_else(|| anyhow!("descriptor does not contain google/storage/v2/storage.proto"))
}
fn validate_method(method: &MethodDescriptorProto, upstream: &MethodDescriptorProto) -> Result<()> {
let name = method.name.as_deref().unwrap_or_default();
if method.input_type != upstream.input_type
|| method.output_type != upstream.output_type
|| method.client_streaming.unwrap_or(false) != upstream.client_streaming.unwrap_or(false)
|| method.server_streaming.unwrap_or(false) != upstream.server_streaming.unwrap_or(false)
{
bail!("method {name} is incompatible with the upstream descriptor");
}
Ok(())
}
fn collect_messages(file: &FileDescriptorProto) -> BTreeMap<String, &DescriptorProto> {
let mut messages = BTreeMap::new();
let package = file.package.as_deref().unwrap_or_default();
for message in &file.message_type {
collect_message(package, message, &mut messages);
}
messages
}
fn collect_message<'a>(
parent: &str,
message: &'a DescriptorProto,
messages: &mut BTreeMap<String, &'a DescriptorProto>,
) {
let name = message.name.as_deref().unwrap_or_default();
let full_name = format!("{parent}.{name}");
messages.insert(full_name.clone(), message);
for nested in &message.nested_type {
collect_message(&full_name, nested, messages);
}
}
fn validate_message(
name: &str,
message: &DescriptorProto,
upstream: &DescriptorProto,
) -> Result<()> {
if message
.options
.as_ref()
.and_then(|options| options.map_entry)
!= upstream
.options
.as_ref()
.and_then(|options| options.map_entry)
{
bail!("message {name} has an incompatible map entry declaration");
}
for field in &message.field {
let number = field.number.unwrap_or_default();
let upstream_field = upstream
.field
.iter()
.find(|candidate| candidate.number == field.number)
.ok_or_else(|| anyhow!("upstream message {name} does not define field {number}"))?;
validate_field(name, message, field, upstream, upstream_field)?;
}
Ok(())
}
fn validate_field(
message_name: &str,
message: &DescriptorProto,
field: &FieldDescriptorProto,
upstream_message: &DescriptorProto,
upstream: &FieldDescriptorProto,
) -> Result<()> {
let field_name = field.name.as_deref().unwrap_or_default();
if field.name != upstream.name
|| field.label != upstream.label
|| field.r#type != upstream.r#type
|| field.type_name != upstream.type_name
|| field.proto3_optional.unwrap_or(false) != upstream.proto3_optional.unwrap_or(false)
|| oneof_name(message, field) != oneof_name(upstream_message, upstream)
{
bail!("field {message_name}.{field_name} is incompatible with the upstream descriptor");
}
Ok(())
}
fn oneof_name<'a>(message: &'a DescriptorProto, field: &FieldDescriptorProto) -> Option<&'a str> {
field
.oneof_index
.and_then(|index| message.oneof_decl.get(index as usize))
.and_then(|oneof| oneof.name.as_deref())
}