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 }