1 module graphql.schema.toschemafile;
2 
3 import std.array;
4 import std.algorithm.iteration : map, joiner;
5 import std.conv : to;
6 import std.stdio;
7 import std.format;
8 import std.typecons;
9 
10 import graphql.graphql;
11 import graphql.schema.types;
12 import graphql.uda;
13 
14 string schemaToString(T)(GQLDSchema!T sch) {
15 	auto app = appender!string();
16 
17 	TraceableType[string] symTab;
18 
19 	formIndent(app, 0, "schema {");
20 	foreach(it; gqlSpecialOps.byKeyValue) {
21 		if(auto mem = it.key in sch.member) {
22 			formIndent(app, 1, "%s: %s", it.value, mem.name);
23 			traceType(*mem, symTab);
24 		}
25 	}
26 	formIndent(app, 0, "}");
27 
28 	foreach(type; symTab.byValue) {
29 		typeImpl(app, type, symTab);
30 	}
31 
32 	return app.data;
33 }
34 
35 string schemaToString(T)() {
36 	import graphql.schema.resolver;
37 	return schemaToString(toSchema!T());
38 }
39 
40 string schemaToString(T, Q)(GraphQLD!(T, Q) gqld) {
41 	return schemaToString(gqld.schema);
42 }
43 
44 private:
45 
46 static immutable string[string] gqlSpecialOps;
47 shared static this() {
48 	gqlSpecialOps = [ "mutationType":     "mutation",
49 	                  "queryType":        "query",
50 	                  "subscriptionType": "subscription"];
51 }
52 
53 // for tracing; taken from gc algorithms
54 enum Colour {
55 	grey,  // currently being traced (need this to deal with recursive types)
56 	black, // done
57 	// there's also white, for as-yet untouched objects.  But we don't need
58 	// this because we cheat by putting all types into a flat table
59 }
60 
61 // indeterminate is a good .init value
62 // but we want the ordering inputOrOutput < indeterminate < inputAndOutput, indeterminate < inputOnly
63 // so that if we want the intersection of two visibilities, we just take the maximum
64 enum Visibility {
65 	indeterminate,
66 	inputOrOutput = indeterminate - 1, // e.g. 'enum'
67 	inputAndOutput = indeterminate + 1, // e.g. struct ('type' / 'input')
68 	inputOnly, // e.g. @GQLDUda(TypeKind.INPUT_OBJECT) struct ('input')
69 }
70 
71 struct TraceableType {
72 	GQLDType type;
73 	Colour colour;
74 	Visibility vis; // only valid for black types
75 }
76 
77 void formIndent(Out, Args...)(ref Out o, size_t indent, string s, Args args) {
78 	foreach(it; 0 .. indent) {
79 		formattedWrite(o, "\t");
80 	}
81 	formattedWrite(o, s, args);
82 	formattedWrite(o, "\n");
83 }
84 
85 bool isNameSpecial(string s) {
86 	import std.algorithm.searching: startsWith, canFind;
87 	// takes care of gql buildins (__Type, __TypeKind, etc.), as well as
88 	// some unuseful pieces from the d side (__ctor, opCmp, etc.)
89 	return s.startsWith("__") || s.startsWith("op") || ["factory", "toHash", "toString"].canFind(s);
90 }
91 
92 bool isPrimitiveType(const(GQLDType) type) {
93 	return type.kind == GQLDKind.String
94 		|| type.kind == GQLDKind.Float
95 		|| type.kind == GQLDKind.Int
96 		|| type.kind == GQLDKind.Bool;
97 }
98 
99 // all members of o, including derived ones
100 inout(GQLDType)[string] allMember(inout(GQLDMap) m) {
101 	import std.algorithm;
102 	GQLDType[string] ret;
103 
104 	void process(GQLDMap m) {
105 		foreach(k,v; m.member.byPair) {
106 			ret.require(k,v);
107 		}
108 
109 		if(auto o = cast(GQLDObject)m) {
110 			if(o.base) {
111 				process(o.base);
112 			}
113 		}
114 	}
115 
116 	// inout(V)[K].require is broken
117 	process(cast()m);
118 	return cast(inout(GQLDType)[string])ret;
119 }
120 
121 Visibility traceType(GQLDType t, ref TraceableType[string] tab) {
122 	import std.algorithm.comparison : max;
123 	import std.algorithm.iteration : filter;
124 	import std.range.primitives : empty;
125 
126 	if(isPrimitiveType(t) || isNameSpecial(t.name)) {
127 		return Visibility.inputOrOutput;
128 	}
129 
130 	if(t.baseTypeName in tab) {
131 		return tab[t.baseTypeName].colour == Colour.black
132 		 ? tab[t.baseTypeName].vis
133 		 : max(tab[t.baseTypeName].vis, Visibility.indeterminate);
134 	}
135 
136 	// identifies itself as an object, but we really want to dump it as a scalar
137 	if((cast(GQLDObject)t && (cast(GQLDObject)t)
138 	                                 .allMember.byKey
139 	                                 .filter!(m => !isNameSpecial(m))
140 	                                 .empty)
141 	    || cast(GQLDUnion)t) {
142 		auto n = new GQLDScalar(GQLDKind.SimpleScalar);
143 		n.name = t.name;
144 		tab[n.name] = TraceableType(n, Colour.black, Visibility.inputOrOutput);
145 		return tab[n.name].vis;
146 	}
147 	if(cast(GQLDScalar)t) {
148 		tab[t.name] = TraceableType(t, Colour.black, Visibility.inputOrOutput);
149 		return tab[t.name].vis;
150 	}
151 
152 	if(auto op = cast(GQLDOperation)t) {
153 		traceType(op.returnType, tab);
154 		foreach(val; op.parameters.byValue) {
155 			traceType(val, tab);
156 		}
157 		return Visibility.inputAndOutput;
158 	} else if(auto l = cast(GQLDList)t) {
159 		return traceType(l.elementType, tab);
160 	} else if(auto nn = cast(GQLDNonNull)t) {
161 		return traceType(nn.elementType, tab);
162 	} else if(auto nul = cast(GQLDNullable)t) {
163 		return traceType(nul.elementType, tab);
164 	}
165 
166 	auto map = cast(GQLDMap)t;
167 	if(!map) {
168 		return Visibility.inputOrOutput; // won't be dumped anyway, so doesn't matter
169 	}
170 
171 	tab[map.name] = TraceableType(map, Colour.grey, Visibility.inputAndOutput);
172 	scope(exit) tab[map.name].colour = Colour.black;
173 	if(cast(GQLDObject)map && (cast(GQLDObject)map).typeKind == TypeKind.INPUT_OBJECT) {
174 		tab[map.name].vis = Visibility.inputOnly;
175 	}
176 
177 	foreach(mem, val; map.allMember) {
178 		if(isNameSpecial(mem) || isPrimitiveType(val)) {
179 			continue;
180 		}
181 		Visibility oldVis = tab[map.name].vis;
182 		Visibility newVis = traceType(val, tab);
183 		if((newVis == Visibility.inputOnly && oldVis == Visibility.inputAndOutput) ||
184 		   (oldVis == Visibility.inputOnly && newVis == Visibility.inputAndOutput)) {
185 			assert(0, map.name ~ " cannot be both an input type and have output-only members");
186 		}
187 		tab[map.name].vis = max(newVis, oldVis);
188 	}
189 
190 	return tab[map.name].vis;
191 }
192 
193 string gqldTypeToString(const(GQLDType) t, string nameSuffix = "", Flag!"nonNullable" nonNullable = Yes.nonNullable) {
194 	if(auto base = cast(const(GQLDNullable))t) {
195 		return gqldTypeToString(base.elementType, nameSuffix, No.nonNullable);
196 	} else if(auto list = cast(const(GQLDList))t) {
197 		return '[' ~ gqldTypeToString(list.elementType, nameSuffix, Yes.nonNullable) ~ ']' ~ (nonNullable ? "!" : "");
198 	} else if(auto nn = cast(const(GQLDNonNull))t) {
199 		return gqldTypeToString(nn.elementType, nameSuffix, Yes.nonNullable);
200 	}
201 	return t.name ~ nameSuffix ~ (nonNullable ? "!" : "");
202 }
203 
204 string baseTypeName(const(GQLDType) t) {
205 	if(auto base = cast(const(GQLDNullable))t) {
206 		return baseTypeName(base.elementType);
207 	} else if(auto list = cast(const(GQLDList))t) {
208 		return baseTypeName(list.elementType);
209 	} else if(auto nn = cast(const(GQLDNonNull))t) {
210 		return baseTypeName(nn.elementType);
211 	}
212 	return t.name;
213 }
214 
215 string typeKindToString(TypeKind tk) {
216 	final switch(tk) {
217 		case TypeKind.UNDEFINED: return "type";
218 		case TypeKind.SCALAR: return "SCALAR";
219 		case TypeKind.OBJECT: return "type";
220 		case TypeKind.INTERFACE: return "interface";
221 		case TypeKind.UNION: return "union";
222 		case TypeKind.ENUM: return "enum";
223 		case TypeKind.INPUT_OBJECT: return "input";
224 		case TypeKind.LIST: return "LIST";
225 		case TypeKind.NON_NULL: return "NON_NULL";
226 	}
227 }
228 
229 void typeImpl(Out)(ref Out o, TraceableType type, in TraceableType[string] tab) {
230 	assert(!isPrimitiveType(type.type) && !isNameSpecial(type.type.baseTypeName));
231 
232 	if(auto enu = cast(GQLDEnum)type.type) {
233 		formIndent(o, 0, "enum %s {", enu.name);
234 
235 		foreach (m; enu.memberNames) {
236 			formIndent(o, 1, "%s,", m);
237 		}
238 
239 		formIndent(o, 0, "}");
240 		return;
241 	}
242 
243 	if(cast(GQLDScalar)type.type) {
244 		// it's not allowed to have, for instance, 'scalar subscriptionType'
245 		// so special-case the top-level operations to have a dummy member
246 		if(type.type.name in gqlSpecialOps) {
247 			formIndent(o, 0, "type %s { _: Boolean }", type.type.name);
248 		} else {
249 			formIndent(o, 0, "scalar %s", type.type.name);
250 		}
251 		return;
252 	}
253 
254 	const map = cast(GQLDMap)type.type;
255 	if(!map) {
256 		formIndent(o, 0, "# graphqld couldn't format type '%s' / '%s' / '%s'", type.type.kind, type.type.name, type.type);
257 		return;
258 	}
259 
260 	void dumpMem(bool inputType) {
261 		string typeToStringMaybeIn(const(GQLDType) t, bool isParam = false) {
262 			return gqldTypeToString(t, (isParam || inputType) && t.baseTypeName in tab
263 						&& tab[t.baseTypeName].vis != Visibility.inputOnly
264 						&& tab[t.baseTypeName].vis != Visibility.inputOrOutput ? "In" : "");
265 		}
266 
267 		foreach(mem, value; map.allMember) {
268 			if(isNameSpecial(mem)
269 					|| (inputType
270 						&& (mem in map.outputOnlyMembers
271 							|| (cast(GQLDOperation)value && map.name != "mutationType")
272 							)
273 						))
274 			{
275 				continue;
276 			}
277 
278 			if(auto op = cast(GQLDOperation)value) {
279 				if(op.parameters.keys().length) {
280 					formIndent(o, 1, "%s(%s): %s%s", mem
281 					        , op.parameters.byKeyValue()
282 								.map!(kv => format("%s: %s", kv.key
283 										, typeToStringMaybeIn(kv.value, true)))
284 								.joiner(", ")
285 					            .to!string
286 					        , map.name == "mutationType"
287 								? gqldTypeToString(op.returnType)
288 					            : typeToStringMaybeIn(op.returnType)
289 							, typeToDeprecationMessage(op));
290 				} else {
291 					// apparently graphql doesn't allow foo(): bar
292 					// so special-case that and turn it into foo: bar
293 					formIndent(o, 1, "%s: %s%s", mem
294 					        , map.name == "mutationType"
295 								? gqldTypeToString(op.returnType)
296 					            : typeToStringMaybeIn(op.returnType)
297 							, typeToDeprecationMessage(op));
298 				}
299 			} else {
300 				formIndent(o, 1, "%s: %s%s", mem, typeToStringMaybeIn(value)
301 						, typeToDeprecationMessage(value)
302 						);
303 			}
304 		}
305 	}
306 
307 	string implementsStr = "";
308 	string typestr = "type";
309 	if(auto unio = cast(GQLDUnion)map) {
310 		typestr = "union";
311 	} else if(auto obj = cast(GQLDObject)map) {
312 		typestr = typeKindToString(obj.typeKind);
313 		if(obj.base && obj.base.typeKind == TypeKind.INTERFACE) {
314 			implementsStr = " implements " ~ obj.base.name;
315 		}
316 	}
317 
318 	formIndent(o, 0, "%s %s%s {", typestr, map.name, implementsStr);
319 	dumpMem(map.name == "mutationType" || typestr == "input");
320 	formIndent(o, 0, "}");
321 
322 	if (type.vis == Visibility.indeterminate) {
323 		formIndent(o, 0, "# note: nestedness of type '%s' not determined; output may be suboptimal", map.name);
324 	}
325 
326 	if(type.vis != Visibility.inputOnly && map.name !in gqlSpecialOps) {
327 		formIndent(o, 0, "input %sIn {", map.name);
328 		dumpMem(true);
329 		formIndent(o, 0, "}");
330 	}
331 }
332 
333 private string typeToDeprecationMessage(const(GQLDType) t) {
334 	return t.deprecatedInfo.isDeprecated == IsDeprecated.yes
335 		? ` @deprecated(reason: "`
336 			~ t.deprecatedInfo.deprecationReason
337 			~ `")`
338 		: "";
339 }