diff --git a/mise.toml b/mise.toml index dec2e5c..35eba7a 100644 --- a/mise.toml +++ b/mise.toml @@ -1,18 +1,21 @@ [tools] -zig = "0.14.1" -zls = "0.14.0" +zig = "0.15.2" +zls = "0.15.1" [tasks.main] run = "zig run src/main.zig -- $@" [tasks.merge] -run = "zig run src/merge.zig" +run = "zig run src/merger.zig" [tasks.unit] run = "zig test src/parser.zig" [tasks.e2e] -run = "zig test --test-filter e2e src/parser.e2e.zig" +run = [ + "zig test --test-filter e2e src/merger.e2e.zig", + # "zig test --test-filter e2e src/parser.e2e.zig", +] [tasks.tests] depends = ["unit", "e2e"] diff --git a/src/ast/document.zig b/src/ast/document.zig index be31051..95475fe 100644 --- a/src/ast/document.zig +++ b/src/ast/document.zig @@ -6,12 +6,12 @@ const ExecutableDefinition = @import("executable_definition.zig").ExecutableDefi pub const Document = struct { allocator: Allocator, - definitions: ArrayList(ExecutableDefinition), + definitions: []ExecutableDefinition, pub fn deinit(self: Document) void { - for (self.definitions.items) |item| { + for (self.definitions) |item| { item.deinit(); } - self.definitions.deinit(); + self.allocator.free(self.definitions); } }; diff --git a/src/ast/object_type_definition.zig b/src/ast/object_type_definition.zig index ef55a3a..8aef06c 100644 --- a/src/ast/object_type_definition.zig +++ b/src/ast/object_type_definition.zig @@ -28,22 +28,26 @@ pub const ObjectTypeDefinition = struct { directives: []Directive, fields: []FieldDefinition, + _is_merge_result: bool = false, + pub fn deinit(self: ObjectTypeDefinition) void { - if (self.description != null) { - self.allocator.free(self.description.?); - } - self.allocator.free(self.name); - for (self.interfaces) |interface| { - interface.deinit(); + if (!self._is_merge_result) { + if (self.description != null) { + self.allocator.free(self.description.?); + } + self.allocator.free(self.name); + for (self.interfaces) |interface| { + interface.deinit(); + } + for (self.directives) |item| { + item.deinit(); + } + for (self.fields) |item| { + item.deinit(); + } } self.allocator.free(self.interfaces); - for (self.directives) |item| { - item.deinit(); - } self.allocator.free(self.directives); - for (self.fields) |item| { - item.deinit(); - } self.allocator.free(self.fields); } diff --git a/src/merge.zig b/src/merge.zig index 691f2eb..6fa0bba 100644 --- a/src/merge.zig +++ b/src/merge.zig @@ -39,7 +39,7 @@ pub const Merger = struct { return MergeError.UnexpectedMemoryError; }, else => return std.fmt.allocPrint(self.allocator, "unknownDefinition_{s}", .{@tagName(definition)}) catch - return MergeError.UnexpectedMemoryError, + MergeError.UnexpectedMemoryError, } } @@ -47,22 +47,18 @@ pub const Merger = struct { var similarDefinitionsMap = std.StringHashMap(ArrayList(ExecutableDefinition)).init(self.allocator); defer similarDefinitionsMap.deinit(); - var similarDefinitionsNames = ArrayList([]const u8).init(self.allocator); - defer similarDefinitionsNames.deinit(); - for (documents) |document| { - for (document.definitions.items) |definition| { + for (document.definitions) |definition| { const definitionName = try self.makeDefinitionName(definition); if (!similarDefinitionsMap.contains(definitionName)) { - var similarDefinitions = ArrayList(ExecutableDefinition).init(self.allocator); - similarDefinitions.append(definition) catch return MergeError.UnexpectedMemoryError; - similarDefinitionsMap.put(definitionName, similarDefinitions) catch return MergeError.UnexpectedMemoryError; - similarDefinitionsNames.append(definitionName) catch return MergeError.UnexpectedMemoryError; + var ar = ArrayList(ExecutableDefinition).init(self.allocator); + ar.append(definition) catch return MergeError.UnexpectedMemoryError; + similarDefinitionsMap.put(definitionName, ar) catch return MergeError.UnexpectedMemoryError; } else { - var similarDefinitions = similarDefinitionsMap.get(definitionName).?; - similarDefinitions.append(definition) catch return MergeError.UnexpectedMemoryError; - similarDefinitionsMap.put(definitionName, similarDefinitions) catch return MergeError.UnexpectedMemoryError; + var ar = similarDefinitionsMap.get(definitionName).?; + ar.append(definition) catch return MergeError.UnexpectedMemoryError; + similarDefinitionsMap.put(definitionName, ar) catch return MergeError.UnexpectedMemoryError; } } } @@ -71,25 +67,28 @@ pub const Merger = struct { // "objectTypeDefinition_Object": [objectTypeExtension_obj1, objectTypeDefinition_obj2], // "objectTypeDefinition_Query": [objectTypeDefinition_obj3, objectTypeExtension_obj4], // } - // ["objectTypeDefinition_Object", "objectTypeDefinition_Query"] var mergedDefinitions = ArrayList(ExecutableDefinition).init(self.allocator); + errdefer mergedDefinitions.deinit(); var unmergeableDefinitions = ArrayList(ExecutableDefinition).init(self.allocator); + defer unmergeableDefinitions.deinit(); + + var iter = similarDefinitionsMap.iterator(); - for (similarDefinitionsNames.items) |definition_name| { - const similarDefinitions = similarDefinitionsMap.get(definition_name).?; + while (iter.next()) |entry| { + const similarDefinitions = entry.value_ptr.*; switch (similarDefinitions.items[0]) { .objectTypeDefinition, .objectTypeExtension => { - var unionDefinition = ArrayList(ObjectTypeDefinition).init(self.allocator); + var objectTypeDefinitions = ArrayList(ObjectTypeDefinition).init(self.allocator); for (similarDefinitions.items) |definition| { - unionDefinition.append(switch (definition) { + objectTypeDefinitions.append(switch (definition) { .objectTypeDefinition => |def| def, .objectTypeExtension => |ext| ObjectTypeDefinition.fromExtension(ext), else => unreachable, }) catch return MergeError.UnexpectedMemoryError; } - const mergedDefinition = try mergeObjectTypeDefinitions(self, unionDefinition.toOwnedSlice() catch return MergeError.UnexpectedMemoryError); + const mergedDefinition = try mergeObjectTypeDefinitions(self, objectTypeDefinitions.toOwnedSlice() catch return MergeError.UnexpectedMemoryError); mergedDefinitions.append(ExecutableDefinition{ .objectTypeDefinition = mergedDefinition }) catch return MergeError.UnexpectedMemoryError; }, .operationDefinition => { @@ -113,7 +112,7 @@ pub const Merger = struct { return Document{ .allocator = self.allocator, - .definitions = mergedDefinitions, + .definitions = mergedDefinitions.toOwnedSlice() catch return MergeError.UnexpectedMemoryError, }; } }; @@ -138,6 +137,7 @@ fn mergeObjectTypeDefinitions(self: *Merger, objectTypeDefinitions: []const Obje } return ObjectTypeDefinition{ + ._is_merge_result = true, .allocator = self.allocator, .name = name.?, .interfaces = interfaces.toOwnedSlice() catch return MergeError.UnexpectedMemoryError, @@ -149,7 +149,7 @@ fn mergeObjectTypeDefinitions(self: *Merger, objectTypeDefinitions: []const Obje pub fn main() !void { const alloc = std.heap.page_allocator; - const typeDefsDir = "graphql-definitions"; + const typeDefsDir = "tests/e2e-merge"; var dir = try std.fs.cwd().openDir(typeDefsDir, .{ .iterate = true }); defer dir.close(); @@ -192,6 +192,7 @@ pub fn main() !void { alloc.free(documentsSlice); } const mergedDocument = try merger.mergeIntoSingleDocument(documentsSlice); + defer mergedDocument.deinit(); var printer = try Printer.init(alloc, mergedDocument); const gql = try printer.getGql(); diff --git a/src/merger.e2e.zig b/src/merger.e2e.zig new file mode 100644 index 0000000..ec473f3 --- /dev/null +++ b/src/merger.e2e.zig @@ -0,0 +1,71 @@ +const std = @import("std"); +const testing = @import("std").testing; +const Parser = @import("parser.zig").Parser; +const Merger = @import("merge.zig").Merger; +const getFileContent = @import("utils/utils.zig").getFileContent; +const ArrayList = std.ArrayList; +const Document = @import("ast/document.zig").Document; +const Printer = @import("printer.zig").Printer; +const normalizeLineEndings = @import("utils/utils.zig").normalizeLineEndings; + +test "e2e-merge" { + const alloc = std.testing.allocator; + const typeDefsDir = "tests/e2e-merge"; + + var dir = try std.fs.cwd().openDir(typeDefsDir, .{ .iterate = true }); + defer dir.close(); + + var filesToParse = ArrayList([]const u8).init(alloc); + defer { + for (filesToParse.items) |path| { + alloc.free(path); + } + filesToParse.deinit(); + } + + var iterator = dir.iterate(); + while (try iterator.next()) |entry| { + if (entry.kind == .file) { + const path = try std.fmt.allocPrint(alloc, "{s}/{s}", .{ typeDefsDir, entry.name }); + try filesToParse.append(path); + } + } + + var documents = ArrayList(Document).init(alloc); + + for (filesToParse.items) |file| { + const content = getFileContent(file, alloc) catch return; + defer alloc.free(content); + + var parser = try Parser.initFromBuffer(alloc, content); + defer parser.deinit(); + + const document = try parser.parse(); + documents.append(document) catch return; + } + + var merger = Merger.init(alloc); + const documentsSlice = try documents.toOwnedSlice(); + defer { + for (documentsSlice) |document| { + document.deinit(); + } + alloc.free(documentsSlice); + } + const mergedDocument = try merger.mergeIntoSingleDocument(documentsSlice); + defer mergedDocument.deinit(); + + var printer = try Printer.init(alloc, mergedDocument); + const gql = try printer.getGql(); + defer alloc.free(gql); + + const expectedText = try getFileContent("tests/merger.e2e.snapshot.graphql", testing.allocator); + defer testing.allocator.free(expectedText); + + const normalizedText = normalizeLineEndings(testing.allocator, gql); + defer testing.allocator.free(normalizedText); + const normalizedExpectedText = normalizeLineEndings(testing.allocator, expectedText); + defer testing.allocator.free(normalizedExpectedText); + + try testing.expectEqualStrings(normalizedExpectedText, normalizedText); +} diff --git a/src/parser.e2e.zig b/src/parser.e2e.zig index 4550d0c..137bc82 100644 --- a/src/parser.e2e.zig +++ b/src/parser.e2e.zig @@ -17,15 +17,15 @@ test "e2e-parse" { const rootNode = try parser.parse(); defer rootNode.deinit(); - try testing.expectEqual(22, rootNode.definitions.items.len); - try testing.expectEqual(2, rootNode.definitions.items[2].unionTypeDefinition.types.len); - try testing.expectEqual(OperationType.query, rootNode.definitions.items[11].operationDefinition.operation); + try testing.expectEqual(22, rootNode.definitions.len); + try testing.expectEqual(2, rootNode.definitions[2].unionTypeDefinition.types.len); + try testing.expectEqual(OperationType.query, rootNode.definitions[11].operationDefinition.operation); - const objectTypeExtension = rootNode.definitions.items[13].objectTypeExtension; + const objectTypeExtension = rootNode.definitions[13].objectTypeExtension; try testing.expectEqual(1, objectTypeExtension.directives.len); try testing.expectEqual(2, objectTypeExtension.fields.len); - const interfaceTypeExtension = rootNode.definitions.items[19].interfaceTypeExtension; + const interfaceTypeExtension = rootNode.definitions[19].interfaceTypeExtension; try testing.expectEqual(1, interfaceTypeExtension.interfaces.len); try testing.expectEqualStrings("NewInterface", interfaceTypeExtension.interfaces[0].type.namedType.name); try testing.expectEqual(1, interfaceTypeExtension.directives.len); @@ -34,14 +34,14 @@ test "e2e-parse" { try testing.expectEqualStrings("newField", interfaceTypeExtension.fields[0].name); try testing.expectEqualStrings("anotherField", interfaceTypeExtension.fields[1].name); - const unionTypeExtension = rootNode.definitions.items[20].unionTypeExtension; + const unionTypeExtension = rootNode.definitions[20].unionTypeExtension; try testing.expectEqual(1, unionTypeExtension.directives.len); try testing.expectEqualStrings("someDirective", unionTypeExtension.directives[0].name); try testing.expectEqual(2, unionTypeExtension.types.len); try testing.expectEqualStrings("NewType", unionTypeExtension.types[0].namedType.name); try testing.expectEqualStrings("AnotherType", unionTypeExtension.types[1].namedType.name); - const scalarTypeExtension = rootNode.definitions.items[21].scalarTypeExtension; + const scalarTypeExtension = rootNode.definitions[21].scalarTypeExtension; try testing.expectEqualStrings("DateTime", scalarTypeExtension.name); try testing.expectEqual(2, scalarTypeExtension.directives.len); try testing.expectEqualStrings("someDirective", scalarTypeExtension.directives[0].name); diff --git a/src/parser.zig b/src/parser.zig index ac45fc0..ba4fc20 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -97,16 +97,12 @@ pub const Parser = struct { } pub fn parse(self: *Parser) ParseError!Document { - const token = try self.processTokens(); - return token; + const document = try self.processTokens(); + return document; } fn processTokens(self: *Parser) ParseError!Document { - var documentNode = Document{ - .allocator = self.allocator, - .definitions = ArrayList(ExecutableDefinition).init(self.allocator), - }; - errdefer documentNode.deinit(); + var docDefinitions = ArrayList(ExecutableDefinition).init(self.allocator); state: switch (Reading.root) { Reading.root => { @@ -177,125 +173,128 @@ pub const Parser = struct { }, Reading.fragment_definition => { const fragmentDefinition = try parseFragmentDefinition(self); - documentNode.definitions.append(ExecutableDefinition{ + docDefinitions.append(ExecutableDefinition{ .fragmentDefinition = fragmentDefinition, }) catch return ParseError.UnexpectedMemoryError; continue :state Reading.root; }, Reading.operation_definition => { const operationDefinition = try parseOperationDefinition(self); - documentNode.definitions.append(ExecutableDefinition{ + docDefinitions.append(ExecutableDefinition{ .operationDefinition = operationDefinition, }) catch return ParseError.UnexpectedMemoryError; continue :state Reading.root; }, Reading.schema_definition => { const schemaDefinition = try parseSchemaDefinition(self); - documentNode.definitions.append(ExecutableDefinition{ + docDefinitions.append(ExecutableDefinition{ .schemaDefinition = schemaDefinition, }) catch return ParseError.UnexpectedMemoryError; continue :state Reading.root; }, Reading.object_type_definition => { const objectTypeDefinition = try parseObjectTypeDefinition(self); - documentNode.definitions.append(ExecutableDefinition{ + docDefinitions.append(ExecutableDefinition{ .objectTypeDefinition = objectTypeDefinition, }) catch return ParseError.UnexpectedMemoryError; continue :state Reading.root; }, Reading.union_type_definition => { const unionTypeDefinition = try parseUnionTypeDefinition(self); - documentNode.definitions.append(ExecutableDefinition{ + docDefinitions.append(ExecutableDefinition{ .unionTypeDefinition = unionTypeDefinition, }) catch return ParseError.UnexpectedMemoryError; continue :state Reading.root; }, Reading.scalar_type_definition => { const scalarTypeDefinition = try parseScalarTypeDefinition(self); - documentNode.definitions.append(ExecutableDefinition{ + docDefinitions.append(ExecutableDefinition{ .scalarTypeDefinition = scalarTypeDefinition, }) catch return ParseError.UnexpectedMemoryError; continue :state Reading.root; }, Reading.directive_definition => { const directiveDefinition = try parseDirectiveDefinition(self); - documentNode.definitions.append(ExecutableDefinition{ + docDefinitions.append(ExecutableDefinition{ .directiveDefinition = directiveDefinition, }) catch return ParseError.UnexpectedMemoryError; continue :state Reading.root; }, Reading.interface_type_definition => { const interfaceTypeDefinition = try parseInterfaceTypeDefinition(self); - documentNode.definitions.append(ExecutableDefinition{ + docDefinitions.append(ExecutableDefinition{ .interfaceTypeDefinition = interfaceTypeDefinition, }) catch return ParseError.UnexpectedMemoryError; continue :state Reading.root; }, Reading.schema_extension => { const schemaExtension = try parseSchemaExtension(self); - documentNode.definitions.append(ExecutableDefinition{ + docDefinitions.append(ExecutableDefinition{ .schemaExtension = schemaExtension, }) catch return ParseError.UnexpectedMemoryError; continue :state Reading.root; }, Reading.object_type_extension => { const objectTypeExtension = try parseObjectTypeExtension(self); - documentNode.definitions.append(ExecutableDefinition{ + docDefinitions.append(ExecutableDefinition{ .objectTypeExtension = objectTypeExtension, }) catch return ParseError.UnexpectedMemoryError; continue :state Reading.root; }, Reading.enum_type_definition => { const enumTypeDefinition = try parseEnumTypeDefinition(self); - documentNode.definitions.append(ExecutableDefinition{ + docDefinitions.append(ExecutableDefinition{ .enumTypeDefinition = enumTypeDefinition, }) catch return ParseError.UnexpectedMemoryError; continue :state Reading.root; }, Reading.enum_type_extension => { const enumTypeExtension = try parseEnumTypeExtension(self); - documentNode.definitions.append(ExecutableDefinition{ + docDefinitions.append(ExecutableDefinition{ .enumTypeExtension = enumTypeExtension, }) catch return ParseError.UnexpectedMemoryError; continue :state Reading.root; }, Reading.input_object_type_definition => { const inputObjectTypeDefinition = try parseInputObjectTypeDefinition(self); - documentNode.definitions.append(ExecutableDefinition{ + docDefinitions.append(ExecutableDefinition{ .inputObjectTypeDefinition = inputObjectTypeDefinition, }) catch return ParseError.UnexpectedMemoryError; continue :state Reading.root; }, Reading.input_object_type_extension => { const inputObjectTypeExtension = try parseInputObjectTypeExtension(self); - documentNode.definitions.append(ExecutableDefinition{ + docDefinitions.append(ExecutableDefinition{ .inputObjectTypeExtension = inputObjectTypeExtension, }) catch return ParseError.UnexpectedMemoryError; continue :state Reading.root; }, Reading.interface_type_extension => { const interfaceTypeExtension = try parseInterfaceTypeExtension(self); - documentNode.definitions.append(ExecutableDefinition{ + docDefinitions.append(ExecutableDefinition{ .interfaceTypeExtension = interfaceTypeExtension, }) catch return ParseError.UnexpectedMemoryError; continue :state Reading.root; }, Reading.union_type_extension => { const unionTypeExtension = try parseUnionTypeExtension(self); - documentNode.definitions.append(ExecutableDefinition{ + docDefinitions.append(ExecutableDefinition{ .unionTypeExtension = unionTypeExtension, }) catch return ParseError.UnexpectedMemoryError; continue :state Reading.root; }, Reading.scalar_type_extension => { const scalarTypeExtension = try parseScalarTypeExtension(self); - documentNode.definitions.append(ExecutableDefinition{ + docDefinitions.append(ExecutableDefinition{ .scalarTypeExtension = scalarTypeExtension, }) catch return ParseError.UnexpectedMemoryError; continue :state Reading.root; }, } - return documentNode; + return Document{ + .allocator = self.allocator, + .definitions = docDefinitions.toOwnedSlice() catch return ParseError.UnexpectedMemoryError, + }; } pub fn peekNextToken(self: *Parser) ?Token { diff --git a/src/printer.zig b/src/printer.zig index 2287d54..c2ddd81 100644 --- a/src/printer.zig +++ b/src/printer.zig @@ -20,6 +20,10 @@ pub const Printer = struct { }; } + pub fn deinit(self: *Printer) void { + self.buffer.deinit(); + } + pub fn getGql(self: *Printer) ![]u8 { self.buffer.clearAndFree(); try getDocumentGql(self); diff --git a/src/printer/graphql.zig b/src/printer/graphql.zig index fddfc22..fc42f9a 100644 --- a/src/printer/graphql.zig +++ b/src/printer/graphql.zig @@ -34,9 +34,9 @@ const UnionTypeExtension = @import("../ast/union_type_extension.zig").UnionTypeE const VariableDefinition = @import("../ast/variable_definition.zig").VariableDefinition; pub fn getDocumentGql(printer: *Printer) !void { - for (printer.document.definitions.items, 0..) |definition, i| { + for (printer.document.definitions, 0..) |definition, i| { try getGqlFromExecutableDefinition(printer, definition); - if (i < printer.document.definitions.items.len - 1) { + if (i < printer.document.definitions.len - 1) { try printer.append("\n\n"); } } diff --git a/src/printer/text.zig b/src/printer/text.zig index dffe37f..7205f24 100644 --- a/src/printer/text.zig +++ b/src/printer/text.zig @@ -48,8 +48,8 @@ pub fn getDocumentText(printer: *Printer) !void { const w = printer.buffer.writer(); try w.print("{s}- Document\n", .{spaces}); - try w.print("{s} definitions: {d}\n", .{ spaces, printer.document.definitions.items.len }); - for (printer.document.definitions.items) |item| { + try w.print("{s} definitions: {d}\n", .{ spaces, printer.document.definitions.len }); + for (printer.document.definitions) |item| { const txt = try getExecutableDefinitionText(item, 1, printer.allocator); defer printer.allocator.free(txt); try w.print("{s}", .{txt}); diff --git a/src/root.zig b/src/root.zig index f986b76..5bf8e6b 100644 --- a/src/root.zig +++ b/src/root.zig @@ -1,3 +1,10 @@ +const std = @import("std"); +const Merger = @import("merge.zig").Merger; const Parser = @import("parser.zig").Parser; +pub const merger = Merger; pub const parser = Parser; + +test { + std.testing.refAllDecls(@This()); +} diff --git a/tests/e2e-merge/base.graphql b/tests/e2e-merge/base.graphql new file mode 100644 index 0000000..1f24ce8 --- /dev/null +++ b/tests/e2e-merge/base.graphql @@ -0,0 +1,12 @@ +""" +Nice object +""" +type Object { + #some comment + name: String! +} + +type Query { + hello: String! + giveMeAnObject: Object! +} diff --git a/tests/e2e-merge/extend.graphql b/tests/e2e-merge/extend.graphql new file mode 100644 index 0000000..3e9f7a8 --- /dev/null +++ b/tests/e2e-merge/extend.graphql @@ -0,0 +1,9 @@ +extend type Query { + "this is a new field" + world: String! +} + +#some comment +extend type Object { + material: String! +} diff --git a/tests/e2e-merge/query.graphql b/tests/e2e-merge/query.graphql new file mode 100644 index 0000000..a855e76 --- /dev/null +++ b/tests/e2e-merge/query.graphql @@ -0,0 +1,3 @@ +query SomeQuery { + hello +} diff --git a/tests/merger.e2e.snapshot.graphql b/tests/merger.e2e.snapshot.graphql new file mode 100644 index 0000000..89d25b4 --- /dev/null +++ b/tests/merger.e2e.snapshot.graphql @@ -0,0 +1,14 @@ +""" +Nice object +""" +type Object { + name: String! + material: String! +} + +type Query { + hello: String! + giveMeAnObject: Object! + "this is a new field" + world: String! +}