diff --git a/Cargo.lock b/Cargo.lock index b1a3482b1f..fbad89449a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -643,7 +643,7 @@ dependencies = [ "bitflags 2.6.0", "cexpr", "clang-sys", - "itertools 0.10.5", + "itertools 0.12.1", "lazy_static", "lazycell", "log", @@ -1515,6 +1515,18 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enum_dispatch" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.86", +] + [[package]] name = "env_filter" version = "0.1.2" @@ -2868,7 +2880,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -5470,6 +5482,7 @@ dependencies = [ "derive_more 0.99.18", "derive_setters", "dotenvy", + "enum_dispatch", "exitcode", "flate2", "fnv", @@ -5495,6 +5508,7 @@ dependencies = [ "lru", "maplit", "markdown", + "miette 7.2.0", "mimalloc", "mime", "moka", diff --git a/Cargo.toml b/Cargo.toml index 7303fb7ca8..5c9f3c11a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,8 @@ thiserror = "1.0.59" url = { version = "2.5.0", features = ["serde"] } convert_case = "0.6.0" tailcall-valid = "0.1.1" +miette = "7.2.0" +enum_dispatch = "0.3.13" [dependencies] # dependencies specific to CLI must have optional = true and the dep should be added to default feature. @@ -176,6 +178,8 @@ tailcall-valid = { workspace = true } dashmap = "6.1.0" urlencoding = "2.1.3" tailcall-chunk = "0.2.5" +miette = { workspace = true } +enum_dispatch = { workspace = true } # to build rquickjs bindings on systems without builtin bindings [target.'cfg(all(target_os = "windows", target_arch = "x86"))'.dependencies] diff --git a/src/core/blueprint/from_service_document/enum_ty.rs b/src/core/blueprint/from_service_document/enum_ty.rs new file mode 100644 index 0000000000..7cae6c6150 --- /dev/null +++ b/src/core/blueprint/from_service_document/enum_ty.rs @@ -0,0 +1,49 @@ +use async_graphql::parser::types::{EnumType, TypeDefinition}; +use async_graphql::Positioned; +use tailcall_valid::{Valid, Validator}; +use crate::core::blueprint; +use crate::core::blueprint::Definition; +use crate::core::blueprint::from_service_document::{Error, helpers, pos_name_to_string}; +use crate::core::blueprint::from_service_document::from_service_document::BlueprintMetadata; +use crate::core::config::Alias; +use crate::core::directive::DirectiveCodec; + +impl BlueprintMetadata { + pub(super) fn to_enum_ty(&self, enum_: &EnumType, type_definition: &Positioned) -> Valid { + Valid::from_iter(enum_.values.iter(), |value| { + let name = value.node.value.node.as_str().to_owned(); + let alias = value + .node + .directives + .iter() + .find(|d| d.node.name.node.as_str() == Alias::directive_name()); + let directives = helpers::extract_directives(value.node.directives.iter()); + + let description = value.node.description.as_ref().map(|d| d.node.to_string()); + directives.and_then(|directives| { + if let Some(alias) = alias { + Alias::from_directive(&alias.node).map(|alias| (directives, name, alias.options, description)) + } else { + Valid::succeed((directives, name, Default::default(), description)) + } + }) + }).and_then(|variants| { + helpers::extract_directives(type_definition.node.directives.iter()).and_then(|directives| { + let enum_def = blueprint::EnumTypeDefinition { + name: pos_name_to_string(&type_definition.node.name), + directives, + description: type_definition.node.description.as_ref().map(|d| d.node.to_string()), + enum_values: variants.into_iter().map(|(directives, name, alias, description)| { + blueprint::EnumValueDefinition { + name, + directives, + description, + alias, + } + }).collect(), + }; + Valid::succeed(Definition::Enum(enum_def)) + }) + }) + } +} diff --git a/src/core/blueprint/from_service_document/from_service_document.rs b/src/core/blueprint/from_service_document/from_service_document.rs new file mode 100644 index 0000000000..c79647b11e --- /dev/null +++ b/src/core/blueprint/from_service_document/from_service_document.rs @@ -0,0 +1,98 @@ +use async_graphql::parser::types::{EnumType, ServiceDocument, TypeDefinition, TypeKind, TypeSystemDefinition, UnionType}; +use async_graphql::Positioned; +use tailcall_valid::{Valid, Validator}; + +use crate::core::blueprint; +use crate::core::blueprint::{Blueprint, Definition, ScalarTypeDefinition}; +use crate::core::blueprint::from_service_document::{Error, helpers, pos_name_to_string}; +use crate::core::config::Alias; +use crate::core::directive::DirectiveCodec; +use crate::core::scalar::Scalar; +use crate::core::try_fold::TryFold; + +pub struct BlueprintMetadata { + pub path: String, +} + +impl BlueprintMetadata { + pub fn new(path: String) -> Self { + Self { path } + } + + pub fn to_blueprint<'a>(&self, doc: ServiceDocument) -> Valid { + let schema = self.to_schema().transform::( + |schema, blueprint| blueprint.schema(schema), + |blueprint| blueprint.schema, + ); + + // Create Definitions with fields and types without resolvers + // and blind conversion to Definition instead of starting from root node(s) + let definitions = self.to_type_defs() + .transform::( + |defs, blueprint| blueprint.definitions(defs), + |blueprint| blueprint.definitions, + ); + + schema + .and(definitions) + .try_fold(&doc, Blueprint::default()) + } + + fn to_schema(&self) -> TryFold { + TryFold::::new(|doc, schema| { + self.schema_definition(doc).and_then(|schema_def| { + self.to_bp_schema_def(doc, &schema_def) + }) + }) + } + fn to_type_defs(&self) -> TryFold, super::Error> { + TryFold::, super::Error>::new(|doc, defs| { + let type_defs = doc.definitions.iter().filter_map(|c| { + match c { + TypeSystemDefinition::Type(ty) => Some(ty), + _ => None, + } + }).collect::>(); + self.populate_defs(type_defs) + .and_then(|defs| self.populate_resolvers(defs)) + }) + } + + fn populate_defs( + &self, + type_defs: Vec<&Positioned>, + ) -> Valid, super::Error> { + Valid::from_iter(type_defs.iter(), |type_definition| { + let type_kind = &type_definition.node.kind; + match type_kind { + TypeKind::Scalar => self.to_scalar_ty(type_definition), + TypeKind::Union(union_) => self.to_union_ty(union_, type_definition), + TypeKind::Enum(enum_) => self.to_enum_ty(enum_, type_definition), + TypeKind::Object(obj) => self.to_object_ty(obj, type_definition), + TypeKind::Interface(interface) => self.to_object_ty(interface, type_definition), + TypeKind::InputObject(inp) => self.to_input_object_ty(inp, type_definition), + } + }) + } +} + +#[cfg(test)] +mod tests { + use tailcall_valid::Validator; + + use crate::core::blueprint::from_service_document::from_service_document::BlueprintMetadata; + + #[test] + fn test_from_bp() { + // Test code here + let path = format!("{}/examples/hello.graphql", env!("CARGO_MANIFEST_DIR")); + println!("{}", path); + let doc = async_graphql::parser::parse_schema(std::fs::read_to_string(&path).unwrap()).unwrap(); + let bp = BlueprintMetadata::new(path) + .to_blueprint(doc) + .to_result() + .unwrap(); + + println!("{:#?}", bp.definitions); + } +} \ No newline at end of file diff --git a/src/core/blueprint/from_service_document/helpers.rs b/src/core/blueprint/from_service_document/helpers.rs new file mode 100644 index 0000000000..2a0e0f527d --- /dev/null +++ b/src/core/blueprint/from_service_document/helpers.rs @@ -0,0 +1,22 @@ +use std::slice::Iter; +use async_graphql::parser::types::ConstDirective; +use async_graphql::Positioned; +use tailcall_valid::{Valid, ValidationError}; +use crate::core::blueprint; +use tailcall_valid::Validator; + +pub fn extract_directives(iter: Iter>) -> Valid, super::Error> { + Valid::from_iter(iter, |directive| { + let directives = Valid::from_iter(directive.node.arguments.iter(), |(k, v)| { + let value = v.clone().node.into_json(); + let value = value.map_err(|e| ValidationError::new(e.to_string())); + Valid::from(value.map(|value| (k.node.to_string(), value))) + }).map(|arguments| { + blueprint::directive::Directive { + name: directive.node.name.node.to_string(), + arguments: arguments.into_iter().collect(), + } + }); + directives + }) +} diff --git a/src/core/blueprint/from_service_document/input_object_ty.rs b/src/core/blueprint/from_service_document/input_object_ty.rs new file mode 100644 index 0000000000..e68406fe80 --- /dev/null +++ b/src/core/blueprint/from_service_document/input_object_ty.rs @@ -0,0 +1,15 @@ +use async_graphql::parser::types::{InputObjectType, TypeDefinition}; +use async_graphql::Positioned; +use tailcall_valid::Valid; +use crate::core::blueprint::Definition; +use crate::core::blueprint::from_service_document::from_service_document::BlueprintMetadata; + +impl BlueprintMetadata { + pub(super) fn to_input_object_ty( + &self, + inp: &InputObjectType, + type_definition: &Positioned, + ) -> Valid { + todo!() + } +} \ No newline at end of file diff --git a/src/core/blueprint/from_service_document/mod.rs b/src/core/blueprint/from_service_document/mod.rs new file mode 100644 index 0000000000..975b5d912a --- /dev/null +++ b/src/core/blueprint/from_service_document/mod.rs @@ -0,0 +1,22 @@ +#![allow(unused)] + +use async_graphql::Positioned; +use async_graphql_value::Name; + +mod from_service_document; +mod schema; +mod helpers; +mod object; +mod union; +mod scalar; +mod enum_ty; +mod input_object_ty; +pub(super) mod resolvers; +mod populate_resolvers; + + +pub(super) type Error = String; + +pub(super) fn pos_name_to_string(pos: &Positioned) -> String { + pos.node.to_string() +} diff --git a/src/core/blueprint/from_service_document/object.rs b/src/core/blueprint/from_service_document/object.rs new file mode 100644 index 0000000000..5473b8732d --- /dev/null +++ b/src/core/blueprint/from_service_document/object.rs @@ -0,0 +1,83 @@ +use async_graphql::parser::types::{FieldDefinition, InterfaceType, ObjectType, TypeDefinition}; +use async_graphql::Positioned; +use async_graphql_value::Name; +use tailcall_valid::{Valid, Validator}; + +use crate::core::{blueprint, Type}; +use crate::core::blueprint::Definition; +use crate::core::blueprint::from_service_document::{Error, helpers, pos_name_to_string}; +use crate::core::blueprint::from_service_document::from_service_document::BlueprintMetadata; + +pub(super) trait ObjectLike { + fn fields(&self) -> &Vec>; + fn implements(&self) -> &Vec>; +} +impl ObjectLike for ObjectType { + fn fields(&self) -> &Vec> { + &self.fields + } + fn implements(&self) -> &Vec> { + &self.implements + } +} +impl ObjectLike for InterfaceType { + fn fields(&self) -> &Vec> { + &self.fields + } + fn implements(&self) -> &Vec> { + &self.implements + } +} + +impl BlueprintMetadata { + pub(super) fn to_object_ty( + &self, + obj: &Obj, + type_definition: &Positioned, + ) -> Valid { + Valid::from_iter(obj.fields().iter(), |f| { + let field = &f.node; + let field_name = field.name.node.to_string(); + let field_type = &field.ty.node; + let field_args = field.arguments.iter().map(|arg| { + let arg = &arg.node; + let arg_name = arg.name.node.to_string(); + let arg_type = &arg.ty.node; + let arg_default = arg.default_value.as_ref().map(|v| v.node.clone().into_json().ok()).flatten(); + blueprint::InputFieldDefinition { + name: arg_name, + of_type: Type::from(arg_type), + default_value: arg_default, + description: arg.description.as_ref().map(|d| d.node.to_string()), + } + }).collect(); + let directives = helpers::extract_directives(field.directives.iter()); + directives.and_then(|directives| { + let field_type = blueprint::FieldDefinition { + name: field_name, + args: field_args, + directives, + description: field.description.as_ref().map(|d| d.node.to_string()), + resolver: None, + of_type: Type::from(field_type), + default_value: None, + }; + Valid::succeed(field_type) + }) + }).and_then(|fields| { + helpers::extract_directives(type_definition.node.directives.iter()).and_then(|directives| { + Valid::succeed( + blueprint::ObjectTypeDefinition { + name: pos_name_to_string(&type_definition.node.name), + directives, + description: type_definition.node.description.as_ref().map(|d| d.node.to_string()), + fields, + implements: obj.implements().iter().map(|i| i.node.to_string()).collect(), + } + ) + }) + }).and_then(|obj| { + Valid::succeed(Definition::Object(obj)) + }) + } +} diff --git a/src/core/blueprint/from_service_document/populate_resolvers.rs b/src/core/blueprint/from_service_document/populate_resolvers.rs new file mode 100644 index 0000000000..730d867237 --- /dev/null +++ b/src/core/blueprint/from_service_document/populate_resolvers.rs @@ -0,0 +1,9 @@ +use tailcall_valid::Valid; +use crate::core::blueprint::Definition; +use crate::core::blueprint::from_service_document::from_service_document::BlueprintMetadata; + +impl BlueprintMetadata { + pub(super) fn populate_resolvers(&self, defs: Vec) -> Valid, super::Error> { + todo!() + } +} \ No newline at end of file diff --git a/src/core/blueprint/from_service_document/resolvers/mod.rs b/src/core/blueprint/from_service_document/resolvers/mod.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/core/blueprint/from_service_document/scalar.rs b/src/core/blueprint/from_service_document/scalar.rs new file mode 100644 index 0000000000..778a98ce3a --- /dev/null +++ b/src/core/blueprint/from_service_document/scalar.rs @@ -0,0 +1,30 @@ +use async_graphql::parser::types::TypeDefinition; +use async_graphql::Positioned; +use tailcall_valid::{Valid, Validator}; +use crate::core::blueprint::{Definition, ScalarTypeDefinition}; +use crate::core::blueprint::from_service_document::from_service_document::BlueprintMetadata; +use crate::core::blueprint::from_service_document::{helpers, pos_name_to_string}; +use crate::core::scalar::Scalar; + +impl BlueprintMetadata { + pub(super) fn to_scalar_ty(&self, type_definition: &Positioned) -> Valid { + let type_name = pos_name_to_string(&type_definition.node.name); + + if Scalar::is_predefined(&type_name) { + Valid::fail(format!("Scalar type `{}` is predefined", type_name)) + } else { + helpers::extract_directives(type_definition.node.directives.iter()).and_then(|directives| { + Valid::succeed( + Definition::Scalar( + ScalarTypeDefinition { + scalar: Scalar::find(&type_name).unwrap_or(&Scalar::Empty).clone(), + name: type_name, + directives, + description: type_definition.node.description.as_ref().map(|d| d.node.to_string()), + } + ) + ) + }) + } + } +} \ No newline at end of file diff --git a/src/core/blueprint/from_service_document/schema.rs b/src/core/blueprint/from_service_document/schema.rs new file mode 100644 index 0000000000..68dbeb3c27 --- /dev/null +++ b/src/core/blueprint/from_service_document/schema.rs @@ -0,0 +1,70 @@ +use async_graphql::parser::types::{SchemaDefinition, ServiceDocument, TypeSystemDefinition}; +use tailcall_valid::{Valid, Validator}; + +use crate::core::blueprint::blueprint; +use crate::core::blueprint::from_service_document::{helpers, pos_name_to_string}; +use crate::core::blueprint::from_service_document::from_service_document::BlueprintMetadata; + +impl BlueprintMetadata { + + pub fn schema_definition(&self, doc: &ServiceDocument) -> Valid { + doc.definitions + .iter() + .find_map(|def| match def { + TypeSystemDefinition::Schema(schema_definition) => Some(&schema_definition.node), + _ => None, + }) + .cloned() + .map_or_else(|| Valid::succeed(SchemaDefinition { + extend: false, + directives: vec![], + query: None, + mutation: None, + subscription: None, + }), Valid::succeed) + } + + // TODO: need to validate if type has resolvers +// on each step + pub fn to_bp_schema_def(&self, doc: &ServiceDocument, schema_definition: &SchemaDefinition) -> Valid { + helpers::extract_directives(schema_definition.directives.iter()) + .fuse(self.validate_query(doc, schema_definition.query.as_ref().map(pos_name_to_string))) + .fuse(self.validate_mutation(schema_definition.mutation.as_ref().map(pos_name_to_string), schema_definition)) + .map(|(directives, query, mutation)| blueprint::SchemaDefinition { directives, query, mutation }) + } + + fn validate_query(&self, doc: &ServiceDocument, qry: Option) -> Valid { + Valid::from_option(qry, "Query root is missing".to_owned()) + .and_then(|query_type_name| { + // println!("{:#?}", schema_definition); + let find = doc.definitions.iter().find_map(|directive| { + if let TypeSystemDefinition::Type(ty) = directive { + if query_type_name.eq(ty.node.name.node.as_ref()) { + Some(()) + } else { + None + } + } else { + None + } + }); + Valid::from_option(find, "Query type is not defined".to_owned()).map(|_: ()| query_type_name) + }) + } + + fn validate_mutation(&self, mutation: Option, schema_definition: &SchemaDefinition) -> Valid, super::Error> { + match mutation { + Some(mutation_type_name) => { + let find = schema_definition.directives.iter().find_map(|directive| { + if mutation_type_name.eq(directive.node.name.node.as_ref()) { + Some(()) + } else { + None + } + }); + Valid::from_option(find, "Mutation type is not defined".to_owned()).map(|_| Some(mutation_type_name)) + } + None => Valid::succeed(None), + } + } +} \ No newline at end of file diff --git a/src/core/blueprint/from_service_document/union.rs b/src/core/blueprint/from_service_document/union.rs new file mode 100644 index 0000000000..90c2c9d6fc --- /dev/null +++ b/src/core/blueprint/from_service_document/union.rs @@ -0,0 +1,29 @@ +use async_graphql::parser::types::{TypeDefinition, UnionType}; +use async_graphql::Positioned; +use tailcall_valid::{Valid, Validator}; +use crate::core::blueprint; +use crate::core::blueprint::Definition; +use crate::core::blueprint::from_service_document::{Error, helpers, pos_name_to_string}; +use crate::core::blueprint::from_service_document::from_service_document::BlueprintMetadata; + +impl BlueprintMetadata { + pub(super) fn to_union_ty(&self, union_: &UnionType, type_definition: &Positioned) -> Valid { + let types = union_ + .members + .iter() + .map(|t| t.node.to_string()) + .collect(); + helpers::extract_directives(type_definition.node.directives.iter()).and_then(|directives| { + Valid::succeed( + Definition::Union( + blueprint::UnionTypeDefinition { + name: pos_name_to_string(&type_definition.node.name), + directives, + description: type_definition.node.description.as_ref().map(|d| d.node.to_string()), + types, + } + ) + ) + }) + } +} diff --git a/src/core/blueprint/mod.rs b/src/core/blueprint/mod.rs index 93c97302d0..c9ab7052d5 100644 --- a/src/core/blueprint/mod.rs +++ b/src/core/blueprint/mod.rs @@ -19,6 +19,7 @@ pub mod telemetry; mod timeout; mod union_resolver; mod upstream; +mod from_service_document; pub use auth::*; pub use blueprint::*; diff --git a/src/core/scalar.rs b/src/core/scalar.rs index 44e7a4b35a..cdd9c7ef21 100644 --- a/src/core/scalar.rs +++ b/src/core/scalar.rs @@ -17,7 +17,7 @@ lazy_static! { #[derive( schemars::JsonSchema, Debug, Clone, strum_macros::Display, strum_macros::EnumIter, Doc, -)] + )] pub enum Scalar { /// Empty scalar type represents an empty value. #[gen_doc(ty = "Null")]