Skip to content

Commit 5006258

Browse files
authored
Support @as for reserved record fields (#42)
1 parent 3df6485 commit 5006258

6 files changed

Lines changed: 271 additions & 19 deletions

File tree

src/ml/GenerateSchema.ml

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -782,7 +782,7 @@ and inputObjectFieldsOfRecordFields ~objectTypeName ~env ~debug ~schemaState
782782
~(full : SharedTypes.full) (fields : SharedTypes.field list) =
783783
fields
784784
|> List.filter_map (fun (field : SharedTypes.field) ->
785-
let name = field.fname.txt in
785+
let name = nameFromAttribute field.attributes ~default:field.fname.txt in
786786
match
787787
findGraphQLType field.typ ~debug ~loc:field.fname.loc ~full ~env
788788
~schemaState
@@ -992,11 +992,11 @@ and objectTypeFieldsOfRecordFields ~objectTypeName ~env ~schemaState ~debug
992992
| Some attr -> Some (field, attr))
993993
|> List.filter_map (fun ((field : SharedTypes.field), _attr) ->
994994
let fieldType = field.typ in
995+
let name = nameFromAttribute field.attributes ~default:field.fname.txt in
995996
let typ =
996997
findGraphQLType fieldType ~debug ~loc:field.fname.loc ~full ~env
997998
~schemaState
998-
~typeContext:
999-
(ObjectField {objectTypeName; fieldName = field.fname.txt})
999+
~typeContext:(ObjectField {objectTypeName; fieldName = name})
10001000
in
10011001
match typ with
10021002
| None ->
@@ -1017,7 +1017,6 @@ and objectTypeFieldsOfRecordFields ~objectTypeName ~env ~schemaState ~debug
10171017
};
10181018
None
10191019
| Some typ ->
1020-
let name = field.fname.txt in
10211020
Some
10221021
{
10231022
name;
@@ -1037,11 +1036,11 @@ and objectTypeFieldsOfInlineRecordFields ~objectTypeName ~env ~schemaState
10371036
fields
10381037
|> List.filter_map (fun (field : SharedTypes.field) ->
10391038
let fieldType = field.typ in
1039+
let name = nameFromAttribute field.attributes ~default:field.fname.txt in
10401040
let typ =
10411041
findGraphQLType fieldType ~debug ~loc:field.fname.loc ~full ~env
10421042
~schemaState
1043-
~typeContext:
1044-
(ObjectField {objectTypeName; fieldName = field.fname.txt})
1043+
~typeContext:(ObjectField {objectTypeName; fieldName = name})
10451044
in
10461045
match typ with
10471046
| None ->
@@ -1062,7 +1061,6 @@ and objectTypeFieldsOfInlineRecordFields ~objectTypeName ~env ~schemaState
10621061
};
10631062
None
10641063
| Some typ ->
1065-
let name = field.fname.txt in
10661064
Some
10671065
{
10681066
name;

src/ml/GenerateSchemaValidation.ml

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,31 @@ let validateName ~name ~(typeLocation : typeLocation)
3737
name;
3838
}
3939

40-
let validateFields ~schemaState (fields : gqlField list) =
40+
let validateFieldNameUniqueness ~schemaState ~(parentTypeName : string)
41+
(fields : gqlField list) =
42+
let seen = Hashtbl.create (List.length fields) in
43+
fields
44+
|> List.iter (fun (field : gqlField) ->
45+
match Hashtbl.find_opt seen field.name with
46+
| None -> Hashtbl.add seen field.name field
47+
| Some firstField ->
48+
schemaState
49+
|> addDiagnostic
50+
~diagnostic:
51+
{
52+
loc = field.loc;
53+
fileUri = field.fileUri;
54+
message =
55+
Printf.sprintf
56+
"Field `%s` appears more than once on GraphQL type `%s`. \
57+
Rename one of the fields or change its @as attribute. \
58+
The first field was declared in %s."
59+
field.name parentTypeName firstField.fileName;
60+
})
61+
62+
let validateFields ~schemaState ~(parentTypeName : string)
63+
(fields : gqlField list) =
64+
validateFieldNameUniqueness ~schemaState ~parentTypeName fields;
4165
fields
4266
|> List.iter (fun (f : gqlField) ->
4367
validateName ~name:f.name
@@ -69,16 +93,11 @@ let validateSchema (schemaState : schemaState) =
6993

7094
schemaState.types
7195
|> Hashtbl.iter (fun _name (typ : gqlObjectType) ->
72-
match typ.typeLocation with
73-
| Some _typeLocation -> validateFields ~schemaState typ.fields
74-
| None -> ());
96+
validateFields ~schemaState ~parentTypeName:typ.displayName typ.fields);
7597

7698
schemaState.inputObjects
7799
|> Hashtbl.iter (fun _name (typ : gqlInputObjectType) ->
78-
(* A lot has already been validated on adding the type itself. *)
79-
match typ.typeLocation with
80-
| Some _typeLocation -> validateFields ~schemaState typ.fields
81-
| None -> ());
100+
validateFields ~schemaState ~parentTypeName:typ.displayName typ.fields);
82101

83102
schemaState.enums
84103
|> Hashtbl.iter (fun _name (typ : gqlEnum) ->
@@ -96,4 +115,4 @@ let validateSchema (schemaState : schemaState) =
96115
|> Hashtbl.iter (fun _name (typ : gqlInterface) ->
97116
(* Subtype rules etc for interface fields are a bit complicated, so we
98117
let graphql-js do it at runtime instead. *)
99-
validateFields ~schemaState typ.fields)
118+
validateFields ~schemaState ~parentTypeName:typ.displayName typ.fields)

tests/runtime-interface-returns.mjs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,4 +101,44 @@ assert.match(
101101
/Interface Labelled resolveType expected a tagged value from Interface_labelled\.Resolver\.t/,
102102
);
103103

104-
console.log("runtime interface return regressions passed");
104+
const reservedWords = await run(`
105+
query ReservedWords {
106+
reservedWordRecord {
107+
constraint
108+
external
109+
include
110+
let
111+
module
112+
open
113+
switch
114+
type
115+
}
116+
}
117+
`);
118+
119+
assert.equal(reservedWords.errors, undefined);
120+
assert.deepEqual(plain(reservedWords.data), {
121+
reservedWordRecord: {
122+
constraint: "constraint",
123+
external: "external",
124+
include: "include",
125+
let: "let",
126+
module: "module",
127+
open: "open",
128+
switch: "switch",
129+
type: "type",
130+
},
131+
});
132+
133+
const reservedInput = await run(`
134+
query ReservedInput {
135+
reservedWordInputEcho(input: {constraint: "input constraint", type: "input type"})
136+
}
137+
`);
138+
139+
assert.equal(reservedInput.errors, undefined);
140+
assert.deepEqual(plain(reservedInput.data), {
141+
reservedWordInputEcho: "input constraint:input type",
142+
});
143+
144+
console.log("runtime regressions passed");

tests/src/AppReScript12.res

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ type res12Record = {
55
@gql.field
66
withDoc: string,
77
/** Deprecated attribute should survive too. */
8-
@deprecated("old field")
9-
@gql.field
8+
@deprecated("old field") @gql.field
109
oldField: int,
1110
}
1211

@@ -18,3 +17,48 @@ type res12Input =
1817
payload: string,
1918
})
2019
| Empty
20+
21+
/** Reserved ReScript field names can be exposed as GraphQL names. */
22+
@gql.type
23+
type reservedWordRecord = {
24+
@as("constraint") @gql.field
25+
constraint_: string,
26+
@as("external") @gql.field
27+
external_: string,
28+
@as("include") @gql.field
29+
include_: string,
30+
@as("let") @gql.field
31+
let_: string,
32+
@as("module") @gql.field
33+
module_: string,
34+
@as("open") @gql.field
35+
open_: string,
36+
@as("switch") @gql.field
37+
switch_: string,
38+
@as("type") @gql.field
39+
type_: string,
40+
}
41+
42+
@gql.inputObject
43+
type reservedWordInput = {
44+
@as("constraint")
45+
constraint_: string,
46+
@as("type")
47+
type_: string,
48+
}
49+
50+
@gql.field
51+
let reservedWordRecord = (_: Query.query): reservedWordRecord => {
52+
constraint_: "constraint",
53+
external_: "external",
54+
include_: "include",
55+
let_: "let",
56+
module_: "module",
57+
open_: "open",
58+
switch_: "switch",
59+
type_: "type",
60+
}
61+
62+
@gql.field
63+
let reservedWordInputEcho = (_: Query.query, ~input: reservedWordInput) =>
64+
input.constraint_ ++ ":" ++ input.type_

tests/src/__generated__/ResGraphSchema.res

Lines changed: 131 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)