oas3-gen 0.20.0

A rust type generator for OpenAPI v3.1.x specification.
use proc_macro2::TokenStream;
use quote::{format_ident, quote};

use crate::generator::ast::{EnumDef, FieldDef, RustPrimitive, RustType, StructDef, VariantContent};

const FALLBACK_ERROR_MESSAGE: &str = "Error";
const MESSAGE_FIELD_NAMES: &[&str] = &["message", "detail", "title"];

pub(crate) fn generate_error_impl(rust_type: &RustType) -> Option<TokenStream> {
  match rust_type {
    RustType::Struct(def) => generate_for_struct(def),
    RustType::Enum(def) => generate_for_enum(def),
    _ => None,
  }
}

fn generate_for_struct(def: &StructDef) -> Option<TokenStream> {
  let type_ident = format_ident!("{}", &def.name);

  if let Some(error_field_name) = find_error_field(&def.fields) {
    let field = def.fields.iter().find(|f| f.name == error_field_name)?;
    let field_ident = format_ident!("{error_field_name}");
    let fallback = FALLBACK_ERROR_MESSAGE;
    let is_optional = field.rust_type.nullable;

    let (display_impl, source_impl) = if field.rust_type.is_array {
      if is_optional {
        (
          quote! {
            impl std::fmt::Display for #type_ident {
              fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                if let Some(ref errs) = self.#field_ident {
                  if let Some(first) = errs.first() {
                    write!(f, "{}", first)
                  } else {
                    write!(f, #fallback)
                  }
                } else {
                  write!(f, #fallback)
                }
              }
            }
          },
          quote! {
            impl std::error::Error for #type_ident {
              fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
                self.#field_ident.as_ref()?.first().map(|e| e as &(dyn std::error::Error + 'static))
              }
            }
          },
        )
      } else {
        (
          quote! {
            impl std::fmt::Display for #type_ident {
              fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                if let Some(first) = self.#field_ident.first() {
                  write!(f, "{}", first)
                } else {
                  write!(f, #fallback)
                }
              }
            }
          },
          quote! {
            impl std::error::Error for #type_ident {
              fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
                self.#field_ident.first().map(|e| e as &(dyn std::error::Error + 'static))
              }
            }
          },
        )
      }
    } else if is_optional {
      (
        quote! {
          impl std::fmt::Display for #type_ident {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
              if let Some(ref err) = self.#field_ident {
                write!(f, "{}", err)
              } else {
                write!(f, #fallback)
              }
            }
          }
        },
        quote! {
          impl std::error::Error for #type_ident {
            fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
              self.#field_ident.as_ref().map(|e| e as &(dyn std::error::Error + 'static))
            }
          }
        },
      )
    } else {
      (
        quote! {
          impl std::fmt::Display for #type_ident {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
              write!(f, "{}", self.#field_ident)
            }
          }
        },
        quote! {
          impl std::error::Error for #type_ident {
            fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
              Some(&self.#field_ident as &(dyn std::error::Error + 'static))
            }
          }
        },
      )
    };

    return Some(quote! {
      #display_impl
      #source_impl
    });
  }

  let message_field = find_message_field(&def.fields)?;
  let field_name = format_ident!("{}", &message_field.name);
  let fallback = FALLBACK_ERROR_MESSAGE;

  let display_impl = if message_field.rust_type.nullable {
    quote! {
      impl std::fmt::Display for #type_ident {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
          if let Some(ref msg) = self.#field_name {
            write!(f, "{}", msg)
          } else {
            write!(f, #fallback)
          }
        }
      }
    }
  } else {
    quote! {
      impl std::fmt::Display for #type_ident {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
          write!(f, "{}", self.#field_name)
        }
      }
    }
  };

  Some(quote! {
    #display_impl

    impl std::error::Error for #type_ident {}
  })
}

fn generate_for_enum(def: &EnumDef) -> Option<TokenStream> {
  if !is_error_enum(def) {
    return None;
  }

  let type_ident = format_ident!("{}", &def.name);
  let (display_arms, source_arms) = generate_enum_error_arms(def);

  Some(quote! {
    impl std::fmt::Display for #type_ident {
      fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
          #(#display_arms)*
        }
      }
    }

    impl std::error::Error for #type_ident {
      fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
          #(#source_arms)*
        }
      }
    }
  })
}

fn is_error_enum(def: &EnumDef) -> bool {
  def.variants.iter().any(|v| match &v.content {
    VariantContent::Tuple(types) => !types.is_empty(),
    VariantContent::Unit => false,
  })
}

fn generate_enum_error_arms(def: &EnumDef) -> (Vec<TokenStream>, Vec<TokenStream>) {
  def
    .variants
    .iter()
    .map(|variant| {
      let variant_ident = format_ident!("{}", &variant.name);

      match &variant.content {
        VariantContent::Tuple(types) if !types.is_empty() => (
          quote! { Self::#variant_ident(err) => write!(f, "{}", err), },
          quote! { Self::#variant_ident(err) => Some(err as &(dyn std::error::Error + 'static)), },
        ),
        VariantContent::Tuple(_) | VariantContent::Unit => (
          quote! { Self::#variant_ident => write!(f, "{}", stringify!(#variant_ident)), },
          quote! { Self::#variant_ident => None, },
        ),
      }
    })
    .unzip()
}

fn find_error_field(fields: &[FieldDef]) -> Option<&str> {
  fields.iter().find_map(|f| {
    let is_error_name = f.name == "error" || f.name == "errors";
    let is_custom_type = matches!(f.rust_type.base_type, RustPrimitive::Custom(_));
    if is_error_name && is_custom_type {
      Some(f.name.as_str())
    } else {
      None
    }
  })
}

fn find_message_field(fields: &[FieldDef]) -> Option<&FieldDef> {
  MESSAGE_FIELD_NAMES.iter().find_map(|&candidate| {
    fields
      .iter()
      .find(|f| f.name == candidate && matches!(f.rust_type.base_type, RustPrimitive::String))
  })
}