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)]
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>,
}
#[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>,
}
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 oneof_variants: Option<Vec<OneofVariant>>,
pub documentation: Option<String>,
}
impl BodyField {
pub fn is_optional(&self) -> bool {
use crate::parsing::types::BaseType;
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 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
}