SDL schema generation improvements (#642)

Add the ability to render field input-arguments and default values in
sdl.

Also add a small CLI tool to help inspect the GraphQL schemas that
result from a piece of engine metadata.

V3_GIT_ORIGIN_REV_ID: ddb7315f12d24eafbf57782f37f840ca44d94789
This commit is contained in:
Philip Lykke Carlsen 2024-06-01 08:41:17 +02:00 committed by hasura-bot
parent 6964b7cf3d
commit 812f44f3c1
4 changed files with 181 additions and 14 deletions

View File

@ -3,7 +3,7 @@
"__typename": "Query",
"_service": {
"__typename": "_Service",
"sdl": "extend schema\n @link(url: \"https://specs.apollo.dev/federation/v2.0\", import: [\"@key\", \"@extends\", \"@external\", \"@shareable\"])\n\nschema {\n query: Query \n}\n\ntype Author@key(fields: \"author_id\") {\n author_id: Int! \n first_name: String! \n last_name: String! \n}\n\nscalar Boolean\n\nscalar Float\n\nscalar ID\n\nscalar Int\n\ntype Query {\n AuthorByID: Author \n _entities: _Entity! \n _service: _Service! \n}\n\nscalar String\n\nscalar _Any\n\nunion _Entity = Author\n\ntype _Service {\n sdl: String! \n}"
"sdl": "extend schema\n @link(url: \"https://specs.apollo.dev/federation/v2.0\", import: [\"@key\", \"@extends\", \"@external\", \"@shareable\"])\n\nschema {\n query: Query \n}\n\ntype Author@key(fields: \"author_id\") {\n author_id: Int!\n first_name: String!\n last_name: String!\n}\n\nscalar Boolean\n\nscalar Float\n\nscalar ID\n\nscalar Int\n\ntype Query {\n AuthorByID(author_id: Int!): Author\n _entities(representations: [_Any!]!): _Entity!\n _service: _Service!\n}\n\nscalar String\n\nscalar _Any\n\nunion _Entity = Author\n\ntype _Service {\n sdl: String!\n}"
}
}
}

View File

