Finish refactoring juniper_codegen crate

This commit is contained in:
tyranron 2022-07-11 17:06:05 +02:00
parent 0c8bcf582f
commit 576d6fb6dd
No known key found for this signature in database
GPG key ID: 762E144FB230A4F0
28 changed files with 769 additions and 940 deletions

View file

@ -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<syn::LitStr>,
}
impl Parse for Directive {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
Ok(Self {
reason: input
.try_parse::<token::Eq>()?
.map(|_| input.parse::<syn::LitStr>())
.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<Option<SpanContainer<Self>>> {
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<Self> {
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);
}
}

View file

@ -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<Self> {
input.parse::<syn::LitStr>().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<Option<SpanContainer<Self>>> {
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<Item = &'i str>) -> Vec<String> {
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",
);
}
}

View file

@ -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<S: AsRef<str>>(&self, span: Span, msg: S) -> Diagnostic {
pub(crate) fn custom<S: AsRef<str>>(&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<S: AsRef<str>>(&self, span: Span, msg: S) {
pub(crate) fn emit_custom<S: AsRef<str>>(&self, span: Span, msg: S) {
self.custom(span, msg).emit()
}
pub fn custom_error<S: AsRef<str>>(&self, span: Span, msg: S) -> syn::Error {
pub(crate) fn custom_error<S: AsRef<str>>(&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,

View file

@ -14,16 +14,13 @@ use syn::{
token,
};
use crate::{
common::{
use crate::common::{
default, diagnostic, filter_attrs,
parse::{
attr::{err, OptionExt as _},
ParseBufferExt as _, TypeExt as _,
},
scalar,
},
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<SpanContainer<syn::LitStr>>,
pub(crate) description: Option<SpanContainer<Description>>,
/// 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<SpanContainer<Option<syn::Expr>>>,
pub(crate) default: Option<SpanContainer<default::Value>>,
/// 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::<token::Eq>()?;
let desc = input.parse::<syn::LitStr>()?;
let desc = input.parse::<Description>()?;
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::<token::Eq>() {
input.parse::<token::Eq>()?;
expr = Some(input.parse::<syn::Expr>()?);
} else if input.is_next::<token::Paren>() {
let inner;
let _ = syn::parenthesized!(inner in input);
expr = Some(inner.parse::<syn::Expr>()?);
}
let val = input.parse::<default::Value>()?;
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<String>,
pub(crate) description: Option<Description>,
/// 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<Option<syn::Expr>>,
pub(crate) default: Option<default::Value>,
}
/// 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<Self> {
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),
})))
}
}

View file

