Skip to content

Commit 180a513

Browse files
authored
Support descriptions of executable documents (#1349, #1347, graphql/graphql-spec#1170)
- add `description` field to `ast::Operation`, `ast::Fragment` and `ast::VariableDefinition` (graphql/graphql-spec#1170) - support full Unicode range (graphql/graphql-spec#849, graphql/graphql-spec#687) - support parsing block string literals - support variable-length escaped Unicode characters in strings (graphql/graphql-spec#849, graphql/graphql-spec#687) - fix incorrect double escaping in `ScalarToken::String` `Display`ing - change `ScalarToken::String` to contain raw quoted and escaped `StringLiteral` - add `LexerError::UnterminatedBlockString` variant Additionally: - move `String` parsing to `StringLiteral::parse()` method - move lexer tests to `parser::lexer` module - satisfy rustc linter when running `juniper` unit tests without features
1 parent 4cda23c commit 180a513

File tree

12 files changed

+2337
-914
lines changed

12 files changed

+2337
-914
lines changed

juniper/CHANGELOG.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,26 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
1515
- [September 2025] GraphQL spec: ([#1347])
1616
- Made `includeDeprecated` argument of `__Type.fields`, `__Type.enumValues`, `__Type.inputFields`, `__Field.args` and `__Directive.args` fields non-`Null`. ([#1348], [graphql/graphql-spec#1142])
1717
- Made `@deprecated(reason:)` argument non-`Null`. ([#1348], [graphql/graphql-spec#1040])
18+
- Added `description` field to `ast::Operation`, `ast::Fragment` and `ast::VariableDefinition`. ([#1349], [graphql/graphql-spec#1170])
19+
- Changed `ScalarToken::String` to contain raw quoted and escaped `StringLiteral` (was unquoted but escaped string before). ([#1349])
20+
- Added `LexerError::UnterminatedBlockString` variant. ([#1349])
1821

1922
### Added
2023

2124
- [September 2025] GraphQL spec: ([#1347])
2225
- `__Type.isOneOf` field. ([#1348], [graphql/graphql-spec#825])
2326
- `SCHEMA`, `OBJECT`, `ARGUMENT_DEFINITION`, `INTERFACE`, `UNION`, `ENUM`, `INPUT_OBJECT` and `INPUT_FIELD_DEFINITION` values to `__DirectiveLocation` enum. ([#1348])
24-
- Arguments and input object fields deprecation: ([#1348], [#864], [graphql/graphql-spec#525], [graphql/graphql-spec#805])
27+
- Arguments and input object fields deprecation: ([#1348], [#864], [graphql/graphql-spec#525], [graphql/graphql-spec#805])
2528
- Placing `#[graphql(deprecated)]` and `#[deprecated]` attributes on struct fields in `#[derive(GraphQLInputObject)]` macro.
2629
- Placing `#[graphql(deprecated)]` attribute on method arguments in `#[graphql_object]` and `#[graphql_interface]` macros.
2730
- Placing `@deprecated` directive on arguments and input object fields.
2831
- `includeDeprecated` argument to `__Type.inputFields`, `__Field.args` and `__Directive.args` fields.
2932
- `__InputValue.isDeprecated` and `__InputValue.deprecationReason` fields.
3033
- `schema::meta::Argument::deprecation_status` field.
34+
- Support for variable-length escaped Unicode characters (e.g. `\u{110000}`) in strings. ([#1349], [graphql/graphql-spec#849], [graphql/graphql-spec#687])
35+
- Full Unicode range support. ([#1349], [graphql/graphql-spec#849], [graphql/graphql-spec#687])
36+
- Support parsing descriptions on operations, fragments and variable definitions. ([#1349], [graphql/graphql-spec#1170])
37+
- Support for [block strings][0180-1]. ([#1349])
3138

3239
### Changed
3340

@@ -38,15 +45,21 @@ All user visible changes to `juniper` crate will be documented in this file. Thi
3845

3946
- Incorrect `__Type.specifiedByUrl` field to `__Type.specifiedByURL`. ([#1348])
4047
- Missing `@specifiedBy(url:)` directive in [SDL] generated by `RootNode::as_sdl()` and `RootNode::as_document()` methods. ([#1348])
48+
- Incorrect double escaping in `ScalarToken::String` `Display`ing. ([#1349])
4149

4250
[#864]: /../../issues/864
4351
[#1347]: /../../issues/1347
4452
[#1348]: /../../pull/1348
53+
[#1349]: /../../pull/1349
4554
[graphql/graphql-spec#525]: https://github.com/graphql/graphql-spec/pull/525
55+
[graphql/graphql-spec#687]: https://github.com/graphql/graphql-spec/issues/687
4656
[graphql/graphql-spec#805]: https://github.com/graphql/graphql-spec/pull/805
4757
[graphql/graphql-spec#825]: https://github.com/graphql/graphql-spec/pull/825
58+
[graphql/graphql-spec#849]: https://github.com/graphql/graphql-spec/pull/849
4859
[graphql/graphql-spec#1040]: https://github.com/graphql/graphql-spec/pull/1040
4960
[graphql/graphql-spec#1142]: https://github.com/graphql/graphql-spec/pull/1142
61+
[graphql/graphql-spec#1170]: https://github.com/graphql/graphql-spec/pull/1170
62+
[0180-1]: https://spec.graphql.org/September2025/#sec-String-Value.Block-Strings
5063

5164

5265

juniper/src/ast.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,7 @@ pub enum InputValue<S = DefaultScalarValue> {
307307

308308
#[derive(Clone, Debug, PartialEq)]
309309
pub struct VariableDefinition<'a, S> {
310+
pub description: Option<Spanning<Cow<'a, str>>>,
310311
pub var_type: Spanning<Type<&'a str>>,
311312
pub default_value: Option<Spanning<InputValue<S>>>,
312313
pub directives: Option<Vec<Spanning<Directive<'a, S>>>>,
@@ -384,6 +385,7 @@ pub enum OperationType {
384385
#[expect(missing_docs, reason = "self-explanatory")]
385386
#[derive(Clone, Debug, PartialEq)]
386387
pub struct Operation<'a, S> {
388+
pub description: Option<Spanning<Cow<'a, str>>>,
387389
pub operation_type: OperationType,
388390
pub name: Option<Spanning<&'a str>>,
389391
pub variable_definitions: Option<Spanning<VariableDefinitions<'a, S>>>,
@@ -394,6 +396,7 @@ pub struct Operation<'a, S> {
394396
#[derive(Clone, Debug, PartialEq)]
395397
pub struct Fragment<'a, S> {
396398
pub name: Spanning<&'a str>,
399+
pub description: Option<Spanning<Cow<'a, str>>>,
397400
pub type_condition: Spanning<&'a str>,
398401
pub directives: Option<Vec<Spanning<Directive<'a, S>>>>,
399402
pub selection_set: Vec<Selection<'a, S>>,
@@ -406,6 +409,16 @@ pub enum Definition<'a, S> {
406409
Fragment(Spanning<Fragment<'a, S>>),
407410
}
408411

412+
impl<'a, S> Definition<'a, S> {
413+
/// Sets or resets the provided `description` for this [`Definition`].
414+
pub(crate) fn set_description(&mut self, description: Option<Spanning<Cow<'a, str>>>) {
415+
match self {
416+
Self::Operation(op) => op.item.description = description,
417+
Self::Fragment(frag) => frag.item.description = description,
418+
}
419+
}
420+
}
421+
409422
#[doc(hidden)]
410423
pub type Document<'a, S> = [Definition<'a, S>];
411424
#[doc(hidden)]

juniper/src/lib.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@ extern crate self as juniper;
1313
mod for_benches_only {
1414
use bencher as _;
1515
}
16+
#[cfg(test)]
17+
mod for_feature_gated_tests_only {
18+
#[cfg(not(feature = "chrono"))]
19+
use chrono as _;
20+
#[cfg(not(feature = "jiff"))]
21+
use jiff as _;
22+
#[cfg(not(feature = "anyhow"))]
23+
use serial_test as _;
24+
}
1625

1726
// These are required by the code generated via the `juniper_codegen` macros.
1827
#[doc(hidden)]
@@ -87,7 +96,7 @@ pub use crate::{
8796
},
8897
introspection::IntrospectionFormat,
8998
macros::helper::subscription::{ExtractTypeFromStream, IntoFieldResult},
90-
parser::{ParseError, ScalarToken, Span, Spanning},
99+
parser::{ParseError, ScalarToken, Span, Spanning, StringLiteral},
91100
schema::{
92101
meta,
93102
model::{RootNode, SchemaType},

juniper/src/parser/document.rs

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
use crate::ast::{
2-
Arguments, Definition, Directive, Field, Fragment, FragmentSpread, InlineFragment, InputValue,
3-
Operation, OperationType, OwnedDocument, Selection, Type, VariableDefinition,
4-
VariableDefinitions,
5-
};
1+
use std::borrow::Cow;
62

73
use crate::{
4+
ast::{
5+
Arguments, Definition, Directive, Field, Fragment, FragmentSpread, InlineFragment,
6+
InputValue, Operation, OperationType, OwnedDocument, Selection, Type, VariableDefinition,
7+
VariableDefinitions,
8+
},
89
parser::{
9-
Lexer, OptionParseResult, ParseError, ParseResult, Parser, Spanning, Token,
10+
Lexer, OptionParseResult, ParseError, ParseResult, Parser, ScalarToken, Spanning, Token,
1011
UnlocatedParseResult, value::parse_value_literal,
1112
},
1213
schema::{
@@ -25,7 +26,7 @@ where
2526
S: ScalarValue,
2627
{
2728
let mut lexer = Lexer::new(s);
28-
let mut parser = Parser::new(&mut lexer).map_err(|s| s.map(ParseError::LexerError))?;
29+
let mut parser = Parser::new(&mut lexer).map_err(|s| s.map(Into::into))?;
2930
parse_document(&mut parser, schema)
3031
}
3132

@@ -54,18 +55,25 @@ fn parse_definition<'a, S>(
5455
where
5556
S: ScalarValue,
5657
{
57-
match parser.peek().item {
58+
let description = parse_description(parser)?;
59+
60+
let mut def = match parser.peek().item {
61+
// Descriptions are not permitted on query shorthand.
62+
// See: https://spec.graphql.org/September2025#sel-GAFTRJABAByBz7P
63+
Token::CurlyOpen if description.is_some() => {
64+
return Err(parser.next_token()?.map(ParseError::unexpected_token));
65+
}
5866
Token::CurlyOpen
5967
| Token::Name("query")
6068
| Token::Name("mutation")
61-
| Token::Name("subscription") => Ok(Definition::Operation(parse_operation_definition(
62-
parser, schema,
63-
)?)),
64-
Token::Name("fragment") => Ok(Definition::Fragment(parse_fragment_definition(
65-
parser, schema,
66-
)?)),
67-
_ => Err(parser.next_token()?.map(ParseError::unexpected_token)),
68-
}
69+
| Token::Name("subscription") => {
70+
Definition::Operation(parse_operation_definition(parser, schema)?)
71+
}
72+
Token::Name("fragment") => Definition::Fragment(parse_fragment_definition(parser, schema)?),
73+
_ => return Err(parser.next_token()?.map(ParseError::unexpected_token)),
74+
};
75+
def.set_description(description);
76+
Ok(def)
6977
}
7078

7179
fn parse_operation_definition<'a, S>(
@@ -85,6 +93,7 @@ where
8593
Operation {
8694
operation_type: OperationType::Query,
8795
name: None,
96+
description: None,
8897
variable_definitions: None,
8998
directives: None,
9099
selection_set: selection_set.item,
@@ -115,6 +124,7 @@ where
115124
Operation {
116125
operation_type: operation_type.item,
117126
name,
127+
description: None,
118128
variable_definitions,
119129
directives: directives.map(|s| s.item),
120130
selection_set: selection_set.item,
@@ -158,6 +168,7 @@ where
158168
&selection_set.span.end,
159169
Fragment {
160170
name,
171+
description: None,
161172
type_condition: type_cond,
162173
directives: directives.map(|s| s.item),
163174
selection_set: selection_set.item,
@@ -429,6 +440,8 @@ fn parse_variable_definition<'a, S>(
429440
where
430441
S: ScalarValue,
431442
{
443+
let description = parse_description(parser)?;
444+
432445
let start_pos = parser.expect(&Token::Dollar)?.span.start;
433446
let var_name = parser.expect_name()?;
434447
parser.expect(&Token::Colon)?;
@@ -452,6 +465,7 @@ where
452465
(
453466
Spanning::start_end(&start_pos, &var_name.span.end, var_name.item),
454467
VariableDefinition {
468+
description,
455469
var_type,
456470
default_value,
457471
directives: directives.map(|s| s.item),
@@ -460,6 +474,21 @@ where
460474
))
461475
}
462476

477+
fn parse_description<'a>(parser: &mut Parser<'a>) -> OptionParseResult<Cow<'a, str>> {
478+
if !matches!(parser.peek().item, Token::Scalar(ScalarToken::String(_))) {
479+
Ok(None)
480+
} else {
481+
let token = parser.next_token()?;
482+
let Token::Scalar(ScalarToken::String(lit)) = token.item else {
483+
unreachable!("already checked to be `ScalarToken::String`")
484+
};
485+
Ok(Some(Spanning::new(
486+
token.span,
487+
lit.parse().map_err(|e| Spanning::new(token.span, e))?,
488+
)))
489+
}
490+
}
491+
463492
fn parse_directives<'a, S>(
464493
parser: &mut Parser<'a>,
465494
schema: &SchemaType<S>,

0 commit comments

Comments
 (0)