corex 0.1.0

Core functions for Servex
Documentation
use modelx;
use modelx::{FieldType, Modelx};
use proc_macro2::TokenStream;
use quote::quote;

pub trait Module {
    fn options(_modelx: &Modelx) -> TokenStream {
        quote! {}
    }
    fn options_embed(_modelx: &Modelx) -> TokenStream {
        quote! {}
    }
    fn init(_modelx: &Modelx) -> TokenStream {
        quote! {}
    }
    fn boot(_modelx: &Modelx) -> TokenStream {
        quote! {}
    }
}

pub struct Model {}

pub struct Http {}

impl Module for Model {
    fn options(_modelx: &Modelx) -> TokenStream {
        quote! {}
    }

    fn options_embed(_modelx: &Modelx) -> TokenStream {
        quote! {}
    }

    fn boot(_modelx: &Modelx) -> TokenStream {
        quote! {}
    }

    fn init(modelx: &Modelx) -> TokenStream {
        // Iterate through each definition in the Modelx struct
        let types = modelx.definitions.iter().map(|(name, definition)| {
            let struct_ident = syn::Ident::new(&name, proc_macro2::Span::call_site());
            let fields = definition.fields.iter().map(|(field_name, field)| {
                let field_name = syn::Ident::new(field_name, proc_macro2::Span::call_site());
                let field_type = match field.field_type {
                    FieldType::String => quote! { String },
                    FieldType::UUID => quote! { String },
                    FieldType::Integer => quote! { i32 },
                };
                if field.is_key {
                    quote! {
                        pub #field_name: #field_type
                    }
                } else {
                    quote! {
                        pub #field_name: Option<#field_type>
                    }
                }
            });

            quote! {
                #[derive(Debug, serde::Serialize, serde::Deserialize, Default, sqlx::FromRow)]
                pub struct #struct_ident {
                    #(#fields),*
                }
            }
        });
        quote! {
            pub mod Model {
                #(#types)*
            }
        }
    }
}

impl Module for Http {
    fn options(_modelx: &Modelx) -> TokenStream {
        quote! {
            pub struct OptHttp {
                pub port: u16
            }

            impl Default for OptHttp {
                fn default() -> Self {
                    OptHttp {
                        port: 8080,
                    }
                }
            }
        }
    }

    fn options_embed(_modelx: &Modelx) -> TokenStream {
        quote! {
            pub http: OptHttp,
        }
    }

    fn init(_modelx: &Modelx) -> TokenStream {
        quote! {}
    }

    fn boot(modelx: &Modelx) -> TokenStream {
        let http_routes = modelx
            .definitions
            .iter()
            .filter(|(_name, definition)| definition.annotations.iter().any(|anno| anno == "@http"))
            .map(|(name, _definition)| {
                let endpoint = format!("/{}", name);
                let fn_name = format!("axum_get_{}", name);
                let fn_ident = syn::Ident::new(&fn_name, proc_macro2::Span::call_site());
                quote! {
                    router = router.route(#endpoint, axum::routing::get(Self::#fn_ident));
                }
            });

        let mut deploy_statements: Vec<String> = vec![];
        modelx
            .definitions
            .iter()
            .filter(|(_name, definition)| definition.annotations.iter().any(|anno| anno == "@http"))
            .for_each(|(name, definition)| {
                let mut deploy_sql = "".to_string();
                deploy_sql.push_str(&format!("\nCREATE TABLE IF NOT EXISTS {} (", &name));
                definition.fields.iter().for_each(|(field_name, field)| {
                    let sql_type = match &field.field_type {
                        FieldType::String => "text",
                        FieldType::UUID => "text",
                        FieldType::Integer => "integer",
                    };
                    let sql_key = if field.is_key { "PRIMARY KEY" } else { "" };
                    deploy_sql
                        .push_str(&format!("\n  {} {} {},", &field_name, &sql_type, &sql_key));
                });
                deploy_sql.pop();
                deploy_sql.push_str("\n);");
                deploy_statements.push(deploy_sql);
            });
        let fn_name = "axum_deploy";
        let fn_ident = syn::Ident::new(&fn_name, proc_macro2::Span::call_site());
        let execute_deploy_statements = deploy_statements.iter().map(|statement| {
            quote! {
               sqlx::query(#statement).execute(&mut tx).await.unwrap();
            }
        });
        let axum_deploy_endpoint = quote! {
               pub async fn #fn_ident(axum::Extension(pool): axum::Extension<sqlx::PgPool>) -> impl axum::response::IntoResponse {
                   println!("Deploy");
                   let mut tx = pool.begin().await.unwrap();
                   #(#execute_deploy_statements)*
                   // sqlx::query(#deploy_sql).execute(&mut tx).await.unwrap();
                   tx.commit().await.unwrap();
                   (axum::http::StatusCode::OK, "success")
             }
        };
        let axum_endpoints = modelx.definitions.iter().filter(|(_name, definition)| definition.annotations.iter().any(|anno| anno == "@http")).map(|(name, _definition)| {
        let fn_name = format!("axum_get_{}", name);
        let struct_ident = syn::Ident::new(&name, proc_macro2::Span::call_site());
        let fn_ident = syn::Ident::new(&fn_name, proc_macro2::Span::call_site());
         quote! {
           pub async fn #fn_ident(axum::Extension(pool): axum::Extension<sqlx::PgPool>) -> impl axum::response::IntoResponse {
               println!("GET for {}", #name);
               let result = sqlx::query_as::<_, Model::#struct_ident>(&format!("SELECT * FROM {}", #name)).fetch_all(&pool).await.unwrap();
               (axum::http::StatusCode::OK, axum::Json(result))
         }
      }
    });

        quote! {
        #[derive(Debug)]
        pub struct Http {
           pub router: axum::Router,
        }

        impl Http {
          pub fn new() -> Self {
                  let mut router = axum::Router::new().route("/health", axum::routing::get(Self::health));
                  router = router.route("/deploy", axum::routing::get(Self::axum_deploy));
                  #(#http_routes)*
                  Self {
                    router
                  }
            }
            #(#axum_endpoints)*
            #axum_deploy_endpoint
            pub async fn health() -> impl axum::response::IntoResponse {
                  (axum::http::StatusCode::OK, "healthy")
            }
            pub async fn boot(mut self, port: u16) {
              let db = sqlx::postgres::PgPoolOptions::new().max_connections(5).connect("postgres://newuser:newUser@localhost:5432/postgres").await.expect("Cannot connect to Postgres database");
              self.router =  self.router.layer(axum::extract::Extension(db));
              let addr = std::net::SocketAddr::from(([127, 0, 0, 1], port));
              println!("Listening on http://localhost:{}", &port);
              axum::Server::bind(&addr)
                  .serve(self.router.into_make_service())
                  .await
                  .unwrap();
            }
        }

        // TODO: separate instance from boot so users can manipulate it before
        let http = Http::new();
        http.boot(*&options.http.port).await;
        }
    }
}