diff --git a/bluejay-parser/benches/parse.rs b/bluejay-parser/benches/parse.rs index 406267c4..add3c3d9 100644 --- a/bluejay-parser/benches/parse.rs +++ b/bluejay-parser/benches/parse.rs @@ -1,5 +1,6 @@ use bluejay_parser::ast::{ definition::{DefaultContext, DefinitionDocument}, + executable::ExecutableDocument, Parse, }; use criterion::{criterion_group, criterion_main, Criterion}; @@ -13,7 +14,12 @@ fn parse(c: &mut Criterion) { let s = std::fs::read_to_string("../data/kitchen_sink.graphql").unwrap(); c.bench_function("parse kitchen sink executable document", |b| { - b.iter(|| DefinitionDocument::::parse(black_box(s.as_str()))) + b.iter(|| ExecutableDocument::parse(black_box(s.as_str()))) + }); + + let s = std::fs::read_to_string("../data/large_executable.graphql").unwrap(); + c.bench_function("parse large executable document", |b| { + b.iter(|| ExecutableDocument::parse(black_box(s.as_str()))) }); } diff --git a/bluejay-parser/src/ast/definition/definition_document.rs b/bluejay-parser/src/ast/definition/definition_document.rs index 3711f90b..ecd6ba20 100644 --- a/bluejay-parser/src/ast/definition/definition_document.rs +++ b/bluejay-parser/src/ast/definition/definition_document.rs @@ -158,19 +158,21 @@ impl<'a, C: Context> Parse<'a> for DefinitionDocument<'a, C> { impl<'a, C: Context> DefinitionDocument<'a, C> { fn new() -> Self { + let mut type_definitions = Vec::with_capacity(64); + type_definitions.extend([ + ObjectTypeDefinition::__schema().into(), + ObjectTypeDefinition::__type().into(), + ObjectTypeDefinition::__field().into(), + ObjectTypeDefinition::__input_value().into(), + ObjectTypeDefinition::__enum_value().into(), + ObjectTypeDefinition::__directive().into(), + EnumTypeDefinition::__type_kind().into(), + EnumTypeDefinition::__directive_location().into(), + ]); Self { schema_definitions: Vec::new(), - directive_definitions: Vec::new(), - type_definitions: vec![ - ObjectTypeDefinition::__schema().into(), - ObjectTypeDefinition::__type().into(), - ObjectTypeDefinition::__field().into(), - ObjectTypeDefinition::__input_value().into(), - ObjectTypeDefinition::__enum_value().into(), - ObjectTypeDefinition::__directive().into(), - EnumTypeDefinition::__type_kind().into(), - EnumTypeDefinition::__directive_location().into(), - ], + directive_definitions: Vec::with_capacity(8), + type_definitions, } } diff --git a/bluejay-parser/src/ast/definition/definition_document/definition_document_error.rs b/bluejay-parser/src/ast/definition/definition_document/definition_document_error.rs index 5b4f1f4d..eb23027e 100644 --- a/bluejay-parser/src/ast/definition/definition_document/definition_document_error.rs +++ b/bluejay-parser/src/ast/definition/definition_document/definition_document_error.rs @@ -84,7 +84,7 @@ impl From> for Error { .map(|definition| { Annotation::new( format!("Directive definition with name `@{name}`"), - definition.name_token().span().clone(), + *definition.name_token().span(), ) }) .collect(), @@ -101,7 +101,7 @@ impl From> for Error { .map(|rotd| { Annotation::new( format!("Root operation type definition for `{operation_type}`"), - rotd.name_token().span().clone(), + *rotd.name_token().span(), ) }) .collect(), @@ -115,7 +115,7 @@ impl From> for Error { .map(|definition| { Annotation::new( "Schema definition", - definition.schema_identifier_span().clone(), + *definition.schema_identifier_span(), ) }) .collect(), @@ -140,7 +140,7 @@ impl From> for Error { .map(|definition| { Annotation::new( format!("Type definition with name `{name}`"), - definition.name_token().unwrap().span().clone(), + *definition.name_token().unwrap().span(), ) }) .collect(), @@ -155,7 +155,7 @@ impl From> for Error { ), Some(Annotation::new( "No definition for referenced type", - root_operation_type_definition.name_token().span().clone(), + *root_operation_type_definition.name_token().span(), )), Vec::new(), ), @@ -164,7 +164,7 @@ impl From> for Error { "Schema definition does not contain a query", Some(Annotation::new( "Does not contain a query", - definition.root_operation_type_definitions_span().clone(), + *definition.root_operation_type_definitions_span(), )), Vec::new(), ) @@ -181,28 +181,28 @@ impl From> for Error { format!("Referenced type `{}` does not exist", name.as_ref()), Some(Annotation::new( "No definition for referenced type", - name.span().clone(), + *name.span(), )), Vec::new(), ), DefinitionDocumentError::ReferencedTypeIsNotAnInputType { name } => Error::new( format!("Referenced type `{}` is not an input type", name.as_ref()), - Some(Annotation::new("Not an input type", name.span().clone())), + Some(Annotation::new("Not an input type", *name.span())), Vec::new(), ), DefinitionDocumentError::ReferencedTypeIsNotAnInterface { name } => Error::new( format!("Referenced type `{}` is not an interface", name.as_ref()), - Some(Annotation::new("Not an interface", name.span().clone())), + Some(Annotation::new("Not an interface", *name.span())), Vec::new(), ), DefinitionDocumentError::ReferencedTypeIsNotAnOutputType { name } => Error::new( format!("Referenced type `{}` is not an output type", name.as_ref()), - Some(Annotation::new("Not an output type", name.span().clone())), + Some(Annotation::new("Not an output type", *name.span())), Vec::new(), ), DefinitionDocumentError::ReferencedUnionMemberTypeIsNotAnObject { name } => Error::new( format!("Referenced type `{}` is not an object", name.as_ref()), - Some(Annotation::new("Not an object type", name.span().clone())), + Some(Annotation::new("Not an object type", *name.span())), Vec::new(), ), DefinitionDocumentError::ImplicitRootOperationTypeNotAnObject { definition } => { @@ -214,14 +214,14 @@ impl From> for Error { Some(Annotation::new( "Not an object type", // ok to unwrap because builtin scalar cannot be an implicit schema definition member - definition.name_token().unwrap().span().clone(), + *definition.name_token().unwrap().span(), )), Vec::new(), ) } DefinitionDocumentError::ExplicitRootOperationTypeNotAnObject { name } => Error::new( format!("Referenced type `{}` is not an object", name.as_ref()), - Some(Annotation::new("Not an object type", name.span().clone())), + Some(Annotation::new("Not an object type", *name.span())), Vec::new(), ), DefinitionDocumentError::ReferencedDirectiveDoesNotExist { directive } => Error::new( @@ -231,7 +231,7 @@ impl From> for Error { ), Some(Annotation::new( "No definition for referenced directive", - directive.span().clone(), + *directive.span(), )), Vec::new(), ), diff --git a/bluejay-parser/src/ast/definition/enum_value_definition.rs b/bluejay-parser/src/ast/definition/enum_value_definition.rs index f29a0463..9d817b0e 100644 --- a/bluejay-parser/src/ast/definition/enum_value_definition.rs +++ b/bluejay-parser/src/ast/definition/enum_value_definition.rs @@ -41,7 +41,7 @@ impl<'a, C: Context> FromTokens<'a> for EnumValueDefinition<'a, C> { let name = tokens.expect_name()?; if matches!(name.as_str(), "null" | "true" | "false") { return Err(ParseError::InvalidEnumValue { - span: name.span().clone(), + span: *name.span(), value: name.as_str().to_string(), }); } diff --git a/bluejay-parser/src/ast/definition/input_type.rs b/bluejay-parser/src/ast/definition/input_type.rs index 56789531..77096e8f 100644 --- a/bluejay-parser/src/ast/definition/input_type.rs +++ b/bluejay-parser/src/ast/definition/input_type.rs @@ -110,7 +110,7 @@ impl<'a, C: Context + 'a> FromTokens<'a> for InputType<'a, C> { let span = if let Some(bang_span) = &bang_span { base_name.span().merge(bang_span) } else { - base_name.span().clone() + *base_name.span() }; let base = BaseInputType { name: base_name, diff --git a/bluejay-parser/src/ast/definition/output_type.rs b/bluejay-parser/src/ast/definition/output_type.rs index 18541024..e8d01a42 100644 --- a/bluejay-parser/src/ast/definition/output_type.rs +++ b/bluejay-parser/src/ast/definition/output_type.rs @@ -122,7 +122,7 @@ impl<'a, C: Context + 'a> FromTokens<'a> for OutputType<'a, C> { let span = if let Some(bang_span) = &bang_span { base_name.span().merge(bang_span) } else { - base_name.span().clone() + *base_name.span() }; let base = BaseOutputType::new(base_name); Ok(Self::Base(base, bang_span.is_some(), span)) diff --git a/bluejay-parser/src/ast/depth_limiter.rs b/bluejay-parser/src/ast/depth_limiter.rs index 1d59bed6..7c781c88 100644 --- a/bluejay-parser/src/ast/depth_limiter.rs +++ b/bluejay-parser/src/ast/depth_limiter.rs @@ -4,6 +4,7 @@ pub const DEFAULT_MAX_DEPTH: usize = 2000; /// A depth limiter is used to limit the depth of the AST. This is useful to prevent stack overflows. /// This intentionally does not implement `Clone` or `Copy` to prevent passing this down the call stack without bumping. +#[derive(Clone, Copy)] pub struct DepthLimiter { max_depth: usize, current_depth: usize, @@ -26,6 +27,7 @@ impl DepthLimiter { } } + #[inline] pub fn bump(&self) -> Result { if self.current_depth >= self.max_depth { Err(ParseError::MaxDepthExceeded) diff --git a/bluejay-parser/src/ast/directives.rs b/bluejay-parser/src/ast/directives.rs index 1c5c34b4..10715c50 100644 --- a/bluejay-parser/src/ast/directives.rs +++ b/bluejay-parser/src/ast/directives.rs @@ -25,7 +25,7 @@ impl<'a, const CONST: bool> FromTokens<'a> for Directives<'a, CONST> { } let span = match directives.as_slice() { [] => None, - [first] => Some(first.span().clone()), + [first] => Some(*first.span()), [first, .., last] => Some(first.span().merge(last.span())), }; Ok(Self { directives, span }) diff --git a/bluejay-parser/src/ast/executable/executable_document.rs b/bluejay-parser/src/ast/executable/executable_document.rs index 892fc650..b11a1e9a 100644 --- a/bluejay-parser/src/ast/executable/executable_document.rs +++ b/bluejay-parser/src/ast/executable/executable_document.rs @@ -176,14 +176,14 @@ mod tests { fn test_depth_limit() { // Depth is bumped to 1 entering the selection set (`{`) // Depth is bumped to 2 entering the field (`foo`) - // Depth is bumped to 3 checking for args on the field (`foo`) + // Depth is only bumped further when args/directives/sub-selections are present let document = r#"query { foo }"#; let errors = ExecutableDocument::parse_with_options( document, ParseOptions { graphql_ruby_compatibility: false, - max_depth: 2, + max_depth: 1, max_tokens: None, }, ) @@ -198,7 +198,7 @@ mod tests { document, ParseOptions { graphql_ruby_compatibility: false, - max_depth: 3, + max_depth: 2, max_tokens: None, }, ) diff --git a/bluejay-parser/src/ast/executable/field.rs b/bluejay-parser/src/ast/executable/field.rs index 73f73e0a..fabb1866 100644 --- a/bluejay-parser/src/ast/executable/field.rs +++ b/bluejay-parser/src/ast/executable/field.rs @@ -1,7 +1,6 @@ use crate::ast::executable::SelectionSet; use crate::ast::{ - DepthLimiter, FromTokens, IsMatch, ParseError, Tokens, TryFromTokens, VariableArguments, - VariableDirectives, + DepthLimiter, FromTokens, IsMatch, ParseError, Tokens, VariableArguments, VariableDirectives, }; use crate::lexical_token::{Name, PunctuatorType}; use crate::{HasSpan, Span}; @@ -24,21 +23,36 @@ impl<'a> FromTokens<'a> for Field<'a> { tokens: &mut impl Tokens<'a>, depth_limiter: DepthLimiter, ) -> Result { - let has_alias = tokens.peek_punctuator_matches(1, PunctuatorType::Colon); - let (alias, name) = if has_alias { - let alias = Some(tokens.expect_name()?); - tokens.expect_punctuator(PunctuatorType::Colon)?; + // Consume the first name, then check if followed by `:` (alias syntax). + // This avoids the peek(1) that requires buffering 2 tokens. + let first_name = tokens.expect_name()?; + let (alias, name) = if tokens.next_if_punctuator(PunctuatorType::Colon).is_some() { let name = tokens.expect_name()?; - (alias, name) + (Some(first_name), name) } else { - (None, tokens.expect_name()?) + (None, first_name) + }; + let arguments = if VariableArguments::is_match(tokens) { + Some(VariableArguments::from_tokens( + tokens, + depth_limiter.bump()?, + )?) + } else { + None + }; + let directives = if VariableDirectives::is_match(tokens) { + Some(VariableDirectives::from_tokens( + tokens, + depth_limiter.bump()?, + )?) + } else { + None + }; + let selection_set = if SelectionSet::is_match(tokens) { + Some(SelectionSet::from_tokens(tokens, depth_limiter.bump()?)?) + } else { + None }; - let arguments = - VariableArguments::try_from_tokens(tokens, depth_limiter.bump()?).transpose()?; - let directives = - VariableDirectives::try_from_tokens(tokens, depth_limiter.bump()?).transpose()?; - let selection_set = - SelectionSet::try_from_tokens(tokens, depth_limiter.bump()?).transpose()?; let start_span = alias.as_ref().unwrap_or(&name).span(); let directives_span = directives.as_ref().and_then(|directives| directives.span()); let end_span = if let Some(selection_set) = &selection_set { diff --git a/bluejay-parser/src/ast/executable/inline_fragment.rs b/bluejay-parser/src/ast/executable/inline_fragment.rs index 787d1331..86b084e3 100644 --- a/bluejay-parser/src/ast/executable/inline_fragment.rs +++ b/bluejay-parser/src/ast/executable/inline_fragment.rs @@ -1,7 +1,5 @@ use crate::ast::executable::{SelectionSet, TypeCondition}; -use crate::ast::{ - DepthLimiter, FromTokens, IsMatch, ParseError, Tokens, TryFromTokens, VariableDirectives, -}; +use crate::ast::{DepthLimiter, FromTokens, IsMatch, ParseError, Tokens, VariableDirectives}; use crate::lexical_token::PunctuatorType; use crate::{HasSpan, Span}; @@ -20,10 +18,19 @@ impl<'a> FromTokens<'a> for InlineFragment<'a> { depth_limiter: DepthLimiter, ) -> Result { let ellipse_span = tokens.expect_punctuator(PunctuatorType::Ellipse)?; - let type_condition = - TypeCondition::try_from_tokens(tokens, depth_limiter.bump()?).transpose()?; - let directives = - VariableDirectives::try_from_tokens(tokens, depth_limiter.bump()?).transpose()?; + let type_condition = if TypeCondition::is_match(tokens) { + Some(TypeCondition::from_tokens(tokens, depth_limiter.bump()?)?) + } else { + None + }; + let directives = if VariableDirectives::is_match(tokens) { + Some(VariableDirectives::from_tokens( + tokens, + depth_limiter.bump()?, + )?) + } else { + None + }; let selection_set = SelectionSet::from_tokens(tokens, depth_limiter.bump()?)?; let span = ellipse_span.merge(selection_set.span()); Ok(Self { diff --git a/bluejay-parser/src/ast/executable/variable_type.rs b/bluejay-parser/src/ast/executable/variable_type.rs index 2f4eb64e..6d068a7e 100644 --- a/bluejay-parser/src/ast/executable/variable_type.rs +++ b/bluejay-parser/src/ast/executable/variable_type.rs @@ -64,7 +64,7 @@ impl<'a> FromTokens<'a> for VariableType<'a> { let is_required = bang_span.is_some(); let span = bang_span .map(|bang_span| name.span().merge(&bang_span)) - .unwrap_or_else(|| name.span().clone()); + .unwrap_or_else(|| *name.span()); Ok(Self::Named { name, is_required, diff --git a/bluejay-parser/src/ast/tokens.rs b/bluejay-parser/src/ast/tokens.rs index 57e96e8b..8b5064dd 100644 --- a/bluejay-parser/src/ast/tokens.rs +++ b/bluejay-parser/src/ast/tokens.rs @@ -3,7 +3,7 @@ use crate::lexer::{LexError, Lexer}; use crate::lexical_token::{ FloatValue, IntValue, LexicalToken, Name, PunctuatorType, StringValue, Variable, }; -use crate::{HasSpan, Span}; +use crate::Span; use std::collections::VecDeque; pub trait Tokens<'a>: Iterator> { @@ -135,52 +135,69 @@ impl<'a, T: Lexer<'a>> LexerTokens<'a, T> { } #[inline] - fn next_if(&mut self, f: F) -> Option - where - F: Fn(&LexicalToken) -> bool, - { - match self.peek_next() { - Some(token) if f(token) => { - let span = token.span().clone(); - self.next(); - Some(span) + pub fn next_if_punctuator(&mut self, punctuator_type: PunctuatorType) -> Option { + self.compute_up_to(0); + match self.buffer.front() { + Some(LexicalToken::Punctuator(p)) if p.r#type() == punctuator_type => { + let token = self.buffer.pop_front().unwrap(); + Some(token.into()) } _ => None, } } - #[inline] - pub fn next_if_punctuator(&mut self, punctuator_type: PunctuatorType) -> Option { - self.next_if(|t| matches!(t, LexicalToken::Punctuator(p) if p.r#type() == punctuator_type)) - } - #[inline] pub fn next_if_int_value(&mut self) -> Option { - matches!(self.peek_next(), Some(LexicalToken::IntValue(_))) - .then(|| self.next().unwrap().into_int_value().unwrap()) + self.compute_up_to(0); + match self.buffer.front() { + Some(LexicalToken::IntValue(_)) => { + self.buffer.pop_front().unwrap().into_int_value().ok() + } + _ => None, + } } #[inline] pub fn next_if_float_value(&mut self) -> Option { - matches!(self.peek_next(), Some(LexicalToken::FloatValue(_))) - .then(|| self.next().unwrap().into_float_value().unwrap()) + self.compute_up_to(0); + match self.buffer.front() { + Some(LexicalToken::FloatValue(_)) => { + self.buffer.pop_front().unwrap().into_float_value().ok() + } + _ => None, + } } #[inline] pub fn next_if_string_value(&mut self) -> Option> { - matches!(self.peek_next(), Some(LexicalToken::StringValue(_))) - .then(|| self.next().unwrap().into_string_value().unwrap()) + self.compute_up_to(0); + match self.buffer.front() { + Some(LexicalToken::StringValue(_)) => { + self.buffer.pop_front().unwrap().into_string_value().ok() + } + _ => None, + } } #[inline] pub fn next_if_name(&mut self) -> Option> { - matches!(self.peek_next(), Some(LexicalToken::Name(_))) - .then(|| self.next().unwrap().into_name().unwrap()) + self.compute_up_to(0); + match self.buffer.front() { + Some(LexicalToken::Name(_)) => self.buffer.pop_front().unwrap().into_name().ok(), + _ => None, + } } #[inline] pub fn next_if_name_matches(&mut self, name: &str) -> Option { - self.next_if(|t| matches!(t, LexicalToken::Name(n) if n.as_str() == name)) + self.compute_up_to(0); + match self.buffer.front() { + Some(LexicalToken::Name(n)) if n.as_str() == name => { + let token = self.buffer.pop_front().unwrap(); + Some(token.into()) + } + _ => None, + } } #[inline] diff --git a/bluejay-parser/src/ast/value.rs b/bluejay-parser/src/ast/value.rs index e2289b74..e62b99de 100644 --- a/bluejay-parser/src/ast/value.rs +++ b/bluejay-parser/src/ast/value.rs @@ -72,7 +72,7 @@ impl<'a, const CONST: bool> FromTokens<'a> for Value<'a, CONST> { Some(LexicalToken::Punctuator(p)) if p.r#type() == PunctuatorType::OpenSquareBracket => { - let open_span = p.span().clone(); + let open_span = *p.span(); let mut list: Vec = Vec::new(); let close_span = loop { if let Some(close_span) = @@ -89,7 +89,7 @@ impl<'a, const CONST: bool> FromTokens<'a> for Value<'a, CONST> { })) } Some(LexicalToken::Punctuator(p)) if p.r#type() == PunctuatorType::OpenBrace => { - let open_span = p.span().clone(); + let open_span = *p.span(); let mut object: Vec<_> = Vec::new(); let close_span = loop { if let Some(close_span) = tokens.next_if_punctuator(PunctuatorType::CloseBrace) diff --git a/bluejay-parser/src/error.rs b/bluejay-parser/src/error.rs index 162d0e22..69548b7f 100644 --- a/bluejay-parser/src/error.rs +++ b/bluejay-parser/src/error.rs @@ -113,7 +113,7 @@ impl Error { error .primary_annotation .as_ref() - .map(|a| a.span().clone().into()) + .map(|a| (*a.span()).into()) .unwrap_or(0..0), ), ) diff --git a/bluejay-parser/src/lexer/logos_lexer/block_string_lexer.rs b/bluejay-parser/src/lexer/logos_lexer/block_string_lexer.rs index f3f4e24c..47afb076 100644 --- a/bluejay-parser/src/lexer/logos_lexer/block_string_lexer.rs +++ b/bluejay-parser/src/lexer/logos_lexer/block_string_lexer.rs @@ -1,116 +1,183 @@ use super::Token as OuterToken; use crate::lexer::LexError; -use logos::{Lexer, Logos}; +use logos::Lexer; use std::borrow::Cow; -use std::cmp::min; -#[derive(Logos, Debug)] -pub(super) enum Token<'a> { - #[regex(r#"[^"\\\n\r \t][^"\\\n\r]*"#)] - #[token("\"")] - #[token("\\")] - BlockStringCharacters(&'a str), +pub(super) struct Token; - #[token("\n")] - #[token("\r\n")] - #[token("\r")] - Newline, - - #[token(" ")] - #[token("\t")] - Whitespace(&'a str), - - #[token("\"\"\"")] - BlockQuote, - - #[token("\\\"\"\"")] - EscapedBlockQuote, -} - -impl<'a> Token<'a> { - /// Returns a result indicating if the string was parsed correctly. - /// Also bumps the outer lexer by the number of characters parsed. - pub(super) fn parse( +impl Token { + /// Parse a block string value from the outer lexer. + /// The opening `"""` has already been consumed. + pub(super) fn parse<'a>( outer_lexer: &mut Lexer<'a, OuterToken<'a>>, ) -> Result, LexError> { - let mut lexer = Self::lexer(outer_lexer.remainder()); - - // starting BlockQuote should already have been parsed - - let mut lines = vec![Vec::new()]; - - while let Some(Ok(token)) = lexer.next() { - match token { - Self::BlockQuote => { - outer_lexer.bump(lexer.span().end); - return Ok(Self::block_string_value(lines)); + let remainder = outer_lexer.remainder(); + let bytes = remainder.as_bytes(); + let len = bytes.len(); + + // Find the closing """ (not preceded by \) + let mut i = 0; + let end_offset; + loop { + if i + 2 >= len { + outer_lexer.bump(len); + return Err(LexError::UnrecognizedToken); + } + if bytes[i] == b'"' && bytes[i + 1] == b'"' && bytes[i + 2] == b'"' { + // Check it's not escaped + if i > 0 && bytes[i - 1] == b'\\' { + i += 3; + continue; } - Self::BlockStringCharacters(_) | Self::EscapedBlockQuote | Self::Whitespace(_) => { - lines.last_mut().unwrap().push(token) + end_offset = i + 3; + break; + } + i += 1; + } + + let raw = &remainder[..i]; + outer_lexer.bump(end_offset); + + // Check if there are any escaped block quotes + let has_escapes = raw.contains("\\\"\"\""); + + // Normalize newlines: split on \r\n, \r, or \n + // Collect line start/end offsets to avoid allocating strings + let raw_bytes = raw.as_bytes(); + let raw_len = raw_bytes.len(); + + // Count lines first for pre-allocation + let mut line_count = 1usize; + { + let mut j = 0; + while j < raw_len { + if raw_bytes[j] == b'\r' { + line_count += 1; + if j + 1 < raw_len && raw_bytes[j + 1] == b'\n' { + j += 2; + } else { + j += 1; + } + } else if raw_bytes[j] == b'\n' { + line_count += 1; + j += 1; + } else { + j += 1; } - Self::Newline => lines.push(Vec::new()), } } - outer_lexer.bump(lexer.span().end); - Err(LexError::UnrecognizedToken) - } + // Collect line ranges + let mut lines: Vec<(usize, usize)> = Vec::with_capacity(line_count); + { + let mut start = 0; + let mut j = 0; + while j < raw_len { + if raw_bytes[j] == b'\r' { + lines.push((start, j)); + if j + 1 < raw_len && raw_bytes[j + 1] == b'\n' { + j += 2; + } else { + j += 1; + } + start = j; + } else if raw_bytes[j] == b'\n' { + lines.push((start, j)); + j += 1; + start = j; + } else { + j += 1; + } + } + lines.push((start, raw_len)); + } - fn block_string_value(lines: Vec>>) -> Cow<'a, str> { + // Compute common indent (skip first line) let common_indent = lines[1..] .iter() - .filter_map(|line| { - line.iter() - .position(|token| !matches!(token, Self::Whitespace(_))) + .filter_map(|&(start, end)| { + let line = &raw[start..end]; + let indent = line.len() - line.trim_start_matches([' ', '\t']).len(); + // Only count lines that have non-whitespace content + if indent < line.len() { + Some(indent) + } else { + None // all-whitespace line + } }) .min() .unwrap_or(0); - let front_offset = lines.iter().enumerate().position(|(idx, line)| { + // Find first non-blank line + let front_offset = lines.iter().enumerate().position(|(idx, &(start, end))| { let indent = if idx == 0 { 0 } else { common_indent }; - line[min(line.len(), indent)..] - .iter() - .any(|token| !matches!(token, Token::Whitespace(_))) + let line = &raw[start..end]; + let after_indent = if indent < line.len() { + &line[indent..] + } else { + "" + }; + after_indent.chars().any(|c| c != ' ' && c != '\t') }); - let end_offset = lines.iter().rev().position(|line| { - line[min(line.len(), common_indent)..] - .iter() - .any(|token| !matches!(token, Token::Whitespace(_))) + // Find last non-blank line + let end_offset_lines = lines.iter().rev().position(|&(start, end)| { + let line = &raw[start..end]; + let after_indent = if common_indent < line.len() { + &line[common_indent..] + } else { + "" + }; + after_indent.chars().any(|c| c != ' ' && c != '\t') }); - let mut formatted = Cow::Borrowed(""); + if let Some((front, end_off)) = front_offset.zip(end_offset_lines) { + let first = front; + let last = lines.len() - end_off; // exclusive - if let Some((front_offset, end_offset)) = front_offset.zip(end_offset) { - let start = front_offset; - let end = lines.len() - end_offset; + if !has_escapes && first + 1 == last && first == 0 { + // Single line, no escapes — can return borrowed + let (start, end) = lines[0]; + let line = &raw[start..end]; + return Ok(Cow::Borrowed(line)); + } - lines[start..end] - .iter() - .enumerate() - .for_each(|(offset_idx, line)| { - let actual_idx = start + offset_idx; - let indent = if actual_idx == 0 { 0 } else { common_indent }; - if offset_idx != 0 { - formatted += "\n"; - } - line[min(line.len(), indent)..] - .iter() - .for_each(|token| match token { - Self::BlockStringCharacters(s) => { - formatted += *s; - } - Self::Whitespace(s) => { - formatted += *s; - } - Self::EscapedBlockQuote => { - formatted += "\"\"\""; - } - _ => {} - }); - }); - } + // Check if we can return a borrowed slice (single content line from source, no escapes, not first line) + if !has_escapes && first + 1 == last && first > 0 { + let (start, end) = lines[first]; + let line = &raw[start..end]; + let after_indent = if common_indent < line.len() { + &line[common_indent..] + } else { + "" + }; + return Ok(Cow::Borrowed(after_indent)); + } - formatted + // Build the result string + let mut result = String::new(); + for (offset_idx, line_idx) in (first..last).enumerate() { + let (start, end) = lines[line_idx]; + let indent = if line_idx == 0 { 0 } else { common_indent }; + if offset_idx != 0 { + result.push('\n'); + } + let line = &raw[start..end]; + let after_indent = if indent < line.len() { + &line[indent..] + } else { + "" + }; + if has_escapes { + result.push_str(&after_indent.replace("\\\"\"\"", "\"\"\"")); + } else { + result.push_str(after_indent); + } + } + + Ok(Cow::Owned(result)) + } else { + Ok(Cow::Borrowed("")) + } } } diff --git a/bluejay-parser/src/span.rs b/bluejay-parser/src/span.rs index 1da4800a..6ab600c9 100644 --- a/bluejay-parser/src/span.rs +++ b/bluejay-parser/src/span.rs @@ -2,37 +2,47 @@ use std::cmp::{max, min}; use std::cmp::{Ord, Ordering, PartialOrd}; use std::ops::{Add, Range}; -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -#[repr(transparent)] -pub struct Span(logos::Span); +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Span { + start: u32, + len: u32, +} impl Span { #[inline] pub(crate) fn new(s: logos::Span) -> Self { - Self(s) + Self { + start: s.start as u32, + len: (s.end - s.start) as u32, + } } #[inline] - pub fn byte_range(&self) -> &Range { - &self.0 + pub fn byte_range(&self) -> Range { + self.start as usize..(self.start + self.len) as usize } #[inline] pub fn merge(&self, other: &Self) -> Self { - Self(min(self.0.start, other.0.start)..max(self.0.end, other.0.end)) + let start = min(self.start, other.start); + let end = max(self.start + self.len, other.start + other.len); + Self { + start, + len: end - start, + } } } impl From for Range { fn from(val: Span) -> Self { - val.0 + val.byte_range() } } impl From for Span { #[inline] fn from(value: logos::Span) -> Self { - Self(value) + Self::new(value) } } @@ -40,13 +50,16 @@ impl Add for Span { type Output = Self; fn add(self, rhs: usize) -> Self::Output { - Self((self.0.start + rhs)..(self.0.end + rhs)) + Self { + start: self.start + rhs as u32, + len: self.len, + } } } impl Ord for Span { fn cmp(&self, other: &Self) -> Ordering { - self.0.start.cmp(&other.0.start) + self.start.cmp(&other.start) } } diff --git a/bluejay-typegen-codegen/src/validation/error.rs b/bluejay-typegen-codegen/src/validation/error.rs index 8a8a6cdb..9b6e4cae 100644 --- a/bluejay-typegen-codegen/src/validation/error.rs +++ b/bluejay-typegen-codegen/src/validation/error.rs @@ -57,18 +57,18 @@ impl<'a, S: SchemaDefinition> From, S>> f ), Some(Annotation::new( "Selection set contains a fragment spread and other selections", - selection_set.span().clone(), + *selection_set.span(), )), vec![Annotation::new( "Fragment spread", - fragment_spread.span().clone(), + *fragment_spread.span(), )], ), Error::InlineFragmentOnObject { inline_fragment } => Self::new( format!("{MACRO_NAME} does not allow inline fragments on objects"), Some(Annotation::new( "Inline fragment on object type", - inline_fragment.span().clone(), + *inline_fragment.span(), )), Vec::new(), ), @@ -76,7 +76,7 @@ impl<'a, S: SchemaDefinition> From, S>> f format!("{MACRO_NAME} does not allow inline fragments on interfaces"), Some(Annotation::new( "Inline fragment on interface type", - inline_fragment.span().clone(), + *inline_fragment.span(), )), Vec::new(), ), @@ -84,7 +84,7 @@ impl<'a, S: SchemaDefinition> From, S>> f format!("{MACRO_NAME} requires unaliased selection of `__typename` on union types to properly deserialize, and for that to be the first in the selection set"), Some(Annotation::new( "Selection set does not contain an unaliased `__typename` selection as the first selection", - selection_set.span().clone(), + *selection_set.span(), )), Vec::new(), ), @@ -96,7 +96,7 @@ impl<'a, S: SchemaDefinition> From, S>> f inline_fragment.type_condition().map_or(union_type_definition.name(), |tc| tc.named_type().as_ref()), union_type_definition.name(), ), - inline_fragment.span().clone(), + *inline_fragment.span(), )), Vec::new(), ), @@ -104,18 +104,18 @@ impl<'a, S: SchemaDefinition> From, S>> f format!("{MACRO_NAME} requires the inline fragments in a selection set have unique type conditions"), Some(Annotation::new( format!("Selection set contains multiple inline fragments targeting {type_condition}"), - selection_set.span().clone(), + *selection_set.span(), )), inline_fragments.into_iter().map(|inline_fragment| Annotation::new( format!("Inline fragment targeting {type_condition}"), - inline_fragment.span().clone(), + *inline_fragment.span(), )).collect(), ), Error::FieldSelectionOnUnion { field } => Self::new( format!("{MACRO_NAME} does not allow field selections directly on union types, with the exception of unaliased `__typename` as the first selection in the set"), Some(Annotation::new( "Field selection on union type", - field.name().span().clone(), + *field.name().span(), )), Vec::new(), ), @@ -123,7 +123,7 @@ impl<'a, S: SchemaDefinition> From, S>> f format!("{MACRO_NAME} requires fragment spreads on interfaces to target either the interface or one of the interfaces it implements"), Some(Annotation::new( "Fragment spread on interface type", - fragment_spread.span().clone(), + *fragment_spread.span(), )), Vec::new(), ), @@ -131,12 +131,12 @@ impl<'a, S: SchemaDefinition> From, S>> f format!("{MACRO_NAME} requires fragment and operation names to be unique, but encountered a clash with name `{}`", fragment_definition.name().as_ref()), Some(Annotation::new( "Fragment definition name collides with operation definition name", - fragment_definition.span().clone(), + *fragment_definition.span(), )), vec![ Annotation::new( "Operation definition", - operation_definition.span().clone(), + *operation_definition.span(), ), ], ), diff --git a/bluejay-validator/src/definition/error.rs b/bluejay-validator/src/definition/error.rs index 576d0843..6ad2d6fb 100644 --- a/bluejay-validator/src/definition/error.rs +++ b/bluejay-validator/src/definition/error.rs @@ -37,7 +37,7 @@ impl<'a> From>> for ParserError { .map(|ivd| { Annotation::new( format!("Input value definition with name `{name}`"), - ivd.name_token().span().clone(), + *ivd.name_token().span(), ) }) .collect(), @@ -53,7 +53,7 @@ impl<'a> From>> for ParserError { .map(|evd| { Annotation::new( format!("Enum value definition with name `{name}`"), - evd.name_token().span().clone(), + *evd.name_token().span(), ) }) .collect(), @@ -69,14 +69,14 @@ impl<'a> From>> for ParserError { ), Some(Annotation::new( "Input object type definition contains circular reference(s) through an unbroken chain of non-null singular fields, which is disallowed", - input_object_type_definition.name_token().span().clone(), + *input_object_type_definition.name_token().span(), )), circular_references .into_iter() .map(|circular_reference| { Annotation::new( "Occurence of circular reference", - circular_reference.span().clone(), + *circular_reference.span(), ) }) .collect(), diff --git a/bluejay-validator/src/executable/document/error.rs b/bluejay-validator/src/executable/document/error.rs index 506b24b5..086f37af 100644 --- a/bluejay-validator/src/executable/document/error.rs +++ b/bluejay-validator/src/executable/document/error.rs @@ -140,7 +140,7 @@ impl<'a, S: SchemaDefinition> From, S>> f operation.name().map(|operation_name| { Annotation::new( format!("Operation definition with name `{name}`"), - operation_name.span().clone(), + *operation_name.span(), ) }) }) @@ -156,7 +156,7 @@ impl<'a, S: SchemaDefinition> From, S>> f .map(|operation| { Annotation::new( "Anonymous operation definition", - operation.as_ref().selection_set().span().clone(), + *operation.as_ref().selection_set().span(), ) }) .collect(), @@ -165,7 +165,7 @@ impl<'a, S: SchemaDefinition> From, S>> f "Subscription root is not a single field", Some(Annotation::new( "Selection set contains multiple fields", - operation.as_ref().selection_set().span().clone(), + *operation.as_ref().selection_set().span(), )), Vec::new(), ), @@ -177,7 +177,7 @@ impl<'a, S: SchemaDefinition> From, S>> f ), Some(Annotation::new( format!("Field does not exist on type `{}`", r#type.name()), - field.name().span().clone(), + *field.name().span(), )), Vec::new(), ), @@ -191,7 +191,7 @@ impl<'a, S: SchemaDefinition> From, S>> f "Schema does not define a {} root", OperationType::from(operation.operation_type()), ), - operation.operation_type().span().clone(), + *operation.operation_type().span(), )), Vec::new(), ), @@ -205,7 +205,7 @@ impl<'a, S: SchemaDefinition> From, S>> f ), Some(Annotation::new( "Selection set on field of leaf type must be empty", - selection_set.span().clone(), + *selection_set.span(), )), Vec::new(), ), @@ -216,7 +216,7 @@ impl<'a, S: SchemaDefinition> From, S>> f ), Some(Annotation::new( "Fields of non-leaf types must have a selection", - field.name().span().clone(), + *field.name().span(), )), Vec::new(), ), @@ -231,7 +231,7 @@ impl<'a, S: SchemaDefinition> From, S>> f .map(|fragment_definition| { Annotation::new( format!("Fragment definition with name `{name}`"), - fragment_definition.name().span().clone(), + *fragment_definition.name().span(), ) }) .collect(), @@ -245,11 +245,10 @@ impl<'a, S: SchemaDefinition> From, S>> f ), Some(Annotation::new( "No type with this name", - fragment_definition + *fragment_definition .type_condition() .named_type() - .span() - .clone(), + .span(), )), Vec::new(), ), @@ -262,7 +261,7 @@ impl<'a, S: SchemaDefinition> From, S>> f .unwrap_or_default() ), inline_fragment.type_condition().map(|tc| { - Annotation::new("No type with this name", tc.named_type().span().clone()) + Annotation::new("No type with this name", *tc.named_type().span()) }), Vec::new(), ), @@ -275,11 +274,10 @@ impl<'a, S: SchemaDefinition> From, S>> f ), Some(Annotation::new( "Fragment definition target types must be composite types", - fragment_definition + *fragment_definition .type_condition() .named_type() - .span() - .clone(), + .span(), )), Vec::new(), ), @@ -294,7 +292,7 @@ impl<'a, S: SchemaDefinition> From, S>> f inline_fragment.type_condition().map(|tc| { Annotation::new( "Inline fragment target types must be composite types", - tc.named_type().span().clone(), + *tc.named_type().span(), ) }), Vec::new(), @@ -308,7 +306,7 @@ impl<'a, S: SchemaDefinition> From, S>> f ), Some(Annotation::new( "Fragment definition is unused", - fragment_definition.name().span().clone(), + *fragment_definition.name().span(), )), Vec::new(), ), @@ -319,7 +317,7 @@ impl<'a, S: SchemaDefinition> From, S>> f ), Some(Annotation::new( "No fragment defined with this name", - fragment_spread.name().span().clone(), + *fragment_spread.name().span(), )), Vec::new(), ), @@ -333,11 +331,11 @@ impl<'a, S: SchemaDefinition> From, S>> f ), Some(Annotation::new( "Cycle introduced by fragment spread", - fragment_spread.name().span().clone(), + *fragment_spread.name().span(), )), vec![Annotation::new( "Affected fragment definition", - fragment_definition.name().span().clone(), + *fragment_definition.name().span(), )], ), Error::FieldSelectionsDoNotMergeDifferingArguments { @@ -348,11 +346,11 @@ impl<'a, S: SchemaDefinition> From, S>> f "Fields in selection set do not merge due to unequal arguments", Some(Annotation::new( "Fields in selection set do not merge", - selection_set.span().clone(), + *selection_set.span(), )), vec![ - Annotation::new("First field", field_a.name().span().clone()), - Annotation::new("Second field", field_b.name().span().clone()), + Annotation::new("First field", *field_a.name().span()), + Annotation::new("Second field", *field_b.name().span()), ], ), Error::FieldSelectionsDoNotMergeDifferingNames { @@ -363,11 +361,11 @@ impl<'a, S: SchemaDefinition> From, S>> f "Fields in selection set do not merge due to unequal field names", Some(Annotation::new( "Fields in selection set do not merge", - selection_set.span().clone(), + *selection_set.span(), )), vec![ - Annotation::new("First field", field_a.name().span().clone()), - Annotation::new("Second field", field_b.name().span().clone()), + Annotation::new("First field", *field_a.name().span()), + Annotation::new("Second field", *field_b.name().span()), ], ), Error::FieldSelectionsDoNotMergeIncompatibleTypes { @@ -380,7 +378,7 @@ impl<'a, S: SchemaDefinition> From, S>> f "Fields in selection set do not merge due to incompatible types", Some(Annotation::new( "Fields in selection set do not merge", - selection_set.span().clone(), + *selection_set.span(), )), vec![ Annotation::new( @@ -388,14 +386,14 @@ impl<'a, S: SchemaDefinition> From, S>> f "First field has type {}", field_definition_a.r#type().display_name(), ), - field_a.name().span().clone(), + *field_a.name().span(), ), Annotation::new( format!( "Second field has type {}", field_definition_b.r#type().display_name(), ), - field_b.name().span().clone(), + *field_b.name().span(), ), ], ), @@ -410,7 +408,7 @@ impl<'a, S: SchemaDefinition> From, S>> f ), Some(Annotation::new( format!("Cannot be spread for type {}", parent_type.name()), - fragment_spread.name().span().clone(), + *fragment_spread.name().span(), )), Vec::new(), ), @@ -428,7 +426,7 @@ impl<'a, S: SchemaDefinition> From, S>> f ), Some(Annotation::new( format!("Cannot be spread for type {}", parent_type.name()), - inline_fragment.span().clone(), + *inline_fragment.span(), )), Vec::new(), ), @@ -449,7 +447,7 @@ impl<'a, S: SchemaDefinition> From, S>> f .map(|variable_definition| { Annotation::new( format!("Variable definition with name ${name}"), - variable_definition.variable().span().clone(), + *variable_definition.variable().span(), ) }) .collect(), @@ -464,7 +462,7 @@ impl<'a, S: SchemaDefinition> From, S>> f ), Some(Annotation::new( "Not an input type", - variable_definition.r#type().span().clone(), + *variable_definition.r#type().span(), )), Vec::new(), ), @@ -485,7 +483,7 @@ impl<'a, S: SchemaDefinition> From, S>> f format!( "No variable definition with this name defined in {operation_name}", ), - variable.span().clone(), + *variable.span(), )), Vec::new(), ) @@ -499,7 +497,7 @@ impl<'a, S: SchemaDefinition> From, S>> f ), Some(Annotation::new( "Variable definition not used", - variable_definition.variable().span().clone(), + *variable_definition.variable().span(), )), Vec::new(), ), @@ -520,7 +518,7 @@ impl<'a, S: SchemaDefinition> From, S>> f variable_type.as_ref().display_name(), location_type.display_name(), ), - variable.span().clone(), + *variable.span(), )), Vec::new(), ), @@ -542,7 +540,7 @@ impl<'a, S: SchemaDefinition> From, S>> f variable_type.as_ref().display_name(), parent_type_name, ), - variable.span().clone(), + *variable.span(), )), Vec::new(), ), diff --git a/bluejay-validator/src/executable/document/error/argument_error.rs b/bluejay-validator/src/executable/document/error/argument_error.rs index 6d6a78b1..81154d0e 100644 --- a/bluejay-validator/src/executable/document/error/argument_error.rs +++ b/bluejay-validator/src/executable/document/error/argument_error.rs @@ -49,7 +49,7 @@ impl<'a, const CONST: bool, S: SchemaDefinition> .map(|argument| { Annotation::new( format!("Argument with name `{name}`"), - argument.name().span().clone(), + *argument.name().span(), ) }) .collect(), @@ -65,7 +65,7 @@ impl<'a, const CONST: bool, S: SchemaDefinition> ), Some(Annotation::new( "No argument definition with this name", - argument.name().span().clone(), + *argument.name().span(), )), Vec::new(), ), @@ -80,7 +80,7 @@ impl<'a, const CONST: bool, S: SchemaDefinition> ), Some(Annotation::new( "No argument definition with this name", - argument.name().span().clone(), + *argument.name().span(), )), Vec::new(), ), @@ -100,7 +100,7 @@ impl<'a, const CONST: bool, S: SchemaDefinition> ), Some(Annotation::new( format!("Missing argument(s): {missing_argument_names}"), - directive.span().clone(), + *directive.span(), )), Vec::new(), ) @@ -116,7 +116,7 @@ impl<'a, const CONST: bool, S: SchemaDefinition> .join(", "); let span = match field.arguments() { Some(arguments) => field.name().span().merge(arguments.span()), - None => field.name().span().clone(), + None => *field.name().span(), }; Self::new( format!( diff --git a/bluejay-validator/src/executable/document/error/directive_error.rs b/bluejay-validator/src/executable/document/error/directive_error.rs index 1d2e88b5..1c9499e3 100644 --- a/bluejay-validator/src/executable/document/error/directive_error.rs +++ b/bluejay-validator/src/executable/document/error/directive_error.rs @@ -37,7 +37,7 @@ impl<'a, const CONST: bool, S: SchemaDefinition> ), Some(Annotation::new( "No directive definition with this name", - directive.name().span().clone(), + *directive.name().span(), )), Vec::new(), ), @@ -53,7 +53,7 @@ impl<'a, const CONST: bool, S: SchemaDefinition> ), Some(Annotation::new( format!("Cannot be used at location {location}"), - directive.span().clone(), + *directive.span(), )), Vec::new(), ), @@ -65,7 +65,7 @@ impl<'a, const CONST: bool, S: SchemaDefinition> None, directives.into_iter().map(|directive| Annotation::new( "Usage of directive", - directive.span().clone(), + *directive.span(), )).collect(), ), } diff --git a/bluejay-validator/src/value/input_coercion/error.rs b/bluejay-validator/src/value/input_coercion/error.rs index 01636aa3..cc61811f 100644 --- a/bluejay-validator/src/value/input_coercion/error.rs +++ b/bluejay-validator/src/value/input_coercion/error.rs @@ -123,10 +123,7 @@ impl<'a, const CONST: bool> From>> for P match &error { Error::NullValueForRequiredType { value, .. } => Self::new( error.message(), - Some(Annotation::new( - "Expected non-null value", - value.span().clone(), - )), + Some(Annotation::new("Expected non-null value", *value.span())), Vec::new(), ), Error::NoImplicitConversion { @@ -137,7 +134,7 @@ impl<'a, const CONST: bool> From>> for P error.message(), Some(Annotation::new( format!("No implicit conversion to {input_type_name}"), - value.span().clone(), + *value.span(), )), Vec::new(), ), @@ -149,7 +146,7 @@ impl<'a, const CONST: bool> From>> for P error.message(), Some(Annotation::new( format!("No such member on enum {enum_type_name}"), - value.span().clone(), + *value.span(), )), Vec::new(), ), @@ -161,7 +158,7 @@ impl<'a, const CONST: bool> From>> for P error.message(), Some(Annotation::new( format!("No value for required fields: {joined_field_names}"), - value.span().clone(), + *value.span(), )), Vec::new(), ) @@ -171,7 +168,7 @@ impl<'a, const CONST: bool> From>> for P None, Vec::from_iter( keys.iter() - .map(|key| Annotation::new("Entry for field", key.span().clone())), + .map(|key| Annotation::new("Entry for field", *key.span())), ), ), Error::NoInputFieldWithName { @@ -182,13 +179,13 @@ impl<'a, const CONST: bool> From>> for P error.message(), Some(Annotation::new( format!("No field with this name on input type {input_object_type_name}"), - field.span().clone(), + *field.span(), )), Vec::new(), ), Error::CustomScalarInvalidValue { value, message, .. } => Self::new( message.clone(), - Some(Annotation::new(message.clone(), value.span().clone())), + Some(Annotation::new(message.clone(), *value.span())), Vec::new(), ), #[cfg(feature = "one-of-input-objects")] @@ -200,7 +197,7 @@ impl<'a, const CONST: bool> From>> for P error.message(), Some(Annotation::new( "oneOf input object must not contain any null values", - value.span().clone(), + *value.span(), )), null_entries .iter() @@ -218,7 +215,7 @@ impl<'a, const CONST: bool> From>> for P error.message(), Some(Annotation::new( "oneOf input object must contain single non-null", - value.span().clone(), + *value.span(), )), non_null_entries .iter() diff --git a/data/large_executable.graphql b/data/large_executable.graphql new file mode 100644 index 00000000..f5de298a --- /dev/null +++ b/data/large_executable.graphql @@ -0,0 +1,912 @@ +query GetProducts($first: Int!, $after: String, $query: String, $sortKey: ProductSortKeys, $reverse: Boolean) { + products(first: $first, after: $after, query: $query, sortKey: $sortKey, reverse: $reverse) { + edges { + cursor + node { + id + title + handle + description + descriptionHtml + vendor + productType + tags + status + createdAt + updatedAt + publishedAt + onlineStoreUrl + options { + id + name + values + } + variants(first: 10) { + edges { + node { + id + title + sku + barcode + price + compareAtPrice + availableForSale + inventoryQuantity + weight + weightUnit + selectedOptions { + name + value + } + image { + id + url + altText + width + height + } + } + } + } + images(first: 5) { + edges { + node { + id + url + altText + width + height + } + } + } + priceRange { + minVariantPrice { + amount + currencyCode + } + maxVariantPrice { + amount + currencyCode + } + } + compareAtPriceRange { + minVariantPrice { + amount + currencyCode + } + maxVariantPrice { + amount + currencyCode + } + } + seo { + title + description + } + media(first: 10) { + edges { + node { + ... on MediaImage { + id + image { + url + altText + width + height + } + } + ... on Video { + id + sources { + url + mimeType + format + height + width + } + } + } + } + } + } + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } +} + +query GetProduct($id: ID!, $selectedOptions: [SelectedOptionInput!]!) { + product(id: $id) { + id + title + handle + description + descriptionHtml + vendor + productType + tags + status + createdAt + updatedAt + publishedAt + onlineStoreUrl + totalInventory + tracksInventory + options { + id + name + values + } + selectedVariant: variantBySelectedOptions(selectedOptions: $selectedOptions) { + id + title + sku + barcode + price + compareAtPrice + availableForSale + inventoryQuantity + weight + weightUnit + selectedOptions { + name + value + } + image { + id + url + altText + width + height + } + } + variants(first: 250) { + edges { + node { + id + title + sku + barcode + price + compareAtPrice + availableForSale + inventoryQuantity + weight + weightUnit + selectedOptions { + name + value + } + image { + id + url + altText + width + height + } + } + } + } + images(first: 20) { + edges { + node { + id + url + altText + width + height + } + } + } + collections(first: 5) { + edges { + node { + id + title + handle + } + } + } + metafields(first: 20) { + edges { + node { + id + namespace + key + value + type + description + } + } + } + priceRange { + minVariantPrice { + amount + currencyCode + } + maxVariantPrice { + amount + currencyCode + } + } + compareAtPriceRange { + minVariantPrice { + amount + currencyCode + } + maxVariantPrice { + amount + currencyCode + } + } + seo { + title + description + } + } +} + +query GetCollection($id: ID!, $first: Int!, $after: String, $filters: [ProductFilter!], $sortKey: ProductCollectionSortKeys, $reverse: Boolean) { + collection(id: $id) { + id + title + handle + description + descriptionHtml + updatedAt + image { + id + url + altText + width + height + } + seo { + title + description + } + products(first: $first, after: $after, filters: $filters, sortKey: $sortKey, reverse: $reverse) { + edges { + cursor + node { + id + title + handle + vendor + productType + tags + availableForSale + priceRange { + minVariantPrice { + amount + currencyCode + } + maxVariantPrice { + amount + currencyCode + } + } + compareAtPriceRange { + minVariantPrice { + amount + currencyCode + } + maxVariantPrice { + amount + currencyCode + } + } + images(first: 1) { + edges { + node { + id + url + altText + width + height + } + } + } + variants(first: 3) { + edges { + node { + id + title + price + compareAtPrice + availableForSale + selectedOptions { + name + value + } + } + } + } + } + } + filters { + id + label + type + values { + id + label + count + input + } + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } + } +} + +query GetCart($id: ID!) { + cart(id: $id) { + id + createdAt + updatedAt + checkoutUrl + totalQuantity + note + buyerIdentity { + email + phone + countryCode + customer { + id + email + firstName + lastName + } + } + lines(first: 100) { + edges { + node { + id + quantity + merchandise { + ... on ProductVariant { + id + title + sku + price + compareAtPrice + availableForSale + selectedOptions { + name + value + } + image { + id + url + altText + width + height + } + product { + id + title + handle + vendor + productType + } + } + } + attributes { + key + value + } + cost { + totalAmount { + amount + currencyCode + } + subtotalAmount { + amount + currencyCode + } + amountPerQuantity { + amount + currencyCode + } + compareAtAmountPerQuantity { + amount + currencyCode + } + } + discountAllocations { + discountedAmount { + amount + currencyCode + } + } + } + } + } + attributes { + key + value + } + cost { + totalAmount { + amount + currencyCode + } + subtotalAmount { + amount + currencyCode + } + totalTaxAmount { + amount + currencyCode + } + totalDutyAmount { + amount + currencyCode + } + } + discountCodes { + code + applicable + } + discountAllocations { + discountedAmount { + amount + currencyCode + } + } + } +} + +mutation CartCreate($input: CartInput!) { + cartCreate(input: $input) { + cart { + id + createdAt + updatedAt + checkoutUrl + totalQuantity + lines(first: 100) { + edges { + node { + id + quantity + merchandise { + ... on ProductVariant { + id + title + price + product { + id + title + handle + } + } + } + } + } + } + cost { + totalAmount { + amount + currencyCode + } + subtotalAmount { + amount + currencyCode + } + } + } + userErrors { + field + message + code + } + } +} + +mutation CartLinesAdd($cartId: ID!, $lines: [CartLineInput!]!) { + cartLinesAdd(cartId: $cartId, lines: $lines) { + cart { + id + totalQuantity + lines(first: 100) { + edges { + node { + id + quantity + merchandise { + ... on ProductVariant { + id + title + price + product { + id + title + handle + } + } + } + } + } + } + cost { + totalAmount { + amount + currencyCode + } + subtotalAmount { + amount + currencyCode + } + } + } + userErrors { + field + message + code + } + } +} + +mutation CartLinesUpdate($cartId: ID!, $lines: [CartLineUpdateInput!]!) { + cartLinesUpdate(cartId: $cartId, lines: $lines) { + cart { + id + totalQuantity + lines(first: 100) { + edges { + node { + id + quantity + merchandise { + ... on ProductVariant { + id + title + price + product { + id + title + handle + } + } + } + } + } + } + cost { + totalAmount { + amount + currencyCode + } + subtotalAmount { + amount + currencyCode + } + } + } + userErrors { + field + message + code + } + } +} + +query GetCustomer($customerAccessToken: String!) { + customer(customerAccessToken: $customerAccessToken) { + id + firstName + lastName + email + phone + acceptsMarketing + createdAt + updatedAt + tags + defaultAddress { + id + firstName + lastName + company + address1 + address2 + city + province + provinceCode + country + countryCodeV2 + zip + phone + formatted + } + addresses(first: 10) { + edges { + node { + id + firstName + lastName + company + address1 + address2 + city + province + provinceCode + country + countryCodeV2 + zip + phone + formatted + } + } + } + orders(first: 20, sortKey: PROCESSED_AT, reverse: true) { + edges { + node { + id + name + orderNumber + processedAt + financialStatus + fulfillmentStatus + currentTotalPrice { + amount + currencyCode + } + currentSubtotalPrice { + amount + currencyCode + } + currentTotalTax { + amount + currencyCode + } + totalShippingPrice { + amount + currencyCode + } + lineItems(first: 50) { + edges { + node { + title + quantity + originalTotalPrice { + amount + currencyCode + } + discountedTotalPrice { + amount + currencyCode + } + variant { + id + title + sku + price + image { + url + altText + width + height + } + selectedOptions { + name + value + } + product { + id + handle + } + } + } + } + } + shippingAddress { + firstName + lastName + address1 + address2 + city + province + country + zip + } + discountApplications(first: 5) { + edges { + node { + ... on DiscountCodeApplication { + code + applicable + value { + ... on MoneyV2 { + amount + currencyCode + } + ... on PricingPercentageValue { + percentage + } + } + } + } + } + } + } + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } + } +} + +query SearchProducts($query: String!, $first: Int!, $after: String, $productFilters: [ProductFilter!], $sortKey: SearchSortKeys, $reverse: Boolean) { + search(query: $query, first: $first, after: $after, productFilters: $productFilters, sortKey: $sortKey, reverse: $reverse, types: [PRODUCT]) { + edges { + cursor + node { + ... on Product { + id + title + handle + vendor + productType + tags + availableForSale + priceRange { + minVariantPrice { + amount + currencyCode + } + maxVariantPrice { + amount + currencyCode + } + } + images(first: 1) { + edges { + node { + id + url + altText + width + height + } + } + } + variants(first: 3) { + edges { + node { + id + title + price + compareAtPrice + availableForSale + selectedOptions { + name + value + } + } + } + } + } + } + } + totalCount + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } +} + +fragment MoneyFields on MoneyV2 { + amount + currencyCode +} + +fragment ImageFields on Image { + id + url + altText + width + height +} + +fragment AddressFields on MailingAddress { + id + firstName + lastName + company + address1 + address2 + city + province + provinceCode + country + countryCodeV2 + zip + phone + formatted +} + +fragment VariantFields on ProductVariant { + id + title + sku + barcode + price + compareAtPrice + availableForSale + inventoryQuantity + weight + weightUnit + selectedOptions { + name + value + } + image { + ...ImageFields + } +} + +fragment ProductCardFields on Product { + id + title + handle + vendor + productType + tags + availableForSale + priceRange { + minVariantPrice { + ...MoneyFields + } + maxVariantPrice { + ...MoneyFields + } + } + compareAtPriceRange { + minVariantPrice { + ...MoneyFields + } + maxVariantPrice { + ...MoneyFields + } + } + images(first: 1) { + edges { + node { + ...ImageFields + } + } + } + variants(first: 3) { + edges { + node { + id + title + price + compareAtPrice + availableForSale + selectedOptions { + name + value + } + } + } + } +}