Skip to content

Stable order in generated schema's SDL (#1134) #1237

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

Merged
merged 4 commits into from
Jan 15, 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
1 change: 0 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,6 @@ jobs:
- { feature: chrono-clock, crate: juniper }
- { feature: chrono-tz, crate: juniper }
- { feature: expose-test-schema, crate: juniper }
- { feature: graphql-parser, crate: juniper }
- { feature: rust_decimal, crate: juniper }
- { feature: schema-language, crate: juniper }
- { feature: time, crate: juniper }
Expand Down
12 changes: 6 additions & 6 deletions book/src/schema/schemas_and_mutations.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,17 +89,17 @@ fn main() {
EmptySubscription::<()>::new(),
);

// Convert the Rust schema into the GraphQL Schema Language.
let result = schema.as_schema_language();
// Convert the Rust schema into the GraphQL Schema Definition Language.
let result = schema.as_sdl();

let expected = "\
type Query {
hello: String!
}

schema {
query: Query
}

type Query {
hello: String!
}
";
# #[cfg(not(target_os = "windows"))]
assert_eq!(result, expected);
Expand Down
6 changes: 6 additions & 0 deletions juniper/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
- Made `LookAheadMethods::children()` method to return slice instead of `Vec`. ([#1200])
- Abstracted `Spanning::start` and `Spanning::end` fields into separate struct `Span`. ([#1207], [#1208])
- Added `Span` to `Arguments` and `LookAheadArguments`. ([#1206], [#1209])
- Removed `graphql-parser-integration` and `graphql-parser` [Cargo feature]s by merging them into `schema-language` [Cargo feature]. ([#1237])
- Renamed `RootNode::as_schema_language()` method as `RootNode::as_sdl()`. ([#1237])
- Renamed `RootNode::as_parser_document()` method as `RootNode::as_document()`. ([#1237])

### Added

Expand Down Expand Up @@ -89,6 +92,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
- Incorrect input value coercion with defaults. ([#1080], [#1073])
- Incorrect error when explicit `null` provided for `null`able list input parameter. ([#1086], [#1085])
- Stack overflow on nested GraphQL fragments. ([CVE-2022-31173])
- Unstable definitions order in schema generated by `RootNode::as_sdl()`. ([#1237], [#1134])

[#103]: /../../issues/103
[#113]: /../../issues/113
Expand Down Expand Up @@ -132,6 +136,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
[#1086]: /../../pull/1086
[#1118]: /../../issues/1118
[#1119]: /../../pull/1119
[#1134]: /../../issues/1134
[#1138]: /../../issues/1138
[#1145]: /../../pull/1145
[#1147]: /../../pull/1147
Expand All @@ -149,6 +154,7 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
[#1227]: /../../pull/1227
[#1228]: /../../pull/1228
[#1235]: /../../pull/1235
[#1237]: /../../pull/1237
[ba1ed85b]: /../../commit/ba1ed85b3c3dd77fbae7baf6bc4e693321a94083
[CVE-2022-31173]: /../../security/advisories/GHSA-4rx6-g5vg-5f3j

Expand Down
3 changes: 1 addition & 2 deletions juniper/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,9 @@ chrono = ["dep:chrono"]
chrono-clock = ["chrono", "chrono/clock"]
chrono-tz = ["dep:chrono-tz", "dep:regex"]
expose-test-schema = ["dep:anyhow", "dep:serde_json"]
graphql-parser = ["dep:graphql-parser", "dep:void"]
js = ["chrono?/wasmbind", "time?/wasm-bindgen", "uuid?/js"]
rust_decimal = ["dep:rust_decimal"]
schema-language = ["graphql-parser"]
schema-language = ["dep:graphql-parser", "dep:void"]
time = ["dep:time"]
url = ["dep:url"]
uuid = ["dep:uuid"]
Expand Down
221 changes: 129 additions & 92 deletions juniper/src/schema/model.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::{borrow::Cow, fmt};

use fnv::FnvHashMap;
#[cfg(feature = "graphql-parser")]
#[cfg(feature = "schema-language")]
use graphql_parser::schema::Document;

use crate::{
Expand All @@ -13,9 +13,6 @@ use crate::{
GraphQLEnum,
};

#[cfg(feature = "graphql-parser")]
use crate::schema::translate::{graphql_parser::GraphQLParserTranslator, SchemaTranslator};

/// Root query node of a schema
///
/// This brings the mutation, subscription and query types together,
Expand Down Expand Up @@ -221,17 +218,40 @@ where
}

#[cfg(feature = "schema-language")]
/// The schema definition as a `String` in the
/// [GraphQL Schema Language](https://graphql.org/learn/schema/#type-language)
/// format.
pub fn as_schema_language(&self) -> String {
self.as_parser_document().to_string()
/// Returns this [`RootNode`] as a [`String`] containing the schema in [SDL (schema definition language)].
///
/// # Sorted
///
/// The order of the generated definitions is stable and is sorted in the "type-then-name" manner.
///
/// If another sorting order is required, then the [`as_document()`] method should be used, which allows to sort the
/// returned [`Document`] in the desired manner and then to convert it [`to_string()`].
///
/// [`as_document()`]: RootNode::as_document
/// [`to_string()`]: ToString::to_string
/// [0]: https://graphql.org/learn/schema#type-language
#[must_use]
pub fn as_sdl(&self) -> String {
use crate::schema::translate::graphql_parser::sort_schema_document;

let mut doc = self.as_document();
sort_schema_document(&mut doc);
doc.to_string()
}

#[cfg(feature = "graphql-parser")]
/// The schema definition as a [`graphql_parser`](https://crates.io/crates/graphql-parser)
/// [`Document`](https://docs.rs/graphql-parser/latest/graphql_parser/schema/struct.Document.html).
pub fn as_parser_document(&'a self) -> Document<'a, &'a str> {
#[cfg(feature = "schema-language")]
/// Returns this [`RootNode`] as a [`graphql_parser`]'s [`Document`].
///
/// # Unsorted
///
/// The order of the generated definitions in the returned [`Document`] is NOT stable and may change without any
/// real schema changes.
#[must_use]
pub fn as_document(&'a self) -> Document<'a, &'a str> {
use crate::schema::translate::{
graphql_parser::GraphQLParserTranslator, SchemaTranslator as _,
};

GraphQLParserTranslator::translate_schema(&self.schema)
}
}
Expand Down Expand Up @@ -666,119 +686,141 @@ impl<'a, S> fmt::Display for TypeType<'a, S> {
}

#[cfg(test)]
mod test {

#[cfg(feature = "graphql-parser")]
mod graphql_parser_integration {
mod root_node_test {
#[cfg(feature = "schema-language")]
mod as_document {
use crate::{graphql_object, EmptyMutation, EmptySubscription, RootNode};

#[test]
fn graphql_parser_doc() {
struct Query;
#[graphql_object]
impl Query {
fn blah() -> bool {
true
}
struct Query;

#[graphql_object]
impl Query {
fn blah() -> bool {
true
}
}

#[test]
fn generates_correct_document() {
let schema = RootNode::new(
Query,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let ast = graphql_parser::parse_schema::<&str>(
//language=GraphQL
r#"
type Query {
blah: Boolean!
blah: Boolean!
}

schema {
query: Query
}
"#,
"#,
)
.unwrap();
assert_eq!(ast.to_string(), schema.as_parser_document().to_string());

assert_eq!(ast.to_string(), schema.as_document().to_string());
}
}

#[cfg(feature = "schema-language")]
mod schema_language {
mod as_sdl {
use crate::{
graphql_object, EmptyMutation, EmptySubscription, GraphQLEnum, GraphQLInputObject,
GraphQLObject, GraphQLUnion, RootNode,
};

#[test]
fn schema_language() {
#[derive(GraphQLObject, Default)]
struct Cake {
fresh: bool,
}
#[derive(GraphQLObject, Default)]
struct IceCream {
cold: bool,
#[derive(GraphQLObject, Default)]
struct Cake {
fresh: bool,
}

#[derive(GraphQLObject, Default)]
struct IceCream {
cold: bool,
}

#[derive(GraphQLUnion)]
enum GlutenFree {
Cake(Cake),
IceCream(IceCream),
}

#[derive(GraphQLEnum)]
enum Fruit {
Apple,
Orange,
}

#[derive(GraphQLInputObject)]
struct Coordinate {
latitude: f64,
longitude: f64,
}

struct Query;

#[graphql_object]
impl Query {
fn blah() -> bool {
true
}
#[derive(GraphQLUnion)]
enum GlutenFree {
Cake(Cake),
IceCream(IceCream),

/// This is whatever's description.
fn whatever() -> String {
"foo".into()
}
#[derive(GraphQLEnum)]
enum Fruit {
Apple,
Orange,

fn arr(stuff: Vec<Coordinate>) -> Option<&'static str> {
(!stuff.is_empty()).then_some("stuff")
}
#[derive(GraphQLInputObject)]
struct Coordinate {
latitude: f64,
longitude: f64,

fn fruit() -> Fruit {
Fruit::Apple
}
struct Query;
#[graphql_object]
impl Query {
fn blah() -> bool {
true
}
/// This is whatever's description.
fn whatever() -> String {
"foo".into()
}
fn arr(stuff: Vec<Coordinate>) -> Option<&'static str> {
(!stuff.is_empty()).then_some("stuff")
}
fn fruit() -> Fruit {
Fruit::Apple
}
fn gluten_free(flavor: String) -> GlutenFree {
if flavor == "savory" {
GlutenFree::Cake(Cake::default())
} else {
GlutenFree::IceCream(IceCream::default())
}
}
#[deprecated]
fn old() -> i32 {
42
}
#[deprecated(note = "This field is deprecated, use another.")]
fn really_old() -> f64 {
42.0

fn gluten_free(flavor: String) -> GlutenFree {
if flavor == "savory" {
GlutenFree::Cake(Cake::default())
} else {
GlutenFree::IceCream(IceCream::default())
}
}

let schema = RootNode::new(
#[deprecated]
fn old() -> i32 {
42
}

#[deprecated(note = "This field is deprecated, use another.")]
fn really_old() -> f64 {
42.0
}
}

#[test]
fn generates_correct_sdl() {
let actual = RootNode::new(
Query,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let ast = graphql_parser::parse_schema::<&str>(
let expected = graphql_parser::parse_schema::<&str>(
//language=GraphQL
r#"
union GlutenFree = Cake | IceCream
schema {
query: Query
}
enum Fruit {
APPLE
ORANGE
}
input Coordinate {
latitude: Float!
longitude: Float!
}
type Cake {
fresh: Boolean!
}
Expand All @@ -795,17 +837,12 @@ mod test {
old: Int! @deprecated
reallyOld: Float! @deprecated(reason: "This field is deprecated, use another.")
}
input Coordinate {
latitude: Float!
longitude: Float!
}
schema {
query: Query
}
"#,
union GlutenFree = Cake | IceCream
"#,
)
.unwrap();
assert_eq!(ast.to_string(), schema.as_schema_language());

assert_eq!(actual.as_sdl(), expected.to_string());
}
}
}
Loading