diff --git a/Cargo.lock b/Cargo.lock index 94fa28d..38abafd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -1542,7 +1542,7 @@ dependencies = [ [[package]] name = "rust-purs-gql" -version = "0.1.15" +version = "0.1.16" dependencies = [ "cynic", "cynic-introspection", @@ -1551,7 +1551,10 @@ dependencies = [ "hashlink 0.8.4", "phf", "reqwest", + "serde", + "serde_derive", "serde_json", + "serde_yaml", "sqlx", "stringcase", "tokio", @@ -1705,6 +1708,19 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2317,6 +2333,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "untrusted" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index ba95ea3..2e46b40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rust-purs-gql" -version = "0.1.15" +version = "0.1.16" edition = "2021" default-run = "pursgql" repository = "https://github.com/OxfordAbstracts/purescript-graphql-schema-gen" @@ -18,6 +18,9 @@ stringcase = "0.3.0" tokio = { version = "1.40.0", features = ["full"] } yaml-rust2 = "0.8.1" serde_json = "1.0" +serde = "1.0" +serde_derive = "1.0" +serde_yaml = "0.9.34" [[bin]] edition = "2021" diff --git a/outside_types_in_actions.yaml b/outside_types_in_actions.yaml index 8ea76cb..bb9aa46 100644 --- a/outside_types_in_actions.yaml +++ b/outside_types_in_actions.yaml @@ -90,7 +90,7 @@ outside_types: DrOrderTicketData: id: drId=DrTicketId ticket_group_id: drId=DrTicketGroupId - description_html: RawHtml, GqlOverrides.RawHTML, oa-ids # changed type name to avoid clashing imports - will need to alias + description_html: RawHtml, GqlOverrides.RawHTML, oa-ids # changed type name to avoid clashing imports - will need to alias DrOrderAddon: status: DrLineItemStatusesEnum, OaGqlEnums.DrLineItemStatusesEnum, oa-gql-enums ticket_id: drId=DrTicketId @@ -132,7 +132,7 @@ outside_types: DrAdminTicket: with: delreg_common id: drId=DrTicketId - description_html: RawHtml, GqlOverrides.RawHTML, oa-override-gql # changed type name to avoid clashing imports - will need to alias + description_html: RawHtml, GqlOverrides.RawHTML, oa-override-gql # changed type name to avoid clashing imports - will need to alias DrAuthorizePaymentLinkInput: with: delreg_common DrAuthorizePaymentLinkOpenAccessInput: diff --git a/spago_workspace_config.yaml b/spago_workspace_config.yaml index 15ffdc6..ea01689 100644 --- a/spago_workspace_config.yaml +++ b/spago_workspace_config.yaml @@ -6,4 +6,4 @@ schema_libs_prefix: oa-gql-schema- schema_libs_dir: ../OxfordAbstracts/application/purs-projects/lib/generated-new/ variant_enums: - QuestionTypesEnum - - ReviewerRecruitmentQuestionTypesEnum + - ReviewerRecruitmentQuestionTypesEnum \ No newline at end of file diff --git a/src/build_schema.rs b/src/build_schema.rs index 015365c..509498c 100644 --- a/src/build_schema.rs +++ b/src/build_schema.rs @@ -6,22 +6,26 @@ use std::{ use cynic::{http::ReqwestExt, QueryBuilder}; use cynic_introspection::{ - Directive, DirectiveLocation, FieldWrapping, InterfaceType, IntrospectionQuery, Type, - UnionType, WrappingType, + Directive, DirectiveLocation, FieldWrapping, InputValue, InterfaceType, IntrospectionQuery, + Type, UnionType, WrappingType, }; use stringcase::{kebab_case, pascal_case}; use tokio::task::spawn_blocking; use crate::{ - config::{parse_outside_types::OutsideTypes, workspace::WorkspaceConfig}, + config::{ + parse_outside_types::{Mod, OutsideTypes}, parse_scalar_types::ScalarTypes, + workspace::WorkspaceConfig, + }, enums::generate_enum::generate_enum, hasura_types::as_gql_field, purescript_gen::{ purescript_argument::Argument, + purescript_gql_union::GqlUnion, purescript_import::PurescriptImport, purescript_instance::{derive_new_type_instance, DeriveInstance}, purescript_print_module::print_module, - purescript_record::{Field, PurescriptRecord}, + purescript_record::{show_field_name, Field, PurescriptRecord}, purescript_type::PurescriptType, purescript_variant::Variant, }, @@ -32,6 +36,7 @@ pub async fn build_schema( role: String, postgres_types: Arc>>, outside_types: Arc>, + scalar_types: Arc>, workspace_config: WorkspaceConfig, ) -> Result<()> { // Fetch the introspection schema @@ -58,6 +63,7 @@ pub async fn build_schema( let mut types: Vec = vec![]; let mut imports: Vec = vec![]; let mut variants: Vec = vec![]; + let mut unions: Vec = vec![]; let mut instances: Vec = vec![]; // Add the purescript GraphQL client imports that are always used, @@ -129,79 +135,80 @@ pub async fn build_schema( // Process the schema types for type_ in schema.types.iter() { - match type_ { - Type::Object(obj) => { - // There are a couple of `__` prefixed Hasura types that we can safely ignore - if obj.name.starts_with("__") { - continue; - } + let mut handle_obj = |obj: &cynic_introspection::ObjectType| { + // There are a builting of `__` prefixed graphql types that we can safely ignore + if obj.name.starts_with("__") { + return; + } - // Convert the hasura_type_name to a PurescriptTypeName - let name = pascal_case(&obj.name); + // Convert the gql_type_name to a PurescriptTypeName + let name = pascal_case(&obj.name); - // Creates a new record for the object - let mut record = PurescriptRecord::new("Ignored"); + // Creates a new record for the object + let mut record = PurescriptRecord::new("Ignored"); - // Add type fields to the record - for field in obj.fields.iter() { - // If the field has arguments then the purescript representation will be: - // field_name :: { | Arguments } -> ReturnType - - // Build the arguments record: - let mut args = PurescriptRecord::new("Arguments"); - for arg in &field.args { - let arg_type = wrap_type( - as_gql_field( - &field.name, - &arg.name, - &arg.ty.name, - &mut imports, - &postgres_types, - &outside_types, - ), - &arg.ty.wrapping, - &mut imports, - ); - let mut arg_field = Field::new(&arg.name); - arg_field.type_name = arg_type; - args.add_field(arg_field); - } + // Add type fields to the record + for field in obj.fields.iter() { + // If the field has arguments then the purescript representation will be: + // field_name :: { | Arguments } -> ReturnType - // Build the return type, - // potentially wrapping values in Array or Maybe - // and resolving any matched outside types - let return_type = return_type_wrapper( + // Build the arguments record: + let mut args = PurescriptRecord::new("Arguments"); + for arg in &field.args { + let arg_type = wrap_type( as_gql_field( - &obj.name, &field.name, - &field.ty.name, + &arg.name, + &arg.ty.name, &mut imports, &postgres_types, &outside_types, ), - &field.ty.wrapping, + &arg.ty.wrapping, &mut imports, ); - - // Add the function argument to the new record field - // and add it to the object record - let function_arg = - Argument::new_function(vec![Argument::new_record(args)], return_type); - let record_field = Field::new(&field.name).with_type_arg(function_arg); - record.add_field(record_field); + let mut arg_field = Field::new(&arg.name); + arg_field.type_name = arg_type; + args.add_field(arg_field); } - // Create the newtype record for the object and append it to the schema module types - let mut query_type = - PurescriptType::new(&name, vec![], Argument::new_record(record)); - query_type.set_newtype(true); - instances.push(derive_new_type_instance(&query_type.name)); - types.push(query_type); + // Build the return type, + // potentially wrapping values in Array or Maybe + // and resolving any matched outside types + let return_type = return_type_wrapper( + as_gql_field( + &obj.name, + &field.name, + &field.ty.name, + &mut imports, + &postgres_types, + &outside_types, + ), + &field.ty.wrapping, + &mut imports, + ); + + // Add the function argument to the new record field + // and add it to the object record + let function_arg = + Argument::new_function(vec![Argument::new_record(args)], return_type); + let record_field = Field::new(&field.name).with_type_arg(function_arg); + record.add_field(record_field); } + + // Create the newtype record for the object and append it to the schema module types + let mut query_type = PurescriptType::new(&name, vec![], Argument::new_record(record)); + query_type.set_newtype(true); + instances.push(derive_new_type_instance(&query_type.name)); + types.push(query_type); + }; + match type_ { + Type::Object(obj) => handle_obj(&obj), Type::Scalar(scalar) => { // Add imports for common scalar types if they are used. // TODO maybe move these to config so they can be updated outside of rust match scalar.name.as_str() { + "ID" => add_import("graphql-client", "GraphQL.Client.ID", "ID", &mut imports), _ if scalar.is_builtin() => {} // ignore built in types like String, Int, etc. "date" => add_import("datetime", "Data.Date", "Date", &mut imports), "timestamp" | "timestamptz" => { @@ -211,11 +218,23 @@ pub async fn build_schema( add_import("argonaut-core", "Data.Argonaut.Core", "Json", &mut imports) } "time" => add_import("datetime", "Data.Time", "Time", &mut imports), - _ => {} + scalar_name => { + match scalar_types.lock().unwrap().get(scalar_name) { + Some(Mod { package, import, name }) => { + add_import(package, import, name, &mut imports); + types.push(PurescriptType::new(scalar_name, vec![], Argument::new_type(name))); + } + None => { + add_import("argonaut-core", "Data.Argonaut.Core", "Json", &mut imports); + types.push(PurescriptType::new(scalar_name, vec![], Argument::new_type("Json"))); + + } + } + } } } Type::Enum(en) => { - // Ignore internal Hasura enums beginning with `__` + // Ignore internal graphql enums beginning with `__` if en.name.starts_with("__") { continue; } @@ -230,12 +249,12 @@ pub async fn build_schema( } } Type::InputObject(obj) => { - // Ignore internal Hasura input objects beginning with `__` + // Ignore internal graphql input objects beginning with `__` if obj.name.starts_with("__") { continue; } - // Convert the hasura_type_name to a PurescriptTypeName + // Convert the gql_type_name to a PurescriptTypeName let name = pascal_case(&obj.name); // Build a purescript record with all fields @@ -267,13 +286,43 @@ pub async fn build_schema( instances.push(derive_new_type_instance(&query_type.name)); types.push(query_type); } - Type::Interface(InterfaceType { name, .. }) => { - // Currently ignored as we don't have any in our schemas - println!("Interface: {name}"); + Type::Interface(InterfaceType { + name, + fields, + description, + .. + }) => { + handle_obj(&cynic_introspection::ObjectType { + name: name.clone(), + fields: fields.clone(), + description: description.clone(), + interfaces: vec![], + }); } - Type::Union(UnionType { name, .. }) => { + Type::Union(UnionType { + name, + description: _, + possible_types, + .. + }) => { // Currently ignored as we don't have any in our schemas - println!("Union: {name}"); + add_import( + "graphql-client", + "GraphQL.Client.Union", + "GqlUnion", + &mut imports, + ); + + let mut union = GqlUnion::new(&name); + + union.with_values( + &possible_types + .iter() + .map(|t| (t.clone(), pascal_case(&t))) + .collect(), + ); + + unions.push(union); } } } @@ -297,18 +346,19 @@ pub async fn build_schema( // Write the schema module to the file system let schema_module_path = format!("{lib_path}/src/Schema/{role}.purs"); - write( - &schema_module_path, - &print_module( - &role, - &mut types, - &mut records, - &mut imports, - &mut variants, - &mut instances, - ), + + let printed = print_module( + &role, + &mut types, + &mut records, + &mut imports, + &mut variants, + &mut unions, + &mut instances, ); + write(&schema_module_path, &printed); + // Write the directives module let path_clone = lib_path.clone(); spawn_blocking(move || build_directives(path_clone, directive_role, schema.directives)); @@ -411,74 +461,94 @@ fn wrap_type( argument } -/// Format the schema directives into a separate module. -/// TODO stop directives from being hardcoded string mods with bad imports just for our simple use... -fn build_directives(lib_path: String, role: String, directives: Vec) -> () { - let mut directive_mod = "".to_string(); - // Push the module header + types type + declaration to the directive module - directive_mod.push_str(&format!( - "module {role}.Directives where \n{DIRECTIVE_IMPORTS}" - )); +fn wrap_type_str(mut str: String, wrapping: &FieldWrapping) -> String { + let wrapping: Vec = wrapping.into_iter().collect(); + for wrapper in wrapping.iter().rev() { + match wrapper { + WrappingType::NonNull => str = format!("NotNull {str}"), + WrappingType::List => str = format!("Array {str}"), + } + } + str +} - let mut directive_types = "".to_string(); - let mut directive_functions = "".to_string(); - for directive in directives { - let directive_name = directive.name; - let locations = &directive.locations; - let allowed_location = locations.iter().any(is_allowed_location); - if allowed_location { - let description = directive.description.clone().unwrap_or("\"\"".to_string()); - - // Give the Directives type a type - let type_type = "type Directives :: List' Type\n"; - // Initialise the directive types argument with name and description (defaulted to "") - let mut directive_argument = Argument::new_type("Directive") - .with_argument(Argument::new_type(&format!(r#""{directive_name}""#))) - .with_argument(Argument::new_type(&format!(r#""{description}""#))); - - // Build the arguments record - let mut directive_args_rec = PurescriptRecord::new("Arguments"); - for arg in directive.args.iter() { - let arg_name = arg.name.clone(); - // TODO use the shared mutable imports as a mutex rather than a placeholder - let arg_type = wrap_type( - Argument::new_type(&arg.ty.name.clone()), - &arg.ty.wrapping, - &mut vec![], - ); +fn directive_type_str(directive: &Directive) -> Option { + let name = &directive.name; + let description = directive.description.clone().unwrap_or("".to_string()); + let args: String = directive + .args + .iter() + .map(input_value_type_str) + .collect::>() + .join("\n , "); + let locations = directive_locations_str(&directive.locations)?; + + Some(format!("( Directive \"{name}\" \n \"{description}\" \n {{ {args} }} \n {locations} \n )")) +} - directive_args_rec.add_field(Field::new(&arg_name).with_type_arg(arg_type)); - } - directive_argument.add_argument(Argument::new_record(directive_args_rec)); - - // Add the locations to the directive type - // TODO Make this work for multiple locations. Ask Rory how this should work. - let locations_type = match locations[0] { - DirectiveLocation::Query => "QUERY", - DirectiveLocation::Mutation => "MUTATION", - DirectiveLocation::Subscription => "SUBSCRIPTION", - _ => "QUERY", - }; - let locations_type_level_list = - Argument::new_type(&format!("({locations_type} :> Nil') :> Nil'")); - directive_argument.add_argument(locations_type_level_list); - - // Define the directives type - let directive_type = PurescriptType::new("Directives", vec![], directive_argument); - directive_types.push_str(&type_type); - directive_types.push_str(&directive_type.to_string()); - } - // Add the apply directive function - let function = format!( - r#" +fn directive_locations_str(locations: &Vec) -> Option { + let locations_strs = locations + .iter() + .filter_map(directive_location_str) + .map(|s| format!("{s} :> ")) + .collect::>(); + + if locations_strs.len() == 0 { + return None; + } + let location_str = locations_strs.join(""); + + Some(format!("({location_str} Nil' )")) +} + +fn directive_location_str(location: &DirectiveLocation) -> Option<&str> { + match location { + DirectiveLocation::Query => Some("QUERY"), + DirectiveLocation::Mutation => Some("MUTATION"), + DirectiveLocation::Subscription => Some("SUBSCRIPTION"), + _ => None, + } +} + +fn input_value_type_str(value: &InputValue) -> String { + let name = show_field_name(value.name.clone()); + let prop_value = wrap_type_str(value.ty.name.clone(), &value.ty.wrapping); + format!("\"{name}\" :: {prop_value}") +} + +fn directive_fn(directive: &Directive) -> String { + let directive_name = &directive.name; + format!( + r#" {directive_name} :: forall q args. args -> q -> ApplyDirective "{directive_name}" args q {directive_name} = applyDir (Proxy :: _ "{directive_name}") "# - ); - directive_functions.push_str(&function); - } + ) +} - directive_mod.push_str([directive_types, directive_functions].join("\n").trim()); +/// Format the schema directives into a separate module. +/// TODO stop directives from being hardcoded string mods with bad imports just for our simple use... +fn build_directives(lib_path: String, role: String, directives: Vec) { + let directive_str: String = directives + .iter() + .filter_map(|directive| directive_type_str(&directive)) + .map(|d| format!("{d}\n :>\n")) + .collect::>() + .join(""); + + let directive_str = format!("type Directives =\n {directive_str}\n Nil'"); + + let directive_fns = directives + .iter() + .map(|directive| directive_fn(&directive)) + .collect::>() + .join("\n"); + + let mut directive_mod = "".to_string(); + // Push the module header + types type + declaration to the directive module + directive_mod.push_str(&format!( + "module {role}.Directives where \n{DIRECTIVE_IMPORTS}\ntype Directives :: List' Type\n{directive_str}\n\n{directive_fns}" + )); write( &format!("{lib_path}/src/{role}/Directives.purs"), @@ -486,21 +556,13 @@ fn build_directives(lib_path: String, role: String, directives: Vec) ); } -fn is_allowed_location(location: &DirectiveLocation) -> bool { - ALLOWED_DIRECTIVE_LOCATIONS.contains(location) -} - -static ALLOWED_DIRECTIVE_LOCATIONS: [DirectiveLocation; 3] = [ - DirectiveLocation::Query, - DirectiveLocation::Mutation, - DirectiveLocation::Subscription, -]; - const DIRECTIVE_IMPORTS: &str = r#" +import Prelude import GraphQL.Client.Args (NotNull) import GraphQL.Client.Directive (ApplyDirective, applyDir) import GraphQL.Client.Directive.Definition (Directive) -import GraphQL.Client.Directive.Location (QUERY) +import GraphQL.Client.Directive.Location (MUTATION, QUERY, SUBSCRIPTION) +import GraphQL.Client.Operation (OpMutation(..), OpQuery(..), OpSubscription(..)) import Type.Data.List (type (:>), List', Nil') import Type.Proxy (Proxy(..)) diff --git a/src/config.rs b/src/config.rs index 37460eb..d98ed3d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,3 +1,4 @@ pub mod parse_outside_types; pub mod parse_roles; pub mod workspace; +pub mod parse_scalar_types; diff --git a/src/config/parse_outside_types.rs b/src/config/parse_outside_types.rs index b59b8e8..3442b94 100644 --- a/src/config/parse_outside_types.rs +++ b/src/config/parse_outside_types.rs @@ -4,6 +4,7 @@ use std::{ io::Read, }; +use serde::Deserialize; use stringcase::pascal_case; use yaml_rust2::{yaml, Yaml}; @@ -201,7 +202,7 @@ fn to_type_value(type_value: &String, types_fn: &impl Fn(&str, &str) -> Option; + +pub fn fetch_all_scalar_types() -> Option { + let scalar_types_env = std::env::var("SCALAR_TYPES_YAML").ok()?; + + let scalar_type_locs: Vec<&str> = scalar_types_env.split(",").collect(); + + let mut scalar_types: ScalarTypes = HashMap::new(); + for loc in scalar_type_locs.iter() { + let types = fetch_scalar_types(loc); + scalar_types.extend(types); + } + Some(scalar_types) +} + +fn fetch_scalar_types(location: &str) -> ScalarTypes { + let f = File::open(location).expect(&format!("Scalar types yaml file not found at {location}")); + + serde_yaml::from_reader(f).unwrap() +} diff --git a/src/config/workspace.rs b/src/config/workspace.rs index 346d3b0..079e99b 100644 --- a/src/config/workspace.rs +++ b/src/config/workspace.rs @@ -1,93 +1,25 @@ -use std::thread::Result; -use hashlink::LinkedHashMap; -use tokio::fs::File; -use tokio::io::AsyncReadExt; -use yaml_rust2::{yaml, Yaml}; +use serde::Deserialize; +use std::fs::File; -pub async fn parse_workspace() -> Result { +pub fn parse_workspace() -> WorkspaceConfig { let file_path: String = std::env::var("SPAGO_WORKSPACE_CONFIG_YAML") .expect("SPAGO_WORKSPACE_CONFIG_YAML must be set"); - let mut f = File::open(file_path) - .await - .expect("Failed to locate or open spago workspace config yaml."); - let mut s = String::new(); - f.read_to_string(&mut s) - .await - .expect("Failed to read spago workspace config yaml file to string."); - - let docs = - yaml::YamlLoader::load_from_str(&s).expect("Failed to parse workspace YAML as YAML."); - - if let Yaml::Hash(hash) = &docs[0] { - match WorkspaceConfig::new(hash) { - Some(config) => return Ok(config), - None => (), - } - } - - panic!("Invalid workspace YAML. Please check your workspaces YAML file matches the example in the README."); + let f = File::open(file_path.clone()) + // .await + .expect(format!("Failed to locate or open spago workspace config yaml at: {}", file_path).as_str()); + serde_yaml::from_reader(f).unwrap() } -#[derive(Clone)] +#[derive(Clone, Deserialize)] pub struct WorkspaceConfig { - pub postgres_enums_lib: String, - pub postgres_enums_dir: String, + pub postgres_enums_lib: Option, + pub postgres_enums_dir: Option, pub shared_graphql_enums_lib: String, pub shared_graphql_enums_dir: String, pub schema_libs_prefix: String, pub schema_libs_dir: String, + #[serde(default = "Vec::new")] pub variant_enums: Vec, } - -impl WorkspaceConfig { - fn new(yaml_hash: &LinkedHashMap) -> Option { - let postgres_enums_lib = yaml_hash.get(&Yaml::String("postgres_enums_lib".to_string()))?; - let postgres_enums_dir = yaml_hash.get(&Yaml::String("postgres_enums_dir".to_string()))?; - let shared_graphql_enums_lib = - yaml_hash.get(&Yaml::String("shared_graphql_enums_lib".to_string()))?; - let shared_graphql_enums_dir = - yaml_hash.get(&Yaml::String("shared_graphql_enums_dir".to_string()))?; - let schema_libs_prefix = yaml_hash.get(&Yaml::String("schema_libs_prefix".to_string()))?; - let schema_libs_dir = yaml_hash.get(&Yaml::String("schema_libs_dir".to_string()))?; - let variant_enums = yaml_hash.get(&Yaml::String("variant_enums".to_string())); - Some(Self { - postgres_enums_lib: postgres_enums_lib - .as_str() - .expect("Workspace yaml should contain postgres_enums_lib key.") - .to_string(), - postgres_enums_dir: postgres_enums_dir - .as_str() - .expect("Workspace yaml should contain postgres_enums_dir key.") - .to_string(), - shared_graphql_enums_lib: shared_graphql_enums_lib - .as_str() - .expect("Workspace yaml should contain shared_graphql_enums_lib key.") - .to_string(), - shared_graphql_enums_dir: shared_graphql_enums_dir - .as_str() - .expect("Workspace yaml should contain shared_graphql_enums_dir key.") - .to_string(), - schema_libs_prefix: schema_libs_prefix - .as_str() - .expect("Workspace yaml should contain schema_libs_prefix key.") - .to_string(), - schema_libs_dir: schema_libs_dir - .as_str() - .expect("Workspace yaml should contain schema_libs_dir key.") - .to_string(), - variant_enums: variant_enums - .unwrap_or(&Yaml::Array(vec![])) - .as_vec() - .unwrap_or(&vec![]) - .iter() - .map(|v| { - v.as_str() - .expect("Workspace yaml variant enums should all be strings") - .to_string() - }) - .collect(), - }) - } -} diff --git a/src/enums/generate_enum.rs b/src/enums/generate_enum.rs index c79e8fb..95e2465 100644 --- a/src/enums/generate_enum.rs +++ b/src/enums/generate_enum.rs @@ -5,6 +5,7 @@ use crate::config::workspace::WorkspaceConfig; use crate::purescript_gen::purescript_enum::Enum; use crate::purescript_gen::purescript_import::PurescriptImport; use crate::purescript_gen::purescript_variant::Variant; +use crate::purescript_gen::upper_first::upper_first; use crate::write::write; pub async fn generate_enum( @@ -31,7 +32,7 @@ pub async fn generate_enum( { vec!["ENUM_PLACEHOLDER".to_string()] } else { - en.values.iter().map(|v| first_upper(&v.name)).collect() + en.values.iter().map(|v| upper_first(&v.name)).collect() }; let original_values: Vec = en.values.iter().map(|v| v.name.clone()).collect(); let name: String = pascal_case(&en.name); @@ -97,7 +98,7 @@ pub async fn generate_enum( } // Otherwise write schema-specific variant enums } else { - Some(Variant::new(&name).with_values(&original_values)) + Some(Variant::new(&name).with_values(&original_values).clone()) } } @@ -105,14 +106,6 @@ fn use_variant(name: &str, workspace_config: &WorkspaceConfig) -> bool { workspace_config.variant_enums.iter().any(|e| name == e) } -fn first_upper(s: &str) -> String { - let mut c = s.chars(); - match c.next() { - None => String::new(), - Some(f) => f.to_uppercase().collect::() + c.as_str(), - } -} - fn enum_instances(name: &str, values: &Vec, original_values: &Vec) -> String { let mut instances = String::new(); instances.push_str(&format!( diff --git a/src/enums/postgres_types.rs b/src/enums/postgres_types.rs index 3a0e158..49906de 100644 --- a/src/enums/postgres_types.rs +++ b/src/enums/postgres_types.rs @@ -14,8 +14,22 @@ pub async fn fetch_types( // when no postgres enums are included, skip the enum generation if db_env.is_err() { + println!("No DATABASE_URL specified. Skipping enum generation."); return Ok(HashMap::new()); } + + let Some(ref postgres_enums_lib) = workspace_config.postgres_enums_lib else { + println!("No postgres enums lib specified in workspace config. Skipping enum generation."); + return Ok(HashMap::new()); + }; + + + let Some(postgres_enums_dir) = &workspace_config.postgres_enums_dir else { + println!("No postgres enums dir specified in workspace config. Skipping enum generation."); + return Ok(HashMap::new()); + }; + + let database_url = db_env.expect("DATABASE_URL should not be required but for some reason is..."); let pool = PgPoolOptions::new() @@ -35,12 +49,9 @@ pub async fn fetch_types( let mut hash_map = HashMap::new(); - let package_name = pascal_case(&workspace_config.postgres_enums_lib); - let lib_path = format!( - "{}{}", - &workspace_config.postgres_enums_dir, &workspace_config.postgres_enums_lib - ); - let package = &workspace_config.postgres_enums_lib; + let package_name = pascal_case(&postgres_enums_lib); + let lib_path = format!("{}{}", &postgres_enums_dir, &postgres_enums_lib); + let package = postgres_enums_lib; for enum_row in res.iter() { let name = enum_row.enumtype.clone(); diff --git a/src/hasura_types.rs b/src/hasura_types.rs index 8f36ff0..8defeca 100644 --- a/src/hasura_types.rs +++ b/src/hasura_types.rs @@ -2,6 +2,7 @@ use std::{ collections::HashMap, sync::{Arc, Mutex}, }; + use stringcase::pascal_case; use crate::{ diff --git a/src/main.rs b/src/main.rs index 7b1b739..848bd61 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,10 @@ use std::{ - fs::remove_dir_all, - sync::{Arc, Mutex}, - thread::Result, + collections::HashMap, fs::remove_dir_all, sync::{Arc, Mutex}, thread::Result }; use build_schema::build_schema; use config::{ - parse_outside_types::{fetch_all_outside_types, OutsideTypes}, - parse_roles::parse_roles, - workspace::parse_workspace, + parse_outside_types::{fetch_all_outside_types, OutsideTypes}, parse_roles::parse_roles, parse_scalar_types::{fetch_all_scalar_types, ScalarTypes}, workspace::parse_workspace }; use dotenv::dotenv; use enums::postgres_types::fetch_types; @@ -29,19 +25,23 @@ async fn main() -> Result<()> { let type_gen_timer = std::time::Instant::now(); // Fetch the workspace config - let workspace_config = parse_workspace().await?; + let workspace_config = parse_workspace(); // Trash existing schema for path in vec![ workspace_config.postgres_enums_dir.clone(), - workspace_config.shared_graphql_enums_dir.clone(), - workspace_config.schema_libs_dir.clone(), + Some(workspace_config.shared_graphql_enums_dir.clone()), + Some(workspace_config.schema_libs_dir.clone()), ] .iter() { - remove_dir_all(path).ok(); + match path { + Some(dir) => { + remove_dir_all(dir).ok(); + } + None => (), + } } - // Generate postgres enum types let postgres_types = fetch_types(&workspace_config) .await @@ -59,6 +59,8 @@ async fn main() -> Result<()> { // Parse all outside type config let outside_types: OutsideTypes = fetch_all_outside_types(&workspace_config); + let scalar_types: ScalarTypes = fetch_all_scalar_types().unwrap_or(HashMap::new()); + // Fetch role config let roles: Vec = parse_roles(); let num_roles = roles.len(); @@ -66,14 +68,17 @@ async fn main() -> Result<()> { // Postgres types are shared between all roles let types_ = Arc::new(Mutex::new(postgres_types)); let outside_types = Arc::new(Mutex::new(outside_types)); + let scalar_types = Arc::new(Mutex::new(scalar_types)); // Run schema gen for each role concurrently let mut tasks = Vec::with_capacity(num_roles); + for role in roles.iter() { tasks.push(spawn(build_schema( role.clone(), types_.clone(), outside_types.clone(), + scalar_types.clone(), workspace_config.clone(), ))); } diff --git a/src/purescript_gen.rs b/src/purescript_gen.rs index bdece34..c9fa613 100644 --- a/src/purescript_gen.rs +++ b/src/purescript_gen.rs @@ -6,3 +6,6 @@ pub mod purescript_print_module; pub mod purescript_record; pub mod purescript_type; pub mod purescript_variant; +pub mod purescript_row; +pub mod purescript_gql_union; +pub mod upper_first; diff --git a/src/purescript_gen/purescript_gql_union.rs b/src/purescript_gen/purescript_gql_union.rs new file mode 100644 index 0000000..fe1cca7 --- /dev/null +++ b/src/purescript_gen/purescript_gql_union.rs @@ -0,0 +1,28 @@ +use super::purescript_row::Row; + +#[derive(Debug, Clone)] +pub struct GqlUnion { + name: String, + row: Row, +} + +impl GqlUnion { + pub fn new(name: &str) -> Self { + GqlUnion { + name: name.to_string(), + row: Row::new(), + } + } + + pub fn with_values(&mut self, values: &Vec<(String, String)>) -> &mut Self { + self.row.with_values(values); + self + } + + pub fn to_string(&self) -> String { + let values = self + .row + .to_string(); + format!("type {} = GqlUnion\n {values}", self.name) + } +} diff --git a/src/purescript_gen/purescript_print_module.rs b/src/purescript_gen/purescript_print_module.rs index 4372a97..1e0cca5 100644 --- a/src/purescript_gen/purescript_print_module.rs +++ b/src/purescript_gen/purescript_print_module.rs @@ -1,7 +1,7 @@ use super::{ - purescript_import::PurescriptImport, purescript_instance::DeriveInstance, - purescript_record::PurescriptRecord, purescript_type::PurescriptType, - purescript_variant::Variant, + purescript_gql_union::GqlUnion, purescript_import::PurescriptImport, + purescript_instance::DeriveInstance, purescript_record::PurescriptRecord, + purescript_type::PurescriptType, purescript_variant::Variant, }; pub fn print_module( @@ -10,6 +10,7 @@ pub fn print_module( records: &mut Vec, imports: &mut Vec, variants: &mut Vec, + unions: &mut Vec, instances: &mut Vec, ) -> String { let mut module = format!("module Schema.{role} where"); @@ -50,6 +51,13 @@ pub fn print_module( .map(|v| v.to_string()) .collect::>() .join("\n\n"); + + let unions: String = unions + .iter_mut() + .map(|u| u.to_string()) + .collect::>() + .join("\n\n"); + let instances = instances .iter_mut() .map(|i| i.to_string()) @@ -70,6 +78,9 @@ pub fn print_module( module.push_str(&variants); module = module.trim().to_string(); module.push_str("\n\n"); + module.push_str(&unions); + module = module.trim().to_string(); + module.push_str("\n\n"); module.push_str(&instances); module.trim().to_string() } diff --git a/src/purescript_gen/purescript_record.rs b/src/purescript_gen/purescript_record.rs index 578876e..6e53b42 100644 --- a/src/purescript_gen/purescript_record.rs +++ b/src/purescript_gen/purescript_record.rs @@ -27,18 +27,34 @@ impl Field { self } pub fn show_field(&self) -> String { - // if first character is uppercase, wrap in quotes - if self - .name - .chars() - .next() - .expect("Field should not be an empty string.") - .is_uppercase() - { - format!("\"{}\"", self.name) - } else { - self.name.clone() - } + show_field_name(self.name.clone()) + } +} + +// if first character is not a lowercase char, wrap in quotes +pub fn show_field_name(field_name: String) -> String { + let head = field_name + .chars() + .next() + .expect("Field should not be an empty string."); + + if head.is_alphabetic() && head.is_lowercase() { + field_name + } else { + format!("\"{}\"", field_name) + } +} + +// tests +#[cfg(test)] +mod tests_show_field_name { + use super::*; + + #[test] + fn test_show_field_name() { + assert_eq!(show_field_name("name".to_string()), "name"); + assert_eq!(show_field_name("Name".to_string()), "\"Name\""); + assert_eq!(show_field_name("_".to_string()), "\"_\""); } } @@ -74,7 +90,7 @@ impl PurescriptRecord { .type_name .get_all_forall_types() .iter() - .any(|t| !all_forall_args.contains(t)) + .any(|t: &String| !all_forall_args.contains(t)) { return Some(format!( "Field '{name}' uses a forall type '{}' that is not defined in the record arguments", diff --git a/src/purescript_gen/purescript_row.rs b/src/purescript_gen/purescript_row.rs new file mode 100644 index 0000000..904ef03 --- /dev/null +++ b/src/purescript_gen/purescript_row.rs @@ -0,0 +1,32 @@ +#[derive(Debug, Clone)] +pub struct Row { + pub values: Vec<(String, String)>, +} + +impl Row { + pub fn new() -> Self { + Row { values: vec![] } + } + + pub fn with_values(&mut self, values: &Vec<(String, String)>) -> &mut Self { + self.values = values.clone(); + self + } + pub fn with_unit_values(&mut self, values: &Vec) -> &mut Self { + self.values = values + .into_iter() + .map(|k| (k.clone(), "Unit".to_string())) + .collect::>(); + self + } + + pub fn to_string(&self) -> String { + let values = self + .values + .iter() + .map(|(k, v): &(String, String)| format!("\"{}\" :: {}", k, v)) + .collect::>() + .join("\n , "); + format!("( {values}\n )") + } +} diff --git a/src/purescript_gen/purescript_variant.rs b/src/purescript_gen/purescript_variant.rs index 58695b9..703dc0f 100644 --- a/src/purescript_gen/purescript_variant.rs +++ b/src/purescript_gen/purescript_variant.rs @@ -1,28 +1,28 @@ +use super::purescript_row::Row; + +#[derive(Debug, Clone)] pub struct Variant { name: String, - values: Vec, + row: Row, } impl Variant { pub fn new(name: &str) -> Self { Variant { name: name.to_string(), - values: vec![], + row: Row::new(), } } - pub fn with_values(mut self, values: &Vec) -> Self { - self.values = values.clone(); + pub fn with_values(&mut self, values: &Vec) -> &mut Self { + self.row.with_unit_values(values); self } pub fn to_string(&self) -> String { let values = self - .values - .iter() - .map(|v| format!("\"{}\" :: Unit", v)) - .collect::>() - .join("\n , "); - format!("type {} = Variant\n ( {values}\n )", self.name) + .row + .to_string(); + format!("type {} = Variant\n {values}", self.name) } } diff --git a/src/purescript_gen/upper_first.rs b/src/purescript_gen/upper_first.rs new file mode 100644 index 0000000..4a72b57 --- /dev/null +++ b/src/purescript_gen/upper_first.rs @@ -0,0 +1,8 @@ + +pub fn upper_first(str: &str) -> String { + let mut c = str.chars(); + match c.next() { + None => String::new(), + Some(f) => f.to_uppercase().collect::() + c.as_str(), + } +} \ No newline at end of file