mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 09:07:09 +08:00
feat(macro): add config validation macro for connectors (#1755)
This commit is contained in:
@ -423,7 +423,7 @@ pub struct SupportedConnectors {
|
||||
pub wallets: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Default)]
|
||||
#[derive(Debug, Deserialize, Clone, Default, router_derive::ConfigValidate)]
|
||||
#[serde(default)]
|
||||
pub struct Connectors {
|
||||
pub aci: ConnectorParams,
|
||||
@ -476,26 +476,23 @@ pub struct Connectors {
|
||||
pub worldline: ConnectorParams,
|
||||
pub worldpay: ConnectorParams,
|
||||
pub zen: ConnectorParams,
|
||||
|
||||
// Keep this field separate from the remaining fields
|
||||
pub supported: SupportedConnectors,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Default)]
|
||||
#[derive(Debug, Deserialize, Clone, Default, router_derive::ConfigValidate)]
|
||||
#[serde(default)]
|
||||
pub struct ConnectorParams {
|
||||
pub base_url: String,
|
||||
pub secondary_base_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Default)]
|
||||
#[derive(Debug, Deserialize, Clone, Default, router_derive::ConfigValidate)]
|
||||
#[serde(default)]
|
||||
pub struct ConnectorParamsWithMoreUrls {
|
||||
pub base_url: String,
|
||||
pub base_url_bank_redirects: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Default)]
|
||||
#[derive(Debug, Deserialize, Clone, Default, router_derive::ConfigValidate)]
|
||||
#[serde(default)]
|
||||
pub struct ConnectorParamsWithFileUploadUrl {
|
||||
pub base_url: String,
|
||||
@ -503,7 +500,7 @@ pub struct ConnectorParamsWithFileUploadUrl {
|
||||
}
|
||||
|
||||
#[cfg(feature = "payouts")]
|
||||
#[derive(Debug, Deserialize, Clone, Default)]
|
||||
#[derive(Debug, Deserialize, Clone, Default, router_derive::ConfigValidate)]
|
||||
#[serde(default)]
|
||||
pub struct ConnectorParamsWithSecondaryBaseUrl {
|
||||
pub base_url: String,
|
||||
@ -675,7 +672,7 @@ impl Settings {
|
||||
}
|
||||
self.secrets.validate()?;
|
||||
self.locker.validate()?;
|
||||
self.connectors.validate()?;
|
||||
self.connectors.validate("connectors")?;
|
||||
|
||||
self.scheduler
|
||||
.as_ref()
|
||||
|
||||
@ -129,68 +129,6 @@ impl super::settings::SupportedConnectors {
|
||||
}
|
||||
}
|
||||
|
||||
impl super::settings::Connectors {
|
||||
pub fn validate(&self) -> Result<(), ApplicationError> {
|
||||
self.aci.validate()?;
|
||||
self.adyen.validate()?;
|
||||
self.applepay.validate()?;
|
||||
self.authorizedotnet.validate()?;
|
||||
self.braintree.validate()?;
|
||||
self.checkout.validate()?;
|
||||
self.cybersource.validate()?;
|
||||
self.globalpay.validate()?;
|
||||
self.klarna.validate()?;
|
||||
self.shift4.validate()?;
|
||||
self.stripe.validate()?;
|
||||
self.worldpay.validate()?;
|
||||
|
||||
self.supported.validate()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl super::settings::ConnectorParams {
|
||||
pub fn validate(&self) -> Result<(), ApplicationError> {
|
||||
common_utils::fp_utils::when(self.base_url.is_default_or_empty(), || {
|
||||
Err(ApplicationError::InvalidConfigurationValueError(
|
||||
"connector base URL must not be empty".into(),
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl super::settings::ConnectorParamsWithFileUploadUrl {
|
||||
pub fn validate(&self) -> Result<(), ApplicationError> {
|
||||
common_utils::fp_utils::when(self.base_url.is_default_or_empty(), || {
|
||||
Err(ApplicationError::InvalidConfigurationValueError(
|
||||
"connector base URL must not be empty".into(),
|
||||
))
|
||||
})?;
|
||||
common_utils::fp_utils::when(self.base_url_file_upload.is_default_or_empty(), || {
|
||||
Err(ApplicationError::InvalidConfigurationValueError(
|
||||
"connector file upload base URL must not be empty".into(),
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "payouts")]
|
||||
impl super::settings::ConnectorParamsWithSecondaryBaseUrl {
|
||||
pub fn validate(&self) -> Result<(), ApplicationError> {
|
||||
common_utils::fp_utils::when(self.base_url.is_default_or_empty(), || {
|
||||
Err(ApplicationError::InvalidConfigurationValueError(
|
||||
"connector base URL must not be empty".into(),
|
||||
))
|
||||
})?;
|
||||
common_utils::fp_utils::when(self.secondary_base_url.is_default_or_empty(), || {
|
||||
Err(ApplicationError::InvalidConfigurationValueError(
|
||||
"connector secondary base URL must not be empty".into(),
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl super::settings::SchedulerSettings {
|
||||
pub fn validate(&self) -> Result<(), ApplicationError> {
|
||||
use common_utils::fp_utils::when;
|
||||
|
||||
@ -509,3 +509,32 @@ pub fn polymorphic_schema(input: proc_macro::TokenStream) -> proc_macro::TokenSt
|
||||
.unwrap_or_else(|error| error.into_compile_error())
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Implements the `Validate` trait to check if the config variable is present
|
||||
/// Usage
|
||||
/// ```
|
||||
/// #[derive(ConfigValidate)]
|
||||
/// struct Connectors {
|
||||
/// pub stripe: ConnectorParams,
|
||||
/// pub checkout: ConnectorParams
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// This will call the `validate()` function for all the fields in the struct
|
||||
///
|
||||
/// ```
|
||||
/// impl Connectors {
|
||||
/// fn validate(&self) -> Result<(), ApplicationError> {
|
||||
/// self.stripe.validate()?;
|
||||
/// self.checkout.validate()?;
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[proc_macro_derive(ConfigValidate)]
|
||||
pub fn validate_config(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
let input = syn::parse_macro_input!(input as syn::DeriveInput);
|
||||
|
||||
macros::misc::validate_config(input)
|
||||
.unwrap_or_else(|error| error.into_compile_error())
|
||||
.into()
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
pub(crate) mod api_error;
|
||||
pub(crate) mod diesel;
|
||||
pub(crate) mod generate_schema;
|
||||
pub(crate) mod misc;
|
||||
pub(crate) mod operation;
|
||||
|
||||
mod helpers;
|
||||
|
||||
@ -19,28 +19,13 @@ fn get_inner_path_ident(attribute: &syn::Attribute) -> syn::Result<Vec<syn::Iden
|
||||
.collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
fn get_struct_fields(data: syn::Data) -> syn::Result<Punctuated<syn::Field, syn::token::Comma>> {
|
||||
if let syn::Data::Struct(syn::DataStruct {
|
||||
fields: syn::Fields::Named(syn::FieldsNamed { ref named, .. }),
|
||||
..
|
||||
}) = data
|
||||
{
|
||||
Ok(named.to_owned())
|
||||
} else {
|
||||
Err(syn::Error::new(
|
||||
proc_macro2::Span::call_site(),
|
||||
"This macro cannot be used on structs with no fields",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn polymorphic_macro_derive_inner(
|
||||
input: syn::DeriveInput,
|
||||
) -> syn::Result<proc_macro2::TokenStream> {
|
||||
let schemas_to_create =
|
||||
helpers::get_metadata_inner::<syn::Ident>("generate_schemas", &input.attrs)?;
|
||||
|
||||
let fields = get_struct_fields(input.data)
|
||||
let fields = helpers::get_struct_fields(input.data)
|
||||
.map_err(|error| syn::Error::new(proc_macro2::Span::call_site(), error))?;
|
||||
|
||||
// Go through all the fields and create a mapping of required fields for a schema
|
||||
|
||||
@ -35,3 +35,20 @@ pub(super) fn get_metadata_inner<'a, T: Parse + Spanned>(
|
||||
Ok(vec)
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn get_struct_fields(
|
||||
data: syn::Data,
|
||||
) -> syn::Result<Punctuated<syn::Field, syn::token::Comma>> {
|
||||
if let syn::Data::Struct(syn::DataStruct {
|
||||
fields: syn::Fields::Named(syn::FieldsNamed { ref named, .. }),
|
||||
..
|
||||
}) = data
|
||||
{
|
||||
Ok(named.to_owned())
|
||||
} else {
|
||||
Err(syn::Error::new(
|
||||
proc_macro2::Span::call_site(),
|
||||
"This macro cannot be used on structs with no fields",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
63
crates/router_derive/src/macros/misc.rs
Normal file
63
crates/router_derive/src/macros/misc.rs
Normal file
@ -0,0 +1,63 @@
|
||||
pub fn get_field_type(field_type: syn::Type) -> Option<syn::Ident> {
|
||||
if let syn::Type::Path(path) = field_type {
|
||||
path.path
|
||||
.segments
|
||||
.last()
|
||||
.map(|last_path_segment| last_path_segment.ident.to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement the `validate` function for the struct by calling `validate` function on the fields
|
||||
pub fn validate_config(input: syn::DeriveInput) -> Result<proc_macro2::TokenStream, syn::Error> {
|
||||
let fields = super::helpers::get_struct_fields(input.data)
|
||||
.map_err(|error| syn::Error::new(proc_macro2::Span::call_site(), error))?;
|
||||
|
||||
let struct_name = input.ident;
|
||||
let function_expansions = fields
|
||||
.into_iter()
|
||||
.flat_map(|field| field.ident.to_owned().zip(get_field_type(field.ty)))
|
||||
.filter_map(|(field_ident, field_type_ident)| {
|
||||
// Check if a field is a leaf field, only String ( connector urls ) is supported for now
|
||||
|
||||
let field_ident_string = field_ident.to_string();
|
||||
let is_optional_field = field_type_ident.eq("Option");
|
||||
|
||||
// Do not call validate if it is an optional field
|
||||
if !is_optional_field {
|
||||
let is_leaf_field = field_type_ident.eq("String");
|
||||
let validate_expansion = if is_leaf_field {
|
||||
quote::quote!(common_utils::fp_utils::when(
|
||||
self.#field_ident.is_empty(),
|
||||
|| {
|
||||
Err(ApplicationError::InvalidConfigurationValueError(
|
||||
format!("{} must not be empty for {}", #field_ident_string, parent_field).into(),
|
||||
))
|
||||
}
|
||||
)?;
|
||||
)
|
||||
} else {
|
||||
quote::quote!(
|
||||
self.#field_ident.validate(#field_ident_string)?;
|
||||
)
|
||||
};
|
||||
Some(validate_expansion)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let expansion = quote::quote! {
|
||||
impl #struct_name {
|
||||
pub fn validate(&self, parent_field: &str) -> Result<(), ApplicationError> {
|
||||
#(#function_expansions)*
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(expansion)
|
||||
}
|
||||
Reference in New Issue
Block a user