From ade8427c722ca003724eee9388e8ad3a7ea9764c Mon Sep 17 00:00:00 2001 From: Jonathan Marler Date: Tue, 30 Jan 2018 22:46:27 -0700 Subject: [PATCH] graphql idea --- src/dmd/graphql/core.d | 340 ++++++++++++++++++++++++++++++++++ src/dmd/graphql/parser.d | 387 +++++++++++++++++++++++++++++++++++++++ src/dmd/graphql/util.d | 66 +++++++ src/dmd/json.d | 67 ++++++- src/dmd/mars.d | 3 +- 5 files changed, 853 insertions(+), 10 deletions(-) create mode 100644 src/dmd/graphql/core.d create mode 100644 src/dmd/graphql/parser.d create mode 100644 src/dmd/graphql/util.d diff --git a/src/dmd/graphql/core.d b/src/dmd/graphql/core.d new file mode 100644 index 000000000000..64d3f8f58730 --- /dev/null +++ b/src/dmd/graphql/core.d @@ -0,0 +1,340 @@ +module dmd.graphql.core; + +enum TypeKind +{ + boolean, + string_, + object_, +} + +struct QueryDataObject +{ + struct Field + { + string name; + QueryData value; + } + Field[] fields; +} +struct QueryData +{ + union + { + QueryDataObject object_; + string str; + bool bool_; + } + TypeKind kind; + this(QueryDataObject object_) + { + this.object_ = object_; + this.kind = TypeKind.object_; + } + this(string str) { this.str = str; this.kind = TypeKind.string_; } + this(bool bool_) { this.bool_ = bool_; this.kind = TypeKind.boolean; } + QueryDataObject asObject() + { + assert(kind == TypeKind.object_); + return object_; + } +} + + +interface IQueryDataHandler +{ + void errorSelectOnValue(const(QuerySelectionSet), TypeKind); + + void objectStart(); + void objectEnd(); + void string_(string str); + void boolean(bool b); +} + + +// +// NOTE: the following data structures are generated from +// the grammar specification of a graphql query +struct QueryValue +{ +} +struct QueryArgument +{ + string name; + QueryValue value; +} +struct QueryDirective +{ + string name; + QueryArgument[] arguments; +} +struct QueryField +{ + string alias_; + string name; + QueryArgument[] arguments; + QueryDirective[] directives; + QuerySelectionSet subSelectionSet; + void toString(scope void delegate(const(char)[]) sink) const + { + if (alias_) + { + sink(alias_); + sink(": "); + } + sink(name); + if (arguments) + { + assert(0, "not implemented"); + } + if (subSelectionSet.selections !is null) + { + assert(0, "not implemented"); + } + } +} + +struct QuerySelection +{ + enum Kind { field, fragmentSpread, inlineFragment } + union + { + QueryField field; + } + Kind kind; + this(QueryField field) + { + this.field = field; + this.kind = Kind.field; + } + void toString(scope void delegate(const(char)[]) sink) const + { + final switch(kind) + { + case Kind.field: field.toString(sink); return; + case Kind.fragmentSpread: assert(0, "not implemented"); return; + case Kind.inlineFragment: assert(0, "not implemented"); return; + } + } +} + +struct QuerySelectionSet +{ + @property static auto nullValue() { return QuerySelectionSet(); } + + QuerySelection[] selections; + void toString(scope void delegate(const(char)[]) sink) const + { + sink("{"); + foreach (ref selection; selections) + { + selection.toString(sink); + } + sink("}"); + } +} +void query(T)(const(QuerySelectionSet) selectionSet, IQueryDataHandler handler, T value) +{ + static if(is(T == string)) + { + if (selectionSet.selections.length > 0) + handler.errorSelectOnValue(selectionSet, TypeKind.string_); + else + handler.string_(value); + } + else static assert(0, "not implemented"); +} +unittest +{ + import dmd.graphql.util : DumpDataHandler; + { + scope handler = new DumpDataHandler(); + query(makeQuery(), handler, "hello"); + assert(handler.json.data == `"hello"`); + } + { + scope handler = new DumpDataHandler(); + query(makeQuery(QueryField(null, "a")), handler, "hello"); + assert(handler.errors.data.length == 1); + } + /* + { + scope handler = new DumpDataHandler(); + query(makeQuery(), handler, ["a":0,"b":"foo"]); + assert(handler.errors.data.length == 1); + } + */ +} + + +auto makeQuery(T...)(T args) +{ + auto selections = new QuerySelection[args.length]; + foreach (i, arg; args) + { + static if (is(typeof(arg) : QueryField)) + { + selections[i] = QuerySelection(arg); + } + else static assert(0); + } + return QuerySelectionSet(selections); +} + +struct DefaultPolicy { } +alias graphql = graphqlTemplate!DefaultPolicy; + +enum TypeFlags +{ + none = 0x00, + optional = 0x01, + array = 0x02, + + optionalArray = optional | array, +} + +template graphqlTemplate(Policy) +{ + struct Type + { + union + { + Field[] fields = void; + } + TypeKind kind; + TypeFlags flags; + this(TypeFlags flags, Field[] fields) + { + this.kind = TypeKind.object_; + this.flags = flags; + this.fields = fields; + } + this(TypeFlags flags, immutable(Field)[] fields) immutable + { + this.kind = TypeKind.object_; + this.flags = flags; + this.fields = fields; + } + + private this(TypeKind kind, TypeFlags flags) + { + this.kind = kind; + this.flags = flags; + } + static auto string_(TypeFlags flags = TypeFlags.none) { return Type(TypeKind.string_, flags); } + static auto boolean(TypeFlags flags = TypeFlags.none) { return Type(TypeKind.boolean, flags); } + + void query(const(QuerySelectionSet) query, QueryData data, IQueryDataHandler handler) + { + import std.stdio : writefln; + writefln("query '%s'", query); + if (query.selections.length == 0) + { + final switch (kind) + { + case TypeKind.boolean: + assert(data.kind == TypeKind.boolean); + handler.boolean(data.bool_); + break; + case TypeKind.string_: + assert(data.kind == TypeKind.string_); + handler.string_(data.str); + break; + case TypeKind.object_: + { + handler.objectStart(); + scope(exit) handler.objectEnd(); + foreach (ref field; fields) + { + field.type.query(QuerySelectionSet.nullValue, + field.resolver.resolveField(data.asObject, &field), handler); + } + } + break; + } + return; + } + + final switch (kind) + { + case TypeKind.boolean: + assert(0, "not implemented"); + case TypeKind.string_: + assert(0, "not implemented"); + case TypeKind.object_: + { + handler.objectStart(); + scope(exit) handler.objectEnd(); + foreach (selection; query.selections) + { + final switch(selection.kind) + { + case QuerySelection.Kind.field: + auto field = getObjectField(selection.field.name); + if (field is null) + { + writefln("Error: this type does not contain a field named '%s'", selection.field.name); + return; + } + field.type.query(selection.field.subSelectionSet, + getObjectFieldValue(data.asObject, field), handler); + break; + case QuerySelection.Kind.fragmentSpread: assert(0, "not implemented"); return; + case QuerySelection.Kind.inlineFragment: assert(0, "not implemented"); return; + } + } + } + break; + } + + } + + private Field* getObjectField(string name) + { + foreach(ref field; fields) + { + if (name == field.name) + return &field; + } + return null; + } + private QueryData getObjectFieldValue(QueryDataObject obj, Field* field) + { + foreach(ref objField; obj.fields) + { + if (objField.name == field.name) + { + return objField.value; + } + } + assert(0, "object missing field " ~ field.name); + } + } + struct Field + { + string name; + Type type; + Resolver resolver; + } + struct Resolver + { + /* + QueryData resolve(QueryData data, Type type) + { + assert(0, "not implemented"); + } + */ + QueryData resolveField(QueryDataObject obj, Field* field) + { + foreach(ref objField; obj.fields) + { + if (objField.name == field.name) + { + return objField.value; + } + } + assert(0, "field missing"); + } + //QueryData resolveField(QueryData + } +} + diff --git a/src/dmd/graphql/parser.d b/src/dmd/graphql/parser.d new file mode 100644 index 000000000000..4c7cd857f5db --- /dev/null +++ b/src/dmd/graphql/parser.d @@ -0,0 +1,387 @@ +/** +This module parses a graphql _SelectionSet_ (see facebook.github.io/graphql/draft) + */ +module dmd.graphql.parser; + +import dmd.graphql.core; + +class GraphQLParseException : Exception +{ + this(string msg, string filename, size_t line) + { + super(msg, filename, line); + } +} + +/** +Policy must contain the following fields: +--- +struct Policy +{ + alias CharType = char; + enum bool useEofChar; + + // If useEofChar is true, then must have: + enum char eofChar; + +} +--- + */ +template graphqlParser(Policy) +{ + alias Char = Policy.CharType; + static if (Policy.useEofChar) + enum eofChar = Policy.eofChar; + else + enum eofChar = dchar.max; + + /** + Note: this parses a single graphql "OperationDefinition". + */ + QuerySelectionSet parseSelectionSet(string str, string filenameForErrors) + { + auto parser = Parser(str.ptr); + parser.filenameForErrors = filenameForErrors; + parser.start = str.ptr; + static if (!Policy.useEofChar) + { + parser.limit = str.ptr + str.length; + } + parser.readNext(); + return parser.parseSelectionSet(); + } + /* + TODO: dmd will probably just support a list of QueryFields + QueryField[] parseQueryFields(string str, string filenameForErrors) + { + .... + } + */ + + + struct Parser + { + immutable(Char)* nextPtr; + immutable(Char)* currentPtr = void; + dchar current = void; + immutable(Char)* start; + string filenameForErrors; + + static if (!Policy.useEofChar) + { + immutable(Char)* limit; + } + + void readNext() + { + currentPtr = nextPtr; + static if (!Policy.useEofChar) + { + if (currentPtr >= limit) + { + current = current.max; // means EOF + return; + } + } + + // TODO: handle UTF8 here + current = *currentPtr; + nextPtr++; + } + auto parseError(T...)(immutable(Char)* location, string fmt, T args) + { + import std.format : format; + return new GraphQLParseException(format(fmt, args), filenameForErrors, countLinesTo(location)); + } + // graphql accepts '\n', '\r\n' or '\r' as line terminators + private size_t countLinesTo(immutable(Char)* to) + { + size_t line = 1; + auto ptr = start; + for (; ptr < to; ptr++) + { + if (*ptr == '\n') + { + line++; + } + else if (*ptr == '\r') + { + ptr++; + if (ptr >= to) + { + static if (Policy.useEofChar) + { + if (*to != '\n') + line++; + } + else + { + if (to >= limit || *to != '\n') + line++; + } + break; + } + line++; + if (*ptr != '\n') + ptr--; + } + } + return line; + } + + /** + Skips whitespace, newline, comments, and commas + Assumption: current holds the first character to check + */ + void skipTrivial() + { + for (;; readNext()) + { + if (current == ' ' || current == '\t' || + current == '\n' || current == '\r' || + current == ',') + continue; + if (current == '#') + assert(0, "not implemented"); + break; + } + } + + QuerySelectionSet parseSelectionSet() + { + skipTrivial(); + if (current == '{') + { + readNext(); + auto set = QuerySelectionSet(parseSelections()); + assert(current == '}'); + readNext(); + return set; + } + if (current == eofChar) + return QuerySelectionSet(); + + throw parseError(currentPtr, "expected selection set but got '%s'", current); + } + QuerySelection[] parseSelections() + { + QuerySelection[] set = null; + for (;;) + { + // TODO: detect fragment and error in that case + skipTrivial(); + if (current == '}') + return set; + + // + // Parse "[ Alias ':' ] Name" + // + string name = void; + if (!isNameStart(current)) + throw parseError(currentPtr, "expected a name but got '%s'", current); + name = scanName(); + + skipTrivial(); + string alias_ = null; + if (current == ':') + { + alias_ = name; + readNext(); + skipTrivial(); + if (!isNameStart(current)) + throw parseError(currentPtr, "expected a name after an alias, but got '%s'", current); + name = scanName(); + } + + // + // Parse optional Arguments + // + skipTrivial(); + QueryArgument[] arguments; + if (current == '(') + { + assert(0, "arguments not implemented"); + } + + // + // Parse optional Directives + // + QueryDirective[] directives; + for (;;) + { + skipTrivial(); + if (current != '@') + { + break; + } + assert(0, "directive not implemented"); + } + + // + // Parse optional SelectionSet + // + skipTrivial(); + QuerySelectionSet subSelectionSet; + if (current == '{') + { + assert(0, "sub selection-set not implemented"); + } + + set ~= QuerySelection(QueryField(alias_, name, arguments, directives, subSelectionSet)); + } + } + // Assumption: current is pointing to the first char of the name + // and it already satisfied isNameStart + string scanName() + { + auto start = currentPtr; + for (;;) + { + readNext(); + if (!isNameChar(current)) + { + return start[0 .. currentPtr - start]; + } + } + } + } +} + +bool isNameStart(dchar c) +{ + if (c >= 'a') return c <= 'z'; + return (c >= 'A') && (c <= 'Z' || c == '_'); +} +bool isNameChar(dchar c) +{ + if (c >= 'a') return c <= 'z'; + if (c >= 'A') return c <= 'Z' || c == '_'; + return c >= '0' && c <= '9'; +} + +version(unittest) +{ + struct Policy1 + { + alias CharType = char; + enum bool useEofChar = false; + } + struct Policy2 + { + alias CharType = char; + enum bool useEofChar = true; + enum char eofChar = '\0'; + } +} + +unittest +{ + static void test(string str, QuerySelection[] expected, size_t testLine = __LINE__) + { + import std.stdio; + writefln("TEST '%s'", str); + import std.conv : to; + string filename = "line-" ~ testLine.to!string; + // test using Policy1 + { + auto actual = graphqlParser!Policy1.parseSelectionSet(str, filename); + assert(actual.selections == expected); + } + // test using Policy2 + { + auto strWithNull = str ~ "\0"; + auto actual = graphqlParser!Policy2.parseSelectionSet(strWithNull[0 .. $-1], filename); + assert(actual.selections == expected); + } + } + static void testBad(size_t line, string str, size_t testLine = __LINE__) + { + import std.stdio; + writefln("TEST-BAD '%s'", str); + import std.conv : to; + string filename = "line-" ~ testLine.to!string; + + // test using Policy1 + try { graphqlParser!Policy1.parseSelectionSet(str, filename); assert(0); } + catch(GraphQLParseException e) + { assert(e.line == line); } + + // test using Policy2 + try { graphqlParser!Policy1.parseSelectionSet(str ~ "\0", filename); assert(0); } + catch(GraphQLParseException e) + { assert(e.line == line); } + } + static QuerySelection field(string alias_, string name) + { + return QuerySelection(QueryField(alias_, name)); + } + + test(null, null); + test("", null); + test(", \n\r ", null); + test("{,\n,\r}", null); + + testBad(1, "{"); + testBad(2, "\r{"); + testBad(1, "a"); + testBad(3, "{\n\ta\n"); + + test("{a}", [field(null, "a")]); + test("{a:b}", [field("a", "b")]); +} + +// test line number accuracy +// +// graphql accepts '\n', '\r\n' or '\r' as line terminators +unittest +{ + static void test(size_t line, size_t offset, string str) + { + // test using Policy1 + { + auto parser = graphqlParser!Policy1.Parser(); + parser.start = str.ptr; + parser.limit = str.ptr + str.length; + assert(line == parser.countLinesTo(str.ptr + offset)); + } + // test using Policy2 + { + auto strWithNull = str ~ "\0"; + auto parser = graphqlParser!Policy2.Parser(); + parser.start = str.ptr; + assert(line == parser.countLinesTo(str.ptr + offset)); + } + } + test(1, 0, null); + test(1, 0, ""); + test(1, 2, " "); + + test(2, 1, "\n"); + test(2, 1, "\r"); + test(2, 2, "\r\n"); + test(1, 1, "\r\n"); + + test(3, 2, "\n\n"); + test(2, 1, "\n\n"); + test(3, 2, "\n\r"); + test(2, 1, "\n\r"); + test(3, 3, "\n\r\n"); + test(2, 2, "\n\r\n"); + + test(3, 2, "\r\r"); + test(2, 1, "\r\r"); + test(3, 3, "\r\r\n"); + test(2, 2, "\r\r\n"); + test(2, 1, "\r\r\n"); + + test(3, 3, "\r\n\n"); + test(2, 2, "\r\n\n"); + test(1, 1, "\r\n\n"); + test(3, 3, "\r\n\r"); + test(2, 2, "\r\n\r"); + test(1, 1, "\r\n\r"); + test(3, 4, "\r\n\r\n"); + test(2, 3, "\r\n\r\n"); + test(2, 2, "\r\n\r\n"); + test(1, 1, "\r\n\r\n"); +} \ No newline at end of file diff --git a/src/dmd/graphql/util.d b/src/dmd/graphql/util.d new file mode 100644 index 000000000000..06263cda5c6a --- /dev/null +++ b/src/dmd/graphql/util.d @@ -0,0 +1,66 @@ +module dmd.graphql.util; + +import std.stdio : write, writeln, writefln; + +import dmd.graphql.core; + +class DumpDataHandler : IQueryDataHandler +{ + import std.array : Appender; + Appender!(char[]) json; + Appender!(string[]) errors; + void dump() + { + writeln(json.data); + } + + void errorSelectOnValue(const(QuerySelectionSet) set, TypeKind kind) + { + import std.format : format; + errors.put(format("cannot select fields from type %s", kind)); + } + + void objectStart() { json.put("{"); } + void objectEnd() { json.put("}"); } + void string_(string str) + { + json.put("\""); + json.put(str); // todo: escape it + json.put("\""); + } + void boolean(bool b) { json.put(b ? "true" : "false"); } +} + +/+ +int main(string[] args) +{ + auto compilerInfoType = graphql.Type([ + graphql.Field("binary", graphql.Type.string_), + graphql.Field("version", graphql.Type.string_), + graphql.Field("supportsIncludeImports", graphql.Type.boolean)]); + + + auto rootType = graphql.Type([ + graphql.Field("compilerInfo", compilerInfoType) + ]); + + // + // Create Fake Data + // + auto data = QueryData(QueryDataObject([ + QueryDataObject.Field("compilerInfo", QueryData(QueryDataObject([ + QueryDataObject.Field("binary", QueryData("dmd")), + QueryDataObject.Field("version", QueryData("1.0")), + QueryDataObject.Field("supportsIncludeImports", QueryData(true)), + ]))), + ])); + + + auto query = makeQuery(QueryField(null, "compilerInfo")); + auto dumpHandler = new DumpDataHandler(); + rootType.query(query, data, dumpHandler); + dumpHandler.dump(); + + return 0; +} ++/ \ No newline at end of file diff --git a/src/dmd/json.d b/src/dmd/json.d index b3dca5489c74..5280dbd4c36a 100644 --- a/src/dmd/json.d +++ b/src/dmd/json.d @@ -27,6 +27,7 @@ import dmd.dtemplate; import dmd.expression; import dmd.func; import dmd.globals; +import dmd.graphql.core; import dmd.hdrgen; import dmd.id; import dmd.identifier; @@ -791,17 +792,65 @@ public: } } -extern (C++) void json_generate(OutBuffer* buf, Modules* modules) +extern (C++) void json_generate(OutBuffer* buf, Modules* modules, const(QuerySelectionSet) selectionSet) { scope ToJsonVisitor json = new ToJsonVisitor(buf); - json.arrayStart(); - for (size_t i = 0; i < modules.dim; i++) + + // if there is no selection set, output the original JSON + // format, which is an array of module syntax objects + if (selectionSet.selections is null) + { + json.arrayStart(); + for (size_t i = 0; i < modules.dim; i++) + { + Module m = (*modules)[i]; + if (global.params.verbose) + fprintf(global.stdmsg, "json gen %s\n", m.toChars()); + m.accept(json); + } + json.arrayEnd(); + json.removeComma(); + } + else { - Module m = (*modules)[i]; - if (global.params.verbose) - fprintf(global.stdmsg, "json gen %s\n", m.toChars()); - m.accept(json); + json.objectStart(); + scope(exit)json.objectEnd(); + + //scope handler = new MyQueryDataHandler(); + //query(selectionSet, handler, modules); + //json.generate(); } - json.arrayEnd(); - json.removeComma(); } + +__gshared immutable graphQLRootType = immutable graphql.Type(TypeFlags.none, [ + immutable graphql.Field("compilerInfo", immutable graphql.Type(TypeFlags.none, [ + immutable graphql.Field("binary", graphql.Type.string_), + immutable graphql.Field("version", graphql.Type.string_), + ])), +]); + +/+ + +auto resolveTypeField(Type type, string field) +{ + if (field == "compilerInfo") + { + // return compilerInfoType + } + else if (field == "buildInfo") + { + // return buildInfoType + } + else if (field == "modules") + { + // return modulesType + } + else if (field == "semantics") + { + // return semantics object + } +} + +class MyQueryDataHandler : IQueryDataHandler +{ +}+/ \ No newline at end of file diff --git a/src/dmd/mars.d b/src/dmd/mars.d index 297581f8be69..6e24edc54de4 100644 --- a/src/dmd/mars.d +++ b/src/dmd/mars.d @@ -38,6 +38,7 @@ import dmd.dsymbolsem; import dmd.errors; import dmd.expression; import dmd.globals; +import dmd.graphql.core; import dmd.hdrgen; import dmd.id; import dmd.identifier; @@ -858,7 +859,7 @@ private int tryMain(size_t argc, const(char)** argv) if (global.params.doJsonGeneration) { OutBuffer buf; - json_generate(&buf, &modules); + json_generate(&buf, &modules, QuerySelectionSet()); // Write buf to file const(char)* name = global.params.jsonfilename; if (name && name[0] == '-' && name[1] == 0)