package query import ( "embed" "fmt" "io/fs" "path/filepath" "github.com/emicklei/proto" "github.com/gobuffalo/genny/v2" "github.com/gobuffalo/plush/v4" "github.com/ignite/cli/v29/ignite/pkg/errors" "github.com/ignite/cli/v29/ignite/pkg/gomodulepath" "github.com/ignite/cli/v29/ignite/pkg/protoanalysis/protoutil" "github.com/ignite/cli/v29/ignite/pkg/xgenny" "github.com/ignite/cli/v29/ignite/templates/field/plushhelpers" "github.com/ignite/cli/v29/ignite/templates/typed" ) //go:embed files/* files/**/* var files embed.FS func Box(box fs.FS, opts *Options, g *genny.Generator) error { if err := g.OnlyFS(box, nil, nil); err != nil { return err } ctx := plush.NewContext() ctx.Set("ModuleName", opts.ModuleName) ctx.Set("AppName", opts.AppName) ctx.Set("ProtoVer", opts.ProtoVer) ctx.Set("QueryName", opts.QueryName) ctx.Set("Description", opts.Description) ctx.Set("ModulePath", opts.ModulePath) ctx.Set("ReqFields", opts.ReqFields) ctx.Set("ResFields", opts.ResFields) ctx.Set("Paginated", opts.Paginated) plushhelpers.ExtendPlushContext(ctx) g.Transformer(xgenny.Transformer(ctx)) g.Transformer(genny.Replace("{{protoDir}}", opts.ProtoDir)) g.Transformer(genny.Replace("{{appName}}", opts.AppName)) g.Transformer(genny.Replace("{{moduleName}}", opts.ModuleName)) g.Transformer(genny.Replace("{{protoVer}}", opts.ProtoVer)) g.Transformer(genny.Replace("{{queryName}}", opts.QueryName.Snake)) return nil } // NewGenerator returns the generator to scaffold a empty query in a module. func NewGenerator(opts *Options) (*genny.Generator, error) { subFs, err := fs.Sub(files, "files") if err != nil { return nil, errors.Errorf("fail to generate sub: %w", err) } g := genny.New() g.RunFn(protoQueryModify(opts)) g.RunFn(cliQueryModify(opts)) return g, Box(subFs, opts, g) } // Modifies query.proto to add the required RPCs and Messages. // // What it depends on: // - Existence of a service with name "Query" since that is where the RPCs will be added. func protoQueryModify(opts *Options) genny.RunFn { return func(r *genny.Runner) error { path := opts.ProtoFile("query.proto") f, err := r.Disk.Find(path) if err != nil { return err } protoFile, err := protoutil.ParseProtoFile(f) if err != nil { return err } // if the query has request fields, they are appended to the rpc query var requestPath string for _, field := range opts.ReqFields { requestPath += "/" requestPath = filepath.Join(requestPath, fmt.Sprintf("{%s}", field.ProtoFieldName())) } serviceQuery, err := protoutil.GetServiceByName(protoFile, "Query") if err != nil { return errors.Errorf("failed while looking up service 'Query' in %s: %w", path, err) } typenamePascal, appModulePath := opts.QueryName.PascalCase, gomodulepath.ExtractAppPath(opts.ModulePath) rpcSingle := protoutil.NewRPC( typenamePascal, fmt.Sprintf("Query%sRequest", typenamePascal), fmt.Sprintf("Query%sResponse", typenamePascal), protoutil.WithRPCOptions( protoutil.NewOption( "google.api.http", fmt.Sprintf( "/%s/%s/%s/%s%s", appModulePath, opts.ModuleName, opts.ProtoVer, opts.QueryName.Snake, requestPath, ), protoutil.Custom(), protoutil.SetField("get"), ), ), ) protoutil.AttachComment(rpcSingle, fmt.Sprintf("%[1]v Queries a list of %[1]v items.", typenamePascal)) protoutil.Append(serviceQuery, rpcSingle) // Fields for request paginationType, paginationName := "cosmos.base.query.v1beta1.Page", "pagination" var reqFields []*proto.NormalField for i, field := range opts.ReqFields { reqFields = append(reqFields, field.ToProtoField(i+1)) } if opts.Paginated { reqFields = append(reqFields, protoutil.NewField(paginationName, paginationType+"Request", len(opts.ReqFields)+1)) } requestMessage := protoutil.NewMessage("Query"+typenamePascal+"Request", protoutil.WithFields(reqFields...)) // Fields for response var resFields []*proto.NormalField for i, field := range opts.ResFields { resFields = append(resFields, field.ToProtoField(i+1)) } if opts.Paginated { resFields = append(resFields, protoutil.NewField(paginationName, paginationType+"Response", len(opts.ResFields)+1)) } responseMessage := protoutil.NewMessage("Query"+typenamePascal+"Response", protoutil.WithFields(resFields...)) protoutil.Append(protoFile, requestMessage, responseMessage) // Ensure custom types are imported var protoImports []*proto.Import for _, imp := range append(opts.ResFields.ProtoImports(), opts.ReqFields.ProtoImports()...) { protoImports = append(protoImports, protoutil.NewImport(imp)) } for _, f := range append(opts.ResFields.Custom(), opts.ReqFields.Custom()...) { protoPath := fmt.Sprintf("%[1]v/%[2]v/%[3]v/%[4]v.proto", opts.AppName, opts.ModuleName, opts.ProtoVer, f) protoImports = append(protoImports, protoutil.NewImport(protoPath)) } if err = protoutil.AddImports(protoFile, true, protoImports...); err != nil { return errors.Errorf("failed to add imports to %s: %w", path, err) } newFile := genny.NewFileS(path, protoutil.Print(protoFile)) return r.File(newFile) } } func cliQueryModify(opts *Options) genny.RunFn { return func(r *genny.Runner) error { path := filepath.Join("x", opts.ModuleName, "module/autocli.go") f, err := r.Disk.Find(path) if err != nil { return err } option := fmt.Sprintf( `{ RpcMethod: "%[1]v", Use: "%[2]v", Short: "%[3]v", PositionalArgs: []*autocliv1.PositionalArgDescriptor{%[4]s}, }`, opts.QueryName.PascalCase, fmt.Sprintf("%s %s", opts.QueryName.Kebab, opts.ReqFields.CLIUsage()), opts.Description, opts.ReqFields.ProtoFieldNameAutoCLI(), ) content, err := typed.AppendAutoCLIQueryOptions(f.String(), option) if err != nil { return err } newFile := genny.NewFileS(path, content) return r.File(newFile) } }