package cosmosgen import ( "context" "fmt" "log" "os" "path/filepath" "strings" "github.com/blang/semver/v4" "github.com/iancoleman/strcase" "github.com/ignite/cli/v29/ignite/pkg/cache" "github.com/ignite/cli/v29/ignite/pkg/cosmosbuf" "github.com/ignite/cli/v29/ignite/pkg/dirchange" "github.com/ignite/cli/v29/ignite/pkg/errors" swaggercombine "github.com/ignite/cli/v29/ignite/pkg/swagger-combine" "github.com/ignite/cli/v29/ignite/pkg/xos" ) const ( specCacheNamespace = "generate.openapi.spec" specFilename = "swagger.config.json" ) func (g *generator) openAPITemplate() string { return filepath.Join(g.appPath, g.protoDir, "buf.gen.swagger.yaml") } func (g *generator) generateOpenAPISpec(ctx context.Context, excludeList ...string) error { var ( specDirs = make([]string, 0) conf = swaggercombine.New("HTTP API Console", g.goModPath) ) defer func() { for _, dir := range specDirs { _ = os.RemoveAll(dir) } }() specCache := cache.New[[]byte](g.cacheStorage, specCacheNamespace) var hasAnySpecChanged bool // gen generates a spec for a module where it's source code resides at src. // and adds needed swaggercombine configure for it. gen := func(appPath, protoDir, name string) error { name = strcase.ToCamel(name) protoPath := filepath.Join(appPath, protoDir) // check if directory exists if _, err := os.Stat(protoPath); os.IsNotExist(err) { var err error protoPath, err = findInnerProtoFolder(appPath) if err != nil { // if proto directory does not exist, we just skip it log.Print(err.Error()) return nil } } dir, err := os.MkdirTemp("", "gen-openapi-module-spec") if err != nil { return err } specDirs = append(specDirs, dir) var noChecksum bool checksum, err := dirchange.ChecksumFromPaths(appPath, protoDir) if errors.Is(err, dirchange.ErrNoFile) { noChecksum = true } else if err != nil { return err } cacheKey := fmt.Sprintf("%x", checksum) if !noChecksum { existingSpec, err := specCache.Get(cacheKey) if err != nil && !errors.Is(err, cache.ErrorNotFound) { return err } if !errors.Is(err, cache.ErrorNotFound) { specPath := filepath.Join(dir, specFilename) if err := os.WriteFile(specPath, existingSpec, 0o600); err != nil { return err } return conf.AddSpec(name, specPath, true) } } hasAnySpecChanged = true if err = g.buf.Generate( ctx, protoPath, dir, g.openAPITemplate(), cosmosbuf.ExcludeFiles( append(excludeList, []string{ "*/strangelove_ventures/poa/*", "*/osmosis-labs/fee-abstraction/*", "*/module.proto", "*/testutil/*", "*/testdata/*", "*/cosmos/orm/*", "*/cosmos/reflection/*", "*/cosmos/app/v1alpha1/*", "*/cosmos/tx/config/v1/config.proto", "*/cosmos/msg/textual/v1/textual.proto", "*/cosmos/vesting/v1beta1/vesting.proto", }...)..., ), cosmosbuf.FileByFile(), ); err != nil { return errors.Wrapf(err, "failed to generate openapi spec %s, probably you need to exclude some proto files", protoPath) } specs, err := xos.FindFiles(dir, xos.WithExtension(xos.JSONFile)) if err != nil { return err } for _, spec := range specs { f, err := os.ReadFile(spec) if err != nil { return err } // if no checksum, the cacheKey is wrong, so we do not save it if !noChecksum { if err := specCache.Put(cacheKey, f); err != nil { return err } } if err := conf.AddSpec(name, spec, true); err != nil { return err } } return nil } // generate specs for each module and persist them in the file system // after add their path and config to swaggercombine.Config so we can combine them // into a single spec. // protoc openapi generator acts weird on concurrent run, so do not use goroutines here. if err := gen(g.appPath, g.protoDir, g.goModPath); err != nil { return err } doneMods := make(map[string]struct{}) for _, modules := range g.thirdModules { for _, m := range modules { path := extractRootModulePath(m.Pkg.Path) if _, ok := doneMods[path]; ok { continue } doneMods[path] = struct{}{} if err := gen(path, "proto", m.Name); err != nil { return err } } } out := g.opts.openAPISpecOut if !hasAnySpecChanged { // In case the generated output has been changed changed, err := dirchange.HasDirChecksumChanged(specCache, out, g.appPath, out) if err != nil { return err } if !changed { return nil } } // combine specs into one and save to out. if err := conf.Combine(out); err != nil { return err } return dirchange.SaveDirChecksum(specCache, out, g.appPath, out) } func extractRootModulePath(fullPath string) string { var ( segments = strings.Split(fullPath, "/") modulePath = "/" ) for _, segment := range segments { modulePath = filepath.Join(modulePath, segment) segmentName := strings.Split(segment, "@") if len(segmentName) > 1 { if _, err := semver.ParseTolerant(segmentName[1]); err == nil { return modulePath } } } return fullPath }