Skip to content
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

Bug in JSONScalar example #1647

Merged
merged 13 commits into from
Feb 28, 2022
7 changes: 7 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Release type: minor

There was a bug with the JSON custom scalar example.

This release fixes the example and adds JSON scalar type into `strawberry.scalars`.

The same happens with `Base64`: We now have Base16, Base32 and Base64 into `strawberry.scalars`
paulo-raca marked this conversation as resolved.
Show resolved Hide resolved
28 changes: 23 additions & 5 deletions docs/types/scalars.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,15 @@ result = schema.execute_sync("{ base64 }")
assert results.data == {"base64": "aGk="}
```

<Note>
The `Base16`, `Base32` and `Base64` scalar types are available in `strawberry.scalars`

```python
from strawberry.scalars import Base16, Base32, Base64
```

</Note>

## Example JSONScalar

```python
Expand All @@ -126,11 +135,11 @@ from typing import Any, NewType

import strawberry

JSONScalar = strawberry.scalar(
NewType("JSONScalar", Any),
JSON = strawberry.scalar(
NewType("JSON", object),
description="The `JSON` scalar type represents JSON values as specified by ECMA-404",
serialize=lambda v: v,
parse_value=lambda v: json.loads(v),
description="The GenericScalar scalar type represents a generic GraphQL scalar value that could be: List or Object."
parse_value=lambda v: v,
)

```
Expand All @@ -141,7 +150,7 @@ Usage:
@strawberry.type
class Query:
@strawberry.field
def data(self, info) -> JSONScalar:
def data(self, info) -> JSON:
return {"hello": {"a": 1}, "someNumbers": [1, 2, 3]}

```
Expand All @@ -161,6 +170,15 @@ query ExampleDataQuery {
}
```

<Note>
The `JSON` scalar type is available in `strawberry.scalars`

```python
from strawberry.scalars import JSON
```

</Note>

## Overriding built in scalars

To override the behaviour of the built in scalars you can pass a map of
Expand Down
19 changes: 12 additions & 7 deletions strawberry/custom_scalar.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ def identity(x):
class ScalarDefinition(StrawberryType):
name: str
description: Optional[str]
specified_by_url: Optional[str]
serialize: Optional[Callable]
parse_value: Optional[Callable]
parse_literal: Optional[Callable]
Expand Down Expand Up @@ -47,11 +48,12 @@ def __call__(self, *args, **kwargs):
def _process_scalar(
cls,
*,
name: str = None,
description: str = None,
serialize: Callable = None,
parse_value: Callable = None,
parse_literal: Callable = None
name: Optional[str] = None,
description: Optional[str] = None,
specified_by_url: Optional[str] = None,
serialize: Optional[Callable] = None,
parse_value: Optional[Callable] = None,
parse_literal: Optional[Callable] = None,
):

name = name or to_camel_case(cls.__name__)
Expand All @@ -60,6 +62,7 @@ def _process_scalar(
wrapper._scalar_definition = ScalarDefinition(
name=name,
description=description,
specified_by_url=specified_by_url,
serialize=serialize,
parse_literal=parse_literal,
parse_value=parse_value,
Expand All @@ -72,10 +75,11 @@ def scalar(
cls=None,
*,
name: str = None,
description: str = None,
description: Optional[str] = None,
specified_by_url: Optional[str] = None,
serialize: Callable = identity,
parse_value: Optional[Callable] = None,
parse_literal: Optional[Callable] = None
parse_literal: Optional[Callable] = None,
):
"""Annotates a class or type as a GraphQL custom scalar.

Expand Down Expand Up @@ -111,6 +115,7 @@ def wrap(cls):
cls,
name=name,
description=description,
specified_by_url=specified_by_url,
serialize=serialize,
parse_value=parse_value,
parse_literal=parse_literal,
Expand Down
40 changes: 39 additions & 1 deletion strawberry/scalars.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,48 @@
import base64
from typing import Any, Dict, NewType, Union

from .custom_scalar import ScalarDefinition, ScalarWrapper
from .custom_scalar import ScalarDefinition, ScalarWrapper, scalar


ID = NewType("ID", str)

JSON = scalar(
NewType("JSON", object), # mypy doesn't like `NewType("name", Any)`
description="The `JSON` scalar type represents JSON values as specified by"
" [ECMA-404](http://www.ecma-international.org"
"/publications/files/ECMA-ST/ECMA-404.pdf).",
specified_by_url="http://www.ecma-international.org"
"/publications/files/ECMA-ST/ECMA-404.pdf",
serialize=lambda v: v,
parse_value=lambda v: v,
)
paulo-raca marked this conversation as resolved.
Show resolved Hide resolved

Base16 = scalar(
NewType("Base16", bytes),
description="Represents binary data as Base16-encoded (hexadecimal) strings.",
specified_by_url="https://datatracker.ietf.org/doc/html/rfc4648.html#section-8",
serialize=lambda v: base64.b16encode(v).decode("utf-8"),
parse_value=lambda v: base64.b16decode(v.encode("utf-8"), casefold=True),
)

Base32 = scalar(
NewType("Base32", bytes),
description="Represents binary data as Base32-encoded strings,"
" using the standard alphabet.",
specified_by_url=("https://datatracker.ietf.org/doc/html/rfc4648.html#section-6"),
serialize=lambda v: base64.b32encode(v).decode("utf-8"),
parse_value=lambda v: base64.b32decode(v.encode("utf-8"), casefold=True),
)
paulo-raca marked this conversation as resolved.
Show resolved Hide resolved

Base64 = scalar(
paulo-raca marked this conversation as resolved.
Show resolved Hide resolved
NewType("Base64", bytes),
description="Represents binary data as Base64-encoded strings,"
" using the standard alphabet.",
specified_by_url="https://datatracker.ietf.org/doc/html/rfc4648.html#section-4",
serialize=lambda v: base64.b64encode(v).decode("utf-8"),
parse_value=lambda v: base64.b64decode(v.encode("utf-8")),
)
paulo-raca marked this conversation as resolved.
Show resolved Hide resolved


def is_scalar(
annotation: Any,
Expand Down
2 changes: 2 additions & 0 deletions strawberry/schema/types/scalar.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def _make_scalar_type(definition: ScalarDefinition) -> GraphQLScalarType:
return GraphQLScalarType(
name=definition.name,
description=definition.description,
specified_by_url=definition.specified_by_url,
serialize=definition.serialize,
parse_value=definition.parse_value,
parse_literal=definition.parse_literal,
Expand All @@ -32,6 +33,7 @@ def _make_scalar_definition(scalar_type: GraphQLScalarType) -> ScalarDefinition:
return ScalarDefinition(
name=scalar_type.name,
description=scalar_type.name,
specified_by_url=scalar_type.specified_by_url,
paulo-raca marked this conversation as resolved.
Show resolved Hide resolved
serialize=scalar_type.serialize,
parse_literal=scalar_type.parse_literal,
parse_value=scalar_type.parse_value,
Expand Down
151 changes: 151 additions & 0 deletions tests/schema/test_scalars.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from datetime import datetime, timedelta, timezone
from textwrap import dedent
from typing import Optional
from uuid import UUID

import pytest

import strawberry
from strawberry.scalars import JSON, Base16, Base32, Base64


def test_uuid_field_string_value():
Expand Down Expand Up @@ -99,6 +101,155 @@ def uuid_input(self, input_id: UUID) -> str:
}


def test_json():
@strawberry.type
class Query:
@strawberry.field
def echo_json(data: JSON) -> JSON:
return data

@strawberry.field
def echo_json_nullable(data: Optional[JSON]) -> Optional[JSON]:
return data

schema = strawberry.Schema(query=Query)

assert (
str(schema)
== dedent(
'''
"""
The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).
"""
scalar JSON @specifiedBy(url: "http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf")

type Query {
echoJson(data: JSON!): JSON!
echoJsonNullable(data: JSON): JSON
}
''' # noqa: E501
).strip()
)

paulo-raca marked this conversation as resolved.
Show resolved Hide resolved
result = schema.execute_sync(
"""
query {
echoJson(data: {hello: {a: 1}, someNumbers: [1, 2, 3], null: null})
echoJsonNullable(data: {hello: {a: 1}, someNumbers: [1, 2, 3], null: null})
}
"""
)

assert not result.errors
assert result.data == {
"echoJson": {"hello": {"a": 1}, "someNumbers": [1, 2, 3], "null": None},
"echoJsonNullable": {"hello": {"a": 1}, "someNumbers": [1, 2, 3], "null": None},
}

result = schema.execute_sync(
"""
query {
echoJson(data: null)
}
"""
)
assert result.errors # echoJson is not-null null

result = schema.execute_sync(
"""
query {
echoJsonNullable(data: null)
}
"""
)
assert not result.errors
assert result.data == {
"echoJsonNullable": None,
}


def test_base16():
@strawberry.type
class Query:
@strawberry.field
def base16_encode(data: str) -> Base16:
return bytes(data, "utf-8")

@strawberry.field
def base16_decode(data: Base16) -> str:
return data.decode("utf-8")

@strawberry.field
def base32_encode(data: str) -> Base32:
return bytes(data, "utf-8")

@strawberry.field
def base32_decode(data: Base32) -> str:
return data.decode("utf-8")

@strawberry.field
def base64_encode(data: str) -> Base64:
return bytes(data, "utf-8")

@strawberry.field
def base64_decode(data: Base64) -> str:
return data.decode("utf-8")

schema = strawberry.Schema(query=Query)

assert (
str(schema)
== dedent(
'''
"""Represents binary data as Base16-encoded (hexadecimal) strings."""
scalar Base16 @specifiedBy(url: "https://datatracker.ietf.org/doc/html/rfc4648.html#section-8")

"""
Represents binary data as Base32-encoded strings, using the standard alphabet.
"""
scalar Base32 @specifiedBy(url: "https://datatracker.ietf.org/doc/html/rfc4648.html#section-6")

"""
Represents binary data as Base64-encoded strings, using the standard alphabet.
"""
scalar Base64 @specifiedBy(url: "https://datatracker.ietf.org/doc/html/rfc4648.html#section-4")

type Query {
base16Encode(data: String!): Base16!
base16Decode(data: Base16!): String!
base32Encode(data: String!): Base32!
base32Decode(data: Base32!): String!
base64Encode(data: String!): Base64!
base64Decode(data: Base64!): String!
}
''' # noqa: E501
).strip()
)

result = schema.execute_sync(
"""
query {
base16Encode(data: "Hello")
base16Decode(data: "48656c6C6f") # < Mix lowercase and uppercase
base32Encode(data: "Hello")
base32Decode(data: "JBSWY3dp") # < Mix lowercase and uppercase
base64Encode(data: "Hello")
base64Decode(data: "SGVsbG8=")
}
"""
)

assert not result.errors
assert result.data == {
"base16Encode": "48656C6C6F",
"base16Decode": "Hello",
"base32Encode": "JBSWY3DP",
"base32Decode": "Hello",
"base64Encode": "SGVsbG8=",
"base64Decode": "Hello",
}


def test_override_built_in_scalars():
EpochDateTime = strawberry.scalar(
datetime,
Expand Down