@ -14,15 +14,13 @@ use syn::{
token,
};
use crate::{
common::{
use crate::common::{
deprecation, filter_attrs,
parse::{
attr::{err, OptionExt as _},
ParseBufferExt as _,
},
scalar,
},
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<SpanContainer<syn::LitStr>>,
pub(crate) description: Option<SpanContainer<Description>>,
/// 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<SpanContainer<Option<syn::LitStr>>>,
pub(crate) deprecated: Option<SpanContainer<deprecation::Directive>>,
/// 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::<token::Eq>()?;
let desc = input.parse::<syn::LitStr>()?;
let desc = input.parse::<Description>()?;
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::<token::Eq>() {
input.parse::<token::Eq>()?;
reason = Some(input.parse::<syn::LitStr>()?);
}
let directive = input.parse::<deprecation::Directive>()?;
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<String>,
pub(crate) description: Option<Description>,
/// [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<Option<String>>,
pub(crate) deprecated: Option<deprecation::Directive>,
/// 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

View file

@ -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<Item = &'a syn::Attribute> + 'a {
attrs
.iter()
.filter(move |attr| path_eq_single(&attr.path, name))
}

View file

@ -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`.

View file

@ -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<Self, Self::Err> {
match rule {
"none" => Ok(Self::None),
"camelCase" => Ok(Self::CamelCase),
"SCREAMING_SNAKE_CASE" => Ok(Self::ScreamingSnakeCase),
_ => Err(()),
}
}
}
impl TryFrom<syn::LitStr> for Policy {
type Error = syn::Error;
fn try_from(lit: syn::LitStr) -> syn::Result<Self> {
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> {
Self::try_from(input.parse::<syn::LitStr>()?)
}
}
// 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::<String>();
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);
}
}
}

View file

@ -6,8 +6,8 @@ use std::{
use proc_macro2::{Span, TokenStream};
use quote::ToTokens;
#[derive(Clone, Debug)]
pub struct SpanContainer<T> {
#[derive(Clone, Copy, Debug)]
pub(crate) struct SpanContainer<T> {
expr: Option<Span>,
ident: Span,
val: T,
@ -20,15 +20,15 @@ impl<T: ToTokens> ToTokens for SpanContainer<T> {
}
impl<T> SpanContainer<T> {
pub fn new(ident: Span, expr: Option<Span>, val: T) -> Self {
pub(crate) fn new(ident: Span, expr: Option<Span>, 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<T> SpanContainer<T> {
}
}
pub fn into_inner(self) -> T {
pub(crate) fn into_inner(self) -> T {
self.val
}
pub fn map<U, F: Fn(T) -> U>(self, f: F) -> SpanContainer<U> {
SpanContainer {
expr: self.expr,
ident: self.ident,
val: f(self.val),
}
}
}
impl<T> AsRef<T> for SpanContainer<T> {

View file

@ -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<TokenStream> {
@ -32,7 +28,7 @@ pub(crate) fn expand(input: TokenStream) -> syn::Result<TokenStream> {
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<TokenStream> {
.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<TokenStream> {
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<TokenStream> {
/// 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<ValueDefinition> {
fn parse_value(v: &syn::Variant, renaming: rename::Policy) -> Option<ValueDefinition> {
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<ValueDefinition
)
.into_boxed_str();
let description = attr.description.map(|d| d.into_inner().into_boxed_str());
let deprecated = attr.deprecated.map(|desc| {
desc.into_inner()
.as_ref()
.map(|lit| lit.value().into_boxed_str())
});
Some(ValueDefinition {
ident: v.ident.clone(),
name,
description,
deprecated,
description: attr.description.map(SpanContainer::into_inner),
deprecated: attr.deprecated.map(SpanContainer::into_inner),
})
}

View file

@ -16,17 +16,13 @@ use syn::{
token,
};
use crate::{
common::{
use crate::common::{
deprecation, filter_attrs,
parse::{
attr::{err, OptionExt as _},
ParseBufferExt as _,
},
scalar,
},
util::{
filter_attrs, get_deprecated, get_doc_comment, span_container::SpanContainer, RenameRule,
},
rename, scalar, Description, SpanContainer,
};
/// Available arguments behind `#[graphql]` attribute placed on a Rust enum
@ -49,7 +45,7 @@ struct ContainerAttr {
///
/// [0]: https://spec.graphql.org/October2021#sec-Enums
/// [2]: https://spec.graphql.org/October2021#sec-Descriptions
description: Option<SpanContainer<String>>,
description: Option<SpanContainer<Description>>,
/// 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<SpanContainer<scalar::AttrValue>>,
/// 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<SpanContainer<RenameRule>>,
rename_values: Option<SpanContainer<rename::Policy>>,
/// 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::<token::Eq>()?;
let desc = input.parse::<syn::LitStr>()?;
let desc = input.parse::<Description>()?;
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<SpanContainer<String>>,
description: Option<SpanContainer<Description>>,
/// 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<SpanContainer<Option<syn::LitStr>>>,
deprecated: Option<SpanContainer<deprecation::Directive>>,
/// 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::<token::Eq>()?;
let desc = input.parse::<syn::LitStr>()?;
let desc = input.parse::<Description>()?;
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::<token::Eq>() {
input.parse::<token::Eq>()?;
reason = Some(input.parse::<syn::LitStr>()?);
}
let directive = input.parse::<deprecation::Directive>()?;
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<Box<str>>,
description: Option<Description>,
/// [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<Option<Box<str>>>,
deprecated: Option<deprecation::Directive>,
}
/// 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<Box<str>>,
description: Option<Description>,
/// 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
}
});

View file

