proto 0.1.2

`proto` allows you to define Protocol Buffers using Rust code to reuse Rust's type system and ecosystem.
Documentation
use crate::{case::RenameRule, keyword, service::Service};
use syn::{
  parse::{Parse, ParseStream},
  Ident, Result, Token, Type,
};

pub struct Proto {
  pub package: Ident,
  pub service: Service,
  pub codec_path: Type,
}

#[derive(Default)]
struct ProtoBuilder {
  package: Option<Ident>,
  service: Option<Service>,
  codec_path: Option<Type>,
}

impl Parse for Proto {
  fn parse(input: ParseStream) -> Result<Self> {
    let mut builder = ProtoBuilder::new();

    while !input.is_empty() {
      if input.peek(keyword::package) {
        input.parse::<keyword::package>()?;

        let package = input.parse::<Ident>()?;

        builder.package(package);

        input.parse::<Token![;]>()?;

        continue;
      }

      if input.peek(keyword::service) {
        let service = input.parse::<Service>()?;

        builder.service(service);

        continue;
      }

      if input.peek(keyword::codec) {
        input.parse::<keyword::codec>()?;

        let codec_path = input.parse::<Type>()?;

        builder.codec_path(codec_path);

        input.parse::<Token![;]>()?;

        continue;
      }

      return Err(input.error("Unexpected token. Expected `package`, `service`, or `codec`"));
    }

    Ok(builder.build())
  }
}

impl Proto {
  pub fn codegen(&self) -> proc_macro2::TokenStream {
    let rpcs = self.service.rpcs.iter().map(|rpc| {
      let name = RenameRule::SnakeCase.apply_to_variant(&rpc.name.to_string());
      let name = Ident::new(&name, rpc.name.span());

      let route_name = RenameRule::PascalCase.apply_to_field(&rpc.name.to_string());
      let route_name = Ident::new(&route_name, rpc.name.span());

      let request = &rpc.request;
      let response = &rpc.response;
      let codec_path = &self.codec_path;
      let request_streaming = rpc.request_streaming;
      let response_streaming = rpc.response_streaming;

      let client_streaming = if request_streaming {
        quote::quote! {
          .client_streaming()
        }
      } else {
        quote::quote! {}
      };

      let server_streaming = if response_streaming {
        quote::quote! {
          .server_streaming()
        }
      } else {
        quote::quote! {}
      };

      quote::quote! {
        tonic_build::manual::Method::builder()
          .name(stringify!(#name))
          .route_name(stringify!(#route_name))
          .input_type(stringify!(#request))
          .output_type(stringify!(#response))
          .codec_path(stringify!(#codec_path))
          #client_streaming
          #server_streaming
          .build()
      }
    });

    let package = &self.package;
    let service = &self.service.name;

    quote::quote! {
      {
        tonic_build::manual::Service::builder()
        .package(stringify!(#package))
        .name(stringify!(#service))
        #(.method(#rpcs))*
        .build()
      }
    }
  }
}

impl ProtoBuilder {
  fn new() -> Self {
    Self::default()
  }

  fn package(&mut self, package: Ident) -> &mut Self {
    self.package = Some(package);

    self
  }

  fn codec_path(&mut self, codec_path: Type) -> &mut Self {
    self.codec_path = Some(codec_path);

    self
  }

  fn service(&mut self, service: Service) -> &mut Self {
    if self.service.is_some() {
      panic!("Currently only one service is supported per proto definition");
    }

    self.service = Some(service);

    self
  }

  fn build(self) -> Proto {
    Proto {
      package: self.package.expect("Package is required"),
      service: self.service.expect("Service is required"),
      codec_path: self.codec_path.expect("Codec path is required"),
    }
  }
}