use crate::helper::error;
use crate::parsed_input::FieldHandling::{OptionedOnly, Required};
use crate::parsed_input::{FieldParsed, StructParsed};
use crate::{TypeHelperAttributesK8sOpenapi, TypeHelperAttributesKube};
use proc_macro2::{Ident, TokenStream};
use quote::{quote, ToTokens};
use std::borrow::Cow;
use syn::{parse_quote, Attribute, Data, Error, ImplGenerics, Path, TypeGenerics, WhereClause};
pub(crate) enum ResourceType {
K8sOpenApi,
Kube,
}
pub(crate) fn k8s_resource_type(
attrs_k8s_openapi: Option<&TypeHelperAttributesK8sOpenapi>,
attrs_kube: Option<&TypeHelperAttributesKube>,
) -> Result<Option<ResourceType>, Error> {
if attrs_k8s_openapi.is_some() && attrs_kube.is_some() {
return error(
"Conflicting configuration. Only one of the `#[optionable(k8s_openapi)]` or `#[optionable(kube)]` attribute is allowed at once per type.",
);
}
if attrs_k8s_openapi
.as_ref()
.is_some_and(|attr| attr.resource.is_some())
{
return Ok(Some(ResourceType::K8sOpenApi));
}
if attrs_kube
.as_ref()
.is_some_and(|attr| attr.resource.is_some())
{
return Ok(Some(ResourceType::Kube));
}
Ok(None)
}
pub(crate) fn k8s_derives(data: &Data) -> Option<Vec<String>> {
match data {
Data::Struct(_) => Some(vec![
"Clone".to_owned(),
"std::fmt::Debug".to_owned(),
"Default".to_owned(),
"PartialEq".to_owned(),
"serde::Serialize".to_owned(),
"serde::Deserialize".to_owned(),
]),
Data::Enum(_) => Some(vec![
"Clone".to_owned(),
"std::fmt::Debug".to_owned(),
"PartialEq".to_owned(),
"serde::Serialize".to_owned(),
"serde::Deserialize".to_owned(),
]),
Data::Union(_) => None,
}
}
pub(crate) fn k8s_adjust_fields(
struct_parsed: &mut StructParsed,
attr_k8s_openapi: Option<&TypeHelperAttributesK8sOpenapi>,
attr_kube: Option<&TypeHelperAttributesKube>,
resource_type: Option<&ResourceType>,
crate_name: &Path,
) -> Result<(), Error> {
if attr_k8s_openapi.is_some() {
k8s_openapi_adjust_field_serde_renames(struct_parsed)?;
}
if attr_k8s_openapi.is_some_and(|attr| attr.metadata.is_some())
|| attr_kube.is_some_and(|attr| attr.resource.is_some())
{
k8s_openapi_set_metadata_required(struct_parsed);
}
if let Some(k8s_resource_type) = &resource_type {
k8s_openapi_field_resource_adjust(struct_parsed, k8s_resource_type, crate_name);
}
Ok(())
}
fn k8s_openapi_set_metadata_required(struct_parsed: &mut StructParsed) {
struct_parsed
.fields
.iter_mut()
.filter(|f| f.field.ident.as_ref().is_some_and(|f| *f == "metadata"))
.for_each(|f| f.handling = Required);
}
fn k8s_openapi_adjust_field_serde_renames(struct_parsed: &mut StructParsed) -> Result<(), Error> {
struct_parsed.fields.iter_mut().try_for_each(|f| {
if let Some(name) = &f.field.ident
&& let Some(name_serialized) = k8s_openapi_serde_rename(name.to_string().as_ref())
{
f.field
.attrs
.push(parse_quote!(#[optionable_attr(serde(rename=#name_serialized))]));
}
Ok(())
})
}
#[allow(clippy::missing_panics_doc)] fn k8s_openapi_serde_rename(input: &str) -> Option<Cow<'static, str>> {
const UPPERCASE_WORDS: &[&str; 15] = &[
"api", "cidr", "cpu", "fqdn", "id", "io", "ip", "ipc", "pid", "tls", "uid", "uuid", "uri",
"url", "wwn",
];
const PLURAL_WORDS: &[&str; 6] = &["cidrs", "ids", "ips", "uris", "urls", "wwns"];
match input {
"ref_path" => Some("$ref".into()),
"schema" => Some("$schema".into()),
"as_" => Some("as".into()),
"continue_" => Some("continue".into()),
"enum_" => Some("enum".into()),
"ref_" => Some("ref".into()),
"type_" => Some("type".into()),
other => {
if !other.split('_').enumerate().any(|(i, el)| {
i != 0 && (UPPERCASE_WORDS.contains(&el) || PLURAL_WORDS.contains(&el))
}) {
return None;
}
let name: String = other
.split('_')
.enumerate()
.map(|(i, el)| {
if i == 0 || el.is_empty() {
return Cow::Borrowed(el);
}
if UPPERCASE_WORDS.contains(&el) {
return el.to_uppercase().into();
}
if PLURAL_WORDS.contains(&el) {
let mut chars: Vec<char> = el.chars().collect();
let last = chars.pop().unwrap().to_string();
return (chars.into_iter().collect::<String>().to_uppercase() + &last)
.into();
}
let mut chars = el.chars();
(chars.next().unwrap().to_uppercase().to_string() + chars.as_str()).into()
})
.collect();
Some(name.into())
}
}
}
#[test]
fn roundtrip_k8s_openapi_adjust_field_serde() {
const SPECIAL_KEYS: &[&str] = &[
"clusterIP",
"clusterIPs",
"diskURI",
"externalIP",
"externalIPs",
"hostIP",
"hostIPs",
"nonResourceURL",
"nonResourceURLs",
"pdID",
"podCIDR",
"podCIDRs",
"podIP",
"podIPs",
"serverAddressByClientCIDR",
"serverAddressByClientCIDRs",
"targetWWN",
"targetWWNs",
"volumeID",
"$ref",
"$schema",
"as",
"continue",
"enum",
"ref",
"type",
];
for key in SPECIAL_KEYS {
let rust_ident = k8s_openapi_codegen_common::get_rust_ident(key);
let key_roundtrip = k8s_openapi_serde_rename(rust_ident.as_ref());
assert!(key_roundtrip.is_some());
assert_eq!(key.to_owned(), key_roundtrip.unwrap().into_owned());
}
}
pub(crate) fn k8s_type_attr(data: &Data) -> Option<Attribute> {
match data {
Data::Struct(_) => Some(parse_quote!(#[serde(rename_all="camelCase")])),
Data::Enum(_) => Some(parse_quote!(#[serde(rename_all_fields="camelCase")])),
Data::Union(_) => None,
}
}
fn k8s_openapi_field_resource_adjust(
struct_parsed: &mut StructParsed,
resource_type: &ResourceType,
crate_name: &Path,
) {
let mut envelope_serde_path = crate_name.to_token_stream().to_string();
match resource_type {
ResourceType::K8sOpenApi => {
envelope_serde_path.push_str("::k8s_openapi");
}
ResourceType::Kube => {
envelope_serde_path.push_str("::kube");
}
}
let mut serialize_fn = envelope_serde_path.clone();
serialize_fn.push_str("::serialize_api_envelope");
let mut deserialize_fn = envelope_serde_path;
deserialize_fn.push_str("::deserialize_api_envelope");
let field = parse_quote!(
#[optionable_attr(serde(flatten,serialize_with=#serialize_fn,deserialize_with=#deserialize_fn))]
pub phantom: std::marker::PhantomData<Self>
);
struct_parsed.fields.push(FieldParsed {
field,
handling: OptionedOnly,
});
}
pub(crate) fn k8s_openapi_impl_resource(
ty_ident: &Path,
ty_ident_opt: &Ident,
impl_generics: &ImplGenerics,
ty_generics: &TypeGenerics,
where_clause_impl_optionable: &WhereClause,
) -> TokenStream {
quote!(
impl #impl_generics k8s_openapi::Resource for #ty_ident_opt #ty_generics #where_clause_impl_optionable {
const API_VERSION: &'static str = <#ty_ident #ty_generics as k8s_openapi::Resource>::API_VERSION;
const GROUP: &'static str = <#ty_ident #ty_generics as k8s_openapi::Resource>::GROUP;
const KIND: &'static str = <#ty_ident #ty_generics as k8s_openapi::Resource>::KIND;
const VERSION: &'static str = <#ty_ident #ty_generics as k8s_openapi::Resource>::VERSION;
const URL_PATH_SEGMENT: &'static str =
<#ty_ident #ty_generics as k8s_openapi::Resource>::URL_PATH_SEGMENT;
type Scope = <#ty_ident #ty_generics as k8s_openapi::Resource>::Scope;
}
)
}
pub(crate) fn k8s_openapi_impl_metadata(
ty_ident: &Path,
ty_ident_opt: &Ident,
impl_generics: &ImplGenerics,
ty_generics: &TypeGenerics,
where_clause_impl_optionable: &WhereClause,
) -> TokenStream {
quote!(
impl #impl_generics k8s_openapi::Metadata for #ty_ident_opt #ty_generics #where_clause_impl_optionable {
type Ty = <#ty_ident #ty_generics as k8s_openapi::Metadata>::Ty;
fn metadata(&self) -> &<Self as k8s_openapi::Metadata>::Ty {
&self.metadata
}
fn metadata_mut(&mut self) -> &mut <Self as k8s_openapi::Metadata>::Ty {
&mut self.metadata
}
}
)
}
#[allow(unused_variables)]
#[allow(clippy::unnecessary_wraps)]
pub(crate) fn error_missing_features(
attrs_k8s_openapi: Option<&TypeHelperAttributesK8sOpenapi>,
attrs_kube: Option<&TypeHelperAttributesKube>,
) -> Result<(), Error> {
#[cfg(not(feature = "k8s_openapi"))]
if attrs_k8s_openapi
.as_ref()
.is_some_and(|attr| attr.resource.is_some())
{
return error(
"helper attributes `#[optionable(k8s_openapi(resource))] require one of the `k8s_openapi_*` features to be enabled for the `optionable` crate.",
);
}
#[cfg(not(feature = "kube"))]
if attrs_kube
.as_ref()
.is_some_and(|attr| attr.resource.is_some())
{
return error(
"helper attributes `#[optionable(kube(resource)] require the `kube` feature to be enabled for the `optionable` crate.",
);
}
Ok(())
}