SeaQL / sea-orm

🐚 An async & dynamic ORM for Rust
https://www.sea-ql.org/SeaORM/
Apache License 2.0
7.3k stars 513 forks source link

allow a feature gate option in codegen for use in frontend environments #491

Closed Madoshakalaka closed 2 years ago

Madoshakalaka commented 2 years ago

When the entities are shared between a backend and web frontend (like one built with yew), it's undesirable (and impossible maybe) to pack sqlx+sea-orm into the frontend, where the data are meant to be fetched by HTTP requests. The frontend does benefit from the structs and the serde traits though.

For this reason it might be desirable to allow a feature gate flag in sea-orm-cli, and it generates something like this when the flag is supplied:

//! SeaORM Entity. Generated by sea-orm-codegen 0.3.1

#[cfg(feature = "backend")]
use sea_orm::entity::prelude::*;

#[cfg(feature = "frontend")]
use rust_decimal::Decimal;

use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "backend", derive(DeriveEntityModel))]
#[cfg_attr(feature = "backend", sea_orm(table_name = "books_book"))]
pub struct Model {
    #[cfg_attr(feature = "backend", sea_orm(primary_key))]
    pub id: i64,
    pub name: String,
    pub isbn: String,
    #[cfg_attr(feature = "backend", sea_orm(column_type = "Decimal(Some((10, 2)))"))]
    pub price: Decimal,
    pub quantity: i32,
    #[cfg_attr(feature = "backend", sea_orm(column_type = "Text"))]
    pub description: String,
}

#[derive(Copy, Clone, Debug)]
#[cfg_attr(feature = "backend", derive(EnumIter))]

pub enum Relation {}

#[cfg(feature = "backend")]
impl RelationTrait for Relation {
    fn def(&self) -> RelationDef {
        match self {
            _ => panic!("No RelationDef"),
        }
    }
}

#[cfg(feature = "backend")]
impl ActiveModelBehavior for ActiveModel {}
Madoshakalaka commented 2 years ago

In the meantime, I crafted this super useful build.rs script. It's meant to be used in a frontend crate. It:

// build.rs
use convert_case::{Case, Casing};
use quote::quote;
use std::fs;
use syn::__private::Span;
use syn::{
    AttrStyle, Attribute, Field, Fields, FieldsNamed, Ident, Item, ItemStruct, Path, PathSegment,
};
use syn::{VisPublic, Visibility};

struct TableFile {
    file_name: String,
    content: Vec<Item>,
}

impl TableFile {
    fn transform_and_write(self) {
        std::fs::create_dir("src/models").ok();
        let Self {
            content,
            file_name: name,
        } = self;
        content
            .into_iter()
            .filter_map(|i| match i {
                Item::Struct(i) => Some(i),
                _ => None,
            })
            .filter_map(|i| match i.ident.to_string().as_str() {
                "Model" => Some(transform_struct(i)),
                _ => None,
            })
            .map(|s| {
                let f: ::syn::File = ::syn::parse_quote!(
                    //! generated by custom build script
                    use serde::Deserialize;
                    #s
                );

                prettyplease::unparse(&f)
            })
            .for_each(|file| std::fs::write(format!("src/models/{name}"), file).unwrap());
    }
}

fn transform_struct(i: ItemStruct) -> ItemStruct {
    ItemStruct {
        attrs: vec![Attribute {
            pound_token: Default::default(),
            style: AttrStyle::Outer,
            bracket_token: Default::default(),
            path: Path {
                leading_colon: None,
                segments: std::iter::once(PathSegment::from(Ident::new(
                    "derive",
                    Span::call_site(),
                )))
                .collect(),
            },
            tokens: quote! {(Deserialize)},
        }],
        vis: Visibility::Public(VisPublic {
            pub_token: Default::default(),
        }),
        struct_token: i.struct_token,
        ident: i.ident,
        generics: i.generics,
        fields: Fields::Named(FieldsNamed {
            brace_token: Default::default(),
            named: i
                .fields
                .into_iter()
                .map(|f| Field {
                    attrs: vec![],
                    vis: Visibility::Public(VisPublic {
                        pub_token: Default::default(),
                    }),
                    ident: f.ident,
                    colon_token: f.colon_token,
                    ty: f.ty,
                })
                .collect(),
        }),
        semi_token: i.semi_token,
    }
}

fn main() {
    let entities_dir = "../backend/src/entities/";
    println!("cargo:rerun-if-changed={entities_dir}");

    let files = std::fs::read_dir(entities_dir).unwrap();
    let (mod_uses, prelude_uses): (Vec<_>, Vec<_>) = files
        .filter_map(|file| file.ok())
        .filter_map(|e| {
            let name = e.file_name();
            (name != "mod.rs" && name != "prelude.rs").then(|| name.into_string().unwrap())
        })
        .map(|file_name| {
            let name = format!("{entities_dir}{file_name}");
            let mod_name = file_name.strip_suffix(".rs").unwrap().to_string();
            let struct_name = mod_name.to_case(Case::Pascal);
            let table = std::fs::read_to_string(&name).unwrap();
            let content = syn::parse_file(&table).unwrap().items;
            let table_file = TableFile { file_name, content };
            table_file.transform_and_write();
            let mod_name = Ident::new(&mod_name, Span::call_site());
            let mod_name_ = mod_name.clone();
            let struct_name = Ident::new(&struct_name, Span::call_site());
            (
                quote! {pub mod #mod_name;},
                quote! {pub use super::#mod_name_::Model as #struct_name;},
            )
        })
        .unzip();
    let mod_file = syn::parse_quote! {
        //! generated by custom build script
        pub mod prelude;

        #(#mod_uses)*
    };

    let prelude_file = syn::parse_quote! {
        //! generated by custom build script

        #(#prelude_uses)*
    };

    fs::write("src/models/mod.rs", prettyplease::unparse(&mod_file)).unwrap();

    fs::write(
        "src/models/prelude.rs",
        prettyplease::unparse(&prelude_file),
    )
    .unwrap();
}
# frontend/Cargo.toml
[build-dependencies]
syn = {version = "1.0", features = ["full"]}
quote = "1.0"
convert_case = "0.5"
prettyplease = "0.1"

The expected directory structure:

├───backend
│   └───src/
│       └───entities/
│
└───frontend/
    └───src/
    └───build.rs 
tyt2y3 commented 2 years ago

Out of curiousity, what will SeaORM do at the frontend?

Madoshakalaka commented 2 years ago

Out of curiousity, what will SeaORM at the frontend?

The model structs generated by sea-orm help deserialize fetched data in the frontend