use std::collections::HashSet;
use convert_case::{Case, Casing};
use quote::format_ident;
use syn::Ident;
use crate::error::{Error, Result};
use crate::google::api::{ResourceDescriptor, http_rule::Pattern};
use crate::parsing::types::UnifiedType;
use crate::parsing::{CodeGenMetadata, HttpPattern, MethodMetadata, OneofVariant};
#[derive(Debug, Clone, PartialEq)]
pub enum RequestType {
List,
Create,
Get,
Update,
Delete,
Custom(Pattern),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MethodShape {
CollectionScoped,
InstanceScoped,
Unbound,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EmitShape {
List,
Create,
GetUpdate,
Delete,
}
pub(crate) fn emit_shape(request_type: &RequestType, is_collection: bool) -> EmitShape {
match request_type {
RequestType::List => EmitShape::List,
RequestType::Create => EmitShape::Create,
RequestType::Delete => EmitShape::Delete,
RequestType::Get | RequestType::Update => EmitShape::GetUpdate,
RequestType::Custom(Pattern::Post(_) | Pattern::Patch(_)) if is_collection => {
EmitShape::Create
}
RequestType::Custom(_) => EmitShape::GetUpdate,
}
}
#[derive(Debug, Clone)]
pub struct SkippedMethod {
pub service_name: String,
pub method_name: String,
pub reason: String,
}
#[derive(Debug)]
pub struct GenerationPlan {
pub services: Vec<ServicePlan>,
pub skipped_methods: Vec<SkippedMethod>,
}
#[derive(Debug, Clone)]
pub struct ServicePlan {
pub service_name: String,
pub handler_name: String,
pub base_path: String,
pub package: String,
pub methods: Vec<MethodPlan>,
pub managed_resources: Vec<ManagedResource>,
pub documentation: Option<String>,
pub hierarchy: Vec<ResourceHierarchy>,
pub resource_accessor_params: Option<Vec<String>>,
pub direct_children: Vec<ChildLink>,
}
#[derive(Debug, Clone)]
pub struct ChildLink {
pub child_singular: String,
pub child_base_path: String,
pub child_accessor_params: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct MethodPlan {
pub metadata: MethodMetadata,
pub handler_function_name: String,
pub http_pattern: HttpPattern,
pub http_method: String,
pub parameters: Vec<RequestParam>,
pub has_response: bool,
pub request_type: RequestType,
pub output_resource_type: Option<String>,
pub has_request_body: bool,
pub needs_request_parts: bool,
pub scoped_verb: Option<String>,
pub shape: MethodShape,
}
impl MethodPlan {
pub fn path_parameters(&self) -> impl Iterator<Item = &PathParam> {
self.parameters.iter().filter_map(|param| match param {
RequestParam::Path(path_param) => Some(path_param),
_ => None,
})
}
pub fn query_parameters(&self) -> impl Iterator<Item = &QueryParam> {
self.parameters.iter().filter_map(|param| match param {
RequestParam::Query(query_param) => Some(query_param),
_ => None,
})
}
pub fn body_fields(&self) -> impl Iterator<Item = &BodyField> {
self.parameters.iter().filter_map(|param| match param {
RequestParam::Body(body_field) => Some(body_field),
_ => None,
})
}
}
#[derive(Debug, Clone)]
pub enum RequestParam {
Path(PathParam),
Query(QueryParam),
Body(BodyField),
}
impl RequestParam {
pub fn name(&self) -> &str {
match self {
RequestParam::Path(param) => ¶m.name,
RequestParam::Query(param) => ¶m.name,
RequestParam::Body(param) => ¶m.name,
}
}
pub fn field_type(&self) -> &UnifiedType {
match self {
RequestParam::Path(param) => ¶m.field_type,
RequestParam::Query(param) => ¶m.field_type,
RequestParam::Body(param) => ¶m.field_type,
}
}
pub fn field_ident(&self) -> Ident {
format_ident!("{}", self.name())
}
pub fn is_optional(&self) -> bool {
match self {
RequestParam::Path(_) => false,
RequestParam::Query(param) => param.is_optional(),
RequestParam::Body(param) => param.is_optional(),
}
}
pub fn is_path_param(&self) -> bool {
matches!(self, RequestParam::Path(_))
}
pub fn documentation(&self) -> Option<&str> {
match self {
RequestParam::Path(param) => param.documentation.as_deref(),
RequestParam::Query(param) => param.documentation.as_deref(),
RequestParam::Body(param) => param.documentation.as_deref(),
}
}
}
#[derive(Debug, Clone)]
pub struct PathParam {
pub name: String,
pub field_type: UnifiedType,
pub documentation: Option<String>,
}
impl From<PathParam> for RequestParam {
fn from(param: PathParam) -> Self {
RequestParam::Path(param)
}
}
#[derive(Debug, Clone)]
pub struct QueryParam {
pub name: String,
pub field_type: UnifiedType,
pub documentation: Option<String>,
pub resource_reference: Option<crate::google::api::ResourceReference>,
}
impl QueryParam {
pub fn is_optional(&self) -> bool {
self.field_type.is_optional
}
}
impl From<QueryParam> for RequestParam {
fn from(param: QueryParam) -> Self {
RequestParam::Query(param)
}
}
#[derive(Debug, Clone)]
pub struct BodyField {
pub name: String,
pub field_type: UnifiedType,
pub repeated: bool,
pub required: bool,
pub oneof_variants: Option<Vec<OneofVariant>>,
pub documentation: Option<String>,
}
impl BodyField {
pub fn is_optional(&self) -> bool {
use crate::parsing::types::BaseType;
if self.required {
return false;
}
self.field_type.is_optional
|| self.repeated
|| matches!(
self.field_type.base_type,
BaseType::Map(_, _) | BaseType::Message(_) | BaseType::OneOf(_)
)
}
}
impl From<BodyField> for RequestParam {
fn from(field: BodyField) -> Self {
RequestParam::Body(field)
}
}
#[derive(Debug, Clone)]
pub struct ManagedResource {
pub type_name: String,
pub descriptor: ResourceDescriptor,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ResourceHierarchy {
pub child_resource_type: String,
pub parent_resource_type: String,
pub parent_field_name: String,
pub parent_singular: Option<String>,
}
pub(crate) struct MethodPlanner<'a> {
method: &'a MethodMetadata,
pattern: Pattern,
path: HttpPattern,
metadata: &'a CodeGenMetadata,
}
impl<'a> MethodPlanner<'a> {
pub(crate) fn try_new(
method: &'a MethodMetadata,
metadata: &'a CodeGenMetadata,
) -> Result<Self> {
let Some(pattern) = &method.http_rule.pattern else {
return Err(Error::MissingAnnotation {
object: method.method_name.clone(),
message: "Missing HTTP rule pattern".to_string(),
});
};
Ok(Self {
method,
path: method.http_pattern.clone(),
pattern: pattern.clone(),
metadata,
})
}
pub(crate) fn into_http_pattern(self) -> HttpPattern {
self.path
}
pub(crate) fn request_type(&self) -> RequestType {
let snake_name = self.method.method_name.to_case(Case::Snake);
let verb_resource = snake_name.split_once('_');
if let Some((verb, resource)) = verb_resource {
#[allow(clippy::type_complexity)]
let standard_ops: &[(
&str,
fn(&Pattern) -> bool,
bool,
bool,
RequestType,
)] = &[
(
"get",
|p| matches!(p, Pattern::Get(_)),
true,
false,
RequestType::Get,
),
(
"list",
|p| matches!(p, Pattern::Get(_)),
false,
true,
RequestType::List,
),
(
"create",
|p| matches!(p, Pattern::Post(_)),
false,
false,
RequestType::Create,
),
(
"update",
|p| matches!(p, Pattern::Patch(_)),
true,
false,
RequestType::Update,
),
(
"delete",
|p| matches!(p, Pattern::Delete(_)),
true,
false,
RequestType::Delete,
),
];
for &(expected_verb, pattern_check, ends_with_param, use_plural, ref result_type) in
standard_ops
{
if verb != expected_verb || !pattern_check(&self.pattern) {
continue;
}
if ends_with_param && self.path.ends_with_static() {
continue;
}
if !ends_with_param && self.path.ends_with_parameter() {
continue;
}
let found = if use_plural {
self.metadata.resource_from_plural(resource).is_some()
} else {
self.metadata.resource_from_singular(resource).is_some()
};
if found {
return result_type.clone();
}
}
}
RequestType::Custom(self.pattern.clone())
}
pub(crate) fn has_response(&self) -> bool {
!self.method.output_type.is_empty() && !self.method.output_type.ends_with("Empty")
}
pub(crate) fn output_resource_type(&self) -> Option<String> {
if self.has_response() {
let output_type = &self.method.output_type;
let simple = output_type
.rfind('.')
.map(|i| &output_type[i + 1..])
.unwrap_or(output_type);
Some(simple.to_string())
} else {
None
}
}
}
pub(crate) fn request_has_body(request_type: &RequestType) -> bool {
matches!(
request_type,
RequestType::Create
| RequestType::Update
| RequestType::Custom(Pattern::Post(_))
| RequestType::Custom(Pattern::Patch(_))
)
}
pub(crate) fn request_needs_request_parts(request_type: &RequestType) -> bool {
matches!(
request_type,
RequestType::List | RequestType::Get | RequestType::Delete
) || matches!(
request_type,
RequestType::Custom(Pattern::Get(_) | Pattern::Delete(_))
)
}
pub(crate) fn is_collection_method(request_type: &RequestType, method_name: &str) -> bool {
matches!(request_type, RequestType::List | RequestType::Create)
|| (matches!(request_type, RequestType::Custom(Pattern::Get(_)))
&& method_name.starts_with("List"))
|| (matches!(request_type, RequestType::Custom(Pattern::Post(_)))
&& method_name.starts_with("Generate"))
}
pub(crate) fn method_shape(
request_type: &RequestType,
method_name: &str,
service_has_resource: bool,
) -> MethodShape {
if !service_has_resource {
MethodShape::Unbound
} else if is_collection_method(request_type, method_name) {
MethodShape::CollectionScoped
} else {
MethodShape::InstanceScoped
}
}
pub(crate) fn scoped_verb(request_type: &RequestType) -> Option<String> {
match request_type {
RequestType::Get => Some("get".to_string()),
RequestType::Update => Some("update".to_string()),
RequestType::Delete => Some("delete".to_string()),
_ => None,
}
}
pub fn split_body_fields(plan: &MethodPlan) -> (Vec<&BodyField>, Vec<&BodyField>) {
let mut required = Vec::new();
let mut optional = Vec::new();
for field in plan.body_fields() {
if field.is_optional() {
optional.push(field);
} else {
required.push(field);
}
}
(required, optional)
}
pub fn extract_managed_resources(
metadata: &CodeGenMetadata,
methods: &[MethodPlan],
) -> Vec<ManagedResource> {
let mut resources = Vec::new();
let mut seen_types = HashSet::<String>::new();
for method in methods {
if let Some(ref resource_type) = method.output_resource_type {
if seen_types.contains(resource_type) {
continue;
}
if let Some(descriptor) = metadata.get_resource_descriptor(resource_type) {
resources.push(ManagedResource {
type_name: resource_type.clone(),
descriptor: descriptor.clone(),
});
seen_types.insert(resource_type.clone());
}
}
}
resources
}
#[cfg(test)]
mod tests {
use super::*;
fn custom(p: Pattern) -> RequestType {
RequestType::Custom(p)
}
#[test]
fn request_has_body_matches_write_verbs() {
assert!(request_has_body(&RequestType::Create));
assert!(request_has_body(&RequestType::Update));
assert!(request_has_body(&custom(Pattern::Post(String::new()))));
assert!(request_has_body(&custom(Pattern::Patch(String::new()))));
assert!(!request_has_body(&RequestType::List));
assert!(!request_has_body(&RequestType::Get));
assert!(!request_has_body(&RequestType::Delete));
assert!(!request_has_body(&custom(Pattern::Get(String::new()))));
assert!(!request_has_body(&custom(Pattern::Delete(String::new()))));
}
#[test]
fn needs_request_parts_matches_read_verbs() {
assert!(request_needs_request_parts(&RequestType::List));
assert!(request_needs_request_parts(&RequestType::Get));
assert!(request_needs_request_parts(&RequestType::Delete));
assert!(request_needs_request_parts(&custom(Pattern::Get(
String::new()
))));
assert!(request_needs_request_parts(&custom(Pattern::Delete(
String::new()
))));
assert!(!request_needs_request_parts(&RequestType::Create));
assert!(!request_needs_request_parts(&RequestType::Update));
assert!(!request_needs_request_parts(&custom(Pattern::Post(
String::new()
))));
assert!(!request_needs_request_parts(&custom(Pattern::Patch(
String::new()
))));
}
#[test]
fn has_body_and_needs_parts_are_mutually_exclusive_for_known_verbs() {
for rt in [
RequestType::List,
RequestType::Create,
RequestType::Get,
RequestType::Update,
RequestType::Delete,
custom(Pattern::Get(String::new())),
custom(Pattern::Post(String::new())),
custom(Pattern::Patch(String::new())),
custom(Pattern::Delete(String::new())),
] {
assert_ne!(
request_has_body(&rt),
request_needs_request_parts(&rt),
"body vs request-parts should partition {rt:?}"
);
}
}
#[test]
fn is_collection_method_standard_and_name_heuristics() {
assert!(is_collection_method(&RequestType::List, "ListThings"));
assert!(is_collection_method(&RequestType::Create, "CreateThing"));
assert!(is_collection_method(
&custom(Pattern::Get(String::new())),
"ListTagAssignments"
));
assert!(!is_collection_method(
&custom(Pattern::Get(String::new())),
"GetTagAssignment"
));
assert!(is_collection_method(
&custom(Pattern::Post(String::new())),
"GenerateCredentials"
));
assert!(!is_collection_method(
&custom(Pattern::Post(String::new())),
"RotateToken"
));
assert!(!is_collection_method(&RequestType::Get, "GetThing"));
assert!(!is_collection_method(&RequestType::Update, "UpdateThing"));
assert!(!is_collection_method(&RequestType::Delete, "DeleteThing"));
}
#[test]
fn method_shape_classification() {
assert_eq!(
method_shape(&RequestType::List, "ListTagAssignments", false),
MethodShape::Unbound
);
assert_eq!(
method_shape(&RequestType::Get, "GetTagAssignment", false),
MethodShape::Unbound
);
assert_eq!(
method_shape(&custom(Pattern::Post(String::new())), "TouchTag", false),
MethodShape::Unbound
);
assert_eq!(
method_shape(&RequestType::List, "ListCatalogs", true),
MethodShape::CollectionScoped
);
assert_eq!(
method_shape(&RequestType::Create, "CreateCatalog", true),
MethodShape::CollectionScoped
);
assert_eq!(
method_shape(
&custom(Pattern::Post(String::new())),
"GenerateCredentials",
true
),
MethodShape::CollectionScoped
);
assert_eq!(
method_shape(&RequestType::Get, "GetCatalog", true),
MethodShape::InstanceScoped
);
assert_eq!(
method_shape(&RequestType::Update, "UpdateCatalog", true),
MethodShape::InstanceScoped
);
assert_eq!(
method_shape(&RequestType::Delete, "DeleteCatalog", true),
MethodShape::InstanceScoped
);
assert_eq!(
method_shape(&custom(Pattern::Post(String::new())), "RotateToken", true),
MethodShape::InstanceScoped
);
}
#[test]
fn emit_shape_classification() {
assert_eq!(emit_shape(&RequestType::List, false), EmitShape::List);
assert_eq!(emit_shape(&RequestType::Create, false), EmitShape::Create);
assert_eq!(emit_shape(&RequestType::Get, false), EmitShape::GetUpdate);
assert_eq!(
emit_shape(&RequestType::Update, false),
EmitShape::GetUpdate
);
assert_eq!(emit_shape(&RequestType::Delete, false), EmitShape::Delete);
assert_eq!(
emit_shape(&custom(Pattern::Post(String::new())), true),
EmitShape::Create
);
assert_eq!(
emit_shape(&custom(Pattern::Patch(String::new())), true),
EmitShape::Create
);
assert_eq!(
emit_shape(&custom(Pattern::Post(String::new())), false),
EmitShape::GetUpdate
);
assert_eq!(
emit_shape(&custom(Pattern::Get(String::new())), true),
EmitShape::GetUpdate
);
assert_eq!(
emit_shape(&custom(Pattern::Get(String::new())), false),
EmitShape::GetUpdate
);
assert_eq!(
emit_shape(&custom(Pattern::Delete(String::new())), false),
EmitShape::GetUpdate
);
}
#[test]
fn scoped_verb_only_for_standard_instance_ops() {
assert_eq!(scoped_verb(&RequestType::Get).as_deref(), Some("get"));
assert_eq!(scoped_verb(&RequestType::Update).as_deref(), Some("update"));
assert_eq!(scoped_verb(&RequestType::Delete).as_deref(), Some("delete"));
assert_eq!(scoped_verb(&RequestType::List), None);
assert_eq!(scoped_verb(&RequestType::Create), None);
assert_eq!(scoped_verb(&custom(Pattern::Post(String::new()))), None);
assert_eq!(scoped_verb(&custom(Pattern::Get(String::new()))), None);
}
}