From 23754751de4e9a2f79f0981cfa2f32138fe165e5 Mon Sep 17 00:00:00 2001 From: Kiryl Mialeshka <8974488+meskill@users.noreply.github.com> Date: Thu, 28 Nov 2024 17:14:19 +0100 Subject: [PATCH] fix: check if graphql schema should be updated (#3173) --- generated/.tailcallrc.graphql | 36 +++------ src/core/document.rs | 12 ++- .../src/enum_definition.rs | 62 +++++++++++++-- tailcall-typedefs/src/main.rs | 76 ++++++++++++++----- 4 files changed, 132 insertions(+), 54 deletions(-) diff --git a/generated/.tailcallrc.graphql b/generated/.tailcallrc.graphql index bc36d8b941..a484e6864e 100644 --- a/generated/.tailcallrc.graphql +++ b/generated/.tailcallrc.graphql @@ -1027,57 +1027,39 @@ enum Method { enum LinkType { """ - Points to another Tailcall Configuration file. The imported - configuration will be merged into the importing configuration. + Points to another Tailcall Configuration file. The imported configuration will be merged into the importing configuration. """ Config - """ - Points to a Protobuf file. The imported Protobuf file will be used by - the `@grpc` directive. If your API exposes a reflection endpoint, you - should set the type to `Grpc` instead. + Points to a Protobuf file. The imported Protobuf file will be used by the `@grpc` directive. If your API exposes a reflection endpoint, you should set the type to `Grpc` instead. """ Protobuf - """ - Points to a JS file. The imported JS file will be used by the `@js` - directive. + Points to a JS file. The imported JS file will be used by the `@js` directive. """ Script - """ - Points to a Cert file. The imported Cert file will be used by the server - to serve over HTTPS. + Points to a Cert file. The imported Cert file will be used by the server to serve over HTTPS. """ Cert - """ - Points to a Key file. The imported Key file will be used by the server - to serve over HTTPS. + Points to a Key file. The imported Key file will be used by the server to serve over HTTPS. """ Key - """ - A trusted document that contains GraphQL operations (queries, mutations) - that can be exposed as a REST API using the `@rest` directive. + A trusted document that contains GraphQL operations (queries, mutations) that can be exposed a REST API using the `@rest` directive. """ Operation - """ - Points to a Htpasswd file. The imported Htpasswd file will be used by - the server to authenticate users. + Points to a Htpasswd file. The imported Htpasswd file will be used by the server to authenticate users. """ Htpasswd - """ - Points to a Jwks file. The imported Jwks file will be used by the server - to authenticate users. + Points to a Jwks file. The imported Jwks file will be used by the server to authenticate users. """ Jwks - """ - Points to a reflection endpoint. The imported reflection endpoint will - be used by the `@grpc` directive to resolve data from gRPC services. + Points to a reflection endpoint. The imported reflection endpoint will be used by the `@grpc` directive to resolve data from gRPC services. """ Grpc } diff --git a/src/core/document.rs b/src/core/document.rs index 26f37c0ae5..baaf7b9ca8 100644 --- a/src/core/document.rs +++ b/src/core/document.rs @@ -252,10 +252,20 @@ fn print_type_def(type_def: &TypeDefinition) -> String { fn print_enum_value(value: &async_graphql::parser::types::EnumValueDefinition) -> String { let directives_str = print_pos_directives(&value.directives); - if directives_str.is_empty() { + let variant_def = if directives_str.is_empty() { format!(" {}", value.value) } else { format!(" {} {}", value.value, directives_str) + }; + + if let Some(desc) = &value.description { + format!( + " \"\"\"\n {}\n \"\"\"\n{}", + desc.node.as_str(), + variant_def + ) + } else { + variant_def } } diff --git a/tailcall-typedefs-common/src/enum_definition.rs b/tailcall-typedefs-common/src/enum_definition.rs index 59b96413f4..6ffa67063f 100644 --- a/tailcall-typedefs-common/src/enum_definition.rs +++ b/tailcall-typedefs-common/src/enum_definition.rs @@ -4,9 +4,21 @@ use async_graphql::parser::types::{ use async_graphql::{Name, Positioned}; use schemars::schema::Schema; +#[derive(Debug)] +pub struct EnumVariant { + pub value: String, + pub description: Option>, +} + +impl EnumVariant { + pub fn new(value: String) -> Self { + Self { value, description: None } + } +} + #[derive(Debug)] pub struct EnumValue { - pub variants: Vec, + pub variants: Vec, pub description: Option>, } @@ -14,15 +26,16 @@ use crate::common::{get_description, pos}; pub fn into_enum_definition(enum_value: EnumValue, name: &str) -> TypeSystemDefinition { let mut enum_value_definition = vec![]; - for enum_value in enum_value.variants { - let formatted_value: String = enum_value + for enum_variant in enum_value.variants { + let formatted_value: String = enum_variant + .value .to_string() .chars() .filter(|ch| ch != &'"') .collect(); enum_value_definition.push(pos(EnumValueDefinition { value: pos(Name::new(formatted_value)), - description: None, + description: enum_variant.description, directives: vec![], })); } @@ -39,16 +52,49 @@ pub fn into_enum_definition(enum_value: EnumValue, name: &str) -> TypeSystemDefi pub fn into_enum_value(obj: &Schema) -> Option { match obj { Schema::Object(schema_object) => { - let description = get_description(schema_object); + let description = + get_description(schema_object).map(|description| pos(description.to_owned())); + + // if it has enum_values then it's raw enum if let Some(enum_values) = &schema_object.enum_values { return Some(EnumValue { variants: enum_values .iter() - .map(|val| val.to_string()) - .collect::>(), - description: description.map(|description| pos(description.to_owned())), + .map(|val| EnumVariant::new(val.to_string())) + .collect::>(), + description, }); } + + // in case enum has description docs for the variants they will be generated + // as schema with `one_of` entry, where every enum variant is separate enum + // entry + if let Some(subschema) = &schema_object.subschemas { + if let Some(one_ofs) = &subschema.one_of { + let variants = one_ofs + .iter() + .filter_map(|one_of| { + // try to parse one_of value as enum + into_enum_value(one_of).and_then(|mut en| { + // if it has only single variant it's our high-level enum + if en.variants.len() == 1 { + Some(EnumVariant { + value: en.variants.pop().unwrap().value, + description: en.description, + }) + } else { + None + } + }) + }) + .collect::>(); + + if !variants.is_empty() { + return Some(EnumValue { variants, description }); + } + } + } + None } _ => None, diff --git a/tailcall-typedefs/src/main.rs b/tailcall-typedefs/src/main.rs index 4023a02089..aecf725b24 100644 --- a/tailcall-typedefs/src/main.rs +++ b/tailcall-typedefs/src/main.rs @@ -1,7 +1,8 @@ mod gen_gql_schema; use std::env; -use std::path::PathBuf; +use std::ops::Deref; +use std::path::{Path, PathBuf}; use std::process::exit; use std::sync::Arc; @@ -15,8 +16,8 @@ use tailcall::core::config::Config; use tailcall::core::tracing::default_tracing_for_name; use tailcall::core::{scalar, FileIO}; -static JSON_SCHEMA_FILE: &str = "../generated/.tailcallrc.schema.json"; -static GRAPHQL_SCHEMA_FILE: &str = "../generated/.tailcallrc.graphql"; +static JSON_SCHEMA_FILE: &str = "generated/.tailcallrc.schema.json"; +static GRAPHQL_SCHEMA_FILE: &str = "generated/.tailcallrc.graphql"; #[tokio::main] async fn main() { @@ -51,9 +52,17 @@ async fn main() { } async fn mode_check() -> Result<()> { - let json_schema = get_file_path(); let rt = cli::runtime::init(&Default::default()); - let file_io = rt.file; + let file_io = rt.file.deref(); + + check_json(file_io).await?; + check_graphql(file_io).await?; + + Ok(()) +} + +async fn check_json(file_io: &dyn FileIO) -> Result<()> { + let json_schema = get_json_path(); let content = file_io .read( json_schema @@ -62,10 +71,26 @@ async fn mode_check() -> Result<()> { ) .await?; let content = serde_json::from_str::(&content)?; - let schema = get_updated_json().await?; + let schema = get_updated_json()?; match content.eq(&schema) { true => Ok(()), - false => Err(anyhow!("Schema mismatch")), + false => Err(anyhow!("Schema file '{}' mismatch", JSON_SCHEMA_FILE)), + } +} + +async fn check_graphql(file_io: &dyn FileIO) -> Result<()> { + let graphql_schema = get_graphql_path(); + let content = file_io + .read( + graphql_schema + .to_str() + .ok_or(anyhow!("Unable to determine path"))?, + ) + .await?; + let schema = get_updated_graphql(); + match content.eq(&schema) { + true => Ok(()), + false => Err(anyhow!("Schema file '{}' mismatch", GRAPHQL_SCHEMA_FILE)), } } @@ -74,27 +99,28 @@ async fn mode_fix() -> Result<()> { let file_io = rt.file; update_json(file_io.clone()).await?; - update_gql(file_io.clone()).await?; + update_graphql(file_io.clone()).await?; Ok(()) } -async fn update_gql(file_io: Arc) -> Result<()> { - let doc = gen_gql_schema::build_service_document(); +async fn update_graphql(file_io: Arc) -> Result<()> { + let schema = get_updated_graphql(); - let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(GRAPHQL_SCHEMA_FILE); + let path = get_graphql_path(); + tracing::info!("Updating Graphql Schema: {}", GRAPHQL_SCHEMA_FILE); file_io .write( path.to_str().ok_or(anyhow!("Unable to determine path"))?, - tailcall::core::document::print(doc).as_bytes(), + schema.as_bytes(), ) .await?; Ok(()) } async fn update_json(file_io: Arc) -> Result<()> { - let path = get_file_path(); - let schema = serde_json::to_string_pretty(&get_updated_json().await?)?; - tracing::info!("Updating JSON Schema: {}", path.to_str().unwrap()); + let path = get_json_path(); + let schema = serde_json::to_string_pretty(&get_updated_json()?)?; + tracing::info!("Updating JSON Schema: {}", JSON_SCHEMA_FILE); file_io .write( path.to_str().ok_or(anyhow!("Unable to determine path"))?, @@ -104,11 +130,19 @@ async fn update_json(file_io: Arc) -> Result<()> { Ok(()) } -fn get_file_path() -> PathBuf { - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(JSON_SCHEMA_FILE) +fn get_root_path() -> &'static Path { + Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap() +} + +fn get_json_path() -> PathBuf { + get_root_path().join(JSON_SCHEMA_FILE) } -async fn get_updated_json() -> Result { +fn get_graphql_path() -> PathBuf { + get_root_path().join(GRAPHQL_SCHEMA_FILE) +} + +fn get_updated_json() -> Result { let mut schema: RootSchema = schemars::schema_for!(Config); let scalar = scalar::Scalar::iter() .map(|scalar| (scalar.name(), scalar.schema())) @@ -118,3 +152,9 @@ async fn get_updated_json() -> Result { let schema = json!(schema); Ok(schema) } + +fn get_updated_graphql() -> String { + let doc = gen_gql_schema::build_service_document(); + + tailcall::core::document::print(doc) +}