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 }