From 10d146f7bcfffd3cae27aa15cd9fd260094f6415 Mon Sep 17 00:00:00 2001 From: candiduslynx Date: Tue, 3 Oct 2023 10:00:18 +0300 Subject: [PATCH 1/5] allow passing single package name for gen --- go.mod | 4 +- go.sum | 27 +++++- interfaces/client_info.go | 76 ++++++++++++++++ interfaces/generate.go | 130 +--------------------------- interfaces/options.go | 46 ++++++++++ interfaces/template.go | 58 +++++++++++++ interfaces/templates/service.go.tpl | 4 +- 7 files changed, 210 insertions(+), 135 deletions(-) create mode 100644 interfaces/client_info.go create mode 100644 interfaces/options.go create mode 100644 interfaces/template.go diff --git a/go.mod b/go.mod index a7df8be..c4a49b1 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,12 @@ module github.com/cloudquery/codegen -go 1.20 +go 1.21.1 require ( github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appconfiguration/armappconfiguration/v2 v2.0.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armmanagedapplications v1.1.1 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/solutions/armmanagedapplications v1.1.1 + github.com/cloudquery/plugin-sdk/v4 v4.12.0 github.com/google/go-cmp v0.5.9 github.com/invopop/jsonschema v0.11.0 github.com/jpillora/longestcommon v0.0.0-20161227235612-adb9d91ee629 @@ -17,6 +18,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect + github.com/kr/text v0.2.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect golang.org/x/net v0.15.0 // indirect diff --git a/go.sum b/go.sum index 08b31c4..5e1a9dd 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,7 @@ github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.2 h1:t5+QXLCK9SVi0PPdaY0PrFvYUo24KwA0QwxnaHRSVd4= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.2/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.2 h1:uqM+VoHjVH6zdlkLF2b6O0ZANcHoj3rO0PoQ3jglUJA= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.2.2/go.mod h1:twTKAa1E6hLmSDjLhaCkbTMQKc7p/rNLU40rLxGEOCI= github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY= github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appconfiguration/armappconfiguration/v2 v2.0.0 h1:PWMXyUvMZxxu+7nMMEQUvhkChyyL5QHZS09l0pFFNy0= @@ -10,37 +11,57 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armmanagedapplic github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/solutions/armmanagedapplications v1.1.1 h1:hioFVSo4plF3RHTlGiTW9NSwdS8lN34yRT1QDpTgDsw= github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/solutions/armmanagedapplications v1.1.1/go.mod h1:AA8RASHl5shSrLLA0/IpBbLNsoSJkjIm68X9EGDQRgo= github.com/AzureAD/microsoft-authentication-library-for-go v0.9.0 h1:UE9n9rkJF62ArLb1F3DEjRt8O3jLwMWdSoypKV4f3MU= +github.com/AzureAD/microsoft-authentication-library-for-go v0.9.0/go.mod h1:kgDmCTgBzIEPFElEF+FK0SdjAor06dRq2Go927dnQ6o= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/cloudquery/plugin-sdk/v4 v4.12.0 h1:vNPptFUZYLAaG/9ktm7bnRBrR72+rN28rpPC62TSuXA= +github.com/cloudquery/plugin-sdk/v4 v4.12.0/go.mod h1:gvX/+uoZSxH+hJ/4qqE56jm6tJhJyQ+4RVroKACtkp4= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/invopop/jsonschema v0.11.0 h1:tdAVvos5ttrsYLyEuVymkVVK31EFpwnTu5hWiyYLGWA= github.com/invopop/jsonschema v0.11.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/longestcommon v0.0.0-20161227235612-adb9d91ee629 h1:1dSBUfGlorLAua2CRx0zFN7kQsTpE2DQSmr7rrTNgY8= github.com/jpillora/longestcommon v0.0.0-20161227235612-adb9d91ee629/go.mod h1:mb5nS4uRANwOJSZj8rlCWAfAcGi72GGMIXx+xGOjA7M= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/interfaces/client_info.go b/interfaces/client_info.go new file mode 100644 index 0000000..34e6035 --- /dev/null +++ b/interfaces/client_info.go @@ -0,0 +1,76 @@ +package interfaces + +import ( + "path" + "reflect" + "regexp" + "strings" + + "github.com/jpillora/longestcommon" +) + +type clientInfo struct { + Import string + ClientName string + Signatures []string + ExtraImports []string +} + +func getClientInfo(client any, opts *Options) clientInfo { + v := reflect.ValueOf(client) + t := v.Type() + pkgPath := t.Elem().PkgPath() + clientName := t.Elem().Name() + signatures := make([]string, 0) + extraImports := make([]string, 0) + for i := 0; i < t.NumMethod(); i++ { + method := t.Method(i) + if opts.ShouldInclude(method) { + sig := signature(method.Name, v.Method(i).Interface()) + signatures = append(signatures, sig) + } + extraImports = append(extraImports, opts.ExtraImports(method)...) + } + return clientInfo{ + Import: pkgPath, + ClientName: clientName, + Signatures: signatures, + ExtraImports: extraImports, + } +} + +func getPackageNames(clientInfos []clientInfo) []string { + versionPattern := regexp.MustCompile(`/v\d+$`) + allImports := make([]string, len(clientInfos)) + for i, clientInfo := range clientInfos { + allImports[i] = clientInfo.Import + } + // To get the shortest possible package name without collisions, we need to find the longest common prefix + importPrefix := longestcommon.Prefix(allImports) + packageNames := make([]string, len(clientInfos)) + for i, clientInfo := range clientInfos { + var pkgName string + if clientInfo.Import == importPrefix { + pkgName = versionPattern.ReplaceAllString(clientInfo.Import, "") + pkgName = path.Base(pkgName) + } else { + pkgName = strings.TrimPrefix(clientInfo.Import, importPrefix) + pkgName = strings.ReplaceAll(versionPattern.ReplaceAllString(pkgName, ""), "/", "_") + } + + packageNames[i] = strings.ReplaceAll(pkgName, "-", "") + } + return packageNames +} + +func (c clientInfo) templateData(singlePackageMode bool) clientTemplateData { + var packageName string + if singlePackageMode { + packageName = getPackageNames([]clientInfo{c})[0] + } + return clientTemplateData{ + packageName: packageName, + Name: c.ClientName, + Signatures: c.Signatures, + } +} diff --git a/interfaces/generate.go b/interfaces/generate.go index ba5ca37..e54fac6 100644 --- a/interfaces/generate.go +++ b/interfaces/generate.go @@ -11,98 +11,11 @@ import ( "regexp" "strings" "text/template" - - "github.com/jpillora/longestcommon" - - "golang.org/x/exp/maps" ) //go:embed templates/*.go.tpl var templatesFS embed.FS -type Options struct { - // ShouldInclude tests whether a method should be included in the generated interfaces. If it returns true, - // the method will be included. MethodHasPrefix and MethodHasSuffix can be used inside a custom function here - // to customize the behavior. - ShouldInclude func(reflect.Method) bool - - // ExtraImports can add extra imports for a method - ExtraImports func(reflect.Method) []string -} - -func (o *Options) SetDefaults() { - if o.ShouldInclude == nil { - o.ShouldInclude = func(reflect.Method) bool { return true } - } - if o.ExtraImports == nil { - o.ExtraImports = func(reflect.Method) []string { return nil } - } -} - -type Option func(*Options) - -func WithIncludeFunc(f func(reflect.Method) bool) Option { - return func(o *Options) { - o.ShouldInclude = f - } -} - -func WithExtraImports(f func(reflect.Method) []string) Option { - return func(o *Options) { - o.ExtraImports = f - } -} - -func getPackageNames(clientInfos []clientInfo) []string { - versionPattern := regexp.MustCompile(`/v\d+$`) - allImports := make([]string, len(clientInfos)) - for i, clientInfo := range clientInfos { - allImports[i] = clientInfo.Import - } - // To get the shortest possible package name without collisions, we need to find the longest common prefix - importPrefix := longestcommon.Prefix(allImports) - packageNames := make([]string, len(clientInfos)) - for i, clientInfo := range clientInfos { - var pkgName string - if clientInfo.Import == importPrefix { - pkgName = versionPattern.ReplaceAllString(clientInfo.Import, "") - pkgName = path.Base(pkgName) - } else { - pkgName = strings.TrimPrefix(clientInfo.Import, importPrefix) - pkgName = strings.ReplaceAll(versionPattern.ReplaceAllString(pkgName, ""), "/", "_") - } - - packageNames[i] = strings.ReplaceAll(pkgName, "-", "") - } - return packageNames -} - -func getTemplateDataFromClientInfos(clientInfos []clientInfo) []serviceTemplateData { - packageNames := getPackageNames(clientInfos) - services := make([]serviceTemplateData, 0) - serviceMap := make(map[string][]clientInfo) - for i, clientInfo := range clientInfos { - serviceMap[packageNames[i]] = append(serviceMap[packageNames[i]], clientInfo) - } - for packageName, infos := range serviceMap { - imports := make(map[string]bool) - clientsTemplateData := make([]clientTemplateData, 0) - for _, clientInfo := range infos { - imports[clientInfo.Import] = true - for _, extraImport := range clientInfo.ExtraImports { - imports[extraImport] = true - } - clientsTemplateData = append(clientsTemplateData, clientTemplateData{Name: clientInfo.ClientName, Signatures: clientInfo.Signatures}) - } - services = append(services, serviceTemplateData{ - PackageName: packageName, - Imports: maps.Keys(imports), - Clients: clientsTemplateData, - }) - } - return services -} - // Generate generates service interfaces to be used for generating // mocks. The clients passed in as the first argument should be structs that will be used to // generate the service interfaces. The second argument, dir, is the path to the output @@ -125,7 +38,7 @@ func Generate(clients []any, dir string, opts ...Option) error { return err } - services := getTemplateDataFromClientInfos(clientInfos) + services := getTemplateDataFromClientInfos(clientInfos, options) for _, service := range services { buff := bytes.Buffer{} @@ -201,47 +114,6 @@ func signature(name string, f any) string { return buf.String() } -type clientInfo struct { - Import string - ClientName string - Signatures []string - ExtraImports []string -} - -type clientTemplateData struct { - Name string - Signatures []string -} - -type serviceTemplateData struct { - PackageName string - Imports []string - Clients []clientTemplateData -} - -func getClientInfo(client any, opts *Options) clientInfo { - v := reflect.ValueOf(client) - t := v.Type() - pkgPath := t.Elem().PkgPath() - clientName := t.Elem().Name() - signatures := make([]string, 0) - extraImports := make([]string, 0) - for i := 0; i < t.NumMethod(); i++ { - method := t.Method(i) - if opts.ShouldInclude(method) { - sig := signature(method.Name, v.Method(i).Interface()) - signatures = append(signatures, sig) - } - extraImports = append(extraImports, opts.ExtraImports(method)...) - } - return clientInfo{ - Import: pkgPath, - ClientName: clientName, - Signatures: signatures, - ExtraImports: extraImports, - } -} - func formatAndWriteFile(filePath string, buff bytes.Buffer) error { content := buff.Bytes() formattedContent, err := format.Source(buff.Bytes()) diff --git a/interfaces/options.go b/interfaces/options.go new file mode 100644 index 0000000..ecfc789 --- /dev/null +++ b/interfaces/options.go @@ -0,0 +1,46 @@ +package interfaces + +import "reflect" + +type Options struct { + // ShouldInclude tests whether a method should be included in the generated interfaces. If it returns true, + // the method will be included. MethodHasPrefix and MethodHasSuffix can be used inside a custom function here + // to customize the behavior. + ShouldInclude func(reflect.Method) bool + + // ExtraImports can add extra imports for a method + ExtraImports func(reflect.Method) []string + + // SinglePackage allows to generate all passed clients into a single package. + // The clients will get their package name as prefix to the interface name (e.g., s3.Client -> S3Client) + SinglePackage string +} + +func (o *Options) SetDefaults() { + if o.ShouldInclude == nil { + o.ShouldInclude = func(reflect.Method) bool { return true } + } + if o.ExtraImports == nil { + o.ExtraImports = func(reflect.Method) []string { return nil } + } +} + +type Option func(*Options) + +func WithIncludeFunc(f func(reflect.Method) bool) Option { + return func(o *Options) { + o.ShouldInclude = f + } +} + +func WithExtraImports(f func(reflect.Method) []string) Option { + return func(o *Options) { + o.ExtraImports = f + } +} + +func WithSinglePackage(name string) Option { + return func(o *Options) { + o.SinglePackage = name + } +} diff --git a/interfaces/template.go b/interfaces/template.go new file mode 100644 index 0000000..947648f --- /dev/null +++ b/interfaces/template.go @@ -0,0 +1,58 @@ +package interfaces + +import ( + "github.com/cloudquery/plugin-sdk/v4/caser" + "golang.org/x/exp/maps" +) + +var ( + csr = caser.New() +) + +type serviceTemplateData struct { + PackageName string + FileName string + Imports []string + Clients []clientTemplateData +} + +func getTemplateDataFromClientInfos(clientInfos []clientInfo, options *Options) []serviceTemplateData { + packageNames := getPackageNames(clientInfos) + services := make([]serviceTemplateData, 0) + serviceMap := make(map[string][]clientInfo) + for i, clientInfo := range clientInfos { + serviceMap[packageNames[i]] = append(serviceMap[packageNames[i]], clientInfo) + } + for packageName, infos := range serviceMap { + imports := make(map[string]bool) + clientsTemplateData := make([]clientTemplateData, 0) + for _, clientInfo := range infos { + imports[clientInfo.Import] = true + for _, extraImport := range clientInfo.ExtraImports { + imports[extraImport] = true + } + clientsTemplateData = append(clientsTemplateData, clientInfo.templateData(len(options.SinglePackage) > 0)) + } + svc := serviceTemplateData{ + PackageName: packageName, + FileName: packageName, + Imports: maps.Keys(imports), + Clients: clientsTemplateData, + } + if len(options.SinglePackage) > 0 { + svc.PackageName = options.SinglePackage + } + services = append(services, svc) + } + return services +} + +type clientTemplateData struct { + packageName string + Name string + Signatures []string +} + +func (c clientTemplateData) ClientName() string { + return csr.ToPascal(c.packageName) + c.Name +} diff --git a/interfaces/templates/service.go.tpl b/interfaces/templates/service.go.tpl index 0623bd9..8d22244 100644 --- a/interfaces/templates/service.go.tpl +++ b/interfaces/templates/service.go.tpl @@ -8,8 +8,8 @@ import ( ) {{ range .Clients }} -//go:generate mockgen -package=mocks -destination=../mocks/{{$.PackageName}}.go -source={{$.PackageName}}.go {{.Name}} -type {{.Name}} interface { +//go:generate mockgen -package=mocks -destination=../mocks/{{$.FileName}}.go -source={{$.FileName}}.go {{.ClientName}} +type {{.ClientName}} interface { {{- range $sig := .Signatures }} {{ $sig }} {{- end }} From fd593c1ff8b25eb3815a9da6b36dd5a7e1bbfde0 Mon Sep 17 00:00:00 2001 From: candiduslynx Date: Tue, 3 Oct 2023 10:07:01 +0300 Subject: [PATCH 2/5] use file path --- interfaces/generate.go | 3 +-- interfaces/template.go | 10 ++++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/interfaces/generate.go b/interfaces/generate.go index e54fac6..fa7a5e2 100644 --- a/interfaces/generate.go +++ b/interfaces/generate.go @@ -45,8 +45,7 @@ func Generate(clients []any, dir string, opts ...Option) error { if err := serviceTpl.Execute(&buff, service); err != nil { return fmt.Errorf("failed to execute template: %w", err) } - filePath := path.Join(dir, service.PackageName, fmt.Sprintf("%s.go", service.PackageName)) - err := formatAndWriteFile(filePath, buff) + err := formatAndWriteFile(service.getFilePath(dir), buff) if err != nil { return fmt.Errorf("failed to format and write file for service %v: %w", service, err) } diff --git a/interfaces/template.go b/interfaces/template.go index 947648f..b8d4665 100644 --- a/interfaces/template.go +++ b/interfaces/template.go @@ -1,6 +1,9 @@ package interfaces import ( + "fmt" + "path" + "github.com/cloudquery/plugin-sdk/v4/caser" "golang.org/x/exp/maps" ) @@ -16,6 +19,13 @@ type serviceTemplateData struct { Clients []clientTemplateData } +func (s serviceTemplateData) getFilePath(baseDir string) string { + if s.FileName == s.PackageName { + return path.Join(baseDir, s.PackageName, fmt.Sprintf("%s.go", s.PackageName)) + } + return path.Join(baseDir, fmt.Sprintf("%s.go", s.FileName)) +} + func getTemplateDataFromClientInfos(clientInfos []clientInfo, options *Options) []serviceTemplateData { packageNames := getPackageNames(clientInfos) services := make([]serviceTemplateData, 0) From 6acd8385a3e6d0581a56608808efdb36a85ddc26 Mon Sep 17 00:00:00 2001 From: candiduslynx Date: Tue, 3 Oct 2023 10:10:17 +0300 Subject: [PATCH 3/5] dbg --- interfaces/template.go | 1 + 1 file changed, 1 insertion(+) diff --git a/interfaces/template.go b/interfaces/template.go index b8d4665..35505aa 100644 --- a/interfaces/template.go +++ b/interfaces/template.go @@ -50,6 +50,7 @@ func getTemplateDataFromClientInfos(clientInfos []clientInfo, options *Options) Clients: clientsTemplateData, } if len(options.SinglePackage) > 0 { + fmt.Printf("updating package name from %s to %s\n", svc.PackageName, options.SinglePackage) svc.PackageName = options.SinglePackage } services = append(services, svc) From 5d9bbf23f0aedca9d7b8c888bc1d5799267fcea2 Mon Sep 17 00:00:00 2001 From: candiduslynx Date: Tue, 3 Oct 2023 10:15:16 +0300 Subject: [PATCH 4/5] rm dbg --- interfaces/template.go | 1 - 1 file changed, 1 deletion(-) diff --git a/interfaces/template.go b/interfaces/template.go index 35505aa..b8d4665 100644 --- a/interfaces/template.go +++ b/interfaces/template.go @@ -50,7 +50,6 @@ func getTemplateDataFromClientInfos(clientInfos []clientInfo, options *Options) Clients: clientsTemplateData, } if len(options.SinglePackage) > 0 { - fmt.Printf("updating package name from %s to %s\n", svc.PackageName, options.SinglePackage) svc.PackageName = options.SinglePackage } services = append(services, svc) From 170fc60469b04be9c804a5a1a572acf626a659b4 Mon Sep 17 00:00:00 2001 From: candiduslynx Date: Tue, 3 Oct 2023 10:17:05 +0300 Subject: [PATCH 5/5] less diff --- interfaces/client_info.go | 76 ------------- interfaces/generate.go | 162 ++++++++++++++++++++++++++++ interfaces/options.go | 46 -------- interfaces/template.go | 68 ------------ interfaces/templates/service.go.tpl | 4 +- 5 files changed, 164 insertions(+), 192 deletions(-) delete mode 100644 interfaces/client_info.go delete mode 100644 interfaces/options.go delete mode 100644 interfaces/template.go diff --git a/interfaces/client_info.go b/interfaces/client_info.go deleted file mode 100644 index 34e6035..0000000 --- a/interfaces/client_info.go +++ /dev/null @@ -1,76 +0,0 @@ -package interfaces - -import ( - "path" - "reflect" - "regexp" - "strings" - - "github.com/jpillora/longestcommon" -) - -type clientInfo struct { - Import string - ClientName string - Signatures []string - ExtraImports []string -} - -func getClientInfo(client any, opts *Options) clientInfo { - v := reflect.ValueOf(client) - t := v.Type() - pkgPath := t.Elem().PkgPath() - clientName := t.Elem().Name() - signatures := make([]string, 0) - extraImports := make([]string, 0) - for i := 0; i < t.NumMethod(); i++ { - method := t.Method(i) - if opts.ShouldInclude(method) { - sig := signature(method.Name, v.Method(i).Interface()) - signatures = append(signatures, sig) - } - extraImports = append(extraImports, opts.ExtraImports(method)...) - } - return clientInfo{ - Import: pkgPath, - ClientName: clientName, - Signatures: signatures, - ExtraImports: extraImports, - } -} - -func getPackageNames(clientInfos []clientInfo) []string { - versionPattern := regexp.MustCompile(`/v\d+$`) - allImports := make([]string, len(clientInfos)) - for i, clientInfo := range clientInfos { - allImports[i] = clientInfo.Import - } - // To get the shortest possible package name without collisions, we need to find the longest common prefix - importPrefix := longestcommon.Prefix(allImports) - packageNames := make([]string, len(clientInfos)) - for i, clientInfo := range clientInfos { - var pkgName string - if clientInfo.Import == importPrefix { - pkgName = versionPattern.ReplaceAllString(clientInfo.Import, "") - pkgName = path.Base(pkgName) - } else { - pkgName = strings.TrimPrefix(clientInfo.Import, importPrefix) - pkgName = strings.ReplaceAll(versionPattern.ReplaceAllString(pkgName, ""), "/", "_") - } - - packageNames[i] = strings.ReplaceAll(pkgName, "-", "") - } - return packageNames -} - -func (c clientInfo) templateData(singlePackageMode bool) clientTemplateData { - var packageName string - if singlePackageMode { - packageName = getPackageNames([]clientInfo{c})[0] - } - return clientTemplateData{ - packageName: packageName, - Name: c.ClientName, - Signatures: c.Signatures, - } -} diff --git a/interfaces/generate.go b/interfaces/generate.go index fa7a5e2..4f3d9b9 100644 --- a/interfaces/generate.go +++ b/interfaces/generate.go @@ -11,11 +11,113 @@ import ( "regexp" "strings" "text/template" + + "github.com/cloudquery/plugin-sdk/v4/caser" + "github.com/jpillora/longestcommon" + "golang.org/x/exp/maps" ) //go:embed templates/*.go.tpl var templatesFS embed.FS +type Options struct { + // ShouldInclude tests whether a method should be included in the generated interfaces. If it returns true, + // the method will be included. MethodHasPrefix and MethodHasSuffix can be used inside a custom function here + // to customize the behavior. + ShouldInclude func(reflect.Method) bool + + // ExtraImports can add extra imports for a method + ExtraImports func(reflect.Method) []string + + // SinglePackage allows to generate all passed clients into a single package. + // The clients will get their package name as prefix to the interface name (e.g., s3.Client -> S3Client) + SinglePackage string +} + +func (o *Options) SetDefaults() { + if o.ShouldInclude == nil { + o.ShouldInclude = func(reflect.Method) bool { return true } + } + if o.ExtraImports == nil { + o.ExtraImports = func(reflect.Method) []string { return nil } + } +} + +type Option func(*Options) + +func WithIncludeFunc(f func(reflect.Method) bool) Option { + return func(o *Options) { + o.ShouldInclude = f + } +} + +func WithExtraImports(f func(reflect.Method) []string) Option { + return func(o *Options) { + o.ExtraImports = f + } +} + +func WithSinglePackage(name string) Option { + return func(o *Options) { + o.SinglePackage = name + } +} + +func getPackageNames(clientInfos []clientInfo) []string { + versionPattern := regexp.MustCompile(`/v\d+$`) + allImports := make([]string, len(clientInfos)) + for i, clientInfo := range clientInfos { + allImports[i] = clientInfo.Import + } + // To get the shortest possible package name without collisions, we need to find the longest common prefix + importPrefix := longestcommon.Prefix(allImports) + packageNames := make([]string, len(clientInfos)) + for i, clientInfo := range clientInfos { + var pkgName string + if clientInfo.Import == importPrefix { + pkgName = versionPattern.ReplaceAllString(clientInfo.Import, "") + pkgName = path.Base(pkgName) + } else { + pkgName = strings.TrimPrefix(clientInfo.Import, importPrefix) + pkgName = strings.ReplaceAll(versionPattern.ReplaceAllString(pkgName, ""), "/", "_") + } + + packageNames[i] = strings.ReplaceAll(pkgName, "-", "") + } + return packageNames +} + +func getTemplateDataFromClientInfos(clientInfos []clientInfo, options *Options) []serviceTemplateData { + packageNames := getPackageNames(clientInfos) + services := make([]serviceTemplateData, 0) + serviceMap := make(map[string][]clientInfo) + for i, clientInfo := range clientInfos { + serviceMap[packageNames[i]] = append(serviceMap[packageNames[i]], clientInfo) + } + for packageName, infos := range serviceMap { + imports := make(map[string]bool) + clientsTemplateData := make([]clientTemplateData, 0) + for _, clientInfo := range infos { + imports[clientInfo.Import] = true + for _, extraImport := range clientInfo.ExtraImports { + imports[extraImport] = true + } + clientsTemplateData = append(clientsTemplateData, clientInfo.templateData(len(options.SinglePackage) > 0)) + } + svc := serviceTemplateData{ + PackageName: packageName, + FileName: packageName, + Imports: maps.Keys(imports), + Clients: clientsTemplateData, + } + if len(options.SinglePackage) > 0 { + svc.PackageName = options.SinglePackage + } + services = append(services, svc) + } + return services +} + // Generate generates service interfaces to be used for generating // mocks. The clients passed in as the first argument should be structs that will be used to // generate the service interfaces. The second argument, dir, is the path to the output @@ -113,6 +215,66 @@ func signature(name string, f any) string { return buf.String() } +type clientInfo struct { + Import string + ClientName string + Signatures []string + ExtraImports []string +} + +func (c clientInfo) templateData(singlePackageMode bool) clientTemplateData { + var packageName string + if singlePackageMode { + packageName = caser.New().ToPascal(getPackageNames([]clientInfo{c})[0]) + } + return clientTemplateData{ + Name: packageName + c.ClientName, + Signatures: c.Signatures, + } +} + +type clientTemplateData struct { + Name string + Signatures []string +} + +type serviceTemplateData struct { + PackageName string + FileName string + Imports []string + Clients []clientTemplateData +} + +func (s serviceTemplateData) getFilePath(baseDir string) string { + if s.FileName == s.PackageName { + return path.Join(baseDir, s.PackageName, fmt.Sprintf("%s.go", s.PackageName)) + } + return path.Join(baseDir, fmt.Sprintf("%s.go", s.FileName)) +} + +func getClientInfo(client any, opts *Options) clientInfo { + v := reflect.ValueOf(client) + t := v.Type() + pkgPath := t.Elem().PkgPath() + clientName := t.Elem().Name() + signatures := make([]string, 0) + extraImports := make([]string, 0) + for i := 0; i < t.NumMethod(); i++ { + method := t.Method(i) + if opts.ShouldInclude(method) { + sig := signature(method.Name, v.Method(i).Interface()) + signatures = append(signatures, sig) + } + extraImports = append(extraImports, opts.ExtraImports(method)...) + } + return clientInfo{ + Import: pkgPath, + ClientName: clientName, + Signatures: signatures, + ExtraImports: extraImports, + } +} + func formatAndWriteFile(filePath string, buff bytes.Buffer) error { content := buff.Bytes() formattedContent, err := format.Source(buff.Bytes()) diff --git a/interfaces/options.go b/interfaces/options.go deleted file mode 100644 index ecfc789..0000000 --- a/interfaces/options.go +++ /dev/null @@ -1,46 +0,0 @@ -package interfaces - -import "reflect" - -type Options struct { - // ShouldInclude tests whether a method should be included in the generated interfaces. If it returns true, - // the method will be included. MethodHasPrefix and MethodHasSuffix can be used inside a custom function here - // to customize the behavior. - ShouldInclude func(reflect.Method) bool - - // ExtraImports can add extra imports for a method - ExtraImports func(reflect.Method) []string - - // SinglePackage allows to generate all passed clients into a single package. - // The clients will get their package name as prefix to the interface name (e.g., s3.Client -> S3Client) - SinglePackage string -} - -func (o *Options) SetDefaults() { - if o.ShouldInclude == nil { - o.ShouldInclude = func(reflect.Method) bool { return true } - } - if o.ExtraImports == nil { - o.ExtraImports = func(reflect.Method) []string { return nil } - } -} - -type Option func(*Options) - -func WithIncludeFunc(f func(reflect.Method) bool) Option { - return func(o *Options) { - o.ShouldInclude = f - } -} - -func WithExtraImports(f func(reflect.Method) []string) Option { - return func(o *Options) { - o.ExtraImports = f - } -} - -func WithSinglePackage(name string) Option { - return func(o *Options) { - o.SinglePackage = name - } -} diff --git a/interfaces/template.go b/interfaces/template.go deleted file mode 100644 index b8d4665..0000000 --- a/interfaces/template.go +++ /dev/null @@ -1,68 +0,0 @@ -package interfaces - -import ( - "fmt" - "path" - - "github.com/cloudquery/plugin-sdk/v4/caser" - "golang.org/x/exp/maps" -) - -var ( - csr = caser.New() -) - -type serviceTemplateData struct { - PackageName string - FileName string - Imports []string - Clients []clientTemplateData -} - -func (s serviceTemplateData) getFilePath(baseDir string) string { - if s.FileName == s.PackageName { - return path.Join(baseDir, s.PackageName, fmt.Sprintf("%s.go", s.PackageName)) - } - return path.Join(baseDir, fmt.Sprintf("%s.go", s.FileName)) -} - -func getTemplateDataFromClientInfos(clientInfos []clientInfo, options *Options) []serviceTemplateData { - packageNames := getPackageNames(clientInfos) - services := make([]serviceTemplateData, 0) - serviceMap := make(map[string][]clientInfo) - for i, clientInfo := range clientInfos { - serviceMap[packageNames[i]] = append(serviceMap[packageNames[i]], clientInfo) - } - for packageName, infos := range serviceMap { - imports := make(map[string]bool) - clientsTemplateData := make([]clientTemplateData, 0) - for _, clientInfo := range infos { - imports[clientInfo.Import] = true - for _, extraImport := range clientInfo.ExtraImports { - imports[extraImport] = true - } - clientsTemplateData = append(clientsTemplateData, clientInfo.templateData(len(options.SinglePackage) > 0)) - } - svc := serviceTemplateData{ - PackageName: packageName, - FileName: packageName, - Imports: maps.Keys(imports), - Clients: clientsTemplateData, - } - if len(options.SinglePackage) > 0 { - svc.PackageName = options.SinglePackage - } - services = append(services, svc) - } - return services -} - -type clientTemplateData struct { - packageName string - Name string - Signatures []string -} - -func (c clientTemplateData) ClientName() string { - return csr.ToPascal(c.packageName) + c.Name -} diff --git a/interfaces/templates/service.go.tpl b/interfaces/templates/service.go.tpl index 8d22244..73a2ebd 100644 --- a/interfaces/templates/service.go.tpl +++ b/interfaces/templates/service.go.tpl @@ -8,8 +8,8 @@ import ( ) {{ range .Clients }} -//go:generate mockgen -package=mocks -destination=../mocks/{{$.FileName}}.go -source={{$.FileName}}.go {{.ClientName}} -type {{.ClientName}} interface { +//go:generate mockgen -package=mocks -destination=../mocks/{{$.FileName}}.go -source={{$.FileName}}.go {{.Name}} +type {{.Name}} interface { {{- range $sig := .Signatures }} {{ $sig }} {{- end }}