Skip to content

Commit a7e15ae

Browse files
authored
Add explicit interface implements
1 parent 3194bcd commit a7e15ae

23 files changed

Lines changed: 1823 additions & 35 deletions

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# ResGraph Changelog
22

3+
## 1.2.0
4+
5+
- Add repeatable `@gql.implements("InterfaceName")` for declaring object and interface implementations without spreading fields into the ReScript record.
6+
- Validate GraphQL interface implementation parity during schema generation, including missing interfaces, missing fields, output field type compatibility, required field arguments, argument type mismatches, and extra required arguments.
7+
38
## 1.1.6
49

510
- Support escaped resolver argument labels for reserved ReScript names, allowing GraphQL arguments like `constraint` while using ReScript-safe local bindings.

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,24 @@ enum TextFormat {
113113

114114
[Check out the docs on getting started](https://zth.github.io/resgraph/docs/getting-started).
115115

116+
Interfaces can be implemented by spreading the interface record into the
117+
implementing record, or explicitly with repeatable `@gql.implements(...)`
118+
attributes when the implementing type declares the shared fields itself:
119+
120+
```rescript
121+
@gql.interface
122+
type hasName = {
123+
@gql.field name: string,
124+
}
125+
126+
@gql.implements("HasName")
127+
@gql.type
128+
type user = {
129+
@gql.field name: string,
130+
@gql.field age: int,
131+
}
132+
```
133+
116134
## Introduction
117135

118136
ResGraph lets you build _implementation first_ GraphQL servers, where your types and code is the source of truth for the schema.

docs/docs/interfaces.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,44 @@ type User implements HasName {
6868
}
6969
```
7070

71+
You can also implement interfaces explicitly with `@gql.implements("InterfaceName")`.
72+
This is useful when the implementing type already declares the fields itself, or
73+
when spreading the interface record would not fit the ReScript type shape you
74+
want.
75+
76+
```rescript
77+
/** An entity with a name. */
78+
@gql.interface
79+
type hasName = {
80+
/** The name of the thing. */
81+
@gql.field name: string
82+
}
83+
84+
/** An entity with a stable rank. */
85+
@gql.interface
86+
type ranked = {
87+
@gql.field rank: int
88+
}
89+
90+
/** A user in the system. */
91+
@gql.implements("HasName")
92+
@gql.implements("Ranked")
93+
@gql.type
94+
type user = {
95+
@gql.field name: string,
96+
@gql.field rank: int,
97+
@gql.field age: int
98+
}
99+
```
100+
101+
```graphql
102+
type User implements HasName & Ranked {
103+
name: String!
104+
rank: Int!
105+
age: Int!
106+
}
107+
```
108+
71109
## Exposing fields from the interface
72110

73111
Just like with [fields on object types](object-types#fields), you can expose fields on interfaces either directly via `@gql.field`, or by defining a function with `@gql.field`.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "resgraph",
3-
"version": "1.1.6",
3+
"version": "1.2.0",
44
"description": "Build GraphQL servers in ReScript.",
55
"main": "index.js",
66
"scripts": {

src/ml/GenerateSchema.ml

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,25 @@ let rec findGraphQLType ~(env : SharedTypes.QueryEnv.t)
226226
let gqlAttribute =
227227
item.attributes |> extractGqlAttribute ~env ~schemaState
228228
in
229+
let gqlImplementsAttributes =
230+
item.attributes |> extractGqlImplementsAttributes ~schemaState ~env
231+
in
232+
(if List.length gqlImplementsAttributes > 0 then
233+
match (gqlAttribute, item) with
234+
| Some ObjectType, {kind = Record _}
235+
| Some Interface, {kind = Record _} ->
236+
()
237+
| _ ->
238+
schemaState
239+
|> addDiagnostic
240+
~diagnostic:
241+
{
242+
loc = item.decl.type_loc;
243+
fileUri = env.file.uri;
244+
message =
245+
"`@gql.implements` can only be used on @gql.type or \
246+
@gql.interface records.";
247+
});
229248
match (gqlAttribute, item) with
230249
| ( Some ObjectType,
231250
{
@@ -247,6 +266,7 @@ let rec findGraphQLType ~(env : SharedTypes.QueryEnv.t)
247266
noticeObjectType id ~displayName ~schemaState ~env
248267
?description:
249268
(GenerateSchemaUtils.attributesToDocstring attributes)
269+
~explicitInterfaces:gqlImplementsAttributes
250270
~makeFields:(fun () ->
251271
fields
252272
|> objectTypeFieldsOfRecordFields ~objectTypeName:displayName
@@ -290,6 +310,7 @@ let rec findGraphQLType ~(env : SharedTypes.QueryEnv.t)
290310
id;
291311
displayName;
292312
interfaces = [];
313+
explicitInterfaces = gqlImplementsAttributes;
293314
fields =
294315
objectTypeFieldsOfRecordFields fields
295316
~objectTypeName:displayName ~env ~full ~schemaState ~debug;
@@ -1175,6 +1196,23 @@ and traverseStructure ?(modulePath = []) ?implStructure ?originModule
11751196
let gqlAttribute =
11761197
attributes |> extractGqlAttribute ~schemaState ~env
11771198
in
1199+
let gqlImplementsAttributes =
1200+
attributes |> extractGqlImplementsAttributes ~schemaState ~env
1201+
in
1202+
(if List.length gqlImplementsAttributes > 0 then
1203+
match (item.kind, gqlAttribute) with
1204+
| Type ({kind = Record _}, _), Some (ObjectType | Interface) -> ()
1205+
| _ ->
1206+
schemaState
1207+
|> addDiagnostic
1208+
~diagnostic:
1209+
{
1210+
loc = item.loc;
1211+
fileUri = env.file.uri;
1212+
message =
1213+
"`@gql.implements` can only be used on @gql.type or \
1214+
@gql.interface records.";
1215+
});
11781216
match (item.kind, gqlAttribute) with
11791217
| Module {type_ = Structure structure; _}, _ ->
11801218
(* Continue into modules (ignore module aliases etc) *)
@@ -1219,7 +1257,7 @@ and traverseStructure ?(modulePath = []) ?implStructure ?originModule
12191257
let displayName = capitalizeFirstChar item.name in
12201258
noticeObjectType ~env ~loc:decl.type_loc ~schemaState
12211259
?description:(attributesToDocstring attributes)
1222-
~displayName
1260+
~displayName ~explicitInterfaces:gqlImplementsAttributes
12231261
~makeFields:(fun () ->
12241262
objectTypeFieldsOfRecordFields fields ~objectTypeName:displayName
12251263
~env ~full ~schemaState ~debug)
@@ -1256,6 +1294,7 @@ and traverseStructure ?(modulePath = []) ?implStructure ?originModule
12561294
~objectTypeName:displayName ~env ~full ~schemaState ~debug;
12571295
description = attributesToDocstring attributes;
12581296
interfaces = [];
1297+
explicitInterfaces = gqlImplementsAttributes;
12591298
typeLocation =
12601299
findTypeLocation item.name ~env ~schemaState
12611300
~loc:decl.type_loc ~expectedType:Interface;

src/ml/GenerateSchemaTypes.ml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ type gqlArg = {
5050

5151
type gqlInterfaceIdentifier = {id: string; displayName: string}
5252

53+
type explicitInterfaceImplementation = {
54+
interfaceName: string;
55+
loc: Location.t;
56+
fileUri: Uri.t;
57+
}
58+
5359
type gqlEnumValue = {
5460
value: string;
5561
description: string option;
@@ -119,6 +125,7 @@ type gqlObjectType = {
119125
typeLocation: typeLocation option;
120126
syntheticTypeLocation: syntheticTypeLocation option;
121127
interfaces: string list;
128+
explicitInterfaces: explicitInterfaceImplementation list;
122129
}
123130

124131
type gqlInterface = {
@@ -128,6 +135,7 @@ type gqlInterface = {
128135
description: string option;
129136
typeLocation: typeLocationLoc;
130137
interfaces: string list;
138+
explicitInterfaces: explicitInterfaceImplementation list;
131139
}
132140

133141
type gqlInputObjectType = {

0 commit comments

Comments
 (0)