@ -40,10 +40,10 @@
use std::collections::BTreeMap;
use crate::{
ast::common as ast,
ast::{common as ast, value::ConstValue},
schema::{
DeprecationStatus, Directive, Enum, Field, InputObject, Interface, Namespaced, Object,
Scalar, Schema, SchemaContext, TypeInfo, Union,
DeprecationStatus, Directive, Enum, Field, InputField, InputObject, Interface, Namespaced,
Object, Scalar, Schema, SchemaContext, TypeInfo, Union,
},
};
@ -95,10 +95,12 @@ impl<S: SchemaContext> InputObject<S> {
field.get(namespace).map(|(data, _)| {
with_description(
&data.description,
format_field_with_type(
format_field_with_type::<S>(
field_name,
&BTreeMap::new(),
&data.field_type,
Some(&data.deprecation_status),
&data.deprecation_status,
namespace,
),
)
})
@ -388,8 +390,10 @@ fn generate_fields_sdl<S: SchemaContext>(
&data.description,
format_field_with_type(
field_name,
&data.arguments,
&data.field_type,
Some(&data.deprecation_status),
&data.deprecation_status,
namespace,
),
));
}
@ -398,22 +402,115 @@ fn generate_fields_sdl<S: SchemaContext>(
fields_sdl
}
fn format_field_with_type(
/// Format an input field as SDL, including default values if present.
///
/// Syntax:
///
/// <field_name>: <field_type> = <default_value>
///
fn format_input_field_with_type(
field_name: &ast::Name,
field_type: &ast::TypeContainer<ast::TypeName>,
deprecation_status: Option<&DeprecationStatus>,
deprecation_status: &DeprecationStatus,
default_value: Option<&ConstValue>,
) -> String {
let field_sdl = format!("{}: {}", field_name, field_type);
match deprecation_status {
Some(deprecation_status) => format!(
let field_sdl = match default_value {
None => format!("{}: {}", field_name, field_type),
Some(default_value) => {
let default_value_sdl = default_value.to_json().to_string();
let mut default_value_lines = default_value_sdl.lines();
let multiple_lines = {
default_value_lines.next();
default_value_lines.next().is_some()
};
if multiple_lines {
format!(
"{}: {}\n = {}",
field_name,
field_type,
with_indent(default_value_sdl.as_str())
)
} else {
format!("{}: {} = {}", field_name, field_type, default_value_sdl)
}
}
};
if deprecation_status == &DeprecationStatus::NotDeprecated {
field_sdl
} else {
format!(
"{} {}",
field_sdl,
generate_directives_sdl(&[], Some(deprecation_status))
),
_ => field_sdl,
)
}
}
/// Format an (output) field as SDL, including field arguments if present.
///
/// Syntax:
///
/// <field_name>(<generate_arguments_sdl(field_arguments)>): <field_type>
///
fn format_field_with_type<S: SchemaContext>(
field_name: &ast::Name,
field_arguments: &BTreeMap<ast::Name, Namespaced<S, InputField<S>>>,
field_type: &ast::TypeContainer<ast::TypeName>,
deprecation_status: &DeprecationStatus,
namespace: &S::Namespace,
) -> String {
let field_sdl = if field_arguments.is_empty() {
format!("{}: {}", field_name, field_type)
} else {
let arguments_sdl = generate_arguments_sdl(field_arguments, namespace);
let mut arguments_lines = arguments_sdl.lines();
let multiple_lines = {
arguments_lines.next();
arguments_lines.next().is_some()
};
if multiple_lines {
format!(
"{}(\n{}\n ): {}",
field_name,
with_indent(arguments_sdl.as_str()),
field_type
)
} else {
format!("{}({}): {}", field_name, arguments_sdl, field_type)
}
};
if deprecation_status == &DeprecationStatus::NotDeprecated {
field_sdl
} else {
format!(
"{} {}",
field_sdl,
generate_directives_sdl(&[], Some(deprecation_status))
)
}
}
fn generate_arguments_sdl<S: SchemaContext>(
field_arguments: &BTreeMap<ast::Name, Namespaced<S, InputField<S>>>,
namespace: &S::Namespace,
) -> String {
field_arguments
.iter()
.filter_map(|(field_name, namespaced)| {
namespaced.get(namespace).map(|(input_field, _)| {
format_input_field_with_type(
field_name,
&input_field.field_type,
&input_field.deprecation_status,
input_field.default_value.as_ref(),
)
})
})
.collect::<Vec<String>>()
.join(",\n")
}
fn with_description(description: &Option<String>, sdl: String) -> String {
if description.is_some() {
format!("{}\n{}", generate_description_sdl(description), sdl)

View File

@ -7,6 +7,11 @@ license.workspace = true
[lib]
bench = false
[[bin]]
name = "build-schema-from-metadata"
path = "bin/build-schema-from-metadata/main.rs"
bench = false
[dependencies]
hasura-authn-core = { path = "../auth/hasura-authn-core" }
json-ext = { path = "../utils/json-ext" }

View File

@ -0,0 +1,65 @@
/// This is a small command-line utility that prints out the GraphQL schemas that result from an
/// engine-metadata value.
///
use std::{
collections::BTreeSet,
io::{stdin, Read},
process::exit,
};
use hasura_authn_core::Role;
use lang_graphql::generate_graphql_schema::build_namespace_schemas;
pub fn main() {
let mut metadata_string = String::new();
let mut h = stdin();
h.read_to_string(&mut metadata_string).unwrap();
if metadata_string.is_empty() {
println!("Usage: Provide a metadata json file via stdin and get the corresponding sdl schema and introspection query results on stdout.");
exit(-1);
}
let metadata =
open_dds::traits::OpenDd::deserialize(serde_json::from_str(&metadata_string).unwrap())
.unwrap();
let gds = schema::GDS::new(metadata).unwrap();
let sch = gds.build_schema().unwrap();
let namespace_schemas = build_namespace_schemas(&sch).unwrap();
let dedup_roles: BTreeSet<Role> = gds.metadata.roles.into_iter().collect();
for role in dedup_roles {
println!("-------------------------------:");
println!("Now comes the SDL schema for {role}:");
println!("-------------------------------:");
println!();
println!("{}", sch.generate_sdl(&role));
println!();
println!();
println!();
print!("-------------------------------:");
print!("Now comes the query-based? schema for {role}:");
print!("-------------------------------:");
println!();
print!(
"{}",
serde_json::ser::to_string_pretty(namespace_schemas.get(&role).unwrap()).unwrap()
);
println!();
println!();
println!();
}
println!("-------------------------------:");
println!(" Debug print of schema :");
println!("-------------------------------:");
println!();
println!("{:#?}", sch);
println!();
println!();
println!();
}