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) {
262 			return gqldTypeToString(t, 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) || (inputType && (mem in map.outputOnlyMembers || (cast(GQLDOperation)value && map.name != "mutationType")))) {
269 				continue;
270 			}
271 
272 			if(auto op = cast(GQLDOperation)value) {
273 				if (op.parameters.keys().length) {
274 					formIndent(o, 1, "%s(%s): %s", mem,
275 					        op.parameters.byKeyValue().map!(kv => format("%s: %s", kv.key,
276 					                                        typeToStringMaybeIn(kv.value)))
277 					                .joiner(", ")
278 					                .to!string,
279 					        map.name == "mutationType" ? gqldTypeToString(op.returnType)
280 					                                   : typeToStringMaybeIn(op.returnType));
281 				} else {
282 					// apparently graphql doesn't allow foo(): bar
283 					// so special-case that and turn it into foo: bar
284 					formIndent(o, 1, "%s: %s", mem,
285 					        map.name == "mutationType" ? gqldTypeToString(op.returnType)
286 					                                   : typeToStringMaybeIn(op.returnType));
287 				}
288 			} else {
289 				formIndent(o, 1, "%s: %s", mem, typeToStringMaybeIn(value));
290 			}
291 		}
292 	}
293 
294 	string implementsStr = "";
295 	string typestr = "type";
296 	if(auto unio = cast(GQLDUnion)map) {
297 		typestr = "union";
298 	} else if(auto obj = cast(GQLDObject)map) {
299 		typestr = typeKindToString(obj.typeKind);
300 		if(obj.base && obj.base.typeKind == TypeKind.INTERFACE) {
301 			implementsStr = " implements " ~ obj.base.name;
302 		}
303 	}
304 
305 	formIndent(o, 0, "%s %s%s {", typestr, map.name, implementsStr);
306 	dumpMem(map.name == "mutationType" || typestr == "input");
307 	formIndent(o, 0, "}");
308 
309 	if (type.vis == Visibility.indeterminate) {
310 		formIndent(o, 0, "# note: nestedness of type '%s' not determined; output may be suboptimal", map.name);
311 	}
312 
313 	if(type.vis != Visibility.inputOnly && map.name !in gqlSpecialOps) {
314 		formIndent(o, 0, "input %sIn {", map.name);
315 		dumpMem(true);
316 		formIndent(o, 0, "}");
317 	}
318 }