From 50dcb22e9a21f467aefae763e9c33a7134421fa5 Mon Sep 17 00:00:00 2001 From: woonki Date: Wed, 6 Mar 2024 02:42:31 +0900 Subject: [PATCH] Support the object extension (#49) * parse object directives * support object extension * add change log --- CHANGELOG.md | 4 ++ lib/graphql.go | 1 + lib/lex.go | 5 ++ lib/schema.go | 41 ++++++++++---- lib/schema_test.go | 36 ++++++++++++ lib/util.go | 75 +++++++++++++++++++++++++ lib/write.go | 1 + test/directives/generated.graphql | 14 +++++ test/directives/schema/object.graphql | 10 ++++ test/object_extension/generated.graphql | 11 ++++ test/object_extension/schema/a.graphql | 6 ++ test/object_extension/schema/b.graphql | 3 + 12 files changed, 195 insertions(+), 12 deletions(-) create mode 100644 test/directives/generated.graphql create mode 100644 test/directives/schema/object.graphql create mode 100644 test/object_extension/generated.graphql create mode 100644 test/object_extension/schema/a.graphql create mode 100644 test/object_extension/schema/b.graphql diff --git a/CHANGELOG.md b/CHANGELOG.md index a578397..38d132b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +## v0.2.13 + +- Support the object extension https://github.com/mununki/gqlmerge/pull/49 + ## v0.2.12 - Fix where the undefined operation type is generated https://github.com/mununki/gqlmerge/pull/47 diff --git a/lib/graphql.go b/lib/graphql.go index e328c46..4b09fbe 100644 --- a/lib/graphql.go +++ b/lib/graphql.go @@ -60,6 +60,7 @@ type Type struct { Fields []*Field Directives []*Directive Descriptions *[]string + Extend bool } type Arg struct { diff --git a/lib/lex.go b/lib/lex.go index 415e083..054290e 100644 --- a/lib/lex.go +++ b/lib/lex.go @@ -51,6 +51,7 @@ const ( tokEnum // enum tokScalar // scalar tokUnion // union + tokExtend // extend tokSchema // schema ) @@ -134,6 +135,8 @@ func (typ tokenType) String() string { return "scalar" case tokUnion: return "union" + case tokExtend: + return "extend" case tokSchema: return "schema" default: @@ -328,6 +331,8 @@ func (l *lexer) next() *token { return mkToken(tokScalar, "scalar") case "union": return mkToken(tokUnion, "union") + case "extend": + return mkToken(tokExtend, "extend") case "schema": return mkToken(tokSchema, "schema") default: diff --git a/lib/schema.go b/lib/schema.go index 3ed96d7..a0ed7f8 100644 --- a/lib/schema.go +++ b/lib/schema.go @@ -60,6 +60,7 @@ func (sc *Schema) ReadSchema(path string) { } func (s *Schema) Parse(p *Parser) { + isExtended := false for { tok := p.lex.next() if tok.typ == tokEOF { @@ -133,6 +134,9 @@ func (s *Schema) Parse(p *Parser) { } s.DirectiveDefinitions = append(s.DirectiveDefinitions, &d) + case tokExtend: + isExtended = true + case tokScalar: c := Scalar{} c.Filename = p.lex.filename @@ -346,16 +350,22 @@ func (s *Schema) Parse(p *Parser) { case tokType: t := Type{} + t.Extend = isExtended + isExtended = false t.Filename = p.lex.filename t.Line = p.lex.line t.Column = p.lex.col + t.Descriptions = p.bufString() name, _ := p.lex.consumeIdent() t.Name = name.String() - t.Descriptions = p.bufString() + t.Directives = p.parseDirectives() next := p.lex.next() switch next.typ { case tokImplements: + if len(t.Directives) > 0 { + errorf(`%s:%d:%d: directives cann't be placed in front of implements`, p.lex.filename, p.lex.line, p.lex.col) + } t.Impl = true name, _ := p.lex.consumeIdent() t.ImplTypes = append(t.ImplTypes, name.String()) @@ -364,6 +374,7 @@ func (s *Schema) Parse(p *Parser) { name, _ = p.lex.consumeIdent() t.ImplTypes = append(t.ImplTypes, name.String()) } + t.Directives = p.parseDirectives() p.lex.consumeToken(tokLBrace) fallthrough case tokLBrace: @@ -531,22 +542,28 @@ func (s *Schema) MergeTypeName(wg *sync.WaitGroup) { if _, ok := seen[v.Name]; ok { for i := 0; i < j; i++ { if s.Types[i].Name == v.Name { - if reflect.DeepEqual(s.Types[i].ImplTypes, v.ImplTypes) && IsEqualWithoutDescriptions(s.Types[i].Directives, v.Directives) { + if v.Extend { s.Types[i].Fields = mergeFields(s.Types[i].Fields, v.Fields) - mergeDescriptionsAndComments(s.Types[i].Directives, v.Directives) + s.Types[i].Directives = mergeDirectives(s.Types[i].Directives, v.Directives) break } else { + if reflect.DeepEqual(s.Types[i].ImplTypes, v.ImplTypes) && IsEqualWithoutDescriptions(s.Types[i].Directives, v.Directives) { + s.Types[i].Fields = mergeFields(s.Types[i].Fields, v.Fields) + mergeDescriptionsAndComments(s.Types[i].Directives, v.Directives) + break + } else { - rel1, err := GetRelPath(s.Types[i].Filename) - if err != nil { - panic(err) - } - rel2, err := GetRelPath(v.Filename) - if err != nil { - panic(err) - } + rel1, err := GetRelPath(s.Types[i].Filename) + if err != nil { + panic(err) + } + rel2, err := GetRelPath(v.Filename) + if err != nil { + panic(err) + } - errorf("Duplicated Types: %s(%s:%v:%v) and (%s:%v:%v)", s.Types[i].Name, *rel1, s.Types[i].Line, s.Types[i].Column, *rel2, v.Line, v.Column) + errorf("Duplicated Types: %s(%s:%v:%v) and (%s:%v:%v)", s.Types[i].Name, *rel1, s.Types[i].Line, s.Types[i].Column, *rel2, v.Line, v.Column) + } } } } diff --git a/lib/schema_test.go b/lib/schema_test.go index d9b54f0..97c71c8 100644 --- a/lib/schema_test.go +++ b/lib/schema_test.go @@ -38,3 +38,39 @@ func TestGetSchema(t *testing.T) { } } + +func TestMergeDirectives(t *testing.T) { + str := make([]string, 0) + a := []*Directive{ + { + Name: "talkable", + DirectiveArgs: []*DirectiveArg{}, + Descriptions: &str, + }, + } + b := []*Directive{ + { + Name: "talkable", + DirectiveArgs: []*DirectiveArg{}, + Descriptions: &str, + }, + } + c := []*Directive{ + { + Name: "walkable", + DirectiveArgs: []*DirectiveArg{}, + Descriptions: &str, + }, + } + ds := mergeDirectives(a, b) + + if len(ds) == 0 { + t.Fatal("should be more than 0") + } + + ds = mergeDirectives(a, c) + + if len(ds) == 0 || len(ds) == 1 { + t.Fatal("should be more than 1") + } +} diff --git a/lib/util.go b/lib/util.go index 0f21f84..6692090 100644 --- a/lib/util.go +++ b/lib/util.go @@ -137,3 +137,78 @@ func mergeDescriptionsAndComments(a, b interface{}) { } } } + +func mergeDirectiveArgs(a, b []*DirectiveArg) []*DirectiveArg { + merged := make([]*DirectiveArg, len(a)) + copy(merged, a) + + for _, bArg := range b { + found := false + for i, mArg := range merged { + if mArg.Name == bArg.Name && compareValuesAndIsList(mArg, bArg) { + merged[i].Descriptions = mergeDescriptions(mArg.Descriptions, bArg.Descriptions) + found = true + break + } + } + if !found { + merged = append(merged, bArg) + } + } + return merged +} + +func compareValuesAndIsList(a, b *DirectiveArg) bool { + if a.IsList != b.IsList { + return false + } + if len(a.Value) != len(b.Value) { + return false + } + for i := range a.Value { + if a.Value[i] != b.Value[i] { + return false + } + } + return true +} + +func mergeDescriptions(a, b *[]string) *[]string { + if a == nil && b == nil { + return nil + } + if a == nil { + return b + } + if b == nil { + return a + } + merged := make([]string, len(*a)+len(*b)) + copy(merged, *a) + merged = append(merged, *b...) + return &merged +} + +func mergeDirectives(a, b []*Directive) []*Directive { + directiveMap := make(map[string]*Directive) + + for _, dir := range a { + directiveMap[dir.Name] = dir + } + + for _, dirB := range b { + if dirA, exists := directiveMap[dirB.Name]; exists { + dirA.DirectiveArgs = mergeDirectiveArgs(dirA.DirectiveArgs, dirB.DirectiveArgs) + dirA.Descriptions = mergeDescriptions(dirA.Descriptions, dirB.Descriptions) + } else { + directiveMap[dirB.Name] = dirB + } + } + + merged := make([]*Directive, 0, len(directiveMap)) + for _, dir := range directiveMap { + merged = append(merged, dir) + } + + return merged +} diff --git a/lib/write.go b/lib/write.go index 8d11ca8..e95b86b 100644 --- a/lib/write.go +++ b/lib/write.go @@ -74,6 +74,7 @@ func (ms *MergedSchema) WriteSchema(s *Schema) string { if len(t.ImplTypes) > 0 { ms.buf.WriteString(" implements " + strings.Join(t.ImplTypes, " & ")) } + ms.stitchDirectives(t.Directives) ms.buf.WriteString(" {\n") for _, p := range t.Fields { ms.writeDescriptions(p.Descriptions, 1, false) diff --git a/test/directives/generated.graphql b/test/directives/generated.graphql new file mode 100644 index 0000000..2a5c698 --- /dev/null +++ b/test/directives/generated.graphql @@ -0,0 +1,14 @@ +type Person @paint { + name: String + age: Int + picture: Url +} + +type ExampleType implements Node @deprecated { + id: ID + oldField: String +} + + + + diff --git a/test/directives/schema/object.graphql b/test/directives/schema/object.graphql new file mode 100644 index 0000000..76ace8c --- /dev/null +++ b/test/directives/schema/object.graphql @@ -0,0 +1,10 @@ +type Person @paint { + name: String + age: Int + picture: Url +} + +type ExampleType implements Node @deprecated { + id: ID + oldField: String +} \ No newline at end of file diff --git a/test/object_extension/generated.graphql b/test/object_extension/generated.graphql new file mode 100644 index 0000000..df1238f --- /dev/null +++ b/test/object_extension/generated.graphql @@ -0,0 +1,11 @@ +type Person implements Node @talkable @walkable { + id: ID! + createTime: Time! + updateTime: Time! + name: String! + hasCar: Boolean! +} + + + + diff --git a/test/object_extension/schema/a.graphql b/test/object_extension/schema/a.graphql new file mode 100644 index 0000000..77200ec --- /dev/null +++ b/test/object_extension/schema/a.graphql @@ -0,0 +1,6 @@ +type Person implements Node @talkable{ + id: ID! + createTime: Time! + updateTime: Time! + name: String! +} diff --git a/test/object_extension/schema/b.graphql b/test/object_extension/schema/b.graphql new file mode 100644 index 0000000..6b9a6b4 --- /dev/null +++ b/test/object_extension/schema/b.graphql @@ -0,0 +1,3 @@ +extend type Person @walkable { + hasCar: Boolean! +}