@ -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<TokenStream> {
@ -31,7 +27,7 @@ pub fn expand(input: TokenStream) -> syn::Result<TokenStream> {
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<TokenStream> {
.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<TokenStream> {
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<TokenStream> {
/// 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<FieldDefinition> {
fn parse_field(
f: &syn::Field,
renaming: rename::Policy,
is_internal: bool,
) -> Option<FieldDefinition> {
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(),
})
}

View file

@ -16,16 +16,13 @@ use syn::{
token,
};
use crate::{
common::{
default,
use crate::common::{
default, filter_attrs,
parse::{
attr::{err, OptionExt as _},
ParseBufferExt as _,
},
scalar,
},
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<SpanContainer<String>>,
description: Option<SpanContainer<Description>>,
/// 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<SpanContainer<scalar::AttrValue>>,
/// 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<SpanContainer<RenameRule>>,
rename_fields: Option<SpanContainer<rename::Policy>>,
/// 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::<token::Eq>()?;
let desc = input.parse::<syn::LitStr>()?;
let desc = input.parse::<Description>()?;
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<SpanContainer<String>>,
description: Option<SpanContainer<Description>>,
/// 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::<token::Eq>()?;
let desc = input.parse::<syn::LitStr>()?;
let desc = input.parse::<Description>()?;
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<Box<str>>,
description: Option<Description>,
/// 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<Box<str>>,
description: Option<Description>,
/// 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 }
})

View file

@ -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,
use crate::common::{
diagnostic, field,
parse::{self, TypeExt as _},
scalar,
},
result::GraphQLScope,
util::{path_eq_single, span_container::SpanContainer, RenameRule},
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<TokenStream> {
@ -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<field::Definition> {
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<field::Definition> {
fn parse_struct_field(
field: &mut syn::Field,
renaming: &rename::Policy,
) -> Option<field::Definition> {
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<f
let mut ty = field.ty.clone();
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: field_ident.clone(),
arguments: None,
has_receiver: false,

View file

@ -4,16 +4,12 @@ use proc_macro2::TokenStream;
use quote::ToTokens as _;
use syn::{ext::IdentExt as _, parse_quote, spanned::Spanned};
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::err_unnamed_field, enum_idents, Attr, Definition};
/// [`GraphQLScope`] of errors for `#[derive(GraphQLInterface)]` macro.
const ERR: GraphQLScope = GraphQLScope::InterfaceDerive;
/// [`diagnostic::Scope`] of errors for `#[derive(GraphQLInterface)]` macro.
const ERR: diagnostic::Scope = diagnostic::Scope::InterfaceDerive;
/// Expands `#[derive(GraphQLInterface)]` macro into generated code.
pub fn expand(input: TokenStream) -> syn::Result<TokenStream> {
@ -52,7 +48,7 @@ pub fn expand(input: TokenStream) -> syn::Result<TokenStream> {
.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<TokenStream> {
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<TokenStream> {
///
/// Returns [`None`] if the parsing fails, or the struct field is ignored.
#[must_use]
fn parse_field(field: &syn::Field, renaming: &RenameRule) -> Option<field::Definition> {
fn parse_field(field: &syn::Field, renaming: &rename::Policy) -> Option<field::Definition> {
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<field::Defin
let mut ty = field.ty.clone();
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: field_ident.clone(),
arguments: None,
has_receiver: false,

View file

@ -19,16 +19,13 @@ use syn::{
visit::Visit,
};
use crate::{
common::{
field, gen,
use crate::common::{
field, filter_attrs, gen,
parse::{
attr::{err, OptionExt as _},
GenericsExt as _, ParseBufferExt as _,
},
scalar,
},
util::{filter_attrs, get_doc_comment, span_container::SpanContainer, RenameRule},
rename, scalar, Description, SpanContainer,
};
/// Returns [`syn::Ident`]s for a generic enum deriving [`Clone`] and [`Copy`]
@ -71,7 +68,7 @@ struct Attr {
///
/// [1]: https://spec.graphql.org/October2021#sec-Interfaces
/// [2]: https://spec.graphql.org/October2021#sec-Descriptions
description: Option<SpanContainer<String>>,
description: Option<SpanContainer<Description>>,
/// 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<SpanContainer<syn::Ident>>,
/// 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<SpanContainer<RenameRule>>,
rename_fields: Option<SpanContainer<rename::Policy>>,
/// 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::<token::Eq>()?;
let desc = input.parse::<syn::LitStr>()?;
let desc = input.parse::<Description>()?;
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<Box<str>>,
description: Option<Description>,
/// 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();

View file

@ -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,
use crate::common::{
diagnostic, field,
parse::{self, TypeExt as _},
scalar,
},
result::GraphQLScope,
util::{path_eq_single, span_container::SpanContainer, RenameRule},
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<TokenStream> {
@ -73,7 +69,7 @@ where
.rename_fields
.as_deref()
.copied()
.unwrap_or(RenameRule::CamelCase);
.unwrap_or(rename::Policy::CamelCase);
let async_only = TypeId::of::<Operation>() != TypeId::of::<Query>();
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<field::Definition> {
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(),

View file

@ -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<TokenStream> {
@ -62,7 +58,7 @@ fn expand_struct(ast: syn::DeriveInput) -> syn::Result<Definition<Query>> {
.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<Definition<Query>> {
///
/// Returns [`None`] if parsing fails, or the struct field is ignored.
#[must_use]
fn parse_field(field: &syn::Field, renaming: &RenameRule) -> Option<field::Definition> {
fn parse_field(field: &syn::Field, renaming: &rename::Policy) -> Option<field::Definition> {
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<field::Defin
let mut ty = field.ty.unparenthesized().clone();
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: field_ident.clone(),
arguments: None,
has_receiver: false,

View file

@ -10,24 +10,21 @@ use std::{any::TypeId, collections::HashSet, convert::TryInto as _, marker::Phan
use proc_macro2::TokenStream;
use quote::{format_ident, quote, ToTokens};
use syn::{
ext::IdentExt as _,
parse::{Parse, ParseStream},
parse_quote,
spanned::Spanned as _,
token,
};
use crate::{
common::{
field, gen,
use crate::common::{
field, filter_attrs, gen,
parse::{
attr::{err, OptionExt as _},
GenericsExt as _, ParseBufferExt as _, TypeExt,
},
scalar,
},
util::{filter_attrs, get_doc_comment, span_container::SpanContainer, RenameRule},
rename, scalar, Description, SpanContainer,
};
use syn::ext::IdentExt;
/// Available arguments behind `#[graphql]` (or `#[graphql_object]`) attribute
/// when generating code for [GraphQL object][1] type.
@ -49,7 +46,7 @@ pub(crate) struct Attr {
///
/// [1]: https://spec.graphql.org/October2021#sec-Objects
/// [2]: https://spec.graphql.org/October2021#sec-Descriptions
pub(crate) description: Option<SpanContainer<String>>,
pub(crate) description: Option<SpanContainer<Description>>,
/// 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<SpanContainer<syn::Type>>,
/// 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<SpanContainer<RenameRule>>,
pub(crate) rename_fields: Option<SpanContainer<rename::Policy>>,
/// 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::<token::Eq>()?;
let desc = input.parse::<syn::LitStr>()?;
let desc = input.parse::<Description>()?;
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<Operation: ?Sized> {
/// Description of this [GraphQL object][1] to put into GraphQL schema.
///
/// [1]: https://spec.graphql.org/October2021#sec-Objects
pub(crate) description: Option<String>,
pub(crate) description: Option<Description>,
/// Rust type of [`Context`] to generate [`GraphQLType`] implementation with
/// for this [GraphQL object][1].
@ -437,10 +431,7 @@ impl<Operation: ?Sized + 'static> Definition<Operation> {
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::<Operation>() != TypeId::of::<Query>();
let fields_meta = self

View file

@ -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<TokenStream> {
@ -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,
};

View file

@ -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<TokenStream> {
@ -27,11 +27,10 @@ pub fn expand(input: TokenStream) -> syn::Result<TokenStream> {
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())

View file

@ -14,15 +14,13 @@ use syn::{
};
use url::Url;
use crate::{
common::{
use crate::common::{
filter_attrs,
parse::{
attr::{err, OptionExt as _},
ParseBufferExt as _,
},
scalar,
},
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<SpanContainer<String>>,
description: Option<SpanContainer<Description>>,
/// Spec [`Url`] of this [GraphQL scalar][1] to put into GraphQL schema.
///
@ -112,13 +110,9 @@ impl Parse for Attr {
}
"desc" | "description" => {
input.parse::<token::Eq>()?;
let desc = input.parse::<syn::LitStr>()?;
let desc = input.parse::<Description>()?;
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<String>,
description: Option<Description>,
/// 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) }

View file

@ -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<TokenStream> {

View file

@ -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<TokenStream> {

View file

@ -17,16 +17,13 @@ use syn::{
token,
};
use crate::{
common::{
gen,
use crate::common::{
filter_attrs, gen,
parse::{
attr::{err, OptionExt as _},
ParseBufferExt as _,
},
scalar,
},
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<SpanContainer<String>>,
description: Option<SpanContainer<Description>>,
/// 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::<token::Eq>()?;
let desc = input.parse::<syn::LitStr>()?;
let desc = input.parse::<Description>()?;
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<String>,
description: Option<Description>,
/// 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);

View file

@ -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)]

View file

@ -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<TokenStream> {

View file

@ -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<String>,
}
/// Filters given `attrs` to contain attributes only with the given `name`.
pub fn filter_attrs<'a>(
name: &'a str,
attrs: &'a [Attribute],
) -> impl Iterator<Item = &'a Attribute> + 'a {
attrs
.iter()
.filter(move |attr| path_eq_single(&attr.path, name))
}
pub fn get_deprecated(attrs: &[Attribute]) -> Option<SpanContainer<DeprecationAttr>> {
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<SpanContainer<String>> {
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<SpanContainer<Vec<String>>> {
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::<Vec<_>>();
span.map(|span| SpanContainer::new(span, None, comments))
}
// Gets doc comment attributes.
fn get_doc_attr(attrs: &[Attribute]) -> Option<Vec<MetaNameValue>> {
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::<String>();
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<Self, Self::Err> {
match rule {
"none" => Ok(Self::None),
"camelCase" => Ok(Self::CamelCase),
"SCREAMING_SNAKE_CASE" => Ok(Self::ScreamingSnakeCase),
_ => Err(()),
}
}
}
impl TryFrom<syn::LitStr> for RenameRule {
type Error = syn::Error;
fn try_from(lit: syn::LitStr) -> syn::Result<Self> {
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> {
Self::try_from(input.parse::<syn::LitStr>()?)
}
}
#[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<String> {
source
.iter()
.map(|x| (*x).to_string())
.collect::<Vec<String>>()
}
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);
}
}