diff --git a/_examples/federation/reviews/graph/federation.go b/_examples/federation/reviews/graph/federation.go index d67dd3f52d9..8283de94996 100644 --- a/_examples/federation/reviews/graph/federation.go +++ b/_examples/federation/reviews/graph/federation.go @@ -121,13 +121,9 @@ func (ec *executionContext) __resolve_entities(ctx context.Context, representati return fmt.Errorf(`resolving Entity "User": %w`, err) } - entity.Host.ID, err = ec.unmarshalNString2string(ctx, rep["host"].(map[string]interface{})["id"]) + err = ec.PopulateUserRequires(ctx, entity, rep) if err != nil { - return err - } - entity.Email, err = ec.unmarshalNString2string(ctx, rep["email"]) - if err != nil { - return err + return fmt.Errorf(`populating requires for Entity "User": %w`, err) } list[idx[i]] = entity return nil diff --git a/_examples/federation/reviews/graph/federation.requires.go b/_examples/federation/reviews/graph/federation.requires.go new file mode 100644 index 00000000000..da43e8df904 --- /dev/null +++ b/_examples/federation/reviews/graph/federation.requires.go @@ -0,0 +1,13 @@ +package graph + +import ( + "context" + "fmt" + + "github.com/99designs/gqlgen/_examples/federation/reviews/graph/model" +) + +// PopulateUserRequires is the requires populator for the User entity. +func (ec *executionContext) PopulateUserRequires(ctx context.Context, entity *model.User, reps map[string]interface{}) error { + panic(fmt.Errorf("not implemented: PopulateUserRequires")) +} diff --git a/plugin/federation/entity.go b/plugin/federation/entity.go index 04a3c033b06..db8717946b8 100644 --- a/plugin/federation/entity.go +++ b/plugin/federation/entity.go @@ -2,6 +2,7 @@ package federation import ( "go/types" + "strings" "github.com/99designs/gqlgen/codegen/config" "github.com/99designs/gqlgen/codegen/templates" @@ -17,6 +18,7 @@ type Entity struct { Resolvers []*EntityResolver Requires []*Requires Multi bool + Type types.Type } type EntityResolver struct { @@ -115,3 +117,9 @@ func (e *Entity) keyFields() []string { } return keyFields } + +// GetTypeInfo - get the imported package & type name combo. package.TypeName +func (e Entity) GetTypeInfo() string { + typeParts := strings.Split(e.Type.String(), "/") + return typeParts[len(typeParts)-1] +} diff --git a/plugin/federation/federation.go b/plugin/federation/federation.go index 4cc8708d4cc..aec7913f5cd 100644 --- a/plugin/federation/federation.go +++ b/plugin/federation/federation.go @@ -3,6 +3,9 @@ package federation import ( _ "embed" "fmt" + "os" + "path/filepath" + "runtime" "sort" "strings" @@ -11,6 +14,7 @@ import ( "github.com/99designs/gqlgen/codegen" "github.com/99designs/gqlgen/codegen/config" "github.com/99designs/gqlgen/codegen/templates" + "github.com/99designs/gqlgen/internal/rewrite" "github.com/99designs/gqlgen/plugin" "github.com/99designs/gqlgen/plugin/federation/fieldset" ) @@ -233,6 +237,13 @@ type Entity { } func (f *federation) GenerateCode(data *codegen.Data) error { + // requires imports + requiresImports := make(map[string]bool, 0) + requiresImports["context"] = true + requiresImports["fmt"] = true + + requiresEntities := make(map[string]*Entity, 0) + if len(f.Entities) > 0 { if data.Objects.ByName("Entity") != nil { data.Objects.ByName("Entity").Root = true @@ -272,9 +283,19 @@ func (f *federation) GenerateCode(data *codegen.Data) error { fmt.Println("skipping @requires field " + reqField.Name + " in " + e.Def.Name) continue } + // keep track of which entities have requires + requiresEntities[e.Def.Name] = e + // make a proper import path + typeString := strings.Split(obj.Type.String(), ".") + requiresImports[strings.Join(typeString[:len(typeString)-1], ".")] = true + cgField := reqField.Field.TypeReference(obj, data.Objects) reqField.Type = cgField.TypeReference } + + // add type info to entity + e.Type = obj.Type + } } @@ -295,6 +316,81 @@ func (f *federation) GenerateCode(data *codegen.Data) error { } } + if len(requiresEntities) > 0 { + // check for existing requires functions + type Populator struct { + FuncName string + Exists bool + Comment string + Implementation string + Entity *Entity + } + populators := make([]Populator, 0) + + rewriter, err := rewrite.New(data.Config.Federation.Dir()) + if err != nil { + return err + } + + for name, entity := range requiresEntities { + populator := Populator{ + FuncName: fmt.Sprintf("Populate%sRequires", name), + Entity: entity, + } + + populator.Comment = strings.TrimSpace(strings.TrimLeft(rewriter.GetMethodComment("executionContext", populator.FuncName), `\`)) + populator.Implementation = strings.TrimSpace(rewriter.GetMethodBody("executionContext", populator.FuncName)) + + if populator.Implementation == "" { + populator.Exists = false + populator.Implementation = fmt.Sprintf("panic(fmt.Errorf(\"not implemented: %v\"))", populator.FuncName) + } + populators = append(populators, populator) + } + + // find and read requires template + _, callerFile, _, _ := runtime.Caller(0) + currentDir := filepath.Dir(callerFile) + requiresTemplate, err := os.ReadFile(currentDir + "/requires.gotpl") + + if err != nil { + return err + } + + requiresFile := data.Config.Federation.Dir() + "/federation.requires.go" + existingImports := rewriter.ExistingImports(requiresFile) + for _, imp := range existingImports { + if imp.Alias == "" { + if _, ok := requiresImports[imp.ImportPath]; ok { + // import exists in both places, remove + delete(requiresImports, imp.ImportPath) + } + } + } + + for k := range requiresImports { + existingImports = append(existingImports, rewrite.Import{ImportPath: k}) + } + + // render requires populators + err = templates.Render(templates.Options{ + PackageName: data.Config.Federation.Package, + Filename: requiresFile, + Data: struct { + federation + ExistingImports []rewrite.Import + Populators []Populator + OriginalSource string + }{*f, existingImports, populators, ""}, + GeneratedHeader: false, + Packages: data.Config.Packages, + Template: string(requiresTemplate), + }) + if err != nil { + return err + } + } + return templates.Render(templates.Options{ PackageName: data.Config.Federation.Package, Filename: data.Config.Federation.Filename, diff --git a/plugin/federation/federation.gotpl b/plugin/federation/federation.gotpl index 7cf84287eb6..caac06a0506 100644 --- a/plugin/federation/federation.gotpl +++ b/plugin/federation/federation.gotpl @@ -103,10 +103,10 @@ func (ec *executionContext) __resolve_entities(ctx context.Context, representati if err != nil { return fmt.Errorf(`resolving Entity "{{$entity.Def.Name}}": %w`, err) } - {{ range $entity.Requires }} - entity.{{.Field.JoinGo `.`}}, err = ec.{{.Type.UnmarshalFunc}}(ctx, rep["{{.Field.Join `"].(map[string]interface{})["`}}"]) + {{ if $entity.Requires }} + err = ec.Populate{{$entity.Def.Name}}Requires(ctx, entity, rep) if err != nil { - return err + return fmt.Errorf(`populating requires for Entity "{{$entity.Def.Name}}": %w`, err) } {{- end }} list[idx[i]] = entity diff --git a/plugin/federation/requires.gotpl b/plugin/federation/requires.gotpl new file mode 100644 index 00000000000..8779571e99b --- /dev/null +++ b/plugin/federation/requires.gotpl @@ -0,0 +1,20 @@ +{{ range .ExistingImports }} +{{ if ne .Alias "" }} +{{ reserveImport .ImportPath .Alias }} +{{ else }} +{{ reserveImport .ImportPath }} +{{ end }} +{{ end }} + +{{ range .Populators -}} +{{ if .Comment -}} +// {{.Comment}} +{{- else -}} +// {{.FuncName}} is the requires populator for the {{.Entity.Def.Name}} entity. +{{- end }} +func (ec *executionContext) {{.FuncName}}(ctx context.Context, entity *{{.Entity.GetTypeInfo}}, reps map[string]interface{}) error { + {{.Implementation}} +} +{{ end }} + +{{ .OriginalSource }} \ No newline at end of file diff --git a/plugin/federation/testdata/entityresolver/generated/federation.go b/plugin/federation/testdata/entityresolver/generated/federation.go index 0d24c351d41..4e5a9e6697f 100644 --- a/plugin/federation/testdata/entityresolver/generated/federation.go +++ b/plugin/federation/testdata/entityresolver/generated/federation.go @@ -172,13 +172,9 @@ func (ec *executionContext) __resolve_entities(ctx context.Context, representati return fmt.Errorf(`resolving Entity "PlanetMultipleRequires": %w`, err) } - entity.Diameter, err = ec.unmarshalNInt2int(ctx, rep["diameter"]) + err = ec.PopulatePlanetMultipleRequiresRequires(ctx, entity, rep) if err != nil { - return err - } - entity.Density, err = ec.unmarshalNInt2int(ctx, rep["density"]) - if err != nil { - return err + return fmt.Errorf(`populating requires for Entity "PlanetMultipleRequires": %w`, err) } list[idx[i]] = entity return nil @@ -200,9 +196,9 @@ func (ec *executionContext) __resolve_entities(ctx context.Context, representati return fmt.Errorf(`resolving Entity "PlanetRequires": %w`, err) } - entity.Diameter, err = ec.unmarshalNInt2int(ctx, rep["diameter"]) + err = ec.PopulatePlanetRequiresRequires(ctx, entity, rep) if err != nil { - return err + return fmt.Errorf(`populating requires for Entity "PlanetRequires": %w`, err) } list[idx[i]] = entity return nil @@ -224,9 +220,9 @@ func (ec *executionContext) __resolve_entities(ctx context.Context, representati return fmt.Errorf(`resolving Entity "PlanetRequiresNested": %w`, err) } - entity.World.Foo, err = ec.unmarshalNString2string(ctx, rep["world"].(map[string]interface{})["foo"]) + err = ec.PopulatePlanetRequiresNestedRequires(ctx, entity, rep) if err != nil { - return err + return fmt.Errorf(`populating requires for Entity "PlanetRequiresNested": %w`, err) } list[idx[i]] = entity return nil diff --git a/plugin/federation/testdata/entityresolver/generated/federation.requires.go b/plugin/federation/testdata/entityresolver/generated/federation.requires.go new file mode 100644 index 00000000000..886b2158701 --- /dev/null +++ b/plugin/federation/testdata/entityresolver/generated/federation.requires.go @@ -0,0 +1,38 @@ +package generated + +import ( + "context" + "fmt" + + "github.com/99designs/gqlgen/plugin/federation/testdata/entityresolver/generated/model" +) + +// PopulateMultiHelloMultipleRequiresRequires is the requires populator for the MultiHelloMultipleRequires entity. +func (ec *executionContext) PopulateMultiHelloMultipleRequiresRequires(ctx context.Context, entity *model.MultiHelloMultipleRequires, reps map[string]interface{}) error { + panic(fmt.Errorf("not implemented: PopulateMultiHelloMultipleRequiresRequires")) +} + +// PopulateMultiHelloRequiresRequires is the requires populator for the MultiHelloRequires entity. +func (ec *executionContext) PopulateMultiHelloRequiresRequires(ctx context.Context, entity *model.MultiHelloRequires, reps map[string]interface{}) error { + panic(fmt.Errorf("not implemented: PopulateMultiHelloRequiresRequires")) +} + +// PopulateMultiPlanetRequiresNestedRequires is the requires populator for the MultiPlanetRequiresNested entity. +func (ec *executionContext) PopulateMultiPlanetRequiresNestedRequires(ctx context.Context, entity *model.MultiPlanetRequiresNested, reps map[string]interface{}) error { + panic(fmt.Errorf("not implemented: PopulateMultiPlanetRequiresNestedRequires")) +} + +// PopulatePlanetMultipleRequiresRequires is the requires populator for the PlanetMultipleRequires entity. +func (ec *executionContext) PopulatePlanetMultipleRequiresRequires(ctx context.Context, entity *model.PlanetMultipleRequires, reps map[string]interface{}) error { + panic(fmt.Errorf("not implemented: PopulatePlanetMultipleRequiresRequires")) +} + +// PopulatePlanetRequiresRequires is the requires populator for the PlanetRequires entity. +func (ec *executionContext) PopulatePlanetRequiresRequires(ctx context.Context, entity *model.PlanetRequires, reps map[string]interface{}) error { + panic(fmt.Errorf("not implemented: PopulatePlanetRequiresRequires")) +} + +// PopulatePlanetRequiresNestedRequires is the requires populator for the PlanetRequiresNested entity. +func (ec *executionContext) PopulatePlanetRequiresNestedRequires(ctx context.Context, entity *model.PlanetRequiresNested, reps map[string]interface{}) error { + panic(fmt.Errorf("not implemented: PopulatePlanetRequiresNestedRequires")) +}