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 inputOutput < indeterminate < hasOutputOnly
63 // so that if we want the intersection of two visibilities, we just take the maximum
64 enum Visibility {
65 	indeterminate,
66 	inputOutput = indeterminate - 1,
67 	hasOutputOnly = indeterminate + 1,
68 }
69 
70 struct TraceableType {
71 	GQLDType type;
72 	Colour colour;
73 	Visibility vis; // only valid for black types
74 }
75 
76 void formIndent(Out, Args...)(ref Out o, size_t indent, string s, Args args) {
77 	foreach(it; 0 .. indent) {
78 		formattedWrite(o, "\t");
79 	}
80 	formattedWrite(o, s, args);
81 	formattedWrite(o, "\n");
82 }
83 
84 bool isNameSpecial(string s) {
85 	import std.algorithm.searching: startsWith, canFind;
86 	// takes care of gql buildins (__Type, __TypeKind, etc.), as well as
87 	// some unuseful pieces from the d side (__ctor, opCmp, etc.)
88 	return s.startsWith("__") || s.startsWith("op") || ["factory", "toHash", "toString"].canFind(s);
89 }
90 
91 bool isPrimitiveType(const(GQLDType) type) {
92 	return type.kind == GQLDKind.String
93 		|| type.kind == GQLDKind.Float
94 		|| type.kind == GQLDKind.Int
95 		|| type.kind == GQLDKind.Bool;
96 }
97 
98 // all members of o, including derived ones
99 inout(GQLDType)[string] allMember(inout(GQLDMap) m) {
100 	import std.algorithm;
101 	GQLDType[string] ret;
102 
103 	void process(GQLDMap m) {
104 		m.member.byPair.each!((k,v) => ret.require(k,v));
105 
106 		if(auto o = cast(GQLDObject)m) {
107 			if(o.base) {
108 				process(o.base);
109 			}
110 		}
111 	}
112 
113 	// inout(V)[K].require is broken
114 	process(cast()m);
115 	return cast(inout(GQLDType)[string])ret;
116 }
117 
118 Visibility traceType(GQLDType t, ref TraceableType[string] tab) {
119 	import std.algorithm.comparison : max;
120 	import std.algorithm.iteration : filter;
121 	import std.range.primitives : empty;
122 
123 	if(isPrimitiveType(t) || isNameSpecial(t.name)) {
124 		return Visibility.inputOutput;
125 	}
126 
127 	if(t.baseTypeName in tab) {
128 		return tab[t.baseTypeName].colour == Colour.black
129 		 ? tab[t.baseTypeName].vis
130 		 : max(tab[t.baseTypeName].vis, Visibility.indeterminate);
131 	}
132 
133 	// identifies itself as an object, but we really want to dump it as a scalar
134 	if((cast(GQLDObject)t && (cast(GQLDObject)t)
135 	                                 .allMember.byKey
136 	                                 .filter!(m => !isNameSpecial(m))
137 	                                 .empty)
138 	    || cast(GQLDUnion)t) {
139 		auto n = new GQLDScalar(GQLDKind.SimpleScalar);
140 		n.name = t.name;
141 		tab[n.name] = TraceableType(n, Colour.black, Visibility.inputOutput);
142 		return tab[n.name].vis;
143 	}
144 	if(cast(GQLDScalar)t) {
145 		tab[t.name] = TraceableType(t, Colour.black, Visibility.inputOutput);
146 		return tab[t.name].vis;
147 	}
148 
149 	if(auto op = cast(GQLDOperation)t) {
150 		traceType(op.returnType, tab);
151 		foreach(val; op.parameters.byValue) {
152 			traceType(val, tab);
153 		}
154 		return Visibility.hasOutputOnly;
155 	} else if(auto l = cast(GQLDList)t) {
156 		return traceType(l.elementType, tab);
157 	} else if(auto nn = cast(GQLDNonNull)t) {
158 		return traceType(nn.elementType, tab);
159 	} else if(auto nul = cast(GQLDNullable)t) {
160 		return traceType(nul.elementType, tab);
161 	}
162 
163 	auto map = cast(GQLDMap)t;
164 	if(!map) {
165 		return Visibility.inputOutput; // won't be dumped anyway, so doesn't matter
166 	}
167 
168 	tab[map.name] = TraceableType(map, Colour.grey, map.outputOnlyMembers.length ? Visibility.hasOutputOnly : Visibility.inputOutput);
169 	scope(exit) tab[map.name].colour = Colour.black;
170 
171 	foreach(mem, val; map.allMember) {
172 		if(isNameSpecial(mem) || isPrimitiveType(val)) {
173 			continue;
174 		}
175 
176 		tab[map.name].vis = max(traceType(val, tab), tab[map.name].vis);
177 	}
178 
179 	return tab[map.name].vis;
180 }
181 
182 string gqldTypeToString(const(GQLDType) t, string nameSuffix = "", Flag!"nonNullable" nonNullable = Yes.nonNullable) {
183 	if(auto base = cast(const(GQLDNullable))t) {
184 		return gqldTypeToString(base.elementType, nameSuffix, No.nonNullable);
185 	} else if(auto list = cast(const(GQLDList))t) {
186 		return '[' ~ gqldTypeToString(list.elementType, nameSuffix, Yes.nonNullable) ~ ']' ~ (nonNullable ? "!" : "");
187 	} else if(auto nn = cast(const(GQLDNonNull))t) {
188 		return gqldTypeToString(nn.elementType, nameSuffix, Yes.nonNullable);
189 	}
190 	return t.name ~ nameSuffix ~ (nonNullable ? "!" : "");
191 }
192 
193 string baseTypeName(const(GQLDType) t) {
194 	if(auto base = cast(const(GQLDNullable))t) {
195 		return baseTypeName(base.elementType);
196 	} else if(auto list = cast(const(GQLDList))t) {
197 		return baseTypeName(list.elementType);
198 	} else if(auto nn = cast(const(GQLDNonNull))t) {
199 		return baseTypeName(nn.elementType);
200 	}
201 	return t.name;
202 }
203 
204 string typeKindToString(TypeKind tk) {
205 	final switch(tk) {
206 		case TypeKind.UNDEFINED: return "type";
207 		case TypeKind.SCALAR: return "SCALAR";
208 		case TypeKind.OBJECT: return "type";
209 		case TypeKind.INTERFACE: return "interface";
210 		case TypeKind.UNION: return "union";
211 		case TypeKind.ENUM: return "enum";
212 		case TypeKind.INPUT_OBJECT: return "input";
213 		case TypeKind.LIST: return "LIST";
214 		case TypeKind.NON_NULL: return "NON_NULL";
215 	}
216 }
217 
218 void typeImpl(Out)(ref Out o, TraceableType type, in TraceableType[string] tab) {
219 	assert(!isPrimitiveType(type.type) && !isNameSpecial(type.type.baseTypeName));
220 
221 	if(auto enu = cast(GQLDEnum)type.type) {
222 		formIndent(o, 0, "enum %s {", enu.name);
223 
224 		foreach (m; enu.memberNames) {
225 			formIndent(o, 1, "%s,", m);
226 		}
227 
228 		formIndent(o, 0, "}");
229 		return;
230 	}
231 
232 	if(cast(GQLDScalar)type.type) {
233 		// it's not allowed to have, for instance, 'scalar subscriptionType'
234 		// so special-case the top-level operations to have a dummy member
235 		if(type.type.name in gqlSpecialOps) {
236 			formIndent(o, 0, "type %s { _: Boolean }", type.type.name);
237 		} else {
238 			formIndent(o, 0, "scalar %s", type.type.name);
239 		}
240 		return;
241 	}
242 
243 	const map = cast(GQLDMap)type.type;
244 	if(!map) {
245 		formIndent(o, 0, "# graphqld couldn't format type '%s' / '%s' / '%s'", type.type.kind, type.type.name, type.type);
246 		return;
247 	}
248 
249 	void dumpMem(bool inputType) {
250 		string typeToStringMaybeIn(const(GQLDType) t) {
251 			return gqldTypeToString(t, inputType && t.baseTypeName in tab && tab[t.baseTypeName].vis != Visibility.inputOutput ? "In" : "");
252 		}
253 
254 		foreach(mem, value; map.allMember) {
255 			if(isNameSpecial(mem) || (inputType && (mem in map.outputOnlyMembers || (cast(GQLDOperation)value && map.name != "mutationType")))) {
256 				continue;
257 			}
258 
259 			if(auto op = cast(GQLDOperation)value) {
260 				if (op.parameters.keys().length) {
261 					formIndent(o, 1, "%s(%s): %s", mem,
262 					        op.parameters.byKeyValue().map!(kv => format("%s: %s", kv.key,
263 					                                        typeToStringMaybeIn(kv.value)))
264 					                .joiner(", ")
265 					                .to!string,
266 					        map.name == "mutationType" ? gqldTypeToString(op.returnType)
267 					                                   : typeToStringMaybeIn(op.returnType));
268 				} else {
269 					// apparently graphql doesn't allow foo(): bar
270 					// so special-case that and turn it into foo: bar
271 					formIndent(o, 1, "%s: %s", mem,
272 					        map.name == "mutationType" ? gqldTypeToString(op.returnType)
273 					                                   : typeToStringMaybeIn(op.returnType));
274 				}
275 			} else {
276 				formIndent(o, 1, "%s: %s", mem, typeToStringMaybeIn(value));
277 			}
278 		}
279 	}
280 
281 	string implementsStr = "";
282 	string typestr = "type";
283 	if(auto unio = cast(GQLDUnion)map) {
284 		typestr = "union";
285 	} else if(auto obj = cast(GQLDObject)map) {
286 		typestr = typeKindToString(obj.typeKind);
287 		if(obj.base && obj.base.typeKind == TypeKind.INTERFACE) {
288 			implementsStr = " implements " ~ obj.base.name;
289 		}
290 	}
291 
292 	formIndent(o, 0, "%s %s%s {", typestr, map.name, implementsStr);
293 	dumpMem(map.name == "mutationType");
294 	formIndent(o, 0, "}");
295 
296 	if (type.vis == Visibility.indeterminate) {
297 		formIndent(o, 0, "# note: nestedness of type '%s' not determined; output may be suboptimal", map.name);
298 	}
299 
300 	if(type.vis != Visibility.inputOutput && map.name !in gqlSpecialOps) {
301 		formIndent(o, 0, "input %sIn {", map.name);
302 		dumpMem(true);
303 		formIndent(o, 0, "}");
304 	}
305 }