Skip to content

Commit 53afe99

Browse files
feat: Adds printSchema
1 parent 4e0c879 commit 53afe99

File tree

2 files changed

+1315
-0
lines changed

2 files changed

+1315
-0
lines changed
Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
import Foundation
2+
3+
public func printSchema(schema: GraphQLSchema) -> String {
4+
return printFilteredSchema(
5+
schema: schema,
6+
directiveFilter: { n in !isSpecifiedDirective(n) },
7+
typeFilter: isDefinedType
8+
)
9+
}
10+
11+
public func printIntrospectionSchema(schema: GraphQLSchema) -> String {
12+
return printFilteredSchema(
13+
schema: schema,
14+
directiveFilter: isSpecifiedDirective,
15+
typeFilter: isIntrospectionType
16+
)
17+
}
18+
19+
func isDefinedType(type: GraphQLNamedType) -> Bool {
20+
return !isSpecifiedScalarType(type) && !isIntrospectionType(type: type)
21+
}
22+
23+
func printFilteredSchema(
24+
schema: GraphQLSchema,
25+
directiveFilter: (GraphQLDirective) -> Bool,
26+
typeFilter: (GraphQLNamedType) -> Bool
27+
) -> String {
28+
let directives = schema.directives.filter { directiveFilter($0) }
29+
let types = schema.typeMap.values.filter { typeFilter($0) }
30+
31+
var result = [printSchemaDefinition(schema: schema)]
32+
result.append(contentsOf: directives.map { printDirective(directive: $0) })
33+
result.append(contentsOf: types.map { printType(type: $0) })
34+
35+
return result.compactMap { $0 }
36+
.joined(separator: "\n\n")
37+
}
38+
39+
func printSchemaDefinition(schema: GraphQLSchema) -> String? {
40+
let queryType = schema.queryType
41+
let mutationType = schema.mutationType
42+
let subscriptionType = schema.subscriptionType
43+
44+
// Special case: When a schema has no root operation types, no valid schema
45+
// definition can be printed.
46+
if queryType == nil, mutationType == nil, subscriptionType == nil {
47+
return nil
48+
}
49+
50+
// Only print a schema definition if there is a description or if it should
51+
// not be omitted because of having default type names.
52+
if schema.description != nil || !hasDefaultRootOperationTypes(schema: schema) {
53+
var result = printDescription(schema.description) +
54+
"schema {\n"
55+
if let queryType = queryType {
56+
result = result + " query: \(queryType.name)\n"
57+
}
58+
if let mutationType = mutationType {
59+
result = result + " mutation: \(mutationType.name)\n"
60+
}
61+
if let subscriptionType = subscriptionType {
62+
result = result + " subscription: \(subscriptionType.name)\n"
63+
}
64+
result = result + "}"
65+
return result
66+
}
67+
return nil
68+
}
69+
70+
/**
71+
* GraphQL schema define root types for each type of operation. These types are
72+
* the same as any other type and can be named in any manner, however there is
73+
* a common naming convention:
74+
*
75+
* ```graphql
76+
* schema {
77+
* query: Query
78+
* mutation: Mutation
79+
* subscription: Subscription
80+
* }
81+
* ```
82+
*
83+
* When using this naming convention, the schema description can be omitted so
84+
* long as these names are only used for operation types.
85+
*
86+
* Note however that if any of these default names are used elsewhere in the
87+
* schema but not as a root operation type, the schema definition must still
88+
* be printed to avoid ambiguity.
89+
*/
90+
func hasDefaultRootOperationTypes(schema: GraphQLSchema) -> Bool {
91+
// The goal here is to check if a type was declared using the default names of "Query",
92+
// "Mutation" or "Subscription". We do so by comparing object IDs to determine if the
93+
// schema operation object is the same as the type object by that name.
94+
return (
95+
schema.queryType.map { ObjectIdentifier($0) }
96+
== (schema.getType(name: "Query") as? GraphQLObjectType).map { ObjectIdentifier($0) } &&
97+
schema.mutationType.map { ObjectIdentifier($0) }
98+
== (schema.getType(name: "Mutation") as? GraphQLObjectType)
99+
.map { ObjectIdentifier($0) } &&
100+
schema.subscriptionType.map { ObjectIdentifier($0) }
101+
== (schema.getType(name: "Subscription") as? GraphQLObjectType)
102+
.map { ObjectIdentifier($0) }
103+
)
104+
}
105+
106+
public func printType(type: GraphQLNamedType) -> String {
107+
if let type = type as? GraphQLScalarType {
108+
return printScalar(type: type)
109+
}
110+
if let type = type as? GraphQLObjectType {
111+
return printObject(type: type)
112+
}
113+
if let type = type as? GraphQLInterfaceType {
114+
return printInterface(type: type)
115+
}
116+
if let type = type as? GraphQLUnionType {
117+
return printUnion(type: type)
118+
}
119+
if let type = type as? GraphQLEnumType {
120+
return printEnum(type: type)
121+
}
122+
if let type = type as? GraphQLInputObjectType {
123+
return printInputObject(type: type)
124+
}
125+
126+
// Not reachable, all possible types have been considered.
127+
fatalError("Unexpected type: " + type.name)
128+
}
129+
130+
func printScalar(type: GraphQLScalarType) -> String {
131+
return printDescription(type.description) +
132+
"scalar \(type.name)" +
133+
printSpecifiedByURL(scalar: type)
134+
}
135+
136+
func printImplementedInterfaces(
137+
interfaces: [GraphQLInterfaceType]
138+
) -> String {
139+
return interfaces.isEmpty
140+
? ""
141+
: " implements " + interfaces.map { $0.name }.joined(separator: " & ")
142+
}
143+
144+
func printObject(type: GraphQLObjectType) -> String {
145+
return
146+
printDescription(type.description) +
147+
"type \(type.name)" +
148+
printImplementedInterfaces(interfaces: (try? type.getInterfaces()) ?? []) +
149+
printFields(fields: (try? type.getFields()) ?? [:])
150+
}
151+
152+
func printInterface(type: GraphQLInterfaceType) -> String {
153+
return
154+
printDescription(type.description) +
155+
"interface \(type.name)" +
156+
printImplementedInterfaces(interfaces: (try? type.getInterfaces()) ?? []) +
157+
printFields(fields: (try? type.getFields()) ?? [:])
158+
}
159+
160+
func printUnion(type: GraphQLUnionType) -> String {
161+
let types = (try? type.getTypes()) ?? []
162+
return
163+
printDescription(type.description) +
164+
"union \(type.name)" +
165+
(types.isEmpty ? "" : " = " + types.map { $0.name }.joined(separator: " | "))
166+
}
167+
168+
func printEnum(type: GraphQLEnumType) -> String {
169+
let values = type.values.enumerated().map { i, value in
170+
printDescription(value.description, indentation: " ", firstInBlock: i == 0) +
171+
" " +
172+
value.name +
173+
printDeprecated(reason: value.deprecationReason)
174+
}
175+
176+
return printDescription(type.description) + "enum \(type.name)" + printBlock(items: values)
177+
}
178+
179+
func printInputObject(type: GraphQLInputObjectType) -> String {
180+
let inputFields = (try? type.getFields()) ?? [:]
181+
let fields = inputFields.values.enumerated().map { i, f in
182+
printDescription(f.description, indentation: " ", firstInBlock: i == 0) + " " +
183+
printInputValue(arg: f)
184+
}
185+
186+
return
187+
printDescription(type.description) +
188+
"input \(type.name)" +
189+
(type.isOneOf ? " @oneOf" : "") +
190+
printBlock(items: fields)
191+
}
192+
193+
func printFields(fields: GraphQLFieldDefinitionMap) -> String {
194+
let fields = fields.values.enumerated().map { i, f in
195+
printDescription(f.description, indentation: " ", firstInBlock: i == 0) +
196+
" " +
197+
f.name +
198+
printArgs(args: f.args, indentation: " ") +
199+
": " +
200+
f.type.debugDescription +
201+
printDeprecated(reason: f.deprecationReason)
202+
}
203+
return printBlock(items: fields)
204+
}
205+
206+
func printBlock(items: [String]) -> String {
207+
return items.isEmpty ? "" : " {\n" + items.joined(separator: "\n") + "\n}"
208+
}
209+
210+
func printArgs(
211+
args: [GraphQLArgumentDefinition],
212+
indentation: String = ""
213+
) -> String {
214+
if args.isEmpty {
215+
return ""
216+
}
217+
218+
// If every arg does not have a description, print them on one line.
219+
if args.allSatisfy({ $0.description == nil }) {
220+
return "(" + args.map { printArgValue(arg: $0) }.joined(separator: ", ") + ")"
221+
}
222+
223+
return
224+
"(\n" +
225+
args.enumerated().map { i, arg in
226+
printDescription(
227+
arg.description,
228+
indentation: " " + indentation,
229+
firstInBlock: i == 0
230+
) +
231+
" " +
232+
indentation +
233+
printArgValue(arg: arg)
234+
}.joined(separator: "\n") +
235+
"\n" +
236+
indentation +
237+
")"
238+
}
239+
240+
func printArgValue(arg: GraphQLArgumentDefinition) -> String {
241+
var argDecl = arg.name + ": " + arg.type.debugDescription
242+
if let defaultValue = arg.defaultValue {
243+
if defaultValue == .null {
244+
argDecl = argDecl + " = null"
245+
} else if let defaultAST = try! astFromValue(value: defaultValue, type: arg.type) {
246+
argDecl = argDecl + " = \(print(ast: defaultAST))"
247+
}
248+
}
249+
return argDecl + printDeprecated(reason: arg.deprecationReason)
250+
}
251+
252+
func printInputValue(arg: InputObjectFieldDefinition) -> String {
253+
var argDecl = arg.name + ": " + arg.type.debugDescription
254+
if let defaultAST = try? astFromValue(value: arg.defaultValue ?? .null, type: arg.type) {
255+
argDecl = argDecl + " = \(print(ast: defaultAST))"
256+
}
257+
return argDecl + printDeprecated(reason: arg.deprecationReason)
258+
}
259+
260+
public func printDirective(directive: GraphQLDirective) -> String {
261+
return
262+
printDescription(directive.description) +
263+
"directive @" +
264+
directive.name +
265+
printArgs(args: directive.args) +
266+
(directive.isRepeatable ? " repeatable" : "") +
267+
" on " +
268+
directive.locations.map { $0.rawValue }.joined(separator: " | ")
269+
}
270+
271+
func printDeprecated(reason: String?) -> String {
272+
guard let reason = reason else {
273+
return ""
274+
}
275+
if reason != defaultDeprecationReason {
276+
let astValue = print(ast: StringValue(value: reason))
277+
return " @deprecated(reason: \(astValue))"
278+
}
279+
return " @deprecated"
280+
}
281+
282+
func printSpecifiedByURL(scalar: GraphQLScalarType) -> String {
283+
guard let specifiedByURL = scalar.specifiedByURL else {
284+
return ""
285+
}
286+
let astValue = StringValue(value: specifiedByURL)
287+
return " @specifiedBy(url: \"\(astValue.value)\")"
288+
}
289+
290+
func printDescription(
291+
_ description: String?,
292+
indentation: String = "",
293+
firstInBlock: Bool = true
294+
) -> String {
295+
guard let description = description else {
296+
return ""
297+
}
298+
299+
let blockString = print(ast: StringValue(
300+
value: description,
301+
block: isPrintableAsBlockString(description)
302+
))
303+
304+
let prefix = (!indentation.isEmpty && !firstInBlock) ? "\n" + indentation : indentation
305+
306+
return prefix + blockString.replacingOccurrences(of: "\n", with: "\n" + indentation) + "\n"
307+
}

0 commit comments

Comments
 (0)