Skip to content

Implement newtype derive for scalars. #368

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 25, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 108 additions & 37 deletions docs/book/content/types/scalars.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,59 +6,130 @@ but this often requires coordination with the client library intended to consume
the API you're building.

Since any value going over the wire is eventually transformed into JSON, you're
also limited in the data types you can use. Typically, you represent your custom
scalars as strings.
also limited in the data types you can use.

In Juniper, you use the `graphql_scalar!` macro to create a custom scalar. In
this example, we're representing a user ID as a string wrapped in a custom type:
There are two ways to define custom scalars.
* For simple scalars that just wrap a primitive type, you can use the newtype pattern with
a custom derive.
* For more advanced use cases with custom validation, you can use
the `graphql_scalar!` macro.

```rust
use juniper::Value;

struct UserID(String);
## Built-in scalars

juniper::graphql_scalar!(UserID where Scalar = <S> {
description: "An opaque identifier, represented as a string"
Juniper has built-in support for:

resolve(&self) -> Value {
Value::scalar(self.0.clone())
}
* `i32` as `Int`
* `f64` as `Float`
* `String` and `&str` as `String`
* `bool` as `Boolean`
* `juniper::ID` as `ID`. This type is defined [in the
spec](http://facebook.github.io/graphql/#sec-ID) as a type that is serialized
as a string but can be parsed from both a string and an integer.

from_input_value(v: &InputValue) -> Option<UserID> {
// If there's a parse error here, simply return None. Juniper will
// present an error to the client.
v.as_scalar_value::<String>().map(|s| UserID(s.to_owned()))
}
**Third party types**:

from_str<'a>(value: ScalarToken<'a>) -> juniper::ParseScalarResult<'a, S> {
<String as juniper::ParseScalarValue<S>>::from_str(value)
}
});
Juniper has built-in support for a few additional types from common third party
crates. They are enabled via features that are on by default.

* uuid::Uuid
* chrono::DateTime
* url::Url

## newtype pattern

Often, you might need a custom scalar that just wraps an existing type.

This can be done with the newtype pattern and a custom derive, similar to how
serde supports this pattern with `#[serde(transparent)]`.

```rust
#[derive(juniper::GraphQLScalarValue)]
pub struct UserId(i32);

#[derive(juniper::GraphQLObject)]
struct User {
id: UserID,
username: String,
id: UserId,
}

# fn main() {}
```

## Built-in scalars
That's it, you can now user `UserId` in your schema.

Juniper has built-in support for:
The macro also allows for more customization:

