diff --git a/juniper_codegen/src/common/deprecation.rs b/juniper_codegen/src/common/deprecation.rs new file mode 100644 index 00000000..9efd6ef0 --- /dev/null +++ b/juniper_codegen/src/common/deprecation.rs @@ -0,0 +1,114 @@ +//! Common functions, definitions and extensions for parsing and code generation +//! of [GraphQL deprecation directive][0]. +//! +//! [0]: https://spec.graphql.org/October2021#sec--deprecated + +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; +use syn::{ + parse::{Parse, ParseStream}, + spanned::Spanned as _, + token, +}; + +use crate::common::{parse::ParseBufferExt as _, SpanContainer}; + +/// [GraphQL deprecation directive][0] defined on a [GraphQL field][1] or a +/// [GraphQL enum value][2] via `#[graphql(deprecated = ...)]` (or +/// `#[deprecated(note = ...)]`) attribute. +/// +/// [0]: https://spec.graphql.org/October2021#sec--deprecated +/// [1]: https://spec.graphql.org/October2021#sec-Language.Fields +/// [2]: https://spec.graphql.org/October2021#sec-Enum-Value +#[derive(Debug, Default)] +pub(crate) struct Directive { + /// Optional [reason][1] attached to this [deprecation][0]. + /// + /// [0]: https://spec.graphql.org/October2021#sec--deprecated + /// [1]: https://spec.graphql.org/October2021#sel-GAHnBZDACEDDGAA_6L + pub(crate) reason: Option, +} + +impl Parse for Directive { + fn parse(input: ParseStream<'_>) -> syn::Result { + Ok(Self { + reason: input + .try_parse::()? + .map(|_| input.parse::()) + .transpose()?, + }) + } +} + +impl Directive { + /// Tries to parse a [`Directive`] from a `#[deprecated(note = ...)]` + /// attribute, by looking up for it in the provided [`syn::Attribute`]s. + /// + /// # Errors + /// + /// If failed to parse a [`Directive`] from a found + /// `#[deprecated(note = ...)]` attribute. + pub(crate) fn parse_from_deprecated_attr( + attrs: &[syn::Attribute], + ) -> syn::Result>> { + for attr in attrs { + return Ok(match attr.parse_meta() { + Ok(syn::Meta::List(ref list)) if list.path.is_ident("deprecated") => { + let directive = Self::parse_from_deprecated_meta_list(list)?; + Some(SpanContainer::new( + list.path.span(), + directive.reason.as_ref().map(|r| r.span()), + directive, + )) + } + Ok(syn::Meta::Path(ref path)) if path.is_ident("deprecated") => { + Some(SpanContainer::new(path.span(), None, Self::default())) + } + _ => continue, + }); + } + Ok(None) + } + + /// Tries to parse a [`Directive`] from the [`syn::MetaList`] of a single + /// `#[deprecated(note = ...)]` attribute. + /// + /// # Errors + /// + /// If the `#[deprecated(note = ...)]` attribute has incorrect format. + fn parse_from_deprecated_meta_list(list: &syn::MetaList) -> syn::Result { + for meta in &list.nested { + if let syn::NestedMeta::Meta(syn::Meta::NameValue(nv)) = meta { + return if !nv.path.is_ident("note") { + Err(syn::Error::new( + nv.path.span(), + "unrecognized setting on #[deprecated(..)] attribute", + )) + } else if let syn::Lit::Str(strlit) = &nv.lit { + Ok(Self { + reason: Some(strlit.clone()), + }) + } else { + Err(syn::Error::new( + nv.lit.span(), + "only strings are allowed for deprecation", + )) + }; + } + } + Ok(Self::default()) + } +} + +impl ToTokens for Directive { + fn to_tokens(&self, into: &mut TokenStream) { + let reason = self + .reason + .as_ref() + .map_or_else(|| quote! { None }, |text| quote! { Some(#text) }); + quote! { + .deprecated(::std::option::Option::#reason) + } + .to_tokens(into); + } +} diff --git a/juniper_codegen/src/common/description.rs b/juniper_codegen/src/common/description.rs new file mode 100644 index 00000000..6b9533e6 --- /dev/null +++ b/juniper_codegen/src/common/description.rs @@ -0,0 +1,200 @@ +//! Common functions, definitions and extensions for parsing and code generation +//! of [GraphQL description][0]. +//! +//! [0]: https://spec.graphql.org/October2021#sec-Descriptions + +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; +use syn::parse::{Parse, ParseStream}; + +use crate::common::SpanContainer; + +/// [GraphQL description][0] defined on a GraphQL definition via +/// `#[graphql(description = ...)]` (or `#[doc = ...]`) attribute. +/// +/// [0]: https://spec.graphql.org/October2021#sec-Descriptions +#[derive(Debug, Eq, PartialEq)] +pub(crate) struct Description(syn::LitStr); + +impl Parse for Description { + fn parse(input: ParseStream<'_>) -> syn::Result { + input.parse::().map(Self) + } +} + +impl Description { + /// Tries to parse a [`Description`] from a `#[doc = ...]` attribute (or + /// Rust doc comment), by looking up for it in the provided + /// [`syn::Attribute`]s. + /// + /// # Errors + /// + /// If failed to parse a [`Description`] from a found `#[doc = ...]` + /// attribute. + pub(crate) fn parse_from_doc_attrs( + attrs: &[syn::Attribute], + ) -> syn::Result>> { + let (mut first_span, mut descriptions) = (None, Vec::new()); + for attr in attrs { + match attr.parse_meta() { + Ok(syn::Meta::NameValue(ref nv)) if nv.path.is_ident("doc") => { + if let syn::Lit::Str(strlit) = &nv.lit { + if first_span.is_none() { + first_span = Some(strlit.span()); + } + descriptions.push(strlit.value()); + } else { + return Err(syn::Error::new( + nv.lit.span(), + "#[doc] attributes may only have a string literal", + )); + } + } + _ => continue, + } + } + Ok(first_span.map(|span| { + SpanContainer::new( + span, + None, + Self(syn::LitStr::new(&Self::concatenate(&descriptions), span)), + ) + })) + } + + /// Concatenates [`Description`] strings into a single one. + fn concatenate(descriptions: &[String]) -> String { + let last_index = descriptions.len() - 1; + descriptions + .iter() + .map(|s| s.as_str().trim_end()) + .map(|s| { + // Trim leading space. + s.strip_prefix(' ').unwrap_or(s) + }) + .enumerate() + .fold(String::new(), |mut buffer, (index, s)| { + // Add newline, except when string ends in a continuation + // backslash or is the last line. + if index == last_index { + buffer.push_str(s); + } else if s.ends_with('\\') { + buffer.push_str(s.trim_end_matches('\\')); + buffer.push(' '); + } else { + buffer.push_str(s); + buffer.push('\n'); + } + buffer + }) + } +} + +impl ToTokens for Description { + fn to_tokens(&self, into: &mut TokenStream) { + let desc = &self.0; + + quote! { + .description(#desc) + } + .to_tokens(into); + } +} + +#[cfg(test)] +mod parse_from_doc_attrs_test { + use quote::quote; + use syn::parse_quote; + + use super::Description; + + #[test] + fn single() { + let desc = Description::parse_from_doc_attrs(&[parse_quote! { #[doc = "foo"] }]) + .unwrap() + .unwrap() + .into_inner(); + assert_eq!( + quote! { #desc }.to_string(), + quote! { .description("foo") }.to_string(), + ); + } + + #[test] + fn many() { + let desc = Description::parse_from_doc_attrs(&[ + parse_quote! { #[doc = "foo"] }, + parse_quote! { #[doc = "\n"] }, + parse_quote! { #[doc = "bar"] }, + ]) + .unwrap() + .unwrap() + .into_inner(); + assert_eq!( + quote! { #desc }.to_string(), + quote! { .description("foo\n\nbar") }.to_string(), + ); + } + + #[test] + fn not_doc() { + let desc = Description::parse_from_doc_attrs(&[parse_quote! { #[blah = "foo"] }]).unwrap(); + assert_eq!(desc, None); + } +} + +#[cfg(test)] +mod concatenate_test { + use super::Description; + + /// Forms a [`Vec`] of [`String`]s out of the provided [`str`]s + /// [`Iterator`]. + fn to_strings<'i>(source: impl IntoIterator) -> Vec { + source.into_iter().map(ToOwned::to_owned).collect() + } + + #[test] + fn single() { + assert_eq!(Description::concatenate(&to_strings(["foo"])), "foo"); + } + + #[test] + fn multiple() { + assert_eq!( + Description::concatenate(&to_strings(["foo", "bar"])), + "foo\nbar", + ); + } + + #[test] + fn trims_spaces() { + assert_eq!( + Description::concatenate(&to_strings([" foo ", "bar ", " baz"])), + "foo\nbar\nbaz", + ); + } + + #[test] + fn empty() { + assert_eq!( + Description::concatenate(&to_strings(["foo", "", "bar"])), + "foo\n\nbar", + ); + } + + #[test] + fn newline_spaces() { + assert_eq!( + Description::concatenate(&to_strings(["foo ", "", " bar"])), + "foo\n\nbar", + ); + } + + #[test] + fn continuation_backslash() { + assert_eq!( + Description::concatenate(&to_strings(["foo\\", "x\\", "y", "bar"])), + "foo x y\nbar", + ); + } +} diff --git a/juniper_codegen/src/result.rs b/juniper_codegen/src/common/diagnostic.rs similarity index 79% rename from juniper_codegen/src/result.rs rename to juniper_codegen/src/common/diagnostic.rs index 24a0fa8f..1d5ebc9c 100644 --- a/juniper_codegen/src/result.rs +++ b/juniper_codegen/src/common/diagnostic.rs @@ -1,14 +1,12 @@ -//! - use std::fmt; use proc_macro2::Span; use proc_macro_error::{Diagnostic, Level}; /// URL of the GraphQL specification (October 2021 Edition). -pub const SPEC_URL: &str = "https://spec.graphql.org/October2021"; +pub(crate) const SPEC_URL: &str = "https://spec.graphql.org/October2021"; -pub enum GraphQLScope { +pub(crate) enum Scope { EnumDerive, InputObjectDerive, InterfaceAttr, @@ -22,8 +20,8 @@ pub enum GraphQLScope { UnionDerive, } -impl GraphQLScope { - pub fn spec_section(&self) -> &str { +impl Scope { + pub(crate) fn spec_section(&self) -> &str { match self { Self::EnumDerive => "#sec-Enums", Self::InputObjectDerive => "#sec-Input-Objects", @@ -36,7 +34,7 @@ impl GraphQLScope { } } -impl fmt::Display for GraphQLScope { +impl fmt::Display for Scope { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let name = match self { Self::EnumDerive => "enum", @@ -51,30 +49,30 @@ impl fmt::Display for GraphQLScope { } } -impl GraphQLScope { +impl Scope { fn spec_link(&self) -> String { format!("{}{}", SPEC_URL, self.spec_section()) } - pub fn custom>(&self, span: Span, msg: S) -> Diagnostic { + pub(crate) fn custom>(&self, span: Span, msg: S) -> Diagnostic { Diagnostic::spanned(span, Level::Error, format!("{} {}", self, msg.as_ref())) .note(self.spec_link()) } - pub fn error(&self, err: syn::Error) -> Diagnostic { + pub(crate) fn error(&self, err: syn::Error) -> Diagnostic { Diagnostic::spanned(err.span(), Level::Error, format!("{} {}", self, err)) .note(self.spec_link()) } - pub fn emit_custom>(&self, span: Span, msg: S) { + pub(crate) fn emit_custom>(&self, span: Span, msg: S) { self.custom(span, msg).emit() } - pub fn custom_error>(&self, span: Span, msg: S) -> syn::Error { + pub(crate) fn custom_error>(&self, span: Span, msg: S) -> syn::Error { syn::Error::new(span, format!("{} {}", self, msg.as_ref())) } - pub fn no_double_underscore(&self, field: Span) { + pub(crate) fn no_double_underscore(&self, field: Span) { Diagnostic::spanned( field, Level::Error, diff --git a/juniper_codegen/src/common/field/arg.rs b/juniper_codegen/src/common/field/arg.rs index 517c9eb4..1874c8ae 100644 --- a/juniper_codegen/src/common/field/arg.rs +++ b/juniper_codegen/src/common/field/arg.rs @@ -14,16 +14,13 @@ use syn::{ token, }; -use crate::{ - common::{ - parse::{ - attr::{err, OptionExt as _}, - ParseBufferExt as _, TypeExt as _, - }, - scalar, +use crate::common::{ + default, diagnostic, filter_attrs, + parse::{ + attr::{err, OptionExt as _}, + ParseBufferExt as _, TypeExt as _, }, - result::GraphQLScope, - util::{filter_attrs, path_eq_single, span_container::SpanContainer, RenameRule}, + path_eq_single, rename, scalar, Description, SpanContainer, }; /// Available metadata (arguments) behind `#[graphql]` attribute placed on a @@ -44,19 +41,16 @@ pub(crate) struct Attr { /// /// [1]: https://spec.graphql.org/October2021#sec-Language.Arguments /// [2]: https://spec.graphql.org/October2021#sec-Descriptions - pub(crate) description: Option>, + pub(crate) description: Option>, /// Explicitly specified [default value][2] of this [GraphQL argument][1]. /// - /// If the exact default expression is not specified, then the [`Default`] - /// value is used. - /// /// If [`None`], then this [GraphQL argument][1] is considered as /// [required][2]. /// /// [1]: https://spec.graphql.org/October2021#sec-Language.Arguments /// [2]: https://spec.graphql.org/October2021#sec-Required-Arguments - pub(crate) default: Option>>, + pub(crate) default: Option>, /// Explicitly specified marker indicating that this method argument doesn't /// represent a [GraphQL argument][1], but is a [`Context`] being injected @@ -98,27 +92,15 @@ impl Parse for Attr { } "desc" | "description" => { input.parse::()?; - let desc = input.parse::()?; + let desc = input.parse::()?; out.description .replace(SpanContainer::new(ident.span(), Some(desc.span()), desc)) .none_or_else(|_| err::dup_arg(&ident))? } "default" => { - let mut expr = None; - if input.is_next::() { - input.parse::()?; - expr = Some(input.parse::()?); - } else if input.is_next::() { - let inner; - let _ = syn::parenthesized!(inner in input); - expr = Some(inner.parse::()?); - } + let val = input.parse::()?; out.default - .replace(SpanContainer::new( - ident.span(), - expr.as_ref().map(|e| e.span()), - expr, - )) + .replace(SpanContainer::new(ident.span(), Some(val.span()), val)) .none_or_else(|_| err::dup_arg(&ident))? } "ctx" | "context" | "Context" => { @@ -241,18 +223,15 @@ pub(crate) struct OnField { /// /// [1]: https://spec.graphql.org/October2021#sec-Language.Arguments /// [2]: https://spec.graphql.org/October2021#sec-Descriptions - pub(crate) description: Option, + pub(crate) description: Option, /// Default value of this [GraphQL field argument][1] in GraphQL schema. /// - /// If outer [`Option`] is [`None`], then this [argument][1] is a - /// [required][2] one. - /// - /// If inner [`Option`] is [`None`], then the [`Default`] value is used. + /// If [`None`], then this [argument][1] is a [required][2] one. /// /// [1]: https://spec.graphql.org/October2021#sec-Language.Arguments /// [2]: https://spec.graphql.org/October2021#sec-Required-Arguments - pub(crate) default: Option>, + pub(crate) default: Option, } /// Possible kinds of Rust method arguments for code generation. @@ -323,16 +302,9 @@ impl OnMethod { let (name, ty) = (&arg.name, &arg.ty); - let description = arg - .description - .as_ref() - .map(|desc| quote! { .description(#desc) }); + let description = &arg.description; let method = if let Some(val) = &arg.default { - let val = val - .as_ref() - .map(|v| quote! { (#v).into() }) - .unwrap_or_else(|| quote! { <#ty as Default>::default() }); quote_spanned! { val.span() => .arg_with_default::<#ty>(#name, &#val, info) } @@ -395,8 +367,8 @@ impl OnMethod { /// given `scope`. pub(crate) fn parse( argument: &mut syn::PatType, - renaming: &RenameRule, - scope: &GraphQLScope, + renaming: &rename::Policy, + scope: &diagnostic::Scope, ) -> Option { let orig_attrs = argument.attrs.clone(); @@ -462,8 +434,8 @@ impl OnMethod { Some(Self::Regular(Box::new(OnField { name, ty: argument.ty.as_ref().clone(), - description: attr.description.as_ref().map(|d| d.as_ref().value()), - default: attr.default.as_ref().map(|v| v.as_ref().clone()), + description: attr.description.map(SpanContainer::into_inner), + default: attr.default.map(SpanContainer::into_inner), }))) } } diff --git a/juniper_codegen/src/common/field/mod.rs b/juniper_codegen/src/common/field/mod.rs index 7487380e..4ed6d79c 100644 --- a/juniper_codegen/src/common/field/mod.rs +++ b/juniper_codegen/src/common/field/mod.rs @@ -14,15 +14,13 @@ use syn::{ token, }; -use crate::{ - common::{ - parse::{ - attr::{err, OptionExt as _}, - ParseBufferExt as _, - }, - scalar, +use crate::common::{ + deprecation, filter_attrs, + parse::{ + attr::{err, OptionExt as _}, + ParseBufferExt as _, }, - util::{filter_attrs, get_deprecated, get_doc_comment, span_container::SpanContainer}, + scalar, Description, SpanContainer, }; pub(crate) use self::arg::OnMethod as MethodArgument; @@ -47,7 +45,7 @@ pub(crate) struct Attr { /// /// [1]: https://spec.graphql.org/October2021#sec-Language.Fields /// [2]: https://spec.graphql.org/October2021#sec-Descriptions - pub(crate) description: Option>, + pub(crate) description: Option>, /// Explicitly specified [deprecation][2] of this [GraphQL field][1]. /// @@ -56,7 +54,7 @@ pub(crate) struct Attr { /// /// [1]: https://spec.graphql.org/October2021#sec-Language.Fields /// [2]: https://spec.graphql.org/October2021#sec-Deprecation - pub(crate) deprecated: Option>>, + pub(crate) deprecated: Option>, /// Explicitly specified marker indicating that this method (or struct /// field) should be omitted by code generation and not considered as the @@ -81,22 +79,18 @@ impl Parse for Attr { } "desc" | "description" => { input.parse::()?; - let desc = input.parse::()?; + let desc = input.parse::()?; out.description .replace(SpanContainer::new(ident.span(), Some(desc.span()), desc)) .none_or_else(|_| err::dup_arg(&ident))? } "deprecated" => { - let mut reason = None; - if input.is_next::() { - input.parse::()?; - reason = Some(input.parse::()?); - } + let directive = input.parse::()?; out.deprecated .replace(SpanContainer::new( ident.span(), - reason.as_ref().map(|r| r.span()), - reason, + directive.reason.as_ref().map(|r| r.span()), + directive, )) .none_or_else(|_| err::dup_arg(&ident))? } @@ -145,17 +139,11 @@ impl Attr { } if attr.description.is_none() { - attr.description = get_doc_comment(attrs).map(|sc| { - let span = sc.span_ident(); - sc.map(|desc| syn::LitStr::new(&desc, span)) - }); + attr.description = Description::parse_from_doc_attrs(attrs)?; } if attr.deprecated.is_none() { - attr.deprecated = get_deprecated(attrs).map(|sc| { - let span = sc.span_ident(); - sc.map(|depr| depr.reason.map(|rsn| syn::LitStr::new(&rsn, span))) - }); + attr.deprecated = deprecation::Directive::parse_from_deprecated_attr(attrs)?; } Ok(attr) @@ -182,16 +170,13 @@ pub(crate) struct Definition { /// /// [1]: https://spec.graphql.org/October2021#sec-Language.Fields /// [2]: https://spec.graphql.org/October2021#sec-Descriptions - pub(crate) description: Option, + pub(crate) description: Option, /// [Deprecation][2] of this [GraphQL field][1] to put into GraphQL schema. /// - /// If inner [`Option`] is [`None`], then deprecation has no message - /// attached. - /// /// [1]: https://spec.graphql.org/October2021#sec-Language.Fields /// [2]: https://spec.graphql.org/October2021#sec-Deprecation - pub(crate) deprecated: Option>, + pub(crate) deprecated: Option, /// Ident of the Rust method (or struct field) representing this /// [GraphQL field][1]. @@ -305,18 +290,8 @@ impl Definition { }; } - let description = self - .description - .as_ref() - .map(|desc| quote! { .description(#desc) }); - - let deprecated = self.deprecated.as_ref().map(|reason| { - let reason = reason - .as_ref() - .map(|rsn| quote! { Some(#rsn) }) - .unwrap_or_else(|| quote! { None }); - quote! { .deprecated(#reason) } - }); + let description = &self.description; + let deprecated = &self.deprecated; let args = self .arguments diff --git a/juniper_codegen/src/common/mod.rs b/juniper_codegen/src/common/mod.rs index e3f72d63..e6e3dd8f 100644 --- a/juniper_codegen/src/common/mod.rs +++ b/juniper_codegen/src/common/mod.rs @@ -1,7 +1,31 @@ //! Common functions, definitions and extensions for code generation, used by this crate. pub(crate) mod default; +pub(crate) mod deprecation; +mod description; +pub(crate) mod diagnostic; pub(crate) mod field; pub(crate) mod gen; pub(crate) mod parse; +pub(crate) mod rename; pub(crate) mod scalar; +mod span_container; + +pub(crate) use self::{description::Description, span_container::SpanContainer}; + +/// Checks whether the specified [`syn::Path`] equals to one-segment string +/// `value`. +pub(crate) fn path_eq_single(path: &syn::Path, value: &str) -> bool { + path.segments.len() == 1 && path.segments[0].ident == value +} + +/// Filters the provided [`syn::Attribute`] to contain only ones with the +/// specified `name`. +pub(crate) fn filter_attrs<'a>( + name: &'a str, + attrs: &'a [syn::Attribute], +) -> impl Iterator + 'a { + attrs + .iter() + .filter(move |attr| path_eq_single(&attr.path, name)) +} diff --git a/juniper_codegen/src/common/parse/attr.rs b/juniper_codegen/src/common/parse/attr.rs index 9e42cc97..03ba8d73 100644 --- a/juniper_codegen/src/common/parse/attr.rs +++ b/juniper_codegen/src/common/parse/attr.rs @@ -4,7 +4,7 @@ use proc_macro2::{Span, TokenStream}; use syn::parse_quote; -use crate::util::path_eq_single; +use crate::common::path_eq_single; /// Prepends the given `attrs` collection with a new [`syn::Attribute`] generated from the given /// `attr_path` and `attr_args`. diff --git a/juniper_codegen/src/common/rename.rs b/juniper_codegen/src/common/rename.rs new file mode 100644 index 00000000..2eda94bc --- /dev/null +++ b/juniper_codegen/src/common/rename.rs @@ -0,0 +1,164 @@ +//! Common functions, definitions and extensions for parsing and code generation +//! of `#[graphql(rename_all = ...)]` attribute. + +use std::{convert::TryFrom, str::FromStr}; + +use syn::parse::{Parse, ParseStream}; + +/// Possible ways to rename all [GraphQL fields][1] or [GrqphQL enum values][2]. +/// +/// [1]: https://spec.graphql.org/October2021#sec-Language.Fields +/// [2]: https://spec.graphql.org/October2021#sec-Enum-Value +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum Policy { + /// Do nothing, and use the default conventions renaming. + None, + + /// Rename in `camelCase` style. + CamelCase, + + /// Rename in `SCREAMING_SNAKE_CASE` style. + ScreamingSnakeCase, +} + +impl Policy { + /// Applies this [`Policy`] to the given `name`. + pub(crate) fn apply(&self, name: &str) -> String { + match self { + Self::None => name.to_owned(), + Self::CamelCase => to_camel_case(name), + Self::ScreamingSnakeCase => to_upper_snake_case(name), + } + } +} + +impl FromStr for Policy { + type Err = (); + + fn from_str(rule: &str) -> Result { + match rule { + "none" => Ok(Self::None), + "camelCase" => Ok(Self::CamelCase), + "SCREAMING_SNAKE_CASE" => Ok(Self::ScreamingSnakeCase), + _ => Err(()), + } + } +} + +impl TryFrom for Policy { + type Error = syn::Error; + + fn try_from(lit: syn::LitStr) -> syn::Result { + Self::from_str(&lit.value()) + .map_err(|_| syn::Error::new(lit.span(), "unknown renaming policy")) + } +} + +impl Parse for Policy { + fn parse(input: ParseStream<'_>) -> syn::Result { + Self::try_from(input.parse::()?) + } +} + +// NOTE: duplicated from juniper crate! +fn to_camel_case(s: &str) -> String { + let mut dest = String::new(); + + // Handle `_` and `__` to be more friendly with the `_var` convention for + // unused variables, and GraphQL introspection identifiers. + let s_iter = if let Some(s) = s.strip_prefix("__") { + dest.push_str("__"); + s + } else { + s.strip_prefix('_').unwrap_or(s) + } + .split('_') + .enumerate(); + + for (i, part) in s_iter { + if i > 0 && part.len() == 1 { + dest.push_str(&part.to_uppercase()); + } else if i > 0 && part.len() > 1 { + let first = part + .chars() + .next() + .unwrap() + .to_uppercase() + .collect::(); + let second = &part[1..]; + + dest.push_str(&first); + dest.push_str(second); + } else if i == 0 { + dest.push_str(part); + } + } + + dest +} + +fn to_upper_snake_case(s: &str) -> String { + let mut last_lower = false; + let mut upper = String::new(); + for c in s.chars() { + if c == '_' { + last_lower = false; + } else if c.is_lowercase() { + last_lower = true; + } else if c.is_uppercase() { + if last_lower { + upper.push('_'); + } + last_lower = false; + } + + for u in c.to_uppercase() { + upper.push(u); + } + } + upper +} + +#[cfg(test)] +mod to_camel_case_tests { + use super::to_camel_case; + + #[test] + fn converts_correctly() { + for (input, expected) in [ + ("test", "test"), + ("_test", "test"), + ("__test", "__test"), + ("first_second", "firstSecond"), + ("first_", "first"), + ("a_b_c", "aBC"), + ("a_bc", "aBc"), + ("a_b", "aB"), + ("a", "a"), + ("", ""), + ] { + assert_eq!(to_camel_case(input), expected); + } + } +} + +#[cfg(test)] +mod to_upper_snake_case_tests { + use super::to_upper_snake_case; + + #[test] + fn converts_correctly() { + for (input, expected) in [ + ("abc", "ABC"), + ("a_bc", "A_BC"), + ("ABC", "ABC"), + ("A_BC", "A_BC"), + ("SomeInput", "SOME_INPUT"), + ("someInput", "SOME_INPUT"), + ("someINpuT", "SOME_INPU_T"), + ("some_INpuT", "SOME_INPU_T"), + ] { + assert_eq!(to_upper_snake_case(input), expected); + } + } +} diff --git a/juniper_codegen/src/util/span_container.rs b/juniper_codegen/src/common/span_container.rs similarity index 77% rename from juniper_codegen/src/util/span_container.rs rename to juniper_codegen/src/common/span_container.rs index 8b4c6b58..bb27a03b 100644 --- a/juniper_codegen/src/util/span_container.rs +++ b/juniper_codegen/src/common/span_container.rs @@ -6,8 +6,8 @@ use std::{ use proc_macro2::{Span, TokenStream}; use quote::ToTokens; -#[derive(Clone, Debug)] -pub struct SpanContainer { +#[derive(Clone, Copy, Debug)] +pub(crate) struct SpanContainer { expr: Option, ident: Span, val: T, @@ -20,15 +20,15 @@ impl ToTokens for SpanContainer { } impl SpanContainer { - pub fn new(ident: Span, expr: Option, val: T) -> Self { + pub(crate) fn new(ident: Span, expr: Option, val: T) -> Self { Self { expr, ident, val } } - pub fn span_ident(&self) -> Span { + pub(crate) fn span_ident(&self) -> Span { self.ident } - pub fn span_joined(&self) -> Span { + pub(crate) fn span_joined(&self) -> Span { if let Some(s) = self.expr { // TODO: Use `Span::join` once stabilized and available on stable: // https://github.com/rust-lang/rust/issues/54725 @@ -41,17 +41,9 @@ impl SpanContainer { } } - pub fn into_inner(self) -> T { + pub(crate) fn into_inner(self) -> T { self.val } - - pub fn map U>(self, f: F) -> SpanContainer { - SpanContainer { - expr: self.expr, - ident: self.ident, - val: f(self.val), - } - } } impl AsRef for SpanContainer { diff --git a/juniper_codegen/src/graphql_enum/derive.rs b/juniper_codegen/src/graphql_enum/derive.rs index 03ae30c2..f0564a26 100644 --- a/juniper_codegen/src/graphql_enum/derive.rs +++ b/juniper_codegen/src/graphql_enum/derive.rs @@ -6,16 +6,12 @@ use proc_macro2::TokenStream; use quote::ToTokens as _; use syn::{ext::IdentExt as _, parse_quote, spanned::Spanned}; -use crate::{ - common::scalar, - result::GraphQLScope, - util::{span_container::SpanContainer, RenameRule}, -}; +use crate::common::{diagnostic, rename, scalar, SpanContainer}; use super::{ContainerAttr, Definition, ValueDefinition, VariantAttr}; -/// [`GraphQLScope`] of errors for `#[derive(GraphQLEnum)]` macro. -const ERR: GraphQLScope = GraphQLScope::EnumDerive; +/// [`diagnostic::Scope`] of errors for `#[derive(GraphQLEnum)]` macro. +const ERR: diagnostic::Scope = diagnostic::Scope::EnumDerive; /// Expands `#[derive(GraphQLEnum)]` macro into generated code. pub(crate) fn expand(input: TokenStream) -> syn::Result { @@ -32,7 +28,7 @@ pub(crate) fn expand(input: TokenStream) -> syn::Result { let renaming = attr .rename_values .map(SpanContainer::into_inner) - .unwrap_or(RenameRule::ScreamingSnakeCase); + .unwrap_or(rename::Policy::ScreamingSnakeCase); let values = data .variants .iter() @@ -80,8 +76,6 @@ pub(crate) fn expand(input: TokenStream) -> syn::Result { .context .map_or_else(|| parse_quote! { () }, SpanContainer::into_inner); - let description = attr.description.map(|d| d.into_inner().into_boxed_str()); - let scalar = scalar::Type::parse(attr.scalar.as_deref(), &ast.generics); proc_macro_error::abort_if_dirty(); @@ -90,7 +84,7 @@ pub(crate) fn expand(input: TokenStream) -> syn::Result { ident: ast.ident, generics: ast.generics, name, - description, + description: attr.description.map(SpanContainer::into_inner), context, scalar, values, @@ -103,7 +97,7 @@ pub(crate) fn expand(input: TokenStream) -> syn::Result { /// Parses a [`ValueDefinition`] from the given Rust enum variant definition. /// /// Returns [`None`] if the parsing fails, or the enum variant is ignored. -fn parse_value(v: &syn::Variant, renaming: RenameRule) -> Option { +fn parse_value(v: &syn::Variant, renaming: rename::Policy) -> Option { let attr = VariantAttr::from_attrs("graphql", &v.attrs) .map_err(|e| proc_macro_error::emit_error!(e)) .ok()?; @@ -124,19 +118,11 @@ fn parse_value(v: &syn::Variant, renaming: RenameRule) -> Option>, + description: Option>, /// Explicitly specified type of [`Context`] to use for resolving this /// [GraphQL enum][0] type with. @@ -71,15 +67,15 @@ struct ContainerAttr { /// [0]: https://spec.graphql.org/October2021#sec-Enums scalar: Option>, - /// Explicitly specified [`RenameRule`] for all [values][1] of this + /// Explicitly specified [`rename::Policy`] for all [values][1] of this /// [GraphQL enum][0]. /// - /// If [`None`], then the [`RenameRule::ScreamingSnakeCase`] rule will be + /// If [`None`], then the [`rename::Policy::ScreamingSnakeCase`] will be /// applied by default. /// /// [0]: https://spec.graphql.org/October2021#sec-Enums /// [1]: https://spec.graphql.org/October2021#EnumValuesDefinition - rename_values: Option>, + rename_values: Option>, /// Indicator whether the generated code is intended to be used only inside /// the [`juniper`] library. @@ -105,13 +101,9 @@ impl Parse for ContainerAttr { } "desc" | "description" => { input.parse::()?; - let desc = input.parse::()?; + let desc = input.parse::()?; out.description - .replace(SpanContainer::new( - ident.span(), - Some(desc.span()), - desc.value(), - )) + .replace(SpanContainer::new(ident.span(), Some(desc.span()), desc)) .none_or_else(|_| err::dup_arg(&ident))? } "ctx" | "context" | "Context" => { @@ -174,7 +166,7 @@ impl ContainerAttr { .try_fold(Self::default(), |prev, curr| prev.try_merge(curr?))?; if attr.description.is_none() { - attr.description = get_doc_comment(attrs); + attr.description = Description::parse_from_doc_attrs(attrs)?; } Ok(attr) @@ -202,19 +194,17 @@ struct VariantAttr { /// /// [1]: https://spec.graphql.org/October2021#sec-Enum-Value /// [2]: https://spec.graphql.org/October2021#sec-Descriptions - description: Option>, + description: Option>, /// Explicitly specified [deprecation][2] of this [GraphQL enum value][1]. /// /// If [`None`], then Rust `#[deprecated]` attribute will be used as the /// [deprecation][2], if any. /// - /// If the inner [`Option`] is [`None`], then no [reason][3] was provided. - /// /// [1]: https://spec.graphql.org/October2021#sec-Enum-Value /// [2]: https://spec.graphql.org/October2021#sec--deprecated /// [3]: https://spec.graphql.org/October2021#sel-GAHnBZDACEDDGAA_6L - deprecated: Option>>, + deprecated: Option>, /// Explicitly specified marker for the Rust enum variant to be ignored and /// not included into the code generated for a [GraphQL enum][0] @@ -243,26 +233,18 @@ impl Parse for VariantAttr { } "desc" | "description" => { input.parse::()?; - let desc = input.parse::()?; + let desc = input.parse::()?; out.description - .replace(SpanContainer::new( - ident.span(), - Some(desc.span()), - desc.value(), - )) + .replace(SpanContainer::new(ident.span(), Some(desc.span()), desc)) .none_or_else(|_| err::dup_arg(&ident))? } "deprecated" => { - let mut reason = None; - if input.is_next::() { - input.parse::()?; - reason = Some(input.parse::()?); - } + let directive = input.parse::()?; out.deprecated .replace(SpanContainer::new( ident.span(), - reason.as_ref().map(|r| r.span()), - reason, + directive.reason.as_ref().map(|r| r.span()), + directive, )) .none_or_else(|_| err::dup_arg(&ident))? } @@ -300,14 +282,11 @@ impl VariantAttr { .try_fold(Self::default(), |prev, curr| prev.try_merge(curr?))?; if attr.description.is_none() { - attr.description = get_doc_comment(attrs); + attr.description = Description::parse_from_doc_attrs(attrs)?; } if attr.deprecated.is_none() { - attr.deprecated = get_deprecated(attrs).map(|sc| { - let span = sc.span_ident(); - sc.map(|depr| depr.reason.map(|rsn| syn::LitStr::new(&rsn, span))) - }); + attr.deprecated = deprecation::Directive::parse_from_deprecated_attr(attrs)?; } Ok(attr) @@ -335,18 +314,14 @@ struct ValueDefinition { /// /// [1]: https://spec.graphql.org/October2021#sec-Enum-Value /// [2]: https://spec.graphql.org/October2021#sec-Descriptions - description: Option>, + description: Option, /// [Deprecation][2] of this [GraphQL enum value][1] to put into GraphQL /// schema. /// - /// If the inner [`Option`] is [`None`], then [deprecation][2] has no - /// [reason][3] attached. - /// /// [1]: https://spec.graphql.org/October2021#sec-Enum-Value /// [2]: https://spec.graphql.org/October2021#sec--deprecated - /// [3]: https://spec.graphql.org/October2021#sel-GAHnBZDACEDDGAA_6L - deprecated: Option>>, + deprecated: Option, } /// Representation of a [GraphQL enum][0] for code generation. @@ -373,7 +348,7 @@ struct Definition { /// /// [0]: https://spec.graphql.org/October2021#sec-Enums /// [2]: https://spec.graphql.org/October2021#sec-Descriptions - description: Option>, + description: Option, /// Rust type of [`Context`] to generate [`GraphQLType`] implementation with /// for this [GraphQL enum][0]. @@ -457,37 +432,17 @@ impl Definition { let (_, ty_generics, _) = self.generics.split_for_impl(); let name = &self.name; - let description = self - .description - .as_ref() - .map(|desc| quote! { .description(#desc) }); + let description = &self.description; let variants_meta = self.values.iter().map(|v| { - let name = &v.name; - let description = v.description.as_ref().map_or_else( - || quote! { None }, - |desc| quote! { Some(String::from(#desc)) }, - ); - let deprecation_status = match &v.deprecated { - None => quote! { ::juniper::meta::DeprecationStatus::Current }, - Some(None) => quote! { - ::juniper::meta::DeprecationStatus::Deprecated(None) - }, - Some(Some(reason)) => { - quote! { - ::juniper::meta::DeprecationStatus::Deprecated( - Some(String::from(#reason)) - ) - } - } - }; + let v_name = &v.name; + let v_description = &v.description; + let v_deprecation = &v.deprecated; quote! { - ::juniper::meta::EnumValue { - name: String::from(#name), - description: #description, - deprecation_status: #deprecation_status, - } + ::juniper::meta::EnumValue::new(#v_name) + #v_description + #v_deprecation } }); diff --git a/juniper_codegen/src/graphql_input_object/derive.rs b/juniper_codegen/src/graphql_input_object/derive.rs index c1c333b1..370fc200 100644 --- a/juniper_codegen/src/graphql_input_object/derive.rs +++ b/juniper_codegen/src/graphql_input_object/derive.rs @@ -6,16 +6,12 @@ use proc_macro2::TokenStream; use quote::ToTokens as _; use syn::{ext::IdentExt as _, parse_quote, spanned::Spanned}; -use crate::{ - common::scalar, - result::GraphQLScope, - util::{span_container::SpanContainer, RenameRule}, -}; +use crate::common::{diagnostic, rename, scalar, SpanContainer}; use super::{ContainerAttr, Definition, FieldAttr, FieldDefinition}; -/// [`GraphQLScope`] of errors for `#[derive(GraphQLInputObject)]` macro. -const ERR: GraphQLScope = GraphQLScope::InputObjectDerive; +/// [`diagnostic::Scope`] of errors for `#[derive(GraphQLInputObject)]` macro. +const ERR: diagnostic::Scope = diagnostic::Scope::InputObjectDerive; /// Expands `#[derive(GraphQLInputObject)]` macro into generated code. pub fn expand(input: TokenStream) -> syn::Result { @@ -31,7 +27,7 @@ pub fn expand(input: TokenStream) -> syn::Result { let renaming = attr .rename_fields .map(SpanContainer::into_inner) - .unwrap_or(RenameRule::CamelCase); + .unwrap_or(rename::Policy::CamelCase); let is_internal = attr.is_internal; let fields = data @@ -73,8 +69,6 @@ pub fn expand(input: TokenStream) -> syn::Result { .context .map_or_else(|| parse_quote! { () }, SpanContainer::into_inner); - let description = attr.description.map(|d| d.into_inner().into_boxed_str()); - let scalar = scalar::Type::parse(attr.scalar.as_deref(), &ast.generics); proc_macro_error::abort_if_dirty(); @@ -83,7 +77,7 @@ pub fn expand(input: TokenStream) -> syn::Result { ident: ast.ident, generics: ast.generics, name, - description, + description: attr.description.map(SpanContainer::into_inner), context, scalar, fields, @@ -95,7 +89,11 @@ pub fn expand(input: TokenStream) -> syn::Result { /// Parses a [`FieldDefinition`] from the given struct field definition. /// /// Returns [`None`] if the parsing fails. -fn parse_field(f: &syn::Field, renaming: RenameRule, is_internal: bool) -> Option { +fn parse_field( + f: &syn::Field, + renaming: rename::Policy, + is_internal: bool, +) -> Option { let field_attr = FieldAttr::from_attrs("graphql", &f.attrs) .map_err(|e| proc_macro_error::emit_error!(e)) .ok()?; @@ -113,17 +111,12 @@ fn parse_field(f: &syn::Field, renaming: RenameRule, is_internal: bool) -> Optio ERR.no_double_underscore(f.span()); } - let default = field_attr.default.map(SpanContainer::into_inner); - let description = field_attr - .description - .map(|d| d.into_inner().into_boxed_str()); - Some(FieldDefinition { ident: ident.clone(), ty: f.ty.clone(), - default, + default: field_attr.default.map(SpanContainer::into_inner), name, - description, + description: field_attr.description.map(SpanContainer::into_inner), ignored: field_attr.ignore.is_some(), }) } diff --git a/juniper_codegen/src/graphql_input_object/mod.rs b/juniper_codegen/src/graphql_input_object/mod.rs index ecb0aaa8..6e0f18ae 100644 --- a/juniper_codegen/src/graphql_input_object/mod.rs +++ b/juniper_codegen/src/graphql_input_object/mod.rs @@ -16,16 +16,13 @@ use syn::{ token, }; -use crate::{ - common::{ - default, - parse::{ - attr::{err, OptionExt as _}, - ParseBufferExt as _, - }, - scalar, +use crate::common::{ + default, filter_attrs, + parse::{ + attr::{err, OptionExt as _}, + ParseBufferExt as _, }, - util::{filter_attrs, get_doc_comment, span_container::SpanContainer, RenameRule}, + rename, scalar, Description, SpanContainer, }; /// Available arguments behind `#[graphql]` attribute placed on a Rust struct @@ -48,7 +45,7 @@ struct ContainerAttr { /// /// [0]: https://spec.graphql.org/October2021#sec-Input-Objects /// [2]: https://spec.graphql.org/October2021#sec-Descriptions - description: Option>, + description: Option>, /// Explicitly specified type of [`Context`] to use for resolving this /// [GraphQL input object][0] type with. @@ -71,14 +68,14 @@ struct ContainerAttr { /// [0]: https://spec.graphql.org/October2021#sec-Input-Objects scalar: Option>, - /// Explicitly specified [`RenameRule`] for all fields of this + /// Explicitly specified [`rename::Policy`] for all fields of this /// [GraphQL input object][0]. /// - /// If [`None`], then the [`RenameRule::CamelCase`] rule will be - /// applied by default. + /// If [`None`], then the [`rename::Policy::CamelCase`] will be applied by + /// default. /// /// [0]: https://spec.graphql.org/October2021#sec-Input-Objects - rename_fields: Option>, + rename_fields: Option>, /// Indicator whether the generated code is intended to be used only inside /// the [`juniper`] library. @@ -104,13 +101,9 @@ impl Parse for ContainerAttr { } "desc" | "description" => { input.parse::()?; - let desc = input.parse::()?; + let desc = input.parse::()?; out.description - .replace(SpanContainer::new( - ident.span(), - Some(desc.span()), - desc.value(), - )) + .replace(SpanContainer::new(ident.span(), Some(desc.span()), desc)) .none_or_else(|_| err::dup_arg(&ident))? } "ctx" | "context" | "Context" => { @@ -173,7 +166,7 @@ impl ContainerAttr { .try_fold(Self::default(), |prev, curr| prev.try_merge(curr?))?; if attr.description.is_none() { - attr.description = get_doc_comment(attrs); + attr.description = Description::parse_from_doc_attrs(attrs)?; } Ok(attr) @@ -212,7 +205,7 @@ struct FieldAttr { /// /// [1]: https://spec.graphql.org/October2021#InputValueDefinition /// [2]: https://spec.graphql.org/October2021#sec-Descriptions - description: Option>, + description: Option>, /// Explicitly specified marker for the Rust struct field to be ignored and /// not included into the code generated for a [GraphQL input object][0] @@ -251,13 +244,9 @@ impl Parse for FieldAttr { } "desc" | "description" => { input.parse::()?; - let desc = input.parse::()?; + let desc = input.parse::()?; out.description - .replace(SpanContainer::new( - ident.span(), - Some(desc.span()), - desc.value(), - )) + .replace(SpanContainer::new(ident.span(), Some(desc.span()), desc)) .none_or_else(|_| err::dup_arg(&ident))? } "ignore" | "skip" => out @@ -294,7 +283,7 @@ impl FieldAttr { .try_fold(Self::default(), |prev, curr| prev.try_merge(curr?))?; if attr.description.is_none() { - attr.description = get_doc_comment(attrs); + attr.description = Description::parse_from_doc_attrs(attrs)?; } Ok(attr) @@ -337,7 +326,7 @@ struct FieldDefinition { /// /// [1]: https://spec.graphql.org/October2021#InputValueDefinition /// [2]: https://spec.graphql.org/October2021#sec-Descriptions - description: Option>, + description: Option, /// Indicator whether the Rust struct field behinds this /// [GraphQL input object field][1] is being ignored and should not be @@ -378,7 +367,7 @@ struct Definition { /// /// [0]: https://spec.graphql.org/October2021#sec-Input-Objects /// [2]: https://spec.graphql.org/October2021#sec-Descriptions - description: Option>, + description: Option, /// Rust type of [`Context`] to generate [`GraphQLType`] implementation with /// for this [GraphQL input object][0]. @@ -468,10 +457,7 @@ impl Definition { let (impl_generics, _, where_clause) = generics.split_for_impl(); let (_, ty_generics, _) = self.generics.split_for_impl(); - let description = self - .description - .as_ref() - .map(|desc| quote! { .description(#desc) }); + let description = &self.description; let fields = self.fields.iter().filter_map(|f| { let ty = &f.ty; @@ -483,10 +469,7 @@ impl Definition { } else { quote! { .arg::<#ty>(#name, info) } }; - let description = f - .description - .as_ref() - .map(|desc| quote! { .description(#desc) }); + let description = &f.description; quote! { registry#arg#description } }) diff --git a/juniper_codegen/src/graphql_interface/attr.rs b/juniper_codegen/src/graphql_interface/attr.rs index e53c99dd..e1d1f44d 100644 --- a/juniper_codegen/src/graphql_interface/attr.rs +++ b/juniper_codegen/src/graphql_interface/attr.rs @@ -6,20 +6,16 @@ use proc_macro2::{Span, TokenStream}; use quote::quote; use syn::{ext::IdentExt as _, parse_quote, spanned::Spanned}; -use crate::{ - common::{ - field, - parse::{self, TypeExt as _}, - scalar, - }, - result::GraphQLScope, - util::{path_eq_single, span_container::SpanContainer, RenameRule}, +use crate::common::{ + diagnostic, field, + parse::{self, TypeExt as _}, + path_eq_single, rename, scalar, SpanContainer, }; use super::{enum_idents, Attr, Definition}; -/// [`GraphQLScope`] of errors for `#[graphql_interface]` macro. -const ERR: GraphQLScope = GraphQLScope::InterfaceAttr; +/// [`diagnostic::Scope`] of errors for `#[graphql_interface]` macro. +const ERR: diagnostic::Scope = diagnostic::Scope::InterfaceAttr; /// Expands `#[graphql_interface]` macro into generated code. pub fn expand(attr_args: TokenStream, body: TokenStream) -> syn::Result { @@ -74,7 +70,7 @@ fn expand_on_trait( .rename_fields .as_deref() .copied() - .unwrap_or(RenameRule::CamelCase); + .unwrap_or(rename::Policy::CamelCase); let fields = ast .items @@ -121,7 +117,7 @@ fn expand_on_trait( enum_ident, enum_alias_ident, name, - description: attr.description.map(|d| d.into_inner().into_boxed_str()), + description: attr.description.map(SpanContainer::into_inner), context, scalar, fields, @@ -151,7 +147,7 @@ fn expand_on_trait( #[must_use] fn parse_trait_method( method: &mut syn::TraitItemMethod, - renaming: &RenameRule, + renaming: &rename::Policy, ) -> Option { let method_ident = &method.sig.ident; let method_attrs = method.attrs.clone(); @@ -205,17 +201,11 @@ fn parse_trait_method( }; ty.lifetimes_anonymized(); - let description = attr.description.as_ref().map(|d| d.as_ref().value()); - let deprecated = attr - .deprecated - .as_deref() - .map(|d| d.as_ref().map(syn::LitStr::value)); - Some(field::Definition { name, ty, - description, - deprecated, + description: attr.description.map(SpanContainer::into_inner), + deprecated: attr.deprecated.map(SpanContainer::into_inner), ident: method_ident.clone(), arguments: Some(arguments), has_receiver: method.sig.receiver().is_some(), @@ -267,7 +257,7 @@ fn expand_on_derive_input( .rename_fields .as_deref() .copied() - .unwrap_or(RenameRule::CamelCase); + .unwrap_or(rename::Policy::CamelCase); let fields = data .fields @@ -308,7 +298,7 @@ fn expand_on_derive_input( enum_ident, enum_alias_ident, name, - description: attr.description.map(|d| d.into_inner().into_boxed_str()), + description: attr.description.map(SpanContainer::into_inner), context, scalar, fields, @@ -337,7 +327,10 @@ fn expand_on_derive_input( /// /// Returns [`None`] if the parsing fails, or the struct field is ignored. #[must_use] -fn parse_struct_field(field: &mut syn::Field, renaming: &RenameRule) -> Option { +fn parse_struct_field( + field: &mut syn::Field, + renaming: &rename::Policy, +) -> Option { let field_ident = field.ident.as_ref().or_else(|| err_unnamed_field(&field))?; let field_attrs = field.attrs.clone(); @@ -373,17 +366,11 @@ fn parse_struct_field(field: &mut syn::Field, renaming: &RenameRule) -> Option syn::Result { @@ -52,7 +48,7 @@ pub fn expand(input: TokenStream) -> syn::Result { .rename_fields .as_deref() .copied() - .unwrap_or(RenameRule::CamelCase); + .unwrap_or(rename::Policy::CamelCase); let fields = data .fields @@ -94,7 +90,7 @@ pub fn expand(input: TokenStream) -> syn::Result { enum_ident, enum_alias_ident, name, - description: attr.description.map(|d| d.into_inner().into_boxed_str()), + description: attr.description.map(SpanContainer::into_inner), context, scalar, fields, @@ -118,7 +114,7 @@ pub fn expand(input: TokenStream) -> syn::Result { /// /// Returns [`None`] if the parsing fails, or the struct field is ignored. #[must_use] -fn parse_field(field: &syn::Field, renaming: &RenameRule) -> Option { +fn parse_field(field: &syn::Field, renaming: &rename::Policy) -> Option { let field_ident = field.ident.as_ref().or_else(|| err_unnamed_field(&field))?; let attr = field::Attr::from_attrs("graphql", &field.attrs) @@ -147,17 +143,11 @@ fn parse_field(field: &syn::Field, renaming: &RenameRule) -> Option>, + description: Option>, /// Explicitly specified identifier of the type alias of Rust enum type /// behind the trait or struct, being an actual implementation of a @@ -126,13 +123,14 @@ struct Attr { /// it contains async methods. asyncness: Option>, - /// Explicitly specified [`RenameRule`] for all fields of this + /// Explicitly specified [`rename::Policy`] for all fields of this /// [GraphQL interface][1] type. /// - /// If [`None`] then the default rule will be [`RenameRule::CamelCase`]. + /// If [`None`], then the [`rename::Policy::CamelCase`] will be applied by + /// default. /// /// [1]: https://spec.graphql.org/October2021#sec-Interfaces - rename_fields: Option>, + rename_fields: Option>, /// Indicator whether the generated code is intended to be used only inside /// the [`juniper`] library. @@ -158,13 +156,9 @@ impl Parse for Attr { } "desc" | "description" => { input.parse::()?; - let desc = input.parse::()?; + let desc = input.parse::()?; out.description - .replace(SpanContainer::new( - ident.span(), - Some(desc.span()), - desc.value(), - )) + .replace(SpanContainer::new(ident.span(), Some(desc.span()), desc)) .none_or_else(|_| err::dup_arg(&ident))? } "ctx" | "context" | "Context" => { @@ -268,7 +262,7 @@ impl Attr { .try_fold(Self::default(), |prev, curr| prev.try_merge(curr?))?; if attr.description.is_none() { - attr.description = get_doc_comment(attrs); + attr.description = Description::parse_from_doc_attrs(attrs)?; } Ok(attr) @@ -312,7 +306,7 @@ struct Definition { /// Description of this [GraphQL interface][0] to put into GraphQL schema. /// /// [0]: https://spec.graphql.org/October2021#sec-Interfaces - description: Option>, + description: Option, /// Rust type of [`Context`] to generate [`GraphQLType`] implementation with /// for this [GraphQL interface][1]. @@ -690,10 +684,7 @@ impl Definition { let (_, ty_generics, _) = self.generics.split_for_impl(); let name = &self.name; - let description = self - .description - .as_ref() - .map(|desc| quote! { .description(#desc) }); + let description = &self.description; // Sorting is required to preserve/guarantee the order of implementers registered in schema. let mut implemented_for = self.implemented_for.clone(); diff --git a/juniper_codegen/src/graphql_object/attr.rs b/juniper_codegen/src/graphql_object/attr.rs index 34a70c21..0c4c7c05 100644 --- a/juniper_codegen/src/graphql_object/attr.rs +++ b/juniper_codegen/src/graphql_object/attr.rs @@ -6,20 +6,16 @@ use proc_macro2::{Span, TokenStream}; use quote::{quote, ToTokens}; use syn::{ext::IdentExt as _, parse_quote, spanned::Spanned}; -use crate::{ - common::{ - field, - parse::{self, TypeExt as _}, - scalar, - }, - result::GraphQLScope, - util::{path_eq_single, span_container::SpanContainer, RenameRule}, +use crate::common::{ + diagnostic, field, + parse::{self, TypeExt as _}, + path_eq_single, rename, scalar, SpanContainer, }; use super::{Attr, Definition, Query}; -/// [`GraphQLScope`] of errors for `#[graphql_object]` macro. -const ERR: GraphQLScope = GraphQLScope::ObjectAttr; +/// [`diagnostic::Scope`] of errors for `#[graphql_object]` macro. +const ERR: diagnostic::Scope = diagnostic::Scope::ObjectAttr; /// Expands `#[graphql_object]` macro into generated code. pub fn expand(attr_args: TokenStream, body: TokenStream) -> syn::Result { @@ -73,7 +69,7 @@ where .rename_fields .as_deref() .copied() - .unwrap_or(RenameRule::CamelCase); + .unwrap_or(rename::Policy::CamelCase); let async_only = TypeId::of::() != TypeId::of::(); let fields: Vec<_> = ast @@ -143,7 +139,7 @@ where fn parse_field( method: &mut syn::ImplItemMethod, async_only: bool, - renaming: &RenameRule, + renaming: &rename::Policy, ) -> Option { let method_attrs = method.attrs.clone(); @@ -216,17 +212,11 @@ fn parse_field( }; ty.lifetimes_anonymized(); - let description = attr.description.as_ref().map(|d| d.as_ref().value()); - let deprecated = attr - .deprecated - .as_deref() - .map(|d| d.as_ref().map(syn::LitStr::value)); - Some(field::Definition { name, ty, - description, - deprecated, + description: attr.description.map(SpanContainer::into_inner), + deprecated: attr.deprecated.map(SpanContainer::into_inner), ident: method_ident.clone(), arguments: Some(arguments), has_receiver: method.sig.receiver().is_some(), diff --git a/juniper_codegen/src/graphql_object/derive.rs b/juniper_codegen/src/graphql_object/derive.rs index 7cbe961b..f4e75b4e 100644 --- a/juniper_codegen/src/graphql_object/derive.rs +++ b/juniper_codegen/src/graphql_object/derive.rs @@ -7,16 +7,12 @@ use proc_macro_error::ResultExt as _; use quote::ToTokens; use syn::{ext::IdentExt as _, parse_quote, spanned::Spanned as _}; -use crate::{ - common::{field, parse::TypeExt as _, scalar}, - result::GraphQLScope, - util::{span_container::SpanContainer, RenameRule}, -}; +use crate::common::{diagnostic, field, parse::TypeExt as _, rename, scalar, SpanContainer}; use super::{Attr, Definition, Query}; -/// [`GraphQLScope`] of errors for `#[derive(GraphQLObject)]` macro. -const ERR: GraphQLScope = GraphQLScope::ObjectDerive; +/// [`diagnostic::Scope`] of errors for `#[derive(GraphQLObject)]` macro. +const ERR: diagnostic::Scope = diagnostic::Scope::ObjectDerive; /// Expands `#[derive(GraphQLObject)]` macro into generated code. pub fn expand(input: TokenStream) -> syn::Result { @@ -62,7 +58,7 @@ fn expand_struct(ast: syn::DeriveInput) -> syn::Result> { .rename_fields .as_deref() .copied() - .unwrap_or(RenameRule::CamelCase); + .unwrap_or(rename::Policy::CamelCase); let mut fields = vec![]; if let syn::Data::Struct(data) = &ast.data { @@ -112,7 +108,7 @@ fn expand_struct(ast: syn::DeriveInput) -> syn::Result> { /// /// Returns [`None`] if parsing fails, or the struct field is ignored. #[must_use] -fn parse_field(field: &syn::Field, renaming: &RenameRule) -> Option { +fn parse_field(field: &syn::Field, renaming: &rename::Policy) -> Option { let attr = field::Attr::from_attrs("graphql", &field.attrs) .map_err(|e| proc_macro_error::emit_error!(e)) .ok()?; @@ -141,17 +137,11 @@ fn parse_field(field: &syn::Field, renaming: &RenameRule) -> Option>, + pub(crate) description: Option>, /// Explicitly specified type of [`Context`] to use for resolving this /// [GraphQL object][1] type with. @@ -81,13 +78,14 @@ pub(crate) struct Attr { /// [2]: https://spec.graphql.org/October2021#sec-Interfaces pub(crate) interfaces: HashSet>, - /// Explicitly specified [`RenameRule`] for all fields of this + /// Explicitly specified [`rename::Policy`] for all fields of this /// [GraphQL object][1] type. /// - /// If [`None`] then the default rule will be [`RenameRule::CamelCase`]. + /// If [`None`], then the [`rename::Policy::CamelCase`] will be applied by + /// default. /// /// [1]: https://spec.graphql.org/October2021#sec-Objects - pub(crate) rename_fields: Option>, + pub(crate) rename_fields: Option>, /// Indicator whether the generated code is intended to be used only inside /// the [`juniper`] library. @@ -113,13 +111,9 @@ impl Parse for Attr { } "desc" | "description" => { input.parse::()?; - let desc = input.parse::()?; + let desc = input.parse::()?; out.description - .replace(SpanContainer::new( - ident.span(), - Some(desc.span()), - desc.value(), - )) + .replace(SpanContainer::new(ident.span(), Some(desc.span()), desc)) .none_or_else(|_| err::dup_arg(&ident))? } "ctx" | "context" | "Context" => { @@ -195,7 +189,7 @@ impl Attr { .try_fold(Self::default(), |prev, curr| prev.try_merge(curr?))?; if attr.description.is_none() { - attr.description = get_doc_comment(attrs); + attr.description = Description::parse_from_doc_attrs(attrs)?; } Ok(attr) @@ -228,7 +222,7 @@ pub(crate) struct Definition { /// Description of this [GraphQL object][1] to put into GraphQL schema. /// /// [1]: https://spec.graphql.org/October2021#sec-Objects - pub(crate) description: Option, + pub(crate) description: Option, /// Rust type of [`Context`] to generate [`GraphQLType`] implementation with /// for this [GraphQL object][1]. @@ -437,10 +431,7 @@ impl Definition { let ty = &self.ty; let name = &self.name; - let description = self - .description - .as_ref() - .map(|desc| quote! { .description(#desc) }); + let description = &self.description; let extract_stream_type = TypeId::of::() != TypeId::of::(); let fields_meta = self diff --git a/juniper_codegen/src/graphql_scalar/attr.rs b/juniper_codegen/src/graphql_scalar/attr.rs index b9ff5b58..90024820 100644 --- a/juniper_codegen/src/graphql_scalar/attr.rs +++ b/juniper_codegen/src/graphql_scalar/attr.rs @@ -4,15 +4,12 @@ use proc_macro2::{Span, TokenStream}; use quote::quote; use syn::{parse_quote, spanned::Spanned}; -use crate::{ - common::{parse, scalar}, - graphql_scalar::TypeOrIdent, - GraphQLScope, -}; +use crate::common::{diagnostic, parse, scalar, SpanContainer}; -use super::{derive::parse_derived_methods, Attr, Definition, Methods, ParseToken}; +use super::{derive::parse_derived_methods, Attr, Definition, Methods, ParseToken, TypeOrIdent}; -const ERR: GraphQLScope = GraphQLScope::ScalarAttr; +/// [`diagnostic::Scope`] of errors for `#[graphql_scalar]` macro. +const ERR: diagnostic::Scope = diagnostic::Scope::ScalarAttr; /// Expands `#[graphql_scalar]` macro into generated code. pub(crate) fn expand(attr_args: TokenStream, body: TokenStream) -> syn::Result { @@ -58,11 +55,10 @@ fn expand_on_type_alias( methods, name: attr .name - .as_deref() - .cloned() + .map(SpanContainer::into_inner) .unwrap_or_else(|| ast.ident.to_string()), - description: attr.description.as_deref().cloned(), - specified_by_url: attr.specified_by_url.as_deref().cloned(), + description: attr.description.map(SpanContainer::into_inner), + specified_by_url: attr.specified_by_url.map(SpanContainer::into_inner), scalar, }; @@ -90,11 +86,10 @@ fn expand_on_derive_input( methods, name: attr .name - .as_deref() - .cloned() + .map(SpanContainer::into_inner) .unwrap_or_else(|| ast.ident.to_string()), - description: attr.description.as_deref().cloned(), - specified_by_url: attr.specified_by_url.as_deref().cloned(), + description: attr.description.map(SpanContainer::into_inner), + specified_by_url: attr.specified_by_url.map(SpanContainer::into_inner), scalar, }; diff --git a/juniper_codegen/src/graphql_scalar/derive.rs b/juniper_codegen/src/graphql_scalar/derive.rs index e16bcd7d..ff4f93b1 100644 --- a/juniper_codegen/src/graphql_scalar/derive.rs +++ b/juniper_codegen/src/graphql_scalar/derive.rs @@ -4,12 +4,12 @@ use proc_macro2::TokenStream; use quote::ToTokens; use syn::{parse_quote, spanned::Spanned}; -use crate::{common::scalar, result::GraphQLScope}; +use crate::common::{diagnostic, scalar, SpanContainer}; use super::{Attr, Definition, Field, Methods, ParseToken, TypeOrIdent}; -/// [`GraphQLScope`] of errors for `#[derive(GraphQLScalar)]` macro. -const ERR: GraphQLScope = GraphQLScope::ScalarDerive; +/// [`diagnostic::Scope`] of errors for `#[derive(GraphQLScalar)]` macro. +const ERR: diagnostic::Scope = diagnostic::Scope::ScalarDerive; /// Expands `#[derive(GraphQLScalar)]` macro into generated code. pub fn expand(input: TokenStream) -> syn::Result { @@ -27,11 +27,10 @@ pub fn expand(input: TokenStream) -> syn::Result { methods, name: attr .name - .as_deref() - .cloned() + .map(SpanContainer::into_inner) .unwrap_or_else(|| ast.ident.to_string()), - description: attr.description.as_deref().cloned(), - specified_by_url: attr.specified_by_url.as_deref().cloned(), + description: attr.description.map(SpanContainer::into_inner), + specified_by_url: attr.specified_by_url.map(SpanContainer::into_inner), scalar, } .to_token_stream()) diff --git a/juniper_codegen/src/graphql_scalar/mod.rs b/juniper_codegen/src/graphql_scalar/mod.rs index 37b78db7..d0758e75 100644 --- a/juniper_codegen/src/graphql_scalar/mod.rs +++ b/juniper_codegen/src/graphql_scalar/mod.rs @@ -14,15 +14,13 @@ use syn::{ }; use url::Url; -use crate::{ - common::{ - parse::{ - attr::{err, OptionExt as _}, - ParseBufferExt as _, - }, - scalar, +use crate::common::{ + filter_attrs, + parse::{ + attr::{err, OptionExt as _}, + ParseBufferExt as _, }, - util::{filter_attrs, get_doc_comment, span_container::SpanContainer}, + scalar, Description, SpanContainer, }; pub mod attr; @@ -42,7 +40,7 @@ struct Attr { /// Description of this [GraphQL scalar][1] to put into GraphQL schema. /// /// [1]: https://spec.graphql.org/October2021#sec-Scalars - description: Option>, + description: Option>, /// Spec [`Url`] of this [GraphQL scalar][1] to put into GraphQL schema. /// @@ -112,13 +110,9 @@ impl Parse for Attr { } "desc" | "description" => { input.parse::()?; - let desc = input.parse::()?; + let desc = input.parse::()?; out.description - .replace(SpanContainer::new( - ident.span(), - Some(desc.span()), - desc.value(), - )) + .replace(SpanContainer::new(ident.span(), Some(desc.span()), desc)) .none_or_else(|_| err::dup_arg(&ident))? } "specified_by_url" => { @@ -255,7 +249,7 @@ impl Attr { .try_fold(Self::default(), |prev, curr| prev.try_merge(curr?))?; if attr.description.is_none() { - attr.description = get_doc_comment(attrs); + attr.description = Description::parse_from_doc_attrs(attrs)?; } Ok(attr) @@ -304,7 +298,7 @@ struct Definition { /// Description of this [GraphQL scalar][1] to put into GraphQL schema. /// /// [1]: https://spec.graphql.org/October2021#sec-Scalars - description: Option, + description: Option, /// Spec [`Url`] of this [GraphQL scalar][1] to put into GraphQL schema. /// @@ -365,12 +359,9 @@ impl Definition { /// [1]: https://spec.graphql.org/October2021#sec-Scalars fn impl_type_tokens(&self) -> TokenStream { let scalar = &self.scalar; - let name = &self.name; - let description = self - .description - .as_ref() - .map(|val| quote! { .description(#val) }); + let name = &self.name; + let description = &self.description; let specified_by_url = self.specified_by_url.as_ref().map(|url| { let url_lit = url.as_str(); quote! { .specified_by_url(#url_lit) } diff --git a/juniper_codegen/src/graphql_union/attr.rs b/juniper_codegen/src/graphql_union/attr.rs index fb9c2914..733cfc99 100644 --- a/juniper_codegen/src/graphql_union/attr.rs +++ b/juniper_codegen/src/graphql_union/attr.rs @@ -6,19 +6,15 @@ use proc_macro2::{Span, TokenStream}; use quote::{quote, ToTokens as _}; use syn::{ext::IdentExt as _, parse_quote, spanned::Spanned as _}; -use crate::{ - common::{parse, scalar}, - result::GraphQLScope, - util::{path_eq_single, span_container::SpanContainer}, -}; +use crate::common::{diagnostic, parse, path_eq_single, scalar, SpanContainer}; use super::{ all_variants_different, emerge_union_variants_from_attr, Attr, Definition, VariantAttr, VariantDefinition, }; -/// [`GraphQLScope`] of errors for `#[graphql_union]` macro. -const ERR: GraphQLScope = GraphQLScope::UnionAttr; +/// [`diagnostic::Scope`] of errors for `#[graphql_union]` macro. +const ERR: diagnostic::Scope = diagnostic::Scope::UnionAttr; /// Expands `#[graphql_union]` macro into generated code. pub fn expand(attr_args: TokenStream, body: TokenStream) -> syn::Result { diff --git a/juniper_codegen/src/graphql_union/derive.rs b/juniper_codegen/src/graphql_union/derive.rs index de6eb495..a10be60c 100644 --- a/juniper_codegen/src/graphql_union/derive.rs +++ b/juniper_codegen/src/graphql_union/derive.rs @@ -5,19 +5,15 @@ use proc_macro_error::ResultExt as _; use quote::{quote, ToTokens}; use syn::{ext::IdentExt as _, parse_quote, spanned::Spanned as _, Data, Fields}; -use crate::{ - common::{parse::TypeExt as _, scalar}, - result::GraphQLScope, - util::span_container::SpanContainer, -}; +use crate::common::{diagnostic, parse::TypeExt as _, scalar, SpanContainer}; use super::{ all_variants_different, emerge_union_variants_from_attr, Attr, Definition, VariantAttr, VariantDefinition, }; -/// [`GraphQLScope`] of errors for `#[derive(GraphQLUnion)]` macro. -const ERR: GraphQLScope = GraphQLScope::UnionDerive; +/// [`diagnostic::Scope`] of errors for `#[derive(GraphQLUnion)]` macro. +const ERR: diagnostic::Scope = diagnostic::Scope::UnionDerive; /// Expands `#[derive(GraphQLUnion)]` macro into generated code. pub fn expand(input: TokenStream) -> syn::Result { diff --git a/juniper_codegen/src/graphql_union/mod.rs b/juniper_codegen/src/graphql_union/mod.rs index 34984ee8..e49f0fbf 100644 --- a/juniper_codegen/src/graphql_union/mod.rs +++ b/juniper_codegen/src/graphql_union/mod.rs @@ -17,16 +17,13 @@ use syn::{ token, }; -use crate::{ - common::{ - gen, - parse::{ - attr::{err, OptionExt as _}, - ParseBufferExt as _, - }, - scalar, +use crate::common::{ + filter_attrs, gen, + parse::{ + attr::{err, OptionExt as _}, + ParseBufferExt as _, }, - util::{filter_attrs, get_doc_comment, span_container::SpanContainer}, + scalar, Description, SpanContainer, }; /// Helper alias for the type of [`Attr::external_resolvers`] field. @@ -52,7 +49,7 @@ struct Attr { /// /// [1]: https://spec.graphql.org/October2021#sec-Unions /// [2]: https://spec.graphql.org/October2021#sec-Descriptions - description: Option>, + description: Option>, /// Explicitly specified type of [`Context`] to use for resolving this /// [GraphQL union][1] type with. @@ -112,13 +109,9 @@ impl Parse for Attr { } "desc" | "description" => { input.parse::()?; - let desc = input.parse::()?; + let desc = input.parse::()?; out.description - .replace(SpanContainer::new( - ident.span(), - Some(desc.span()), - desc.value(), - )) + .replace(SpanContainer::new(ident.span(), Some(desc.span()), desc)) .none_or_else(|_| err::dup_arg(&ident))? } "ctx" | "context" | "Context" => { @@ -182,7 +175,7 @@ impl Attr { .try_fold(Self::default(), |prev, curr| prev.try_merge(curr?))?; if meta.description.is_none() { - meta.description = get_doc_comment(attrs); + meta.description = Description::parse_from_doc_attrs(attrs)?; } Ok(meta) @@ -284,7 +277,7 @@ struct Definition { /// Description of this [GraphQL union][1] to put into GraphQL schema. /// /// [1]: https://spec.graphql.org/October2021#sec-Unions - description: Option, + description: Option, /// Rust type of [`Context`] to generate [`GraphQLType`] implementation with /// for this [GraphQL union][1]. @@ -471,10 +464,7 @@ impl Definition { let (impl_generics, ty_full, where_clause) = self.impl_generics(false); let name = &self.name; - let description = self - .description - .as_ref() - .map(|desc| quote! { .description(#desc) }); + let description = &self.description; let variant_tys = self.variants.iter().map(|var| &var.ty); diff --git a/juniper_codegen/src/lib.rs b/juniper_codegen/src/lib.rs index 19275174..d77a21b5 100644 --- a/juniper_codegen/src/lib.rs +++ b/juniper_codegen/src/lib.rs @@ -1,9 +1,6 @@ #![doc = include_str!("../README.md")] #![recursion_limit = "1024"] -mod result; -mod util; - // NOTICE: Unfortunately this macro MUST be defined here, in the crate's root module, because Rust // doesn't allow to export `macro_rules!` macros from a `proc-macro` crate type currently, // and so we cannot move the definition into a sub-module and use the `#[macro_export]` @@ -16,8 +13,8 @@ mod util; /// By default, [`SpanContainer::span_ident`] is used. /// /// [`Span`]: proc_macro2::Span -/// [`SpanContainer`]: crate::util::span_container::SpanContainer -/// [`SpanContainer::span_ident`]: crate::util::span_container::SpanContainer::span_ident +/// [`SpanContainer`]: crate::common::SpanContainer +/// [`SpanContainer::span_ident`]: crate::common::SpanContainer::span_ident macro_rules! try_merge_opt { ($field:ident: $self:ident, $another:ident => $span:ident) => {{ if let Some(v) = $self.$field { @@ -47,8 +44,8 @@ macro_rules! try_merge_opt { /// /// [`HashMap`]: std::collections::HashMap /// [`Span`]: proc_macro2::Span -/// [`SpanContainer`]: crate::util::span_container::SpanContainer -/// [`SpanContainer::span_ident`]: crate::util::span_container::SpanContainer::span_ident +/// [`SpanContainer`]: crate::common::SpanContainer +/// [`SpanContainer::span_ident`]: crate::common::SpanContainer::span_ident macro_rules! try_merge_hashmap { ($field:ident: $self:ident, $another:ident => $span:ident) => {{ if !$self.$field.is_empty() { @@ -80,8 +77,8 @@ macro_rules! try_merge_hashmap { /// /// [`HashSet`]: std::collections::HashSet /// [`Span`]: proc_macro2::Span -/// [`SpanContainer`]: crate::util::span_container::SpanContainer -/// [`SpanContainer::span_ident`]: crate::util::span_container::SpanContainer::span_ident +/// [`SpanContainer`]: crate::common::SpanContainer +/// [`SpanContainer::span_ident`]: crate::common::SpanContainer::span_ident macro_rules! try_merge_hashset { ($field:ident: $self:ident, $another:ident => $span:ident) => {{ if !$self.$field.is_empty() { @@ -112,7 +109,6 @@ mod scalar_value; use proc_macro::TokenStream; use proc_macro_error::{proc_macro_error, ResultExt as _}; -use result::GraphQLScope; /// `#[derive(GraphQLInputObject)]` macro for deriving a /// [GraphQL input object][0] implementation for a Rust struct. Each @@ -257,6 +253,8 @@ pub fn derive_input_object(input: TokenStream) -> TokenStream { /// attribute's argument, or with regular a Rust `#[deprecated]` attribute. /// /// ```rust +/// # #![allow(deprecated)] +/// # /// # use juniper::GraphQLEnum; /// # /// #[derive(GraphQLEnum)] diff --git a/juniper_codegen/src/scalar_value/mod.rs b/juniper_codegen/src/scalar_value/mod.rs index 023ba148..fe9aa6de 100644 --- a/juniper_codegen/src/scalar_value/mod.rs +++ b/juniper_codegen/src/scalar_value/mod.rs @@ -12,14 +12,14 @@ use syn::{ visit::Visit, }; -use crate::{ - common::parse::{attr::err, ParseBufferExt as _}, - util::{filter_attrs, span_container::SpanContainer}, - GraphQLScope, +use crate::common::{ + diagnostic, filter_attrs, + parse::{attr::err, ParseBufferExt as _}, + SpanContainer, }; -/// [`GraphQLScope`] of errors for `#[derive(ScalarValue)]` macro. -const ERR: GraphQLScope = GraphQLScope::ScalarValueDerive; +/// [`diagnostic::Scope`] of errors for `#[derive(ScalarValue)]` macro. +const ERR: diagnostic::Scope = diagnostic::Scope::ScalarValueDerive; /// Expands `#[derive(ScalarValue)]` macro into generated code. pub fn expand_derive(input: TokenStream) -> syn::Result { diff --git a/juniper_codegen/src/util/mod.rs b/juniper_codegen/src/util/mod.rs deleted file mode 100644 index 7edc1f44..00000000 --- a/juniper_codegen/src/util/mod.rs +++ /dev/null @@ -1,431 +0,0 @@ -#![allow(clippy::single_match)] - -pub mod span_container; - -use std::{convert::TryFrom, str::FromStr}; - -use proc_macro_error::abort; -use span_container::SpanContainer; -use syn::{ - parse::{Parse, ParseStream}, - spanned::Spanned, - Attribute, Lit, Meta, MetaList, MetaNameValue, NestedMeta, -}; - -/// Compares a path to a one-segment string value, -/// return true if equal. -pub fn path_eq_single(path: &syn::Path, value: &str) -> bool { - path.segments.len() == 1 && path.segments[0].ident == value -} - -#[derive(Debug)] -pub struct DeprecationAttr { - pub reason: Option, -} - -/// Filters given `attrs` to contain attributes only with the given `name`. -pub fn filter_attrs<'a>( - name: &'a str, - attrs: &'a [Attribute], -) -> impl Iterator + 'a { - attrs - .iter() - .filter(move |attr| path_eq_single(&attr.path, name)) -} - -pub fn get_deprecated(attrs: &[Attribute]) -> Option> { - attrs - .iter() - .filter_map(|attr| match attr.parse_meta() { - Ok(Meta::List(ref list)) if list.path.is_ident("deprecated") => { - let val = get_deprecated_meta_list(list); - Some(SpanContainer::new(list.path.span(), None, val)) - } - Ok(Meta::Path(ref path)) if path.is_ident("deprecated") => Some(SpanContainer::new( - path.span(), - None, - DeprecationAttr { reason: None }, - )), - _ => None, - }) - .next() -} - -fn get_deprecated_meta_list(list: &MetaList) -> DeprecationAttr { - for meta in &list.nested { - if let NestedMeta::Meta(Meta::NameValue(ref nv)) = *meta { - if nv.path.is_ident("note") { - match nv.lit { - Lit::Str(ref strlit) => { - return DeprecationAttr { - reason: Some(strlit.value()), - }; - } - _ => abort!(syn::Error::new( - nv.lit.span(), - "only strings are allowed for deprecation", - )), - } - } else { - abort!(syn::Error::new( - nv.path.span(), - "unrecognized setting on #[deprecated(..)] attribute", - )); - } - } - } - DeprecationAttr { reason: None } -} - -// Gets doc comment. -pub fn get_doc_comment(attrs: &[Attribute]) -> Option> { - if let Some(items) = get_doc_attr(attrs) { - if let Some(doc_strings) = get_doc_strings(&items) { - return Some(doc_strings.map(|strings| join_doc_strings(&strings))); - } - } - None -} - -// Concatenates doc strings into one string. -fn join_doc_strings(docs: &[String]) -> String { - // Note: this is guaranteed since this function is only called - // from get_doc_strings(). - debug_assert!(!docs.is_empty()); - - let last_index = docs.len() - 1; - docs.iter() - .map(|s| s.as_str().trim_end()) - // Trim leading space. - .map(|s| s.strip_prefix(' ').unwrap_or(s)) - // Add newline, exept when string ends in a continuation backslash or is the last line. - .enumerate() - .fold(String::new(), |mut buffer, (index, s)| { - if index == last_index { - buffer.push_str(s); - } else if s.ends_with('\\') { - buffer.push_str(s.trim_end_matches('\\')); - buffer.push(' '); - } else { - buffer.push_str(s); - buffer.push('\n'); - } - buffer - }) -} - -// Gets doc strings from doc comment attributes. -fn get_doc_strings(items: &[MetaNameValue]) -> Option>> { - let mut span = None; - let comments = items - .iter() - .filter_map(|item| { - if item.path.is_ident("doc") { - match item.lit { - Lit::Str(ref strlit) => { - if span.is_none() { - span = Some(strlit.span()); - } - Some(strlit.value()) - } - _ => abort!(syn::Error::new( - item.lit.span(), - "doc attributes only have string literal" - )), - } - } else { - None - } - }) - .collect::>(); - span.map(|span| SpanContainer::new(span, None, comments)) -} - -// Gets doc comment attributes. -fn get_doc_attr(attrs: &[Attribute]) -> Option> { - let mut docs = Vec::new(); - for attr in attrs { - match attr.parse_meta() { - Ok(Meta::NameValue(ref nv)) if nv.path.is_ident("doc") => docs.push(nv.clone()), - _ => {} - } - } - if !docs.is_empty() { - return Some(docs); - } - None -} - -// Note: duplicated from juniper crate! -#[doc(hidden)] -pub fn to_camel_case(s: &str) -> String { - let mut dest = String::new(); - - // Handle `_` and `__` to be more friendly with the `_var` convention for unused variables, and - // GraphQL introspection identifiers. - let s_iter = if let Some(s) = s.strip_prefix("__") { - dest.push_str("__"); - s - } else { - s.strip_prefix('_').unwrap_or(s) - } - .split('_') - .enumerate(); - - for (i, part) in s_iter { - if i > 0 && part.len() == 1 { - dest.push_str(&part.to_uppercase()); - } else if i > 0 && part.len() > 1 { - let first = part - .chars() - .next() - .unwrap() - .to_uppercase() - .collect::(); - let second = &part[1..]; - - dest.push_str(&first); - dest.push_str(second); - } else if i == 0 { - dest.push_str(part); - } - } - - dest -} - -pub(crate) fn to_upper_snake_case(s: &str) -> String { - let mut last_lower = false; - let mut upper = String::new(); - for c in s.chars() { - if c == '_' { - last_lower = false; - } else if c.is_lowercase() { - last_lower = true; - } else if c.is_uppercase() { - if last_lower { - upper.push('_'); - } - last_lower = false; - } - - for u in c.to_uppercase() { - upper.push(u); - } - } - upper -} - -/// The different possible ways to change case of fields in a struct, or variants in an enum. -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum RenameRule { - /// Don't apply a default rename rule. - None, - /// Rename to "camelCase" style. - CamelCase, - /// Rename to "SCREAMING_SNAKE_CASE" style - ScreamingSnakeCase, -} - -impl RenameRule { - pub fn apply(&self, field: &str) -> String { - match self { - Self::None => field.to_owned(), - Self::CamelCase => to_camel_case(field), - Self::ScreamingSnakeCase => to_upper_snake_case(field), - } - } -} - -impl FromStr for RenameRule { - type Err = (); - - fn from_str(rule: &str) -> Result { - match rule { - "none" => Ok(Self::None), - "camelCase" => Ok(Self::CamelCase), - "SCREAMING_SNAKE_CASE" => Ok(Self::ScreamingSnakeCase), - _ => Err(()), - } - } -} - -impl TryFrom for RenameRule { - type Error = syn::Error; - - fn try_from(lit: syn::LitStr) -> syn::Result { - Self::from_str(&lit.value()).map_err(|_| syn::Error::new(lit.span(), "unknown rename rule")) - } -} - -impl Parse for RenameRule { - fn parse(input: ParseStream<'_>) -> syn::Result { - Self::try_from(input.parse::()?) - } -} - -#[cfg(test)] -mod test { - use proc_macro2::Span; - use syn::{Ident, LitStr}; - - use super::*; - - fn is_valid_name(field_name: &str) -> bool { - let mut chars = field_name.chars(); - - match chars.next() { - // first char can't be a digit - Some(c) if c.is_ascii_alphabetic() || c == '_' => (), - // can't be an empty string or any other character - _ => return false, - }; - - chars.all(|c| c.is_ascii_alphanumeric() || c == '_') - } - - fn strs_to_strings(source: Vec<&str>) -> Vec { - source - .iter() - .map(|x| (*x).to_string()) - .collect::>() - } - - fn litstr(s: &str) -> Lit { - Lit::Str(LitStr::new(s, Span::call_site())) - } - - fn ident(s: &str) -> Ident { - quote::format_ident!("{}", s) - } - - mod test_get_doc_strings { - use super::*; - - #[test] - fn test_single() { - let result = get_doc_strings(&[MetaNameValue { - path: ident("doc").into(), - eq_token: Default::default(), - lit: litstr("foo"), - }]); - assert_eq!( - &result.unwrap(), - Some(&strs_to_strings(vec!["foo"])).unwrap() - ); - } - - #[test] - fn test_many() { - let result = get_doc_strings(&[ - MetaNameValue { - path: ident("doc").into(), - eq_token: Default::default(), - lit: litstr("foo"), - }, - MetaNameValue { - path: ident("doc").into(), - eq_token: Default::default(), - lit: litstr("\n"), - }, - MetaNameValue { - path: ident("doc").into(), - eq_token: Default::default(), - lit: litstr("bar"), - }, - ]); - assert_eq!( - &result.unwrap(), - Some(&strs_to_strings(vec!["foo", "\n", "bar"])).unwrap() - ); - } - - #[test] - fn test_not_doc() { - let result = get_doc_strings(&[MetaNameValue { - path: ident("blah").into(), - eq_token: Default::default(), - lit: litstr("foo"), - }]); - assert_eq!(&result, &None); - } - } - - mod test_join_doc_strings { - use super::*; - - #[test] - fn test_single() { - let result = join_doc_strings(&strs_to_strings(vec!["foo"])); - assert_eq!(&result, "foo"); - } - #[test] - fn test_multiple() { - let result = join_doc_strings(&strs_to_strings(vec!["foo", "bar"])); - assert_eq!(&result, "foo\nbar"); - } - - #[test] - fn test_trims_spaces() { - let result = join_doc_strings(&strs_to_strings(vec![" foo ", "bar ", " baz"])); - assert_eq!(&result, "foo\nbar\nbaz"); - } - - #[test] - fn test_empty() { - let result = join_doc_strings(&strs_to_strings(vec!["foo", "", "bar"])); - assert_eq!(&result, "foo\n\nbar"); - } - - #[test] - fn test_newline_spaces() { - let result = join_doc_strings(&strs_to_strings(vec!["foo ", "", " bar"])); - assert_eq!(&result, "foo\n\nbar"); - } - - #[test] - fn test_continuation_backslash() { - let result = join_doc_strings(&strs_to_strings(vec!["foo\\", "x\\", "y", "bar"])); - assert_eq!(&result, "foo x y\nbar"); - } - } - - #[test] - fn test_to_camel_case() { - assert_eq!(&to_camel_case("test")[..], "test"); - assert_eq!(&to_camel_case("_test")[..], "test"); - assert_eq!(&to_camel_case("__test")[..], "__test"); - assert_eq!(&to_camel_case("first_second")[..], "firstSecond"); - assert_eq!(&to_camel_case("first_")[..], "first"); - assert_eq!(&to_camel_case("a_b_c")[..], "aBC"); - assert_eq!(&to_camel_case("a_bc")[..], "aBc"); - assert_eq!(&to_camel_case("a_b")[..], "aB"); - assert_eq!(&to_camel_case("a")[..], "a"); - assert_eq!(&to_camel_case("")[..], ""); - } - - #[test] - fn test_to_upper_snake_case() { - assert_eq!(to_upper_snake_case("abc"), "ABC"); - assert_eq!(to_upper_snake_case("a_bc"), "A_BC"); - assert_eq!(to_upper_snake_case("ABC"), "ABC"); - assert_eq!(to_upper_snake_case("A_BC"), "A_BC"); - assert_eq!(to_upper_snake_case("SomeInput"), "SOME_INPUT"); - assert_eq!(to_upper_snake_case("someInput"), "SOME_INPUT"); - assert_eq!(to_upper_snake_case("someINpuT"), "SOME_INPU_T"); - assert_eq!(to_upper_snake_case("some_INpuT"), "SOME_INPU_T"); - } - - #[test] - fn test_is_valid_name() { - assert_eq!(is_valid_name("yesItIs"), true); - assert_eq!(is_valid_name("NoitIsnt"), true); - assert_eq!(is_valid_name("iso6301"), true); - assert_eq!(is_valid_name("thisIsATest"), true); - assert_eq!(is_valid_name("i6Op"), true); - assert_eq!(is_valid_name("i!"), false); - assert_eq!(is_valid_name(""), false); - assert_eq!(is_valid_name("aTest"), true); - assert_eq!(is_valid_name("__Atest90"), true); - } -}