Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: effect schema validator generator #89

Merged
merged 8 commits into from
Jul 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions integrationos-api/src/endpoints/connection_definition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ impl RequestExt for CreateRequest {
test_connection: self.test_connection,
auth_secrets,
auth_method: self.auth_method.clone(),
multi_env: self.multi_env.clone(),
multi_env: self.multi_env,
paths: self.paths.clone(),
settings: self.settings.clone(),
hidden: false,
Expand All @@ -332,7 +332,7 @@ impl RequestExt for CreateRequest {
record.frontend.spec.tags.clone_from(&self.tags);
record.test_connection = self.test_connection;
record.platform.clone_from(&self.platform);
record.multi_env = self.multi_env.clone();
record.multi_env = self.multi_env;
record.record_metadata.active = self.active;
record
}
Expand Down
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)
}
2 changes: 1 addition & 1 deletion integrationos-api/src/endpoints/unified.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use convert_case::{Case, Casing};
use http::{HeaderMap, HeaderName};
use integrationos_domain::{
connection_model_definition::CrudAction, destination::Action,
encrypted_access_key::EncryptedAccessKey, encrypted_data::PASSWORD_LENGTH, environment,
encrypted_access_key::EncryptedAccessKey, encrypted_data::PASSWORD_LENGTH,
event_access::EventAccess, AccessKey, ApplicationError, Event, InternalError,
};
use serde::{Deserialize, Serialize};
Expand Down
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@ pub enum Environment {

impl Environment {
pub fn is_production(&self) -> bool {
match self {
Environment::Production | Environment::Live => true,
_ => false,
}
matches!(self, Environment::Production | Environment::Live)
}
}

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
2 changes: 1 addition & 1 deletion integrationos-unified/src/unified.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ use mongodb::{
options::{Collation, CollationStrength, FindOneOptions},
Client,
};
use serde_json::{json, Map, Number, Value};
use serde_json::{json, Number, Value};
use std::{cell::RefCell, collections::HashMap, str::FromStr, sync::Arc};
use tracing::{debug, error};

Expand Down
Loading