* `i32` as `Int`
* `f64` as `Float`
* `String` and `&str` as `String`
* `bool` as `Boolean`
* `juniper::ID` as `ID`. This type is defined [in the
spec](http://facebook.github.io/graphql/#sec-ID) as a type that is serialized
as a string but can be parsed from both a string and an integer.
```rust
/// You can use a doc comment to specify a description.
#[derive(juniper::GraphQLScalarValue)]
#[graphql(
transparent,
// Overwrite the GraphQL type name.
name = "MyUserId",
// Specify a custom description.
// A description in the attribute will overwrite a doc comment.
description = "My user id description",
)]
pub struct UserId(i32);

# fn main() {}
```

## Custom scalars

For more complex situations where you also need custom parsing or validation,
you can use the `graphql_scalar!` macro.

Typically, you represent your custom scalars as strings.

The example below implements a custom scalar for a custom `Date` type.

Note: juniper already has built-in support for the `chrono::DateTime` type
via `chrono` feature, which is enabled by default and should be used for this
purpose.

### Non-standard scalars
The example below is used just for illustration.

Juniper has built-in support for UUIDs from the [uuid
crate](https://doc.rust-lang.org/uuid/uuid/index.html). This support is enabled
by default, but can be disabled if you want to reduce the number of dependencies
in your application.
**Note**: the example assumes that the `Date` type implements
`std::fmt::Display` and `std::str::FromStr`.


```rust
# mod date {
# pub struct Date;
# impl std::str::FromStr for Date{
# type Err = String; fn from_str(_value: &str) -> Result<Self, Self::Err> { unimplemented!() }
# }
# // And we define how to represent date as a string.
# impl std::fmt::Display for Date {
# fn fmt(&self, _f: &mut std::fmt::Formatter) -> std::fmt::Result {
# unimplemented!()
# }
# }
# }

use juniper::{Value, ParseScalarResult, ParseScalarValue};
use date::Date;

juniper::graphql_scalar!(Date where Scalar = <S> {
description: "Date"

// Define how to convert your custom scalar into a primitive type.
resolve(&self) -> Value {
Value::scalar(self.to_string())
}

// Define how to parse a primitive type into your custom scalar.
from_input_value(v: &InputValue) -> Option<Date> {
v.as_scalar_value::<String>()
.and_then(|s| s.parse().ok())
}

// Define how to parse a string value.
from_str<'a>(value: ScalarToken<'a>) -> ParseScalarResult<'a, S> {
<String as ParseScalarValue<S>>::from_str(value)
}
});

# fn main() {}
```
5 changes: 3 additions & 2 deletions docs/book/tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ build = "build.rs"
juniper = { path = "../../../juniper" }
juniper_iron = { path = "../../../juniper_iron" }

iron = "^0.5.0"
mount = "^0.3.0"
iron = "0.5.0"
mount = "0.4.0"

skeptic = "0.13"
serde_json = "1.0.39"
uuid = "0.7.4"

[build-dependencies]
skeptic = "0.13"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
#[cfg(test)]
use fnv::FnvHashMap;

use juniper::DefaultScalarValue;
Expand Down
1 change: 1 addition & 0 deletions integration_tests/juniper_tests/src/codegen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ mod util;
mod derive_enum;
mod derive_input_object;
mod derive_object;
mod scalar_value_transparent;
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
use fnv::FnvHashMap;
use juniper::{DefaultScalarValue, FromInputValue, GraphQLType, InputValue, ToInputValue};

#[derive(juniper::GraphQLScalarValue, PartialEq, Eq, Debug)]
#[graphql(transparent)]
struct UserId(String);

#[derive(juniper::GraphQLScalarValue, PartialEq, Eq, Debug)]
#[graphql(transparent, name = "MyUserId", description = "custom description...")]
struct CustomUserId(String);

/// The doc comment...
#[derive(juniper::GraphQLScalarValue, PartialEq, Eq, Debug)]
#[graphql(transparent)]
struct IdWithDocComment(i32);

#[derive(juniper::GraphQLObject)]
struct User {
id: UserId,
id_custom: CustomUserId,
}

struct User2;

#[juniper::object]
impl User2 {
fn id(&self) -> UserId {
UserId("id".to_string())
}
}

#[test]
fn test_scalar_value_simple() {
assert_eq!(<UserId as GraphQLType>::name(&()), Some("UserId"));

let mut registry: juniper::Registry = juniper::Registry::new(FnvHashMap::default());
let meta = UserId::meta(&(), &mut registry);
assert_eq!(meta.name(), Some("UserId"));
assert_eq!(meta.description(), None);

let input: InputValue = serde_json::from_value(serde_json::json!("userId1")).unwrap();
let output: UserId = FromInputValue::from_input_value(&input).unwrap();
assert_eq!(output, UserId("userId1".into()),);

let id = UserId("111".into());
let output = ToInputValue::<DefaultScalarValue>::to_input_value(&id);
assert_eq!(output, InputValue::scalar("111"),);
}

#[test]
fn test_scalar_value_custom() {
assert_eq!(<CustomUserId as GraphQLType>::name(&()), Some("MyUserId"));

let mut registry: juniper::Registry = juniper::Registry::new(FnvHashMap::default());
let meta = CustomUserId::meta(&(), &mut registry);
assert_eq!(meta.name(), Some("MyUserId"));
assert_eq!(
meta.description(),
Some(&"custom description...".to_string())
);

let input: InputValue = serde_json::from_value(serde_json::json!("userId1")).unwrap();
let output: CustomUserId = FromInputValue::from_input_value(&input).unwrap();
assert_eq!(output, CustomUserId("userId1".into()),);

let id = CustomUserId("111".into());
let output = ToInputValue::<DefaultScalarValue>::to_input_value(&id);
assert_eq!(output, InputValue::scalar("111"),);
}

#[test]
fn test_scalar_value_doc_comment() {
let mut registry: juniper::Registry = juniper::Registry::new(FnvHashMap::default());
let meta = IdWithDocComment::meta(&(), &mut registry);
assert_eq!(meta.description(), Some(&"The doc comment...".to_string()));
}
17 changes: 17 additions & 0 deletions juniper/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# master

### newtype ScalarValue derive

See [#345](https://github.com/graphql-rust/juniper/pull/345).

The newtype pattern can now be used with the `GraphQLScalarValue` custom derive
to easily implement custom scalar values that just wrap another scalar,
similar to serdes `#[serde(transparent)]` functionality.

Example:

```rust
#[derive(juniper::GraphQLScalarValue)]
struct UserId(i32);
```

### Other Changes

- The `ID` scalar now implements Serde's `Serialize` and `Deserialize`

# [[0.12.0] 2019-05-16](https://github.com/graphql-rust/juniper/releases/tag/juniper-0.12.0)
Expand Down
Loading