Skip to content

Commit

Permalink
chore: effect schema validator generator (#89)
Browse files Browse the repository at this point in the history
  • Loading branch information
sagojez authored Jul 18, 2024
1 parent bf78451 commit 9aaafca
Show file tree
Hide file tree
Showing 4 changed files with 260 additions and 1 deletion.
1 change: 1 addition & 0 deletions integrationos-api/src/endpoints/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ pub mod passthrough;
pub mod pipeline;
pub mod platform;
pub mod platform_page;
pub mod schema_generator;
pub mod transactions;
pub mod unified;
pub mod utils;
Expand Down
85 changes: 85 additions & 0 deletions integrationos-api/src/endpoints/schema_generator.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
use super::ReadResponse;
use crate::server::AppState;
use axum::{
extract::{Path, State},
routing::get,
Json, Router,
};
use bson::{doc, Document};
use futures::StreamExt;
use integrationos_domain::{ApplicationError, Id, IntegrationOSError, InternalError, Store};
use mongodb::options::FindOptions;
use std::sync::Arc;

pub fn get_router() -> Router<Arc<AppState>> {
Router::new()
.route("/projection", get(get_common_model_proj))
.route("/:id", get(generate_schema))
}

pub async fn get_common_model_proj(
state: State<Arc<AppState>>,
) -> Result<Json<ReadResponse<Document>>, IntegrationOSError> {
let collection = state
.app_stores
.db
.collection::<Document>(&Store::CommonModels.to_string());

let filter = doc! {
"deleted": false,
"primary": true,
"active": true,
};
let options = FindOptions::builder()
.projection(doc! { "_id": 1, "name": 1 })
.build();

let mut cursor = collection.find(filter, options).await?;
let mut common_models: Vec<Document> = Vec::new();

while let Some(result) = cursor.next().await {
match result {
Ok(document) => {
common_models.push(document);
}
_ => {
return Err(InternalError::unknown(
"Error while fetching common models",
None,
));
}
}
}

let len = common_models.len();

Ok(Json(ReadResponse {
rows: common_models,
total: len as u64,
skip: 0,
limit: 0,
}))
}

pub async fn generate_schema(
state: State<Arc<AppState>>,
Path(id): Path<Id>,
) -> Result<String, IntegrationOSError> {
let cm_store = state.app_stores.common_model.clone();
let ce_store = state.app_stores.common_enum.clone();

let common_model = cm_store
.get_one_by_id(&id.to_string())
.await
.map_err(IntegrationOSError::from)?
.ok_or(ApplicationError::not_found(
&format!("CommonModel with id {} not found", id),
None,
))?;

let schema = common_model
.as_typescript_schema_expanded(&cm_store, &ce_store)
.await;

Ok(schema)
}
3 changes: 2 additions & 1 deletion integrationos-api/src/routes/public.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::{
endpoints::{
common_enum, common_model, connection_definition, connection_model_schema,
connection_oauth_definition, event_access::create_event_access_for_new_user, openapi, read,
utils,
schema_generator, utils,
},
middleware::jwt_auth::{self, JwtState},
server::AppState,
Expand Down Expand Up @@ -31,6 +31,7 @@ pub fn get_router(state: &Arc<AppState>) -> Router<Arc<AppState>> {
"/connection-definitions",
get(read::<connection_definition::CreateRequest, ConnectionDefinition>),
)
.nest("/schemas", schema_generator::get_router())
.route(
"/connection-oauth-definition-schema",
get(read::<
Expand Down
172 changes: 172 additions & 0 deletions integrationos-domain/src/domain/schema/common_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,14 @@ impl Field {
self.datatype.as_typescript_ref(self.name.clone())
)
}

fn as_typescript_schema(&self) -> String {
format!(
"{}: {}",
replace_reserved_keyword(&self.name, Lang::TypeScript).camel_case(),
self.datatype.as_typescript_schema(self.name.clone())
)
}
}

#[derive(Debug, Default, Serialize, Deserialize, Clone, Eq, PartialEq)]
Expand Down Expand Up @@ -179,6 +187,40 @@ impl CommonEnum {
)
}

/// Generates a effect Schema for the enum
pub fn as_typescript_schema(&self) -> String {
let name = replace_reserved_keyword(&self.name, Lang::TypeScript)
.replace("::", "")
.pascal_case();
let native_enum = format!(
"export enum {}Enum {{ {} }}\n",
name,
self.options
.iter()
.map(|option| {
let option_name = option.pascal_case();
let option_value = if option.chars().all(char::is_uppercase) {
option.to_lowercase()
} else {
option.kebab_case()
};

format!("{} = '{}'", option_name, option_value)
})
.collect::<HashSet<_>>()
.into_iter()
.collect::<Vec<_>>()
.join(", ")
);

let schema = format!(
"export const {} = Schema.Enums({}Enum)\n // __SEPARATOR__\n",
name, name
);

format!("{}\n{}", native_enum, schema)
}

pub fn as_typescript_type(&self) -> String {
format!(
"export const enum {} {{ {} }}\n",
Expand Down Expand Up @@ -239,6 +281,46 @@ impl DataType {
}
}

fn as_typescript_schema(&self, e_name: String) -> String {
match self {
DataType::String => "Schema.optional(Schema.NullishOr(Schema.String))".into(),
DataType::Number => "Schema.optional(Schema.NullishOr(Schema.Number))".into(),
DataType::Boolean => "Schema.optional(Schema.NullishOr(Schema.Boolean))".into(),
DataType::Date => "Schema.optional(Schema.NullishOr(Schema.String.pipe(Schema.filter((d) => !isNaN(new Date(d).getTime())))))".into(),
DataType::Enum { reference, .. } => {
if reference.is_empty() {
format!(
"Schema.optional(Schema.NullishOr({}))",
e_name.pascal_case()
)
} else {
format!("Schema.optional(Schema.NullishOr({}))", reference)
}
}
DataType::Expandable(expandable) => {
format!(
"Schema.optional(Schema.NullishOr({}))",
expandable.reference()
)
}
DataType::Array { element_type } => {
let name = (*element_type).as_typescript_schema(e_name);
let refined = if name.contains("Schema.optional") {
name.replace("Schema.optional(", "")
.replace(')', "")
.replace("Schema.NullishOr(", "")
.replace(')', "")
} else {
name
};
format!(
"Schema.optional(Schema.NullishOr(Schema.Array({})))",
refined
)
}
}
}

pub fn schema(&self, format: Option<String>) -> ReferenceOr<Box<Schema>> {
match self {
DataType::String => ReferenceOr::Item(Box::new(Schema {
Expand Down Expand Up @@ -543,6 +625,10 @@ impl CommonModel {
}
}

pub fn generate_as_typescript_schema(&self) -> String {
self.as_typescript_schema()
}

/// Generates the model as a string in the specified language
/// with recursively expanded inner models and enums.
/// This is useful for generating the entire model and its
Expand Down Expand Up @@ -581,6 +667,23 @@ impl CommonModel {
)
}

/// Generates a zod schema for the model in TypeScript
fn as_typescript_schema(&self) -> String {
format!(
"export const {} = Schema.Struct({{ {} }});\n",
replace_reserved_keyword(&self.name, Lang::TypeScript)
.replace("::", "")
.pascal_case(),
self.fields
.iter()
.map(|field| field.as_typescript_schema())
.collect::<HashSet<String>>()
.into_iter()
.collect::<Vec<_>>()
.join(",\n ")
)
}

fn as_typescript_ref(&self) -> String {
format!(
"export interface {} {{ {} }}\n",
Expand All @@ -597,6 +700,75 @@ impl CommonModel {
)
}

pub async fn as_typescript_schema_expanded(
&self,
cm_store: &MongoStore<CommonModel>,
ce_store: &MongoStore<CommonEnum>,
) -> String {
let mut visited_enums = HashSet::new();
let mut visited_common_models = HashSet::new();

let enums = self
.fetch_all_enum_references(cm_store.clone(), ce_store.clone())
.await
.map(|enums| {
enums
.iter()
.filter_map(|enum_model| {
if visited_enums.contains(&enum_model.id) {
return None;
}

visited_enums.insert(enum_model.id);

Some(enum_model.as_typescript_schema())
})
.collect::<HashSet<String>>()
.into_iter()
.collect::<Vec<_>>()
})
.ok()
.unwrap_or_default()
.into_iter()
.collect::<Vec<_>>();

let children = self
.fetch_all_children_common_models(cm_store.clone())
.await
.ok()
.unwrap_or_default();

let children_types = children
.0
.into_values()
.filter_map(|child| {
if visited_common_models.contains(&child.id) {
return None;
}
visited_common_models.insert(child.id);

Some(child.as_typescript_schema())
})
.collect::<Vec<_>>()
.join("\n // __SEPARATOR__ \n");

let ce_types = enums.join("\n");

let cm_types = self.as_typescript_schema();

if visited_common_models.contains(&self.id) {
format!(
"// __SEPARATOR \n {}\n // __SEPARATOR__ \n {}",
ce_types, children_types
)
} else {
format!(
"// __SEPARATOR__ \n {}\n{}\n // __SEPARATOR__ \n{}",
ce_types, children_types, cm_types
)
}
}

async fn as_typescript_expanded(
&self,
cm_store: &MongoStore<CommonModel>,
Expand Down

0 comments on commit 9aaafca

Please sign in to comment.