diff --git a/go.mod b/go.mod index d0db24b..eb774cc 100644 --- a/go.mod +++ b/go.mod @@ -8,19 +8,24 @@ require ( github.com/mailru/mapstructure v0.0.0-20230117153631-a4140f9ccc45 github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.8.4 - golang.org/x/mod v0.7.0 - golang.org/x/net v0.7.0 - golang.org/x/sync v0.1.0 - golang.org/x/sys v0.5.0 - golang.org/x/text v0.7.0 - golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 - golang.org/x/tools v0.5.0 - gopkg.in/yaml.v3 v3.0.1 + github.com/tarantool/go-tarantool v1.6.0 + golang.org/x/mod v0.9.0 + golang.org/x/net v0.15.0 + golang.org/x/sys v0.12.0 + golang.org/x/text v0.13.0 + golang.org/x/time v0.3.0 + golang.org/x/tools v0.7.0 gotest.tools v2.2.0+incompatible ) require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/kr/text v0.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/objx v0.5.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/vmihailenco/msgpack.v2 v2.9.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 4d072af..6a83ddb 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,25 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 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/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/mapstructure v0.0.0-20230117153631-a4140f9ccc45 h1:x3Zw96Gt6HbEPUWsTbQYj/nfaNv5lWHy6CeEkl8gwqw= github.com/mailru/mapstructure v0.0.0-20230117153631-a4140f9ccc45/go.mod h1:guLmlFj8yjd0hoz+QWxRU4Gn+VOb2nOQZ4EqRmMHarw= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -19,22 +32,38 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= -golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE= -golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.5.0 h1:+bSpV5HIeWkuvgaMfI3UmKRThoTA5ODJTUd8T17NO+4= -golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +github.com/tarantool/go-tarantool v1.6.0 h1:D/GW7hw9r8MbvSfcHqr6tT7brO7nqwhtWKJMj6OtArw= +github.com/tarantool/go-tarantool v1.6.0/go.mod h1:SFamRDArn3Be+qwzMzdzQqGz9/D9mgSb3xCpKxbqMjQ= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +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.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +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.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +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= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/vmihailenco/msgpack.v2 v2.9.2 h1:gjPqo9orRVlSAH/065qw3MsFCDpH7fa1KpiizXyllY4= +gopkg.in/vmihailenco/msgpack.v2 v2.9.2/go.mod h1:/3Dn1Npt9+MYyLpYYXjInO/5jvMLamn+AEGwNEOatn8= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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/internal/app/argen.go b/internal/app/argen.go index 479c1ef..2ecfaf2 100644 --- a/internal/app/argen.go +++ b/internal/app/argen.go @@ -209,9 +209,6 @@ func (a *ArGen) saveGenerateResult(name, dst string, genRes []generator.Generate // Процесс генерации пакетов по подготовленным данным func (a *ArGen) generate() error { - metadata := generator.MetaData{ - AppInfo: a.appInfo.String(), - } // Запускаем цикл с проходом по всем полученным файлам для генерации // результирующих пакетов for name, cl := range a.packagesParsed { @@ -230,17 +227,22 @@ func (a *ArGen) generate() error { if err := a.saveGenerateResult(name, a.dst, genRes); err != nil { return fmt.Errorf("error save result: %w", err) } - - metadata.Namespaces = append(metadata.Namespaces, cl) } - genRes, genErr := generator.GenerateMeta(metadata) - if genErr != nil { - return fmt.Errorf("generate meta error: %s", genErr) + metadata, err := a.prepareMetaData() + if err != nil { + return fmt.Errorf("prepare metadata generate error: %s", err) } - if err := a.saveGenerateResult("meta", a.dst, genRes); err != nil { - return fmt.Errorf("error save meta result: %w", err) + if len(metadata.Namespaces) > 0 { + genRes, genErr := generator.GenerateMeta(metadata) + if genErr != nil { + return fmt.Errorf("generate meta error: %s", genErr) + } + + if genErr := a.saveGenerateResult("meta", a.dst, genRes); genErr != nil { + return fmt.Errorf("error save meta result: %w", err) + } } if a.skipGenerateFixture() { @@ -248,12 +250,12 @@ func (a *ArGen) generate() error { } // Генерация пакета со сторами фикстур для тестов - err := a.prepareFixturesStorage() + err = a.prepareFixturesStorage() if err != nil { return fmt.Errorf("prepare fixture store error: %s", err) } - dir, pkg := filepath.Split(a.dstFixture) + fixtureDir, fxtPkg := filepath.Split(a.dstFixture) for name, cl := range a.packagesParsed { // Подготовка информации по ссылкам на другие пакеты @@ -263,16 +265,33 @@ func (a *ArGen) generate() error { } // Процесс генерации - genRes, genErr := generator.GenerateFixture(a.appInfo.String(), *cl, name, pkg) + genRes, genErr := generator.GenerateFixture(a.appInfo.String(), *cl, name, fxtPkg) if genErr != nil { return fmt.Errorf("generate %s fixture store error: %w", name, genErr) } - if err := a.saveGenerateResult(name, dir, genRes); err != nil { + if err := a.saveGenerateResult(name, fixtureDir, genRes); err != nil { return fmt.Errorf("error save generated %s fixture result: %w", name, err) } } + namespaces := map[string][]*ds.RecordPackage{} + + for _, cl := range a.packagesParsed { + for _, backend := range cl.Backends { + namespaces[backend] = append(namespaces[backend], cl) + } + } + + genRes, genErr := generator.GenerateFixtureMeta(namespaces, a.appInfo.String(), fxtPkg) + if genErr != nil { + return fmt.Errorf("generate fixture meta error: %s", genErr) + } + + if err := a.saveGenerateResult("fixture_meta", fixtureDir, genRes); err != nil { + return fmt.Errorf("error save fixture meta result: %w", err) + } + return nil } @@ -517,3 +536,22 @@ func (a *ArGen) getExists() ([]string, error) { return existsFile, nil } + +func (a *ArGen) prepareMetaData() (generator.MetaData, error) { + metadata := generator.MetaData{ + AppInfo: a.appInfo.String(), + } + + for _, cl := range a.packagesParsed { + for _, backend := range cl.Backends { + switch backend { + case "tarantool15": + fallthrough + case "octopus": + metadata.Namespaces = append(metadata.Namespaces, cl) + } + } + } + + return metadata, nil +} diff --git a/internal/pkg/arerror/generator.go b/internal/pkg/arerror/generator.go index fd79327..df10f45 100644 --- a/internal/pkg/arerror/generator.go +++ b/internal/pkg/arerror/generator.go @@ -7,6 +7,7 @@ var ErrGeneratorBackendNotImplemented = errors.New("backend not implemented") var ErrGeneragorGetTmplLine = errors.New("can't get error lines") var ErrGeneragorEmptyTmplLine = errors.New("tmpl lines not set") var ErrGeneragorErrorLineNotFound = errors.New("template lines not found in error") +var ErrGeneratorTemplateUnkhown = errors.New("template unknown") // Описание ошибки генерации type ErrGeneratorPkg struct { diff --git a/internal/pkg/checker/checker.go b/internal/pkg/checker/checker.go index 16ea6b0..eb40c42 100644 --- a/internal/pkg/checker/checker.go +++ b/internal/pkg/checker/checker.go @@ -213,6 +213,14 @@ func Check(files map[string]*ds.RecordPackage, linkedObjects map[string]string) // Бекендозависимые проверки for _, backend := range cl.Backends { switch backend { + case "tarantool16": + fallthrough + case "tarantool2": + if err := checkTarantool(cl); err != nil { + return err + } + case "tarantool15": + fallthrough case "octopus": if err := checkOctopus(cl); err != nil { return err @@ -276,3 +284,38 @@ func checkOctopus(cl *ds.RecordPackage) error { return nil } + +//nolint:gocognit,gocyclo +func checkTarantool(cl *ds.RecordPackage) error { + if cl.Server.Host == "" && cl.Server.Conf == "" { + return &arerror.ErrCheckPackageDecl{Pkg: cl.Namespace.PackageName, Err: arerror.ErrCheckServerEmpty} + } + + if cl.Server.Host == "" && cl.Server.Port != "" { + return &arerror.ErrCheckPackageDecl{Pkg: cl.Namespace.PackageName, Err: arerror.ErrCheckPortEmpty} + } + + if cl.Server.Host != "" && cl.Server.Conf != "" { + return &arerror.ErrCheckPackageDecl{Pkg: cl.Namespace.PackageName, Err: arerror.ErrCheckServerConflict} + } + + for _, fl := range cl.Fields { + if (fl.Format == "string" || fl.Format == "[]byte") && fl.Size == 0 { + log.Printf("Warn: field `%s` declaration. Field with type string or []byte not contain size.", fl.Name) + } + } + + for _, ind := range cl.Indexes { + if len(ind.Fields) == 0 { + return &arerror.ErrCheckPackageIndexDecl{Pkg: cl.Namespace.PackageName, Index: ind.Name, Err: arerror.ErrCheckFieldIndexEmpty} + } + } + + for _, fld := range cl.ProcInFields { + if fld.Format != octopus.String && len(fld.Serializer) == 0 { + return &arerror.ErrCheckPackageFieldDecl{Pkg: cl.Namespace.PackageName, Field: fld.Name, Err: arerror.ErrCheckFieldSerializerNotFound} + } + } + + return nil +} diff --git a/internal/pkg/generator/fixture.go b/internal/pkg/generator/fixture.go index 5b3a2c3..67c2af5 100644 --- a/internal/pkg/generator/fixture.go +++ b/internal/pkg/generator/fixture.go @@ -4,13 +4,10 @@ import ( "bufio" "bytes" _ "embed" - "io" - "strings" - "text/template" "github.com/mailru/activerecord/internal/pkg/arerror" "github.com/mailru/activerecord/internal/pkg/ds" - "github.com/mailru/activerecord/pkg/iproto/util/text" + "golang.org/x/tools/imports" ) type FixturePkgData struct { @@ -30,50 +27,79 @@ type FixturePkgData struct { AppInfo string } -func generateFixture(params FixturePkgData) (map[string]bytes.Buffer, *arerror.ErrGeneratorPhases) { - fixtureWriter := bytes.Buffer{} - - fixtureFile := bufio.NewWriter(&fixtureWriter) +type FixtureMetaData struct { + MetaData + FixturePkg string +} - err := GenerateFixtureTmpl(fixtureFile, params) - if err != nil { - return nil, err - } +//nolint:revive +//go:embed tmpl/fixture_meta.tmpl +var fixtureMetaTmpl string - fixtureFile.Flush() +func generateFixtureMeta(params FixtureMetaData) (*bytes.Buffer, *arerror.ErrGeneratorFile) { + metaWriter := new(bytes.Buffer) + metaFile := bufio.NewWriter(metaWriter) - ret := map[string]bytes.Buffer{ - "fixture": fixtureWriter, + if err := GenerateByTmpl(metaFile, params, "fixture_meta", fixtureMetaTmpl); err != nil { + return nil, &arerror.ErrGeneratorFile{Name: "repository.go", Backend: "fixture_meta", Filename: "repository.go", Err: err} } - return ret, nil + metaFile.Flush() + + return metaWriter, nil } -//go:embed tmpl/octopus/fixturestore.tmpl -var tmpl string +func GenerateFixtureMeta(packageNamespaces map[string][]*ds.RecordPackage, appInfo, pkgFixture string) ([]GenerateFile, error) { + var ret = make([]GenerateFile, 0, len(packageNamespaces)) + + for backend, namespaces := range packageNamespaces { + metaData := FixtureMetaData{ + MetaData: MetaData{ + Namespaces: namespaces, + AppInfo: appInfo, + }, + FixturePkg: pkgFixture, + } + var generated *bytes.Buffer + + switch backend { + case "tarantool15": + fallthrough + case "octopus": + fallthrough + case "tarantool16": + fallthrough + case "tarantool2": + var err *arerror.ErrGeneratorFile + + generated, err = generateFixtureMeta(metaData) + if err != nil { + err.Name = "fixture_meta" + return nil, err + } + case "postgres": + return nil, &arerror.ErrGeneratorFile{Name: "fixture_meta", Backend: backend, Err: arerror.ErrGeneratorBackendNotImplemented} + default: + return nil, &arerror.ErrGeneratorFile{Name: "fixture_meta", Backend: backend, Err: arerror.ErrGeneratorBackendUnknown} + } -func GenerateFixtureTmpl(dstFile io.Writer, params FixturePkgData) *arerror.ErrGeneratorPhases { - templatePackage, err := template.New(TemplateName).Funcs(templateFuncs).Funcs(OctopusTemplateFuncs).Parse(disclaimer + tmpl) - if err != nil { - tmplLines, errgetline := getTmplErrorLine(strings.SplitAfter(disclaimer+tmpl, "\n"), err.Error()) - if errgetline != nil { - tmplLines = errgetline.Error() + genRes := GenerateFile{ + Dir: pkgFixture, + Name: "stores.go", + Backend: "fixture_meta", } - return &arerror.ErrGeneratorPhases{Backend: "fixture", Phase: "parse", TmplLines: tmplLines, Err: err} - } + genData := generated.Bytes() - err = templatePackage.Execute(dstFile, params) - if err != nil { - tmplLines, errgetline := getTmplErrorLine(strings.SplitAfter(disclaimer+tmpl, "\n"), err.Error()) - if errgetline != nil { - tmplLines = errgetline.Error() + var err error + + genRes.Data, err = imports.Process("", genData, nil) + if err != nil { + return nil, &arerror.ErrGeneratorFile{Name: "repository.go", Backend: "fixture_meta", Filename: genRes.Name, Err: ErrorLine(err, string(genData))} } - return &arerror.ErrGeneratorPhases{Backend: "fixture", Phase: "execute", TmplLines: tmplLines, Err: err} + ret = append(ret, genRes) } - return nil + return ret, nil } - -var templateFuncs = template.FuncMap{"snakeCase": text.ToSnakeCase, "split": strings.Split} diff --git a/internal/pkg/generator/fixture_test.go b/internal/pkg/generator/fixture_test.go index 75b5590..3ff9c61 100644 --- a/internal/pkg/generator/fixture_test.go +++ b/internal/pkg/generator/fixture_test.go @@ -222,7 +222,7 @@ func TestGenerateFixture(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ret, got := generateFixture(tt.args.params) + ret, got := GenerateOctopusFixtureStore(tt.args.params) if got != tt.want { t.Errorf("GenerateFixture() = %v, want %v", got, tt.want) } diff --git a/internal/pkg/generator/generator.go b/internal/pkg/generator/generator.go index 37ce6a9..c77585a 100644 --- a/internal/pkg/generator/generator.go +++ b/internal/pkg/generator/generator.go @@ -12,6 +12,7 @@ import ( "strings" "text/template" + "github.com/mailru/activerecord/pkg/iproto/util/text" "github.com/pkg/errors" "golang.org/x/tools/imports" @@ -115,8 +116,13 @@ func GenerateMeta(params MetaData) ([]GenerateFile, *arerror.ErrGeneratorFile) { return []GenerateFile{genRes}, nil } -func GenerateByTmpl(dstFile io.Writer, params any, name, tmpl string) *arerror.ErrGeneratorPhases { - templatePackage, err := template.New(TemplateName).Funcs(funcs).Funcs(OctopusTemplateFuncs).Parse(disclaimer + tmpl) +func GenerateByTmpl(dstFile io.Writer, params any, name, tmpl string, tmplFuncs ...template.FuncMap) *arerror.ErrGeneratorPhases { + template := template.New(TemplateName).Funcs(funcs) + for _, f := range tmplFuncs { + template = template.Funcs(f) + } + + templatePackage, err := template.Parse(disclaimer + tmpl) if err != nil { tmplLines, errgetline := getTmplErrorLine(strings.SplitAfter(disclaimer+tmpl, "\n"), err.Error()) if errgetline != nil { @@ -162,7 +168,18 @@ func Generate(appInfo string, cl ds.RecordPackage, linkObject map[string]ds.Reco case "tarantool16": fallthrough case "tarantool2": - return nil, &arerror.ErrGeneratorFile{Name: cl.Namespace.PublicName, Backend: backend, Err: arerror.ErrGeneratorBackendNotImplemented} + params := NewPkgData(appInfo, cl) + params.LinkedObject = linkObject + + log.Printf("Generate tarantool package (%v)", cl) + + var err *arerror.ErrGeneratorPhases + + generated, err = GenerateTarantool(params) + if err != nil { + err.Name = cl.Namespace.PublicName + return nil, err + } case "postgres": return nil, &arerror.ErrGeneratorFile{Name: cl.Namespace.PublicName, Backend: backend, Err: arerror.ErrGeneratorBackendNotImplemented} default: @@ -224,9 +241,7 @@ func ErrorLine(errIn error, genData string) error { } func GenerateFixture(appInfo string, cl ds.RecordPackage, pkg string, pkgFixture string) ([]GenerateFile, error) { - var generated map[string]bytes.Buffer - - ret := make([]GenerateFile, 0, 1) + var ret []GenerateFile params := FixturePkgData{ FixturePkg: pkgFixture, @@ -247,30 +262,60 @@ func GenerateFixture(appInfo string, cl ds.RecordPackage, pkg string, pkgFixture log.Printf("Generate package (%v)", cl) - var err *arerror.ErrGeneratorPhases + for _, backend := range cl.Backends { + var generated map[string]bytes.Buffer - generated, err = generateFixture(params) - if err != nil { - err.Name = cl.Namespace.PublicName - return nil, err - } + switch backend { + case "tarantool15": + fallthrough + case "octopus": - for _, data := range generated { - genRes := GenerateFile{ - Dir: pkgFixture, - Name: cl.Namespace.PackageName + "_gen.go", - } + var err *arerror.ErrGeneratorPhases + + generated, err = GenerateOctopusFixtureStore(params) + if err != nil { + err.Name = cl.Namespace.PublicName + return nil, err + } + case "tarantool16": + fallthrough + case "tarantool2": - genData := data.Bytes() + var err *arerror.ErrGeneratorPhases - dataImp, err := imports.Process("", genData, nil) - if err != nil { - return nil, &arerror.ErrGeneratorFile{Name: cl.Namespace.PublicName, Backend: "fixture", Filename: genRes.Name, Err: ErrorLine(err, string(genData))} + generated, err = GenerateTarantoolFixtureStore(params) + if err != nil { + err.Name = cl.Namespace.PublicName + return nil, err + } + case "postgres": + return nil, &arerror.ErrGeneratorFile{Name: cl.Namespace.PublicName, Backend: backend, Err: arerror.ErrGeneratorBackendNotImplemented} + default: + return nil, &arerror.ErrGeneratorFile{Name: cl.Namespace.PublicName, Backend: backend, Err: arerror.ErrGeneratorBackendUnknown} } - genRes.Data = dataImp - ret = append(ret, genRes) + for _, data := range generated { + genRes := GenerateFile{ + Dir: pkgFixture, + Name: cl.Namespace.PackageName + "_gen.go", + } + + genData := data.Bytes() + + dataImp, err := imports.Process("", genData, nil) + if err != nil { + return nil, &arerror.ErrGeneratorFile{Name: cl.Namespace.PublicName, Backend: "fixture", Filename: genRes.Name, Err: ErrorLine(err, string(genData))} + } + + genRes.Data = dataImp + ret = append(ret, genRes) + } } return ret, nil } + +var funcs = template.FuncMap{ + "snakeCase": text.ToSnakeCase, + "split": strings.Split, +} diff --git a/internal/pkg/generator/octopus.go b/internal/pkg/generator/octopus.go index ef30e34..f427bd7 100644 --- a/internal/pkg/generator/octopus.go +++ b/internal/pkg/generator/octopus.go @@ -10,7 +10,6 @@ import ( "github.com/mailru/activerecord/internal/pkg/arerror" "github.com/mailru/activerecord/internal/pkg/ds" - "github.com/mailru/activerecord/pkg/iproto/util/text" "github.com/mailru/activerecord/pkg/octopus" "golang.org/x/text/cases" "golang.org/x/text/language" @@ -28,8 +27,6 @@ var OctopusRootRepositoryTmpl string //go:embed tmpl/octopus/fixture.tmpl var OctopusFixtureRepositoryTmpl string -var funcs = template.FuncMap{"snakeCase": text.ToSnakeCase} - func GenerateOctopus(params PkgData) (map[string]bytes.Buffer, *arerror.ErrGeneratorPhases) { octopusWriter := bytes.Buffer{} mockWriter := bytes.Buffer{} @@ -38,7 +35,7 @@ func GenerateOctopus(params PkgData) (map[string]bytes.Buffer, *arerror.ErrGener octopusFile := bufio.NewWriter(&octopusWriter) //TODO возможно имеет смысл разделить большой шаблон OctopusRootRepositoryTmpl для удобства поддержки - err := GenerateByTmpl(octopusFile, params, "octopus", OctopusRootRepositoryTmpl) + err := GenerateByTmpl(octopusFile, params, "octopus", OctopusRootRepositoryTmpl, OctopusTemplateFuncs) if err != nil { return nil, err } @@ -47,7 +44,7 @@ func GenerateOctopus(params PkgData) (map[string]bytes.Buffer, *arerror.ErrGener mockFile := bufio.NewWriter(&mockWriter) - err = GenerateByTmpl(mockFile, params, "octopus", OctopusMockRepositoryTmpl) + err = GenerateByTmpl(mockFile, params, "octopus", OctopusMockRepositoryTmpl, OctopusTemplateFuncs) if err != nil { return nil, err } @@ -56,7 +53,7 @@ func GenerateOctopus(params PkgData) (map[string]bytes.Buffer, *arerror.ErrGener fixtureFile := bufio.NewWriter(&fixtureWriter) - err = GenerateByTmpl(fixtureFile, params, "octopus", OctopusFixtureRepositoryTmpl) + err = GenerateByTmpl(fixtureFile, params, "octopus", OctopusFixtureRepositoryTmpl, OctopusTemplateFuncs) if err != nil { return nil, err } @@ -72,6 +69,43 @@ func GenerateOctopus(params PkgData) (map[string]bytes.Buffer, *arerror.ErrGener return ret, nil } +//go:embed tmpl/octopus/fixturestore.tmpl +var fixtureStoreTmpl string + +func GenerateOctopusFixtureStore(params FixturePkgData) (map[string]bytes.Buffer, *arerror.ErrGeneratorPhases) { + fixtureWriter := bytes.Buffer{} + + file := bufio.NewWriter(&fixtureWriter) + + templatePackage, err := template.New(TemplateName).Funcs(funcs).Funcs(OctopusTemplateFuncs).Parse(disclaimer + fixtureStoreTmpl) + if err != nil { + tmplLines, errgetline := getTmplErrorLine(strings.SplitAfter(disclaimer+fixtureStoreTmpl, "\n"), err.Error()) + if errgetline != nil { + tmplLines = errgetline.Error() + } + + return nil, &arerror.ErrGeneratorPhases{Backend: "fixture", Phase: "parse", TmplLines: tmplLines, Err: err} + } + + err = templatePackage.Execute(file, params) + if err != nil { + tmplLines, errgetline := getTmplErrorLine(strings.SplitAfter(disclaimer+fixtureStoreTmpl, "\n"), err.Error()) + if errgetline != nil { + tmplLines = errgetline.Error() + } + + return nil, &arerror.ErrGeneratorPhases{Backend: "fixture", Phase: "execute", TmplLines: tmplLines, Err: err} + } + + file.Flush() + + ret := map[string]bytes.Buffer{ + "fixture": fixtureWriter, + } + + return ret, nil +} + var OctopusTemplateFuncs = template.FuncMap{ "packerParam": func(format octopus.Format) OctopusFormatParam { ret, ex := OctopusFormatMapper[format] diff --git a/internal/pkg/generator/octopus_b_test.go b/internal/pkg/generator/octopus_b_test.go index a502571..4601b08 100644 --- a/internal/pkg/generator/octopus_b_test.go +++ b/internal/pkg/generator/octopus_b_test.go @@ -151,7 +151,7 @@ func TestGenerateOctopus(t *testing.T) { "fixture": { `type FooFT struct {`, `func MarshalFixtures(objs []*Foo) ([]byte, error) {`, - `func UnmarshalFixtures(source []byte) []*Foo {`, + `func UnmarshalFromYaml(source []byte) []*Foo {`, `func (objs FooList) String() string {`, }, }, @@ -205,7 +205,7 @@ func TestGenerateOctopus(t *testing.T) { `type FooFTPK struct {`, `type FooFT struct {`, `func MarshalFixtures(objs []*Foo) ([]byte, error) {`, - `func UnmarshalFixtures(source []byte) []*Foo {`, + `func UnmarshalFromYaml(source []byte) []*Foo {`, `func (objs FooList) String() string {`, }, }, @@ -289,7 +289,7 @@ func TestGenerateOctopus(t *testing.T) { `type FooFTPK struct {`, `type FooFT struct {`, `func MarshalFixtures(objs []*Foo) ([]byte, error) {`, - `func UnmarshalFixtures(source []byte) []*Foo {`, + `func UnmarshalFromYaml(source []byte) []*Foo {`, `func (objs FooList) String() string {`, }, }, diff --git a/internal/pkg/generator/tarantool.go b/internal/pkg/generator/tarantool.go new file mode 100644 index 0000000..0cba23c --- /dev/null +++ b/internal/pkg/generator/tarantool.go @@ -0,0 +1,160 @@ +package generator + +import ( + "bufio" + "bytes" + _ "embed" + "log" + "strings" + "text/template" + + "github.com/mailru/activerecord/internal/pkg/arerror" + "github.com/mailru/activerecord/internal/pkg/ds" + "github.com/mailru/activerecord/pkg/iproto/util/text" + "github.com/mailru/activerecord/pkg/octopus" +) + +//nolint:revive +//go:embed tmpl/tarantool/main.tmpl +var tarantoolRootRepositoryTmpl string + +//nolint:revive +//go:embed tmpl/tarantool/procedure.tmpl +var tarantoolProcRepositoryTmpl string + +//nolint:revive +//go:embed tmpl/tarantool/fixture.tmpl +var tarantoolFixtureRepositoryTmpl string + +func GenerateTarantool(params PkgData) (map[string]bytes.Buffer, *arerror.ErrGeneratorPhases) { + ret := map[string]bytes.Buffer{} + + mainWriter := bytes.Buffer{} + + var repositoryTmpl string + if len(params.FieldList) > 0 { + repositoryTmpl = tarantoolRootRepositoryTmpl + } else if len(params.ProcOutFieldList) > 0 { + repositoryTmpl = tarantoolProcRepositoryTmpl + } else { + return nil, &arerror.ErrGeneratorPhases{Backend: "tarantool", Err: arerror.ErrGeneratorTemplateUnkhown} + } + + octopusFile := bufio.NewWriter(&mainWriter) + + err := GenerateByTmpl(octopusFile, params, "tarantool", repositoryTmpl, Tarantool2TmplFunc) + if err != nil { + return nil, err + } + + octopusFile.Flush() + + ret["tarantool"] = mainWriter + + if len(params.FieldList) > 0 { + fixtureWriter := bytes.Buffer{} + + fixtureFile := bufio.NewWriter(&fixtureWriter) + + err = GenerateByTmpl(fixtureFile, params, "tarantool", tarantoolFixtureRepositoryTmpl, Tarantool2TmplFunc) + if err != nil { + return nil, err + } + + fixtureFile.Flush() + + ret["fixture"] = fixtureWriter + } + + return ret, nil +} + +//go:embed tmpl/tarantool/fixturestore.tmpl +var tarantoolFixtureStoreTmpl string + +func GenerateTarantoolFixtureStore(params FixturePkgData) (map[string]bytes.Buffer, *arerror.ErrGeneratorPhases) { + fixtureWriter := bytes.Buffer{} + + file := bufio.NewWriter(&fixtureWriter) + + templatePackage, err := template.New(TemplateName).Funcs(funcs).Parse(disclaimer + tarantoolFixtureStoreTmpl) + if err != nil { + tmplLines, errgetline := getTmplErrorLine(strings.SplitAfter(disclaimer+tarantoolFixtureStoreTmpl, "\n"), err.Error()) + if errgetline != nil { + tmplLines = errgetline.Error() + } + + return nil, &arerror.ErrGeneratorPhases{Backend: "fixture", Phase: "parse", TmplLines: tmplLines, Err: err} + } + + err = templatePackage.Execute(file, params) + if err != nil { + tmplLines, errgetline := getTmplErrorLine(strings.SplitAfter(disclaimer+tarantoolFixtureStoreTmpl, "\n"), err.Error()) + if errgetline != nil { + tmplLines = errgetline.Error() + } + + return nil, &arerror.ErrGeneratorPhases{Backend: "fixture", Phase: "execute", TmplLines: tmplLines, Err: err} + } + + file.Flush() + + ret := map[string]bytes.Buffer{ + "fixture": fixtureWriter, + } + + return ret, nil +} + +var Tarantool2TmplFunc = template.FuncMap{ + "packerParam": func(format octopus.Format) TarantoolFormatParam { + ret, ex := PrimitiveTypeFormatConverter[format] + if !ex { + log.Fatalf("packer for type `%s` not found", format) + } + + return ret + }, + "addImport": func(flds []ds.FieldDeclaration) (imports []string) { + var needStrconv bool + + for _, fld := range flds { + if fld.PrimaryKey && fld.Format != "string" { + needStrconv = true + } + } + + if needStrconv { + imports = append(imports, "strconv") + } + + return + }, + "trimPrefix": strings.TrimPrefix, + "hasPrefix": strings.HasPrefix, + "lowerCase": strings.ToLower, + "snakeToCamelCase": text.SnakeToCamelCase, +} + +type TarantoolFormatParam string + +func (p TarantoolFormatParam) ToString() []string { + return strings.SplitN(string(p), `%%`, 2) +} + +var PrimitiveTypeFormatConverter = map[octopus.Format]TarantoolFormatParam{ + octopus.Bool: "strconv.FormatBool(%%)", + octopus.Uint8: "strconv.FormatUint(uint64(%%), 10)", + octopus.Uint16: "strconv.FormatUint(uint64(%%), 10)", + octopus.Uint32: "strconv.FormatUint(uint64(%%), 10)", + octopus.Uint64: "strconv.FormatUint(%%, 10)", + octopus.Uint: "strconv.FormatUint(uint64(%%), 10)", + octopus.Int8: "strconv.FormatInt(int64(%%), 10)", + octopus.Int16: "strconv.FormatInt(int64(%%), 10)", + octopus.Int32: "strconv.FormatInt(int64(%%), 10)", + octopus.Int64: "strconv.FormatInt(%%, 10)", + octopus.Int: "strconv.FormatInt(int64(%%), 10)", + octopus.Float32: "strconv.FormatFloat(%%, 32)", + octopus.Float64: "strconv.FormatFloat(%%, 64)", + octopus.String: "%%", +} diff --git a/internal/pkg/generator/tmpl/fixture_meta.tmpl b/internal/pkg/generator/tmpl/fixture_meta.tmpl new file mode 100644 index 0000000..9c552cb --- /dev/null +++ b/internal/pkg/generator/tmpl/fixture_meta.tmpl @@ -0,0 +1,47 @@ +package {{ .FixturePkg }} + +import ( + "context" + "fmt" +) + +type FixtureMeta struct { + StoreIterator func(it func(any) error) error + Unpacker func(ctx context.Context, source []byte) (res []any, err error) + PrimaryKeyFields []string +} + +// NSFixtures Репозиторий сторов фикстур +type NSFixtures map[string]FixtureMeta + +{{ $nss := .Namespaces }} +var NamespaceFixtures = NSFixtures{ + {{ range $_, $ns := $nss -}} + {{- if $ns.Fields }} + "{{ $ns.Namespace.ObjectName }}": FixtureMeta{ + StoreIterator:{{ $ns.Namespace.PublicName }}StoreIterator(), + Unpacker: func(ctx context.Context, source []byte) (res []any, err error) { + fxts, err := {{ $ns.Namespace.PackageName }}.UnmarshalFixtures(source) + if err != nil { + return nil, fmt.Errorf("can't decode tuple: %s", err) + } + + for _, v := range fxts { + res = append(res, v) + } + + return + }, + PrimaryKeyFields: []string{ + {{- if $ns.Indexes }} + {{- $pk := index $ns.Indexes 0 }} + {{- range $_, $fieldNum := $pk.Fields }} + {{- $ifield := index $ns.Fields $fieldNum }} + "{{$ifield.Name}}", + {{- end }} + {{ end -}} + }, + }, + {{ end }} + {{ end }} +} diff --git a/internal/pkg/generator/tmpl/meta.tmpl b/internal/pkg/generator/tmpl/meta.tmpl index 8e8ce37..2f57a5a 100644 --- a/internal/pkg/generator/tmpl/meta.tmpl +++ b/internal/pkg/generator/tmpl/meta.tmpl @@ -46,7 +46,7 @@ var NamespacePackages = NSPackage { return {{ $ns.Namespace.PackageName }}.MarshalFixtures([]*{{ $ns.Namespace.PackageName }}.{{ $ns.Namespace.PublicName }}{obj}) }, FixtureUnpacker: func(ctx context.Context, source []byte) (res []any, err error) { - fxts, err := {{ $ns.Namespace.PackageName }}.UnmarshalFixturesFromJSON(source) + fxts, err := {{ $ns.Namespace.PackageName }}.UnmarshalFixtures(source) if err != nil { return nil, fmt.Errorf("can't decode tuple: %s", err) } diff --git a/internal/pkg/generator/tmpl/octopus/fixture.tmpl b/internal/pkg/generator/tmpl/octopus/fixture.tmpl index f42cbea..7b5459a 100644 --- a/internal/pkg/generator/tmpl/octopus/fixture.tmpl +++ b/internal/pkg/generator/tmpl/octopus/fixture.tmpl @@ -78,7 +78,7 @@ func MarshalFixtures(objs []*{{$PublicStructName}}) ([]byte, error) { return yaml.Marshal(fts) } -func UnmarshalFixtures(source []byte) []*{{$PublicStructName}} { +func UnmarshalFromYaml(source []byte) []*{{$PublicStructName}} { var fixtures []{{$PublicStructName}}FT if err := yaml.Unmarshal(source, &fixtures); err != nil { @@ -108,7 +108,7 @@ func UnmarshalFixtures(source []byte) []*{{$PublicStructName}} { return objs } -func UnmarshalFixturesFromJSON(source []byte) ([]{{$PublicStructName}}FT, error) { +func UnmarshalFixtures(source []byte) ([]{{$PublicStructName}}FT, error) { source = bytes.TrimLeft(source, " \t\r\n") if len(source) > 0 && source[0] == '{' { @@ -159,7 +159,7 @@ type {{ $PublicStructName }}FT struct { {{- end }} } -func UnmarshalFixturesFromJSON(source []byte) ([]{{$PublicStructName}}FT, error) { +func UnmarshalFixtures(source []byte) ([]{{$PublicStructName}}FT, error) { source = bytes.TrimLeft(source, " \t\r\n") if len(source) > 0 && source[0] == '{' { @@ -187,7 +187,7 @@ func MarshalFixtures(objs []*{{$PublicStructName}}) ([]byte, error) { return yaml.Marshal(fts) } -func UnmarshalFixtures(source []byte) []*{{$PublicStructName}} { +func UnmarshalFromYaml(source []byte) []*{{$PublicStructName}} { var fixtures []{{$PublicStructName}}FT if err := yaml.Unmarshal(source, &fixtures); err != nil { @@ -262,7 +262,7 @@ type {{ $PublicStructName }}UpdateFixtureOptions struct { {{ end }} {{- end }} -func UnmarshalUpdateFixtures(source []byte) []*{{$PublicStructName}} { +func UnmarshalUpdateFromYaml(source []byte) []*{{$PublicStructName}} { var fixtures []{{$PublicStructName}}UpdateFT if err := yaml.Unmarshal(source, &fixtures); err != nil { @@ -295,8 +295,8 @@ func UnmarshalUpdateFixtures(source []byte) []*{{$PublicStructName}} { return objs } -func UnmarshalInsertReplaceFixtures(source []byte) []*{{$PublicStructName}} { - return UnmarshalFixtures(source) +func UnmarshalInsertReplaceFromYaml(source []byte) []*{{$PublicStructName}} { + return UnmarshalFromYaml(source) } func SetFixtureUpdateOptions(obj *{{$PublicStructName}}, updateOptions []{{$PublicStructName}}UpdateFixtureOptions) { @@ -316,6 +316,6 @@ func SetFixtureUpdateOptions(obj *{{$PublicStructName}}, updateOptions []{{$Pub } {{ end }} -func UnmarshalDeleteFixtures(source []byte) []*{{$PublicStructName}} { - return UnmarshalFixtures(source) +func UnmarshalDeleteFromYaml(source []byte) []*{{$PublicStructName}} { + return UnmarshalFromYaml(source) } diff --git a/internal/pkg/generator/tmpl/octopus/fixturestore.tmpl b/internal/pkg/generator/tmpl/octopus/fixturestore.tmpl index 0581d90..be93b06 100644 --- a/internal/pkg/generator/tmpl/octopus/fixturestore.tmpl +++ b/internal/pkg/generator/tmpl/octopus/fixturestore.tmpl @@ -41,7 +41,7 @@ var {{$PackageName}}Source []byte func init{{$PublicStructName}}() { {{$PackageName}}Once.Do(func() { - {{$PackageName}}Fixtures = {{$PackageName}}.UnmarshalFixtures({{$PackageName}}Source) + {{$PackageName}}Fixtures = {{$PackageName}}.UnmarshalFromYaml({{$PackageName}}Source) {{$PackageName}}Store = map[{{$typePK}}]int{} for i, f := range {{$PackageName}}Fixtures { @@ -128,7 +128,7 @@ var {{$PackageName}}{{ $mockOperation }}Source []byte func init{{$mockOperation}}{{$PublicStructName}}() { {{$PackageName}}{{ $mockOperation }}Once.Do(func() { - {{$PackageName}}{{$mockOperation}}Fixtures = {{$PackageName}}.Unmarshal{{$mockOperation}}Fixtures({{$PackageName}}{{ $mockOperation }}Source) + {{$PackageName}}{{$mockOperation}}Fixtures = {{$PackageName}}.Unmarshal{{$mockOperation}}FromYaml({{$PackageName}}{{ $mockOperation }}Source) {{$PackageName}}{{$mockOperation}}Store = map[{{$typePK}}]int{} for i, f := range {{$PackageName}}{{$mockOperation}}Fixtures { @@ -192,9 +192,9 @@ func GetUpdate{{$PublicStructName}}FixtureBy{{ $fieldNamePK }}(ctx context.Conte } func {{$PublicStructName}}StoreIterator() func(it func(any) error) error { - init{{$PublicStructName}}() - return func(it func(e any) error) error { + init{{$PublicStructName}}() + for _, e := range {{$PackageName}}Fixtures { if err := it(e); err != nil { return err diff --git a/internal/pkg/generator/tmpl/tarantool/fixture.tmpl b/internal/pkg/generator/tmpl/tarantool/fixture.tmpl new file mode 100644 index 0000000..2f55f06 --- /dev/null +++ b/internal/pkg/generator/tmpl/tarantool/fixture.tmpl @@ -0,0 +1,101 @@ +package {{ .ARPkg }} + +import ( + "context" + "log" + + "gopkg.in/vmihailenco/msgpack.v2" + "gopkg.in/yaml.v3" +{{- range $ind, $imp := .Imports }} +{{ if ne $imp.ImportName "" }}{{ $imp.ImportName }} {{ end }}"{{ $imp.Path }}" +{{- end }} +{{- range $i, $imp := addImport .FieldList }} + "{{ $imp }}" +{{- end }} +) + +{{ $PublicStructName := .ARPkgTitle -}} +{{ $serializers := .Serializers -}} +{{ $fields := .FieldList }} +{{ $procfields := .ProcOutFieldList }} + + +type {{ $PublicStructName }}FT struct { + {{- range $ind, $fstruct := .FieldList -}} + {{ $rtype := $fstruct.Format -}} + {{ $serlen := len $fstruct.Serializer -}} + {{ if ne $serlen 0 -}} + {{ $sname := index $fstruct.Serializer 0 -}} + {{ $serializer := index $serializers $sname -}} + {{ $rtype = $serializer.Type -}} + {{ end }} + {{ $fstruct.Name }} {{ $rtype -}} `yaml:"{{ $fstruct.Name | snakeCase -}}" mapstructure:"{{ $fstruct.Name | snakeCase -}}" json:"{{ $fstruct.Name | snakeCase -}}"` + {{- end }} +} + +func UnmarshalFixtures(source []byte) ([]{{$PublicStructName}}FT, error) { + var fts []{{$PublicStructName}}FT + if err := msgpack.Unmarshal(source, &fts); err != nil { + return nil, err + } + + return fts, nil +} + +func (obj *{{ $PublicStructName }}FT) DecodeMsgpack(dec *msgpack.Decoder) error { + l, err := dec.DecodeArrayLen() + if err != nil { + return err + } + + if l != {{ len .FieldList }} { + return fmt.Errorf("unexpected count of fields") + } + + {{ range $ind, $fstruct := .FieldList -}} + {{ $rtype := $fstruct.Format -}} + {{ $sname := $fstruct.Serializer.Name -}} + {{ if ne $sname "" -}} + {{ $serializer := index $serializers $sname -}} + {{ $serparams := $fstruct.Serializer.Params }} + var v{{ $fstruct.Name }} {{ $rtype }} + if err := dec.Decode(&v{{ $fstruct.Name }}); err != nil { + return fmt.Errorf("can't decode '{{ $fstruct.Name }}' field: %w", err) + } + + if err := {{ $serializer.ImportName }}.{{ $serializer.Unmarshaler }}({{ $serparams }}v{{ $fstruct.Name }}, &obj.{{ $fstruct.Name }}); err != nil { + return fmt.Errorf("can't unpack '{{ $fstruct.Name }}' field: %w", err) + } + {{- else }} + if err := dec.Decode(&obj.{{ $fstruct.Name }}); err != nil { + return fmt.Errorf("can't decode field '{{ $fstruct.Name }}': %w", err) + } + {{ end }} + {{ end }} + + return nil +} + +func UnmarshalFromYaml(source []byte) []*{{$PublicStructName}} { + var fixtures []{{$PublicStructName}}FT + + if err := yaml.Unmarshal(source, &fixtures); err != nil { + log.Fatalf("unmarshal {{$PublicStructName}}FT fixture: %v", err) + } + + objs := make([]*{{$PublicStructName}}, 0, len(fixtures)) + + for _, ft := range fixtures { + + o := New(context.Background()) + {{- range $ind, $fstruct := .FieldList }} + if err := o.Set{{$fstruct.Name}}(ft.{{$fstruct.Name}}); err != nil { + log.Fatalf("can't set value %v to field {{$fstruct.Name}} of {{$PublicStructName}} fixture: %s", ft.{{$fstruct.Name}}, err) + } + {{- end }} + + objs = append(objs, o) + } + + return objs +} \ No newline at end of file diff --git a/internal/pkg/generator/tmpl/tarantool/fixturestore.tmpl b/internal/pkg/generator/tmpl/tarantool/fixturestore.tmpl new file mode 100644 index 0000000..9ad44e0 --- /dev/null +++ b/internal/pkg/generator/tmpl/tarantool/fixturestore.tmpl @@ -0,0 +1,91 @@ +package {{ .FixturePkg }} + +{{ $fields := .FieldList }} + +{{ if $fields }} + +import ( + _ "embed" + "context" + "fmt" + "log" + "sync" + + "gopkg.in/yaml.v3" + + "github.com/mailru/activerecord/pkg/activerecord" + +{{- range $ind, $imp := .Imports }} +{{ if ne $imp.ImportName "" }}{{- $imp.ImportName }} {{ end }}"{{ $imp.Path }}" +{{- end }} +) + +{{ $serializers := .Serializers -}} +{{ $PackageName := .ARPkg -}} +{{ $PublicStructName := .ARPkgTitle -}} +{{ $typePK := "" -}} +{{ $fieldNamePK := "" -}} + +{{ range $num, $ind := .Indexes -}} +{{ $lenfld := len $ind.Fields -}} + {{ if $ind.Primary }} + {{ if ne $lenfld 1 }} + {{ $typePK = print $PackageName "." $ind.Type }} + {{ else }} + {{- $typePK = $ind.Type -}} + {{ end }} + {{- $fieldNamePK = $ind.Name -}} + {{ end }} +{{ end }} + +var {{$PackageName}}Once sync.Once +var {{$PackageName}}Store map[{{$typePK}}]int +var {{$PackageName}}Fixtures []*{{$PackageName}}.{{$PublicStructName}} + +//go:embed data/{{$PackageName}}.yaml +var {{$PackageName}}Source []byte + +func init{{$PublicStructName}}() { + {{$PackageName}}Once.Do(func() { + {{$PackageName}}Fixtures = {{$PackageName}}.UnmarshalFromYaml({{$PackageName}}Source) + + {{$PackageName}}Store = map[{{$typePK}}]int{} + for i, f := range {{$PackageName}}Fixtures { + if _, ok := {{$PackageName}}Store[f.Primary()]; ok { + log.Fatalf("{{$PackageName}} fixture with {{$fieldNamePK}} %v is duplicated", f.Primary()) + } + + {{$PackageName}}Store[f.Primary()] = i + } + }) +} + + +func Get{{$PublicStructName}}By{{$fieldNamePK}}({{$fieldNamePK}} {{$typePK}}) *{{$PackageName}}.{{$PublicStructName}} { + init{{$PublicStructName}}() + + idx, ex := {{$PackageName}}Store[{{$fieldNamePK}}] + if !ex { + log.Fatalf("{{$PublicStructName}} fixture with {{$fieldNamePK}} %v not found", {{$fieldNamePK}}) + } + + res := {{$PackageName}}Fixtures[idx] + + return res +} + +func {{$PublicStructName}}StoreIterator() func(it func(any) error) error { + return func(it func(e any) error) error { + init{{$PublicStructName}}() + + for _, e := range {{$PackageName}}Fixtures { + if err := it(e); err != nil { + return err + } + } + + return nil + } +} + +{{ end }} \ No newline at end of file diff --git a/internal/pkg/generator/tmpl/tarantool/main.tmpl b/internal/pkg/generator/tmpl/tarantool/main.tmpl new file mode 100644 index 0000000..a1da49a --- /dev/null +++ b/internal/pkg/generator/tmpl/tarantool/main.tmpl @@ -0,0 +1,890 @@ +package {{ .ARPkg }} + +import ( + "bytes" + "context" + "fmt" + "log" +{{ if eq .Server.Conf "" -}} + "time" +{{ end }} + "strings" + + "github.com/mailru/activerecord/pkg/activerecord" + "github.com/mailru/activerecord/pkg/tarantool" + tarantool2 "github.com/tarantool/go-tarantool" + "gopkg.in/vmihailenco/msgpack.v2" + +{{- range $ind, $imp := .Imports }} +{{ if ne $imp.ImportName "" }}{{ $imp.ImportName }} {{ end }}"{{ $imp.Path }}" +{{- end }} +{{- range $i, $imp := addImport .FieldList }} + "{{ $imp }}" +{{- end }} +) + +{{ $pkgName := .ARPkg }} +{{ $serializers := .Serializers -}} +{{ $mutators := .Mutators -}} +{{ $PublicStructName := .ARPkgTitle -}} +{{ $LinkedObject := .LinkedObject }} +{{ $flags := .Flags }} +{{ $fields := .FieldList }} +{{ $procfields := .ProcOutFieldList }} +{{ $procInLen := len .ProcInFieldList }} +{{ $mutatorLen := len .Mutators }} + +{{ if $fields }} + +type {{ $PublicStructName }} struct { +tarantool.BaseField +{{- range $ind, $fstruct := .FieldList -}} +{{ $rtype := $fstruct.Format -}} +{{ $serlen := len $fstruct.Serializer -}} +{{ if ne $serlen 0 -}} +{{ $sname := index $fstruct.Serializer 0 -}} +{{ $serializer := index $serializers $sname -}} +{{ $rtype = $serializer.Type -}} +{{ end }} +field{{ $fstruct.Name }} {{ $rtype -}} +{{ end }} +} + +const ( + namespace = "{{ .Container.ObjectName }}" + cntFields uint32 = {{ len .FieldList }} +{{- range $fieldname, $flag := .Flags -}} +{{ range $i, $flagname := $flag.Flags }} +{{ $fieldname }}{{ $flagname }}Flag = 1 << {{ $i -}} +{{ end -}} +{{ end }} +) + +{{ if eq .Server.Conf "" -}} +var boxOption, _ = tarantool.NewOptions( + "{{ .Server.Host }}:{{ .Server.Port }}", + activerecord.ModeMaster, + tarantool.WithTimeout(time.Millisecond * {{ .Server.Timeout }}), +) + +var optionCreator = func(sic activerecord.ShardInstanceConfig) (activerecord.OptionInterface, error) { + return boxOption, nil +} + +var clusterInfo = activerecord.NewClusterInfo( + activerecord.WithShard([]activerecord.OptionInterface{boxOption}, []activerecord.OptionInterface{}), +) +{{ end }} +var cfgName = "{{.Server.Conf }}" + +func New(ctx context.Context) *{{ $PublicStructName }} { + newObj := {{ $PublicStructName }}{} + {{- if $fields }} + newObj.BaseField.UpdateOps = []tarantool2.Op{} + newObj.BaseField.Objects = map[string][]tarantool.ModelStruct{} + {{ end }} + return &newObj +} + +func Create( + ctx context.Context, +{{- range $ind, $fstruct := .FieldList -}} +{{ $rtype := $fstruct.Format -}} +{{ $serlen := len $fstruct.Serializer -}} +{{ if ne $serlen 0 -}} +{{ $sname := index $fstruct.Serializer 0 -}} +{{ $serializer := index $serializers $sname -}} +{{ $rtype = $serializer.Type -}} +{{ end }} + v{{ $fstruct.Name }} {{ $rtype -}}, +{{- end -}} +) (*{{ $PublicStructName }}, error) { + obj := New(ctx) + + {{ range $ind, $fstruct := .FieldList }} + if err := obj.Set{{$fstruct.Name}}(v{{$fstruct.Name}}); err != nil { + return nil, fmt.Errorf("can't create new {{ $PublicStructName }}: %w", err) + } + {{ end }} + + return obj, nil +} + +func (obj *{{ $PublicStructName }}) packTuple() (any, error) { + tuple := make([]any, 0, cntFields) + + var ( + v any + err error + ) + + {{- range $ind, $fstruct := .FieldList -}} + {{ $rtype := $fstruct.Format -}} + {{ $serlen := len $fstruct.Serializer -}} + {{ if ne $serlen 0 }} + v, err = Marshal{{ $fstruct.Name }}(obj.field{{ $fstruct.Name }}) + if err != nil { + return nil, fmt.Errorf("can't pack tuple field '{{ $fstruct.Name }}': %w", err) + } + {{ else }} + v = obj.field{{ $fstruct.Name }} + {{ end }} + + tuple = append(tuple, v) + {{ end }} + + return tuple, err +} + + +func (obj *{{ $PublicStructName }}) DecodeMsgpack(dec *msgpack.Decoder) error { + l, err := dec.DecodeArrayLen() + if err != nil { + return err + } + + if l != int(cntFields) { + return fmt.Errorf("unexpected count of fields") + } + + {{ range $ind, $fstruct := .FieldList -}} + {{ $rtype := $fstruct.Format -}} + {{ $sname := $fstruct.Serializer.Name -}} + {{ if ne $sname "" -}} + {{ $serializer := index $serializers $sname -}} + {{ $serparams := $fstruct.Serializer.Params }} + var v{{ $fstruct.Name }} {{ $rtype }} + if err := dec.Decode(&v{{ $fstruct.Name }}); err != nil { + return fmt.Errorf("can't decode '{{ $fstruct.Name }}' field: %w", err) + } + + if err := {{ $serializer.ImportName }}.{{ $serializer.Unmarshaler }}({{ $serparams }}v{{ $fstruct.Name }}, &obj.field{{ $fstruct.Name }}); err != nil { + return fmt.Errorf("can't unpack '{{ $fstruct.Name }}' field: %w", err) + } + {{- else }} + if err := dec.Decode(&obj.field{{ $fstruct.Name }}); err != nil { + return fmt.Errorf("can't decode field '{{ $fstruct.Name }}': %w", err) + } + {{ end }} + {{ end }} + + return nil +} + +func (obj *{{ $PublicStructName }}) PrimaryString() string { + ret := []string{ + {{- range $ind, $fstruct := .FieldList }} + {{- if $fstruct.PrimaryKey }} + {{- $packerparam := packerParam $fstruct.Format }} + {{- $tostr := $packerparam.ToString }} + {{ index $tostr 0 }}obj.Get{{ $fstruct.Name }}(){{ index $tostr 1 }}, + {{- end }} + {{- end }} + } + + return strings.Join(ret, ", ") +} + + +{{ $pktype := "" }} +{{ $pklenfld := 1 }} +{{ $pkind := index .Indexes 0 }} +{{ range $num, $ind := .Indexes -}} +{{ $lenfld := len $ind.Fields -}} +{{ if $ind.Primary }} +{{ $pktype = $ind.Type }} +{{ $pklenfld = len $ind.Fields }} +{{ $pkind = $ind }} +func (obj *{{ $PublicStructName }}) Primary() {{ $ind.Type }} { + {{ if ne $lenfld 1 }} + return {{ $ind.Type }}{ + {{- range $_, $fieldNum := $ind.Fields }} + {{- $ifield := index $fields $fieldNum }} + {{ $ifield.Name }}: obj.Get{{ $ifield.Name }}(), + {{- end }} + } + {{ else }} + {{- range $_, $fieldNum := $ind.Fields }} + {{- $ifield := index $fields $fieldNum }} + return obj.Get{{ $ifield.Name }}() + {{- end }} + {{ end -}} +} + +func (obj *{{ $PublicStructName }}) packPrimary() []any { + return []any{ + {{- range $_, $fieldNum := $ind.Fields }} + {{- $ifield := index $fields $fieldNum }} + {{ $rtype := $ifield.Format -}} + obj.Get{{ $ifield.Name }}(), + {{- end }} + } +} + +func SelectByPrimary(ctx context.Context, pk {{ $ind.Type }}) (*{{ $PublicStructName }}, error) { + return {{ $ind.Selector }}(ctx, pk) +} + +{{ end }} +{{ end }} + + + +{{end}} + + +// Induces + +{{ range $num, $ind := .Indexes -}} +{{ $lenfld := len $ind.Fields -}} +{{ if ne $lenfld 1 }} +type {{ $ind.Type }} struct { +{{- range $_, $fieldNum := $ind.Fields }} +{{- $ifield := index $fields $fieldNum }} +{{ $rtype := $ifield.Format -}} +{{ $serlen := len $ifield.Serializer -}} +{{ if ne $serlen 0 -}} +{{ $sname := index $ifield.Serializer 0 -}} +{{ $serializer := index $serializers $sname -}} +{{ $rtype = $serializer.Type -}} +{{ end }} +{{ $ifield.Name }} {{ $rtype -}} +{{- end }} +} + +type {{ $ind.Type }}s []{{ $ind.Type }} + +func (idx {{ $ind.Type }}) pack() []any { + return []any{ + {{- range $_, $fieldNum := $ind.Fields }} + {{- $ifield := index $fields $fieldNum }} + {{ $rtype := $ifield.Format -}} + idx.{{ $ifield.Name }}, + {{- end }} + } +} + +// возвращает предикат для sql запроса формата IN (?,..) OR IN (?,..) и список параметров для него +func (idxs {{ $ind.Type }}s) buildSQLPredicateIN() (string, []any) { + args := make([]any, 0, len(idxs)) + + var ( + buf strings.Builder + predicate string + ) + + {{- range $_, $fieldNum := $ind.Fields }} + {{- $ifield := index $fields $fieldNum }} + {{ $rtype := $ifield.Format -}} + predicate = tarantool.BuildSQLPredicateIN("{{ $ifield.Name | lowerCase -}}", len(idxs)) + buf.WriteString(predicate) + + for _, idx := range idxs { + args = append(args, idx.{{ $ifield.Name }}) + } + + buf.WriteString(" AND ") + {{ end }} + + buf.WriteString(" 1 = 1 ") + + return buf.String(), args +} +{{- else -}} +{{ $ifld := index $ind.Fields 0 }} +{{ $ifield := index $fields $ifld }} +type {{ $ind.Name }}s []{{ $ind.Type }} + +func (idxs {{ $ind.Name }}s) buildSQLPredicateIN() (string, []any) { + args := make([]any, 0, len(idxs)) + for _, key := range idxs { + args = append(args, key) + } + + return tarantool.BuildSQLPredicateIN("{{ $ifield.Name | lowerCase -}}", len(idxs)), args +} +{{ end }} + +func {{ $ind.Selector }}s(ctx context.Context, keys []{{ $ind.Type }}{{ if not $ind.Unique }}, limiter activerecord.SelectorLimiter{{ end }}) ([]*{{ $PublicStructName }}, error) { + ctx = activerecord.Logger().SetLoggerValueToContext(ctx, map[string]interface{}{"{{ $ind.Selector }}s": keys, "Repo": "{{ $PublicStructName }}"}) + + inPredicate, args := {{ if ne $lenfld 1 -}} {{ $ind.Type }} {{- else }} {{ $ind.Name }} {{- end }}s(keys).buildSQLPredicateIN() + + return executeSQL(ctx, "SELECT * FROM \"" + namespace + "\" WHERE " + inPredicate, args) +} + +func {{ $ind.Selector }}(ctx context.Context, key {{ $ind.Type }}{{ if not $ind.Unique }}, limiter activerecord.SelectorLimiter{{ end }}) ({{ if $ind.Unique }}{{ else }}[]{{ end }}*{{ $PublicStructName }}, error) { + ctx = activerecord.Logger().SetLoggerValueToContext(ctx, map[string]interface{}{"{{ $ind.Selector }}": key, "Repo": "{{ $PublicStructName }}"}) + + {{ if $ind.Unique }} + limiter := activerecord.EmptyLimiter() + {{ end }} + selected, err := selectBox(ctx, {{$ind.Num}}, {{ if ne $lenfld 1 -}} key.pack() {{- else }} []any{key} {{- end }}, tarantool2.IterEq, limiter) + if err != nil { + return nil, err + } + + {{ if $ind.Unique -}} + if len(selected) > 0 { + if len(selected) > 1 { + activerecord.Logger().Error(ctx, "{{ $PublicStructName }}", "More than one tuple for uniq key ID '%s': %d", key, len(selected)) + } + + return selected[0], nil + } + + return nil, nil + {{- else }} + + return selected, nil + {{- end }} +} + + +{{ end }} +// End Induces + + +// linked objects +{{ range $name, $fobj := .FieldObject -}} +{{ $linkedobj := index $LinkedObject $fobj.ObjectName }} +func (obj *{{ $PublicStructName }}) Get{{ $name }}(ctx context.Context) ({{ if not $fobj.Unique }}[]{{ end }}*{{ $linkedobj.Namespace.PackageName }}.{{ $linkedobj.Namespace.PublicName }}, error){ + {{- if $fobj.Unique }} + if ret, ok := obj.BaseField.Objects["{{ $name }}"]; ok && len(ret) == 1 { + return ret[0].(*{{ $linkedobj.Namespace.PackageName }}.{{ $linkedobj.Namespace.PublicName }}), nil + } + + ret, err := {{ $linkedobj.Namespace.PackageName }}.SelectBy{{ $fobj.Key }}(ctx, obj.Get{{ $fobj.Field }}()) + if err != nil { + return nil, err + } + + obj.BaseField.Objects["{{ $name }}"] = []tarantool.ModelStruct{ret} + {{- else }} + + var ret []*{{ $linkedobj.Namespace.PackageName }}.{{ $linkedobj.Namespace.PublicName }} + + if retI, ok := obj.BaseField.Objects["{{ $name }}"]; ok && len(retI) > 0 { + for _, ri := range retI { + ret = append(ret, ri.(*{{ $linkedobj.Namespace.PackageName }}.{{ $linkedobj.Namespace.PublicName }})) + } + + return ret, nil + } + + ret, err := {{ $linkedobj.Namespace.PackageName }}.SelectBy{{ $fobj.Key }}(ctx, obj.Get{{ $fobj.Field }}(), activerecord.NewLimiter(100)) //ToDo default limit for multi object + if err != nil { + return nil, err + } + + if len(ret) == 100 { + activerecord.Logger().Warn(ctx, "limit for multiple linked object riched '{{ $linkedobj.Namespace.PackageName }}.{{ $linkedobj.Namespace.PublicName }}' '{{ $PublicStructName }}'") + } + + for _, r := range ret { + obj.BaseField.Objects["{{ $name }}"] = append(obj.BaseField.Objects["{{ $name }}"], r) + } + {{- end }} + + return ret, nil +} + +{{ end -}} +// End linked objects + + +// Getters,Setters and Mutators + +{{ range $ind, $fstruct := .FieldList -}} +{{ $rtype := $fstruct.Format -}} +{{ $sname := $fstruct.Serializer.Name -}} + {{ if ne $sname "" -}} + {{ $serializer := index $serializers $sname -}} + {{ $rtype = $serializer.Type -}} + func Marshal{{ $fstruct.Name }}({{ $fstruct.Name }} {{ $rtype }}) (any, error) { + {{ $serparams := $fstruct.Serializer.Params -}} + pvar, err := {{ $serializer.ImportName }}.{{ $serializer.Marshaler }}({{ $serparams }}{{ $fstruct.Name }}) + if err != nil { + return nil, fmt.Errorf("error marshal field {{ $fstruct.Name }}: %w", err) + } + + return pvar, nil + } + + {{ end -}} + +func (obj *{{ $PublicStructName }}) Get{{ $fstruct.Name }}() {{ $rtype }} { + return obj.field{{ $fstruct.Name }} +} + +func (obj *{{ $PublicStructName }}) Set{{ $fstruct.Name }}({{ $fstruct.Name }} {{ $rtype }}) error { + {{- if $fstruct.PrimaryKey }} + if obj.BaseField.Exists { + return fmt.Errorf("can't modify field included in primary key") + } + + {{ end -}} + + {{ if ne $sname "" -}} + {{ $serializer := index $serializers $sname -}} + {{ $serparams := $fstruct.Serializer.Params -}} + data, err := {{ $serializer.ImportName }}.{{ $serializer.Marshaler }}({{ $serparams }}{{ $fstruct.Name }}) + if err != nil { + return fmt.Errorf("error marshal field {{ $fstruct.Name }}: %w", err) + } + {{- else }} + data := {{ $fstruct.Name }} + {{ end }} + + {{- if eq $fstruct.Format "string" "[]byte" -}} + {{- if gt $fstruct.Size 0 }} + + if len(data) > {{ $fstruct.Size }} { + return fmt.Errorf("max length of field '{{ $PublicStructName }}.{{ $fstruct.Name }}' is '%d' (received '%d')", {{ $fstruct.Size }}, len(data)) + } + {{- else }} + + logger := activerecord.Logger() + + logger.Warn(context.TODO(), "{{ $PublicStructName }}", obj.PrimaryString(), fmt.Sprintf("Size for field '{{ $fstruct.Name }}' not set. Cur field size: %d. Object: '{{ $PublicStructName }}'", len(data))) + {{- end }} + {{- end }} + + obj.BaseField.UpdateOps = append(obj.BaseField.UpdateOps, tarantool2.Op{"=", {{ $ind }}, data }) + obj.field{{ $fstruct.Name }} = {{ $fstruct.Name}} + + {{- if ne $fstruct.ObjectLink "" }} + delete(obj.BaseField.Objects, "{{ $fstruct.ObjectLink }}") + {{- end }} + + return nil +} + +{{- range $i, $mut := $fstruct.Mutators -}} +{{ $customMutator := index $mutators $mut -}} + {{- if $customMutator.Name }} + + {{ range $i, $f := $customMutator.PartialFields }} +// Set{{ $customMutator.Name }}{{ $f.Name }} is a stub for backport compatibility +func (obj *{{ $PublicStructName }}) Set{{ $customMutator.Name }}{{ $f.Name }}({{ $f.Name }} {{ $f.Type }}) error { + pVar := obj.Get{{ $fstruct.Name }}() + {{ $underlyingType := trimPrefix (printf "%s" $rtype) "*"}} + {{- $isPointer := hasPrefix (printf "%s" $rtype) "*" -}} + {{- if $isPointer -}} + if pVar == nil { + pVar = new({{$underlyingType}}) + } + {{ end}} + + pVar.{{ $f.Name }} = {{ $f.Name }} + + return obj.Set{{ $fstruct.Name }}(pVar) +} + {{ end }} + {{ else }} + {{ $mtype := $fstruct.Format }} +func (obj *{{ $PublicStructName }}) {{ $mut | snakeToCamelCase }}{{ $fstruct.Name }}(mutArg {{ $mtype }}) error { + {{- if eq $mut "inc" }} + if mutArg == 0 { + return nil + } + + obj.BaseField.UpdateOps = append(obj.BaseField.UpdateOps, tarantool2.Op{Field: {{ $ind }}, Op: "+", Arg: mutArg}) + obj.field{{ $fstruct.Name }} += mutArg + {{- else if eq $mut "dec" }} + if mutArg == 0 { + return nil + } + + obj.BaseField.UpdateOps = append(obj.BaseField.UpdateOps, tarantool2.Op{Field: {{ $ind }}, Op: "-", Arg: mutArg}) + obj.field{{ $fstruct.Name }} -= mutArg + {{- else if eq $mut "and" }} + if obj.field{{ $fstruct.Name }} == 0 || obj.field{{ $fstruct.Name }} & mutArg == obj.field{{ $fstruct.Name }} { + return nil + } + + obj.BaseField.UpdateOps = append(obj.BaseField.UpdateOps, tarantool2.Op{Field: {{ $ind }}, Op: "&", Arg: mutArg}) + obj.field{{ $fstruct.Name }} &= mutArg + {{- else if eq $mut "or" "set_bit" }} + if mutArg == 0 || obj.field{{ $fstruct.Name }} | mutArg == obj.field{{ $fstruct.Name }} { + return nil + } + + obj.BaseField.UpdateOps = append(obj.BaseField.UpdateOps, tarantool2.Op{Field: {{ $ind }}, Op: "|", Arg: mutArg}) + obj.field{{ $fstruct.Name }} |= mutArg + {{- else if eq $mut "clear_bit" }} + if mutArg == 0 || obj.field{{ $fstruct.Name }} & ^mutArg == obj.field{{ $fstruct.Name }} { + return nil + } + + mutArg &= ^mutArg + + obj.BaseField.UpdateOps = append(obj.BaseField.UpdateOps, tarantool2.Op{Field: {{ $ind }}, Op: "=", Arg: mutArg}) + obj.field{{ $fstruct.Name }} = mutArg + {{- else if eq $mut "xor" }} + if mutArg == 0 || obj.field{{ $fstruct.Name }} ^ mutArg == obj.field{{ $fstruct.Name }} { + return nil + } + + obj.BaseField.UpdateOps = append(obj.BaseField.UpdateOps, tarantool2.Op{Field: {{ $ind }}, Op: "^", Arg: mutArg}) + obj.field{{ $fstruct.Name }} ^= mutArg + {{- else }} + Unknown mutator type in template!!! + {{- end }} + + return nil +} + {{- end }} +{{ end}} + +{{- $fl := index $flags $fstruct.Name }} +{{- if $fl }} +{{- range $i, $flag := $fl.Flags }} + +func (obj *{{ $PublicStructName }}) Set{{ $fstruct.Name }}{{ $flag }}() error { + return obj.SetBit{{ $fstruct.Name }}( {{ $fstruct.Name }}{{ $flag }}Flag ) +} + +func (obj *{{ $PublicStructName }}) Clear{{ $fstruct.Name }}{{ $flag }}() error { + return obj.ClearBit{{ $fstruct.Name }}( {{ $fstruct.Name }}{{ $flag }}Flag ) +} + +func (obj *{{ $PublicStructName }}) Is{{ $fstruct.Name }}{{ $flag }}() bool { + return obj.Get{{ $fstruct.Name }}() & {{ $fstruct.Name }}{{ $flag }}Flag == {{ $fstruct.Name }}{{ $flag }}Flag +} +{{- end }} +{{- end }} + +{{ end -}} + +// End of Getters, Setters and Mutators + +func SelectAll(ctx context.Context, limiter activerecord.SelectorLimiter) ([]*{{ $PublicStructName }}, error) { + ctx = activerecord.Logger().SetLoggerValueToContext(ctx, map[string]interface{}{"SelectAll": "", "Repo": "{{ $PublicStructName }}"}) + + res, err := selectBox(ctx, 0, []any{}, tarantool2.IterAll, limiter) + if err != nil { + return res, err + } + + return res, nil +} + +func (obj *{{ $PublicStructName }}) Delete(ctx context.Context) error { + logger := activerecord.Logger() + metricTimer := activerecord.Metric().Timer("tarantool", "{{ $PublicStructName }}") + metricStatCnt := activerecord.Metric().StatCount("tarantool", "{{ $PublicStructName }}") + metricErrCnt := activerecord.Metric().ErrorCount("tarantool", "{{ $PublicStructName }}") + + metricStatCnt.Inc(ctx, "delete_request", 1) + + if !obj.BaseField.Exists { + return fmt.Errorf("can't delete not exists object") + } + + connection, err := tarantool.Box(ctx, 0, activerecord.MasterInstanceType, cfgName, {{ if eq .Server.Conf "" -}}optionCreator{{ else }}nil{{ end -}}) + if err != nil { + metricErrCnt.Inc(ctx, "delete_preparebox", 1) + logger.Error(ctx, "PromoBunches", obj.PrimaryString(), fmt.Sprintf("Error get box '%s'", err)) + + return err + } + + _, errCall := connection.Delete(namespace, 0, obj.packPrimary()) + if errCall != nil { + metricErrCnt.Inc(ctx, "delete_box", 1) + logger.Error(ctx, "{{ $PublicStructName }}", obj.PrimaryString(), "Error delete from box", errCall, connection.Info()) + + return errCall + } + + metricStatCnt.Inc(ctx, "delete_success", 1) + + obj.BaseField.Exists = false + obj.BaseField.UpdateOps = []tarantool2.Op{} + + logger.Debug(ctx, "{{ $PublicStructName }}", obj.PrimaryString(), "Success delete") + + metricTimer.Finish(ctx, "delete") + + return nil +} + +func (obj *{{ $PublicStructName }}) Update(ctx context.Context) error { + logger := activerecord.Logger() + metricTimer := activerecord.Metric().Timer("tarantool", "{{ $PublicStructName }}") + metricStatCnt := activerecord.Metric().StatCount("tarantool", "{{ $PublicStructName }}") + metricErrCnt := activerecord.Metric().ErrorCount("tarantool", "{{ $PublicStructName }}") + + metricStatCnt.Inc(ctx, "update_request", 1) + + if !obj.BaseField.Exists { + metricErrCnt.Inc(ctx, "update_notexists", 1) + return fmt.Errorf("can't update not exists object") + } + + /* if obj.BaseField.Repaired { + metricStatCnt.Inc(ctx, "update_repaired", 1) + logger.Debug(ctx, "", obj.PrimaryString(), "Flag 'Repaired' is true! Insert instead Update") + + return obj.Replace(ctx) + }*/ + + connection, err := tarantool.Box(ctx, 0, activerecord.MasterInstanceType, cfgName, {{ if eq .Server.Conf "" -}}optionCreator{{ else }}nil{{ end -}}) + if err != nil { + metricErrCnt.Inc(ctx, "update_preparebox", 1) + logger.Error(ctx, "{{ $PublicStructName }}", obj.PrimaryString(), fmt.Sprintf("Error get box '%s'", err)) + return err + } + + if len(obj.BaseField.UpdateOps) == 0 { + metricStatCnt.Inc(ctx, "update_empty", 1) + logger.Debug(ctx, "", obj.PrimaryString(), "Empty update") + + return nil + } + + _, errCall := connection.Update(namespace, 0, obj.packPrimary(), obj.UpdateOps) + if errCall != nil { + metricErrCnt.Inc(ctx, "update_box", 1) + logger.Error(ctx, "{{ $PublicStructName }}", obj.PrimaryString(), "Error update ia a box", errCall, connection.Info()) + return errCall + } + + metricTimer.Timing(ctx, "update_box") + + obj.BaseField.UpdateOps = []tarantool2.Op{} + + logger.Debug(ctx, "{{ $PublicStructName }}", obj.PrimaryString(), "Success update") + + metricStatCnt.Inc(ctx, "update_success", 1) + metricTimer.Finish(ctx, "update") + + return nil +} + +func (obj *{{ $PublicStructName }}) Insert(ctx context.Context) error { + logger := activerecord.Logger() + metricStatCnt := activerecord.Metric().StatCount("tarantool", "{{ $PublicStructName }}") + metricErrCnt := activerecord.Metric().ErrorCount("tarantool", "{{ $PublicStructName }}") + + metricStatCnt.Inc(ctx, "insert_request", 1) + + if obj.BaseField.Exists { + metricErrCnt.Inc(ctx, "insert_exists", 1) + return fmt.Errorf("can't insert already exists object") + } + + connection, err := tarantool.Box(ctx, 0, activerecord.MasterInstanceType, cfgName, {{ if eq .Server.Conf "" -}}optionCreator{{ else }}nil{{ end -}}) + if err != nil { + metricErrCnt.Inc(ctx, "insertreplace_preparebox", 1) + logger.Error(ctx, "{{ $PublicStructName }}", obj.PrimaryString(), fmt.Sprintf("Error get box '%s'", err)) + return err + } + + tuple, err := obj.packTuple() + if err != nil { + metricErrCnt.Inc(ctx, "insertreplace_packfield", 1) + + return err + } + + _, errCall := connection.Insert(namespace, tuple) + if errCall != nil { + metricErrCnt.Inc(ctx, "insertreplace_box", 1) + logger.Error(ctx, "{{ $PublicStructName }}", obj.PrimaryString(), "Error insert into box", errCall, connection.Info()) + + return errCall + } + + if err == nil { + metricStatCnt.Inc(ctx, "insert_success", 1) + } + + obj.UpdateOps = []tarantool2.Op{} + obj.BaseField.Exists = true + + return err +} + +func (obj *{{ $PublicStructName }}) Replace(ctx context.Context) error { + logger := activerecord.Logger() + metricStatCnt := activerecord.Metric().StatCount("tarantool", "{{ $PublicStructName }}") + metricErrCnt := activerecord.Metric().ErrorCount("tarantool", "{{ $PublicStructName }}") + + metricStatCnt.Inc(ctx, "replace_request", 1) + + if !obj.BaseField.Exists { + metricErrCnt.Inc(ctx, "replace_notexists", 1) + return fmt.Errorf("can't replace not exists object") + } + + connection, err := tarantool.Box(ctx, 0, activerecord.MasterInstanceType, cfgName, {{ if eq .Server.Conf "" -}}optionCreator{{ else }}nil{{ end -}}) + if err != nil { + metricErrCnt.Inc(ctx, "insertreplace_preparebox", 1) + logger.Error(ctx, "{{ $PublicStructName }}", obj.PrimaryString(), fmt.Sprintf("Error get box '%s'", err)) + + return err + } + + tuple, err := obj.packTuple() + if err != nil { + metricErrCnt.Inc(ctx, "insertreplace_packfield", 1) + + return err + } + + _, errCall := connection.Replace(namespace, tuple) + if errCall != nil { + metricErrCnt.Inc(ctx, "insertreplace_box", 1) + logger.Error(ctx, "PromoBunches", obj.PrimaryString(), "Error replace on box", errCall, connection.Info()) + + return errCall + } + + if err == nil { + metricStatCnt.Inc(ctx, "replace_success", 1) + } + + obj.UpdateOps = []tarantool2.Op{} + obj.BaseField.Exists = true + + return err +} + +func (obj *{{ $PublicStructName }}) InsertOrReplace(ctx context.Context) error { + logger := activerecord.Logger() + metricStatCnt := activerecord.Metric().StatCount("tarantool", "{{ $PublicStructName }}") + metricErrCnt := activerecord.Metric().ErrorCount("tarantool", "{{ $PublicStructName }}") + + metricStatCnt.Inc(ctx, "insertorreplace_request", 1) + + connection, err := tarantool.Box(ctx, 0, activerecord.MasterInstanceType, cfgName, {{ if eq .Server.Conf "" -}}optionCreator{{ else }}nil{{ end -}}) + if err != nil { + metricErrCnt.Inc(ctx, "insertreplace_preparebox", 1) + logger.Error(ctx, "{{ $PublicStructName }}", obj.PrimaryString(), fmt.Sprintf("Error get box '%s'", err)) + return err + } + + tuple, err := obj.packTuple() + if err != nil { + metricErrCnt.Inc(ctx, "insertreplace_packfield", 1) + + return err + } + + _, errCall := connection.Upsert(namespace, tuple, obj.UpdateOps) + if errCall != nil { + metricErrCnt.Inc(ctx, "insertreplace_box", 1) + logger.Error(ctx, "{{ $PublicStructName }}", obj.PrimaryString(), "Error upsert box", errCall, connection.Info()) + + return errCall + } + + if err == nil { + metricStatCnt.Inc(ctx, "insertorreplace_success", 1) + } + + obj.UpdateOps = []tarantool2.Op{} + obj.BaseField.Exists = true + + return err +} + +func selectBox(ctx context.Context, indexnum uint32, keys []interface{}, iterType uint32, limiter activerecord.SelectorLimiter) ([]*{{ $PublicStructName }}, error) { + logger := activerecord.Logger() + ctx = logger.SetLoggerValueToContext(ctx, activerecord.ValueLogPrefix{"limiter": limiter.String()}) + metricTimer := activerecord.Metric().Timer("tarantool", "{{ $PublicStructName }}") + metricStatCnt := activerecord.Metric().StatCount("tarantool", "{{ $PublicStructName }}") + metricErrCnt := activerecord.Metric().ErrorCount("tarantool", "{{ $PublicStructName }}") + + connection, err := tarantool.Box(ctx, 0, activerecord.ReplicaOrMasterInstanceType, cfgName, {{ if eq .Server.Conf "" -}}optionCreator{{ else }}nil{{ end -}}) + if err != nil { + metricErrCnt.Inc(ctx, "select_preparebox", 1) + logger.Error(ctx, fmt.Sprintf("Error get box '%s'", err)) + + return nil, err + } + + var res []*{{ $PublicStructName }} + + limit := limiter.Limit() + if limiter.Limit() == 0 { + limit = math.MaxInt32 + } + + errCall := connection.SelectTyped(namespace, indexnum, limiter.Offset(), limit, iterType, keys, &res) + if errCall != nil { + metricErrCnt.Inc(ctx, "select_box", 1) + logger.Error(ctx, "Error select from box", errCall, connection.Info()) + + return nil, errCall + } + + metricTimer.Timing(ctx, "select_box") + metricStatCnt.Inc(ctx, "select_tuples_res", float64(len(res))) + + if limiter.FullfillWarn() && len(res) == int(limiter.Limit()) { + logger.Warn(ctx, "Select limit reached. Result may less than db records.") + } + + mode, ok := connection.InstanceMode().(activerecord.ServerModeType) + if !ok || mode == activerecord.ModeReplica { + if !ok { + logger.Error(ctx, "Invalid server mode type: %T", connection.InstanceMode()) + } + + for _, r := range res { + r.BaseField.IsReplica = true + r.BaseField.ReadOnly = true + } + } + + for _, r := range res { + r.BaseField.Objects = map[string][]tarantool.ModelStruct{} + r.BaseField.Exists = true + } + + logger.Debug(ctx, "Success select") + + metricTimer.Finish(ctx, "select") + + return res, nil +} + +func executeSQL(ctx context.Context, sqlText string, args []any) ([]*{{ $PublicStructName }}, error) { + logger := activerecord.Logger() + metricTimer := activerecord.Metric().Timer("tarantool", "{{ $PublicStructName }}") + metricStatCnt := activerecord.Metric().StatCount("tarantool", "{{ $PublicStructName }}") + metricErrCnt := activerecord.Metric().ErrorCount("tarantool", "{{ $PublicStructName }}") + + connection, err := tarantool.Box(ctx, 0, activerecord.ReplicaOrMasterInstanceType, cfgName, {{ if eq .Server.Conf "" -}}optionCreator{{ else }}nil{{ end -}}) + if err != nil { + metricErrCnt.Inc(ctx, "execute_preparebox", 1) + logger.Error(ctx, fmt.Sprintf("Error get box '%s'", err)) + + return nil, err + } + + var res []*{{ $PublicStructName }} + + if _, _, errCall := connection.ExecuteTyped(sqlText, args, &res); err != nil { + metricErrCnt.Inc(ctx, "execute_box", 1) + logger.Error(ctx, "Error execute from box", errCall, connection.Info()) + + return nil, errCall + } + + for _, r := range res { + r.BaseField.Exists = true + r.BaseField.Objects = map[string][]tarantool.ModelStruct{} + } + + metricStatCnt.Inc(ctx, "execute_tuples_res", float64(len(res))) + + logger.Debug(ctx, "Success execute") + + metricTimer.Finish(ctx, "execute") + + return res, nil +} \ No newline at end of file diff --git a/internal/pkg/generator/tmpl/tarantool/procedure.tmpl b/internal/pkg/generator/tmpl/tarantool/procedure.tmpl new file mode 100644 index 0000000..b1aedd9 --- /dev/null +++ b/internal/pkg/generator/tmpl/tarantool/procedure.tmpl @@ -0,0 +1,252 @@ +package {{ .ARPkg }} + +import ( + "bytes" + "context" + "fmt" + "log" +{{ if eq .Server.Conf "" -}} + "time" +{{ end }} + "strings" + + "github.com/mailru/activerecord/pkg/activerecord" + "github.com/mailru/activerecord/pkg/tarantool" + tarantool2 "github.com/tarantool/go-tarantool" + "gopkg.in/vmihailenco/msgpack.v2" + +{{- range $ind, $imp := .Imports }} +{{ if ne $imp.ImportName "" }}{{ $imp.ImportName }} {{ end }}"{{ $imp.Path }}" +{{- end }} +{{- range $i, $imp := addImport .FieldList }} + "{{ $imp }}" +{{- end }} +) + +{{ $pkgName := .ARPkg }} +{{ $serializers := .Serializers -}} +{{ $mutators := .Mutators -}} +{{ $PublicStructName := .ARPkgTitle -}} +{{ $LinkedObject := .LinkedObject }} +{{ $flags := .Flags }} +{{ $fields := .FieldList }} +{{ $procfields := .ProcOutFieldList }} +{{ $procInLen := len .ProcInFieldList }} +{{ $mutatorLen := len .Mutators }} + +{{ if $procfields }} +// proc struct +type {{ $PublicStructName }} struct { +params {{ $PublicStructName }}Params +{{- range $ind, $fstruct := .ProcOutFieldList }} +{{ $rtype := $fstruct.Format -}} +{{ $serlen := len $fstruct.Serializer -}} +{{ if ne $serlen 0 -}} +{{ $sname := index $fstruct.Serializer 0 -}} +{{ $serializer := index $serializers $sname -}} +{{ $rtype = $serializer.Type -}} +{{ end -}} +field{{- $fstruct.Name }} {{ $rtype -}} +{{ end }} +} + +type {{ $PublicStructName }}List []*{{ $PublicStructName }} + +const ( + procName string = "{{ .Container.ObjectName }}" + cntOutFields uint32 = {{ len .ProcOutFieldList }} +) + +{{ if eq .Server.Conf "" -}} +var boxOption, _ = tarantool.NewOptions( + "{{ .Server.Host }}:{{ .Server.Port }}", + activerecord.ModeMaster, + tarantool.WithTimeout(time.Millisecond * {{ .Server.Timeout }}), +) + +var optionCreator = func(sic activerecord.ShardInstanceConfig) (activerecord.OptionInterface, error) { + return boxOption, nil +} + +var clusterInfo = activerecord.NewClusterInfo( + activerecord.WithShard([]activerecord.OptionInterface{boxOption}, []activerecord.OptionInterface{}), +) +{{ end }} +var cfgName = "{{.Server.Conf }}" + +func (obj *{{ $PublicStructName }}) DecodeMsgpack(dec *msgpack.Decoder) error { + rowCnt, err := dec.DecodeArrayLen() + if err != nil { + return err + } + + if rowCnt != 1 { + return fmt.Errorf("unexpected response rows count: %d", rowCnt) + } + + rowFieldCount, err := dec.DecodeArrayLen() + if err != nil { + return err + } + + if rowFieldCount != int(cntOutFields) { + return fmt.Errorf("unexpected count of fields") + } + + {{ range $ind, $fstruct := .ProcOutFieldList -}} + {{ $rtype := $fstruct.Format -}} + {{ $sname := $fstruct.Serializer.Name -}} + {{ if ne $sname "" -}} + {{ $serializer := index $serializers $sname -}} + {{ $serparams := $fstruct.Serializer.Params }} + var v{{ $fstruct.Name }} {{ $rtype }} + if err := dec.Decode(&v{{ $fstruct.Name }}); err != nil { + return fmt.Errorf("can't decode '{{ $fstruct.Name }}' field: %w", err) + } + + if err := {{ $serializer.ImportName }}.{{ $serializer.Unmarshaler }}({{ $serparams }}v{{ $fstruct.Name }}, &obj.field{{ $fstruct.Name }}); err != nil { + return fmt.Errorf("can't unpack '{{ $fstruct.Name }}' field: %w", err) + } + {{- else }} + if err := dec.Decode(&obj.field{{ $fstruct.Name }}); err != nil { + return fmt.Errorf("can't decode field '{{ $fstruct.Name }}': %w", err) + } + {{ end }} + {{ end }} + + return nil +} + +{{- range $ind, $fstruct := .ProcOutFieldList -}} +{{ $rtype := $fstruct.Format -}} +{{ $sname := $fstruct.Serializer.Name -}} +{{ if ne $sname "" -}} +{{ $serializer := index $serializers $sname -}} +{{ $rtype = $serializer.Type -}} +{{ end }} +func (obj *{{ $PublicStructName }}) Get{{ $fstruct.Name }}() {{ $rtype }} { + return obj.field{{ $fstruct.Name }} +} +{{ end }} + +type {{ $PublicStructName }}Params struct { + {{- range $ind, $fstruct := .ProcInFieldList -}} + {{ $rtype := $fstruct.Format -}} + {{ $serlen := len $fstruct.Serializer -}} + {{ if ne $serlen 0 -}} + {{ $sname := index $fstruct.Serializer 0 -}} + {{ $serializer := index $serializers $sname -}} + {{ $rtype = $serializer.Type -}} + {{ end }} + {{ $fstruct.Name }} {{ $rtype -}} + {{ end }} +} + +func (obj *{{ $PublicStructName }}) GetParams() {{ $PublicStructName }}Params { + return obj.params +} + +func (obj *{{ $PublicStructName }}) setParams(params {{ $PublicStructName }}Params) error { + obj.params = params + + return nil +} + +{{ if ne $procInLen 0 }} +func (obj *{{ $PublicStructName }}Params) arrayValues() ([]string, error) { + ret := []string{} + {{ range $ind, $fstruct := .ProcInFieldList -}} + {{ $sname := $fstruct.Serializer.Name -}} + {{ $bvar := $fstruct.Name -}} + {{ if ne $sname "" -}} + {{ $serializer := index $serializers $sname -}} + {{ $serparams := $fstruct.Serializer.Params -}} + pvar{{ $fstruct.Name }}, err := {{ $serializer.ImportName }}.{{ $serializer.Marshaler }}({{ $serparams }}obj.{{ $bvar }}) + if err != nil { + return nil, fmt.Errorf("error marshal param field {{ $fstruct.Name }}: %w", err) + } + + {{ if eq $fstruct.Format "[]string" }} + ret = append(ret, pvar{{ $fstruct.Name }}...) + {{ else }} + ret = append(ret, string(pvar{{ $fstruct.Name }})) + {{- end }} + {{- else -}} + ret = append(ret, string(obj.{{ $fstruct.Name }})) + {{- end }} + {{ end }} + return ret, nil +} +{{ end }} + +func (obj {{ $PublicStructName }}Params) PK() string { + return fmt.Sprint({{ if ne $procInLen 0 }}obj.arrayValues(){{ end }}) +} + +func Call(ctx context.Context{{ if ne $procInLen 0 }}, params {{ $PublicStructName }}Params{{ end }}) (*{{ $PublicStructName }}, error) { + return call(ctx{{ if ne $procInLen 0 }}, params{{ end }}, activerecord.ReplicaOrMasterInstanceType) +} + +func CallOnMaster(ctx context.Context{{ if ne $procInLen 0 }}, params {{ $PublicStructName }}Params{{ end }}) (*{{ $PublicStructName }}, error) { + return call(ctx{{ if ne $procInLen 0 }}, params{{ end }}, activerecord.MasterInstanceType) +} + +func call(ctx context.Context{{ if ne $procInLen 0 }}, params {{ $PublicStructName }}Params{{ end }}, instanceType activerecord.ShardInstanceType) (*{{ $PublicStructName }}, error) { + logger := activerecord.Logger() + ctx = logger.SetLoggerValueToContext(ctx, map[string]interface{}{"LuaProc": procName}) + metricTimer := activerecord.Metric().Timer("octopus", "{{ $PublicStructName }}") + metricErrCnt := activerecord.Metric().ErrorCount("octopus", "{{ $PublicStructName }}") + + metricTimer.Timing(ctx, "call_proc") + + connection, err := tarantool.Box(ctx, 0, instanceType, cfgName, {{ if eq .Server.Conf "" -}}optionCreator{{ else }}nil{{ end -}}) + if err != nil { + metricErrCnt.Inc(ctx, "call_proc_preparebox", 1) + logger.Error(ctx, fmt.Sprintf("Error get box '%s'", err)) + + return nil, err + } + + var args []string + + args, err = params.arrayValues() + if err != nil { + metricErrCnt.Inc(ctx, "call_proc_preparebox", 1) + + return nil, fmt.Errorf("Error parse args of procedure %s: %w", procName, err) + } + + var res *{{ $PublicStructName }} + + errCall := connection.Call17Typed(procName, args, &res) + if errCall != nil { + metricErrCnt.Inc(ctx, "call_proc", 1) + logger.Error(ctx, "Error execute from box", errCall, connection.Info()) + + return nil, fmt.Errorf("call lua procedure %s: %w", procName, errCall) + } + + + metricTimer.Finish(ctx, "call_proc") + + return res, nil +} + +{{ range $ind, $fstruct := .ProcOutFieldList -}} +{{ $packerparam := packerParam $fstruct.Format -}} +{{ $rtype := $fstruct.Format -}} +{{ $sname := $fstruct.Serializer.Name -}} +{{ if ne $sname "" -}} +{{ $serializer := index $serializers $sname -}} +{{ $rtype = $serializer.Type -}} +{{ end -}} +func (obj *{{ $PublicStructName }}) Set{{ $fstruct.Name }}({{ $fstruct.Name }} {{ $rtype }}) error { + obj.field{{ $fstruct.Name }} = {{ $fstruct.Name}} + + return nil +} + +{{ end }} +// end proc struct + +{{end}} \ No newline at end of file diff --git a/internal/pkg/testutil/fixture.go b/internal/pkg/testutil/fixture.go deleted file mode 100644 index 110b2e6..0000000 --- a/internal/pkg/testutil/fixture.go +++ /dev/null @@ -1 +0,0 @@ -package testutil diff --git a/pkg/activerecord/cluster.go b/pkg/activerecord/cluster.go index b442d1c..64edd6b 100644 --- a/pkg/activerecord/cluster.go +++ b/pkg/activerecord/cluster.go @@ -43,6 +43,8 @@ type ShardInstanceConfig struct { Mode ServerModeType PoolSize int Addr string + User string + Password string } // Структура описывающая инстанс в кластере @@ -322,7 +324,10 @@ func getShardInfoFromCfg(ctx context.Context, path string, globParam MapGlobPara shardTimeout := cfg.GetDuration(ctx, path+"/Timeout", globParam.Timeout) shardPoolSize := cfg.GetInt(ctx, path+"/PoolSize", globParam.PoolSize) - // информация по местерам + user, _ := cfg.GetStringIfExists(ctx, path+"/user") + password, _ := cfg.GetStringIfExists(ctx, path+"/password") + + // информация по мастерам master, exMaster := cfg.GetStringIfExists(ctx, path+"/master") if !exMaster { master, exMaster = cfg.GetStringIfExists(ctx, path) @@ -342,6 +347,8 @@ func getShardInfoFromCfg(ctx context.Context, path string, globParam MapGlobPara Mode: ModeMaster, PoolSize: shardPoolSize, Timeout: shardTimeout, + User: user, + Password: password, } opt, err := optionCreator(shardCfg) @@ -370,6 +377,8 @@ func getShardInfoFromCfg(ctx context.Context, path string, globParam MapGlobPara Mode: ModeReplica, PoolSize: shardPoolSize, Timeout: shardTimeout, + User: user, + Password: password, } opt, err := optionCreator(shardCfg) diff --git a/pkg/activerecord/cluster_test.go b/pkg/activerecord/cluster_test.go index 67e8082..75b40f3 100644 --- a/pkg/activerecord/cluster_test.go +++ b/pkg/activerecord/cluster_test.go @@ -36,6 +36,8 @@ func TestGetClusterInfoFromCfg(t *testing.T) { mockConfig.EXPECT().GetStringIfExists(mock.Anything, "testconfig/master").Return("", false) mockConfig.EXPECT().GetStringIfExists(mock.Anything, "testconfig").Return("host1,host2", true) mockConfig.EXPECT().GetStringIfExists(mock.Anything, "testconfig/replica").Return("", false) + mockConfig.EXPECT().GetStringIfExists(mock.Anything, "testconfig/user").Return("", false) + mockConfig.EXPECT().GetStringIfExists(mock.Anything, "testconfig/password").Return("", false) }, args: args{ ctx: ctx, @@ -80,6 +82,8 @@ func TestGetClusterInfoFromCfg(t *testing.T) { mockConfig.EXPECT().GetDurationIfExists(mock.Anything, mock.Anything).Return(0, false) mockConfig.EXPECT().GetStringIfExists(mock.Anything, "testconfig/master").Return("host2", true) mockConfig.EXPECT().GetStringIfExists(mock.Anything, "testconfig/replica").Return("host1", true) + mockConfig.EXPECT().GetStringIfExists(mock.Anything, "testconfig/user").Return("", false) + mockConfig.EXPECT().GetStringIfExists(mock.Anything, "testconfig/password").Return("", false) }, args: args{ ctx: ctx, @@ -126,6 +130,8 @@ func TestGetClusterInfoFromCfg(t *testing.T) { mockConfig.EXPECT().GetStringIfExists(mock.Anything, "testconfig/master").Return("", false) mockConfig.EXPECT().GetStringIfExists(mock.Anything, "testconfig").Return("host1", true) mockConfig.EXPECT().GetStringIfExists(mock.Anything, "testconfig/replica").Return("host2", true) + mockConfig.EXPECT().GetStringIfExists(mock.Anything, "testconfig/user").Return("", false) + mockConfig.EXPECT().GetStringIfExists(mock.Anything, "testconfig/password").Return("", false) }, args: args{ ctx: ctx, diff --git a/pkg/activerecord/hash.go b/pkg/activerecord/hash.go new file mode 100644 index 0000000..649119e --- /dev/null +++ b/pkg/activerecord/hash.go @@ -0,0 +1,49 @@ +package activerecord + +import ( + "encoding/binary" + "encoding/hex" + "fmt" + "hash" +) + +type GroupHash struct { + hash hash.Hash32 + calculated bool +} + +func NewGroupHash(hash hash.Hash32) *GroupHash { + return &GroupHash{hash: hash} +} + +func (o *GroupHash) UpdateHash(data ...interface{}) error { + if o.calculated { + return fmt.Errorf("can't update hash after calculate") + } + + for _, v := range data { + var err error + + switch v := v.(type) { + case string: + err = binary.Write(o.hash, binary.LittleEndian, []byte(v)) + case int: + err = binary.Write(o.hash, binary.LittleEndian, int64(v)) + default: + err = binary.Write(o.hash, binary.LittleEndian, v) + } + + if err != nil { + return fmt.Errorf("can't calculate connectionID: %w", err) + } + } + + return nil +} + +func (o *GroupHash) GetHash() string { + o.calculated = true + hashInBytes := o.hash.Sum(nil)[:] + + return hex.EncodeToString(hashInBytes) +} diff --git a/pkg/iproto/util/text/text.go b/pkg/iproto/util/text/text.go index 311bf81..9be38c8 100644 --- a/pkg/iproto/util/text/text.go +++ b/pkg/iproto/util/text/text.go @@ -3,6 +3,8 @@ package text import ( "bytes" + "regexp" + "strings" "unicode" ) @@ -80,3 +82,11 @@ func ToSnakeCase(name string) string { return ret.String() } + +var snakePattern = regexp.MustCompile("(^[A-Za-z])|_([A-Za-z])") + +func SnakeToCamelCase(str string) string { + return snakePattern.ReplaceAllStringFunc(str, func(s string) string { + return strings.ToUpper(strings.Replace(s, "_", "", -1)) + }) +} diff --git a/pkg/octopus/options.go b/pkg/octopus/options.go index 8580795..5e75c1c 100644 --- a/pkg/octopus/options.go +++ b/pkg/octopus/options.go @@ -1,10 +1,7 @@ package octopus import ( - "encoding/binary" - "encoding/hex" "fmt" - "hash" "hash/crc32" "time" @@ -36,11 +33,10 @@ const ( // ConnectionOptions - опции используемые для подключения type ConnectionOptions struct { - server string - Mode ServerModeType - poolCfg *iproto.PoolConfig - connectionHash hash.Hash32 - calculated bool + *activerecord.GroupHash + server string + Mode ServerModeType + poolCfg *iproto.PoolConfig } // NewOptions - cоздание структуры с опциями и дефолтными значениями. Для мидификации значений по умолчанию, @@ -65,9 +61,10 @@ func NewOptions(server string, mode ServerModeType, opts ...ConnectionOption) (* PingInterval: DefaultPingInterval, }, }, - connectionHash: crc32.New(crc32table), } + octopusOpts.GroupHash = activerecord.NewGroupHash(crc32.New(crc32table)) + for _, opt := range opts { if err := opt.apply(octopusOpts); err != nil { return nil, fmt.Errorf("error apply options: %w", err) @@ -84,25 +81,8 @@ func NewOptions(server string, mode ServerModeType, opts ...ConnectionOption) (* // UpdateHash - функция расчета ConnectionID, необходима для шаринга конектов между моделями. func (o *ConnectionOptions) UpdateHash(data ...interface{}) error { - if o.calculated { - return fmt.Errorf("can't update hash after calculate") - } - - for _, data := range data { - var err error - - switch v := data.(type) { - case string: - err = binary.Write(o.connectionHash, binary.LittleEndian, []byte(v)) - case int: - err = binary.Write(o.connectionHash, binary.LittleEndian, int64(v)) - default: - err = binary.Write(o.connectionHash, binary.LittleEndian, v) - } - - if err != nil { - return fmt.Errorf("can't calculate connectionID: %w", err) - } + if err := o.GroupHash.UpdateHash(data...); err != nil { + return fmt.Errorf("can't calculate group hash: %w", err) } return nil @@ -110,10 +90,7 @@ func (o *ConnectionOptions) UpdateHash(data ...interface{}) error { // GetConnectionID - получение ConnecitionID. После первого получения, больше нельзя его модифицировать. Можно только новый Options создать func (o *ConnectionOptions) GetConnectionID() string { - o.calculated = true - hashInBytes := o.connectionHash.Sum(nil)[:] - - return hex.EncodeToString(hashInBytes) + return o.GroupHash.GetHash() } // InstanceMode - метод для получения режима аботы инстанса RO или RW diff --git a/pkg/octopus/types.go b/pkg/octopus/types.go index 64767ab..8e9f2ea 100644 --- a/pkg/octopus/types.go +++ b/pkg/octopus/types.go @@ -16,12 +16,6 @@ type TupleData struct { Data [][]byte } -type Ops struct { - Field uint32 - Op OpCode - Value []byte -} - type ModelStruct interface { Insert(ctx context.Context) error Replace(ctx context.Context) error @@ -30,6 +24,12 @@ type ModelStruct interface { Delete(ctx context.Context) error } +type Ops struct { + Field uint32 + Op OpCode + Value []byte +} + type BaseField struct { Collection []ModelStruct UpdateOps []Ops diff --git a/pkg/tarantool/box.go b/pkg/tarantool/box.go new file mode 100644 index 0000000..5afabc4 --- /dev/null +++ b/pkg/tarantool/box.go @@ -0,0 +1,112 @@ +package tarantool + +import ( + "context" + "fmt" + + "github.com/mailru/activerecord/pkg/activerecord" +) + +var DefaultOptionCreator = func(sic activerecord.ShardInstanceConfig) (activerecord.OptionInterface, error) { + return NewOptions(sic.Addr, sic.Mode, WithTimeout(sic.Timeout), WithCredential(sic.User, sic.Password)) +} + +func Box(ctx context.Context, shard int, instType activerecord.ShardInstanceType, configPath string, optionCreator func(activerecord.ShardInstanceConfig) (activerecord.OptionInterface, error)) (*Connection, error) { + if optionCreator == nil { + optionCreator = DefaultOptionCreator + } + + clusterInfo, err := activerecord.ConfigCacher().Get( + ctx, + configPath, + DefaultConnectionParams, + optionCreator, + ) + if err != nil { + return nil, fmt.Errorf("can't get cluster %s info: %w", configPath, err) + } + + if clusterInfo.Shards() < shard { + return nil, fmt.Errorf("invalid shard num %d, max = %d", shard, clusterInfo.Shards()) + } + + var ( + configBox activerecord.ShardInstance + ok bool + ) + + switch instType { + case activerecord.ReplicaInstanceType: + configBox, ok = clusterInfo.NextReplica(shard) + if !ok { + return nil, fmt.Errorf("replicas not set") + } + case activerecord.ReplicaOrMasterInstanceType: + configBox, ok = clusterInfo.NextReplica(shard) + if ok { + break + } + + fallthrough + case activerecord.MasterInstanceType: + configBox = clusterInfo.NextMaster(shard) + } + + conn, err := activerecord.ConnectionCacher().GetOrAdd(configBox, func(options interface{}) (activerecord.ConnectionInterface, error) { + octopusOpt, ok := options.(*ConnectionOptions) + if !ok { + return nil, fmt.Errorf("invalit type of options %T, want Options", options) + } + + return GetConnection(ctx, octopusOpt) + }) + if err != nil { + return nil, fmt.Errorf("error from connectionCacher: %w", err) + } + + box, ex := conn.(*Connection) + if !ex { + return nil, fmt.Errorf("invalid connection type %T, want *tarantool.Connection", conn) + } + + return box, nil +} + +func CheckShardInstance(ctx context.Context, instance activerecord.ShardInstance) (activerecord.OptionInterface, error) { + opts, ok := instance.Options.(*ConnectionOptions) + if !ok { + return nil, fmt.Errorf("invalit type of options %T, want Options", instance.Options) + } + + var err error + c := activerecord.ConnectionCacher().Get(instance) + if c == nil { + c, err = GetConnection(ctx, opts) + if err != nil { + return nil, fmt.Errorf("error from connectionCacher: %w", err) + } + } + + conn, ok := c.(*Connection) + if !ok { + return nil, fmt.Errorf("invalid connection type %T, want *tarantool.Connection", conn) + } + + var res []bool + + if err = conn.Call17Typed("dostring", []interface{}{"return box.info.ro"}, &res); err != nil { + return nil, fmt.Errorf("can't get instance status: %w", err) + } + + if len(res) == 1 { + ret := res[0] + switch ret { + case false: + return NewOptions(opts.server, activerecord.ModeMaster) + default: + return NewOptions(opts.server, activerecord.ModeReplica) + } + } + + return nil, fmt.Errorf("can't parse instance status: %w", err) +} diff --git a/pkg/tarantool/connection.go b/pkg/tarantool/connection.go new file mode 100644 index 0000000..0956164 --- /dev/null +++ b/pkg/tarantool/connection.go @@ -0,0 +1,49 @@ +package tarantool + +import ( + "context" + "fmt" + + "github.com/mailru/activerecord/pkg/activerecord" + "github.com/tarantool/go-tarantool" +) + +var DefaultConnectionParams = activerecord.MapGlobParam{ + Timeout: DefaultConnectionTimeout, +} + +type Connection struct { + *tarantool.Connection + opts *ConnectionOptions +} + +func GetConnection(_ context.Context, opts *ConnectionOptions) (*Connection, error) { + conn, err := tarantool.Connect(opts.server, opts.cfg) + if err != nil { + return nil, fmt.Errorf("error connect to tarantool %s with connect timeout '%d': %s", opts.server, opts.cfg.Timeout, err) + } + + return &Connection{ + Connection: conn, + opts: opts, + }, nil +} + +func (c *Connection) InstanceMode() any { + return c.opts.InstanceMode() +} + +func (c *Connection) Close() { + if err := c.Connection.Close(); err != nil { + panic(err) + } + +} + +func (c *Connection) Done() <-chan struct{} { + return nil +} + +func (c *Connection) Info() string { + return fmt.Sprintf("Server: %s, timeout; %d, user: %s", c.opts.server, c.opts.cfg.Timeout, c.opts.cfg.User) +} diff --git a/pkg/tarantool/options.go b/pkg/tarantool/options.go new file mode 100644 index 0000000..f8eefef --- /dev/null +++ b/pkg/tarantool/options.go @@ -0,0 +1,85 @@ +package tarantool + +import ( + "fmt" + "hash/crc32" + "time" + + "github.com/mailru/activerecord/pkg/activerecord" + "github.com/tarantool/go-tarantool" +) + +const DefaultConnectionTimeout = 20 * time.Millisecond + +type ConnectionOptions struct { + *activerecord.GroupHash + cfg tarantool.Opts + server string + Mode activerecord.ServerModeType +} + +type ConnectionOption interface { + apply(*ConnectionOptions) error +} + +type optionConnectionFunc func(*ConnectionOptions) error + +func (o optionConnectionFunc) apply(c *ConnectionOptions) error { + return o(c) +} + +// WithTimeout - опция для изменений таймаутов +func WithTimeout(request time.Duration) ConnectionOption { + return optionConnectionFunc(func(opts *ConnectionOptions) error { + opts.cfg.Timeout = request + + return opts.UpdateHash("T", request) + }) +} + +// WithCredential - опция авторизации +func WithCredential(user, pass string) ConnectionOption { + return optionConnectionFunc(func(opts *ConnectionOptions) error { + opts.cfg.User = user + opts.cfg.Pass = pass + + return opts.UpdateHash("L", user, pass) + }) +} + +func NewOptions(server string, mode activerecord.ServerModeType, opts ...ConnectionOption) (*ConnectionOptions, error) { + if server == "" { + return nil, fmt.Errorf("invalid param: server is empty") + } + + connectionOpts := &ConnectionOptions{ + cfg: tarantool.Opts{ + Timeout: DefaultConnectionTimeout, + }, + server: server, + Mode: mode, + } + + connectionOpts.GroupHash = activerecord.NewGroupHash(crc32.NewIEEE()) + + for _, opt := range opts { + if err := opt.apply(connectionOpts); err != nil { + return nil, fmt.Errorf("error apply options: %w", err) + } + } + + err := connectionOpts.UpdateHash("S", server) + if err != nil { + return nil, fmt.Errorf("can't get pool: %w", err) + } + + return connectionOpts, nil +} + +func (c *ConnectionOptions) GetConnectionID() string { + return c.GetHash() +} + +func (c *ConnectionOptions) InstanceMode() activerecord.ServerModeType { + return c.Mode +} diff --git a/pkg/tarantool/sqlbuilder.go b/pkg/tarantool/sqlbuilder.go new file mode 100644 index 0000000..13c3847 --- /dev/null +++ b/pkg/tarantool/sqlbuilder.go @@ -0,0 +1,21 @@ +package tarantool + +import "strings" + +func BuildSQLPredicateIN(fieldname string, fieldCndt int) string { + if fieldCndt == 0 { + return "" + } + + var b strings.Builder + b.Grow(len(fieldname) + 2*fieldCndt + 10) + b.WriteString(" \"" + fieldname + "\" IN (?") + + for i := 0; i < fieldCndt-1; i++ { + b.WriteString(", ?") + } + + b.WriteString(")") + + return b.String() +} diff --git a/pkg/tarantool/types.go b/pkg/tarantool/types.go new file mode 100644 index 0000000..5dbe6b3 --- /dev/null +++ b/pkg/tarantool/types.go @@ -0,0 +1,24 @@ +package tarantool + +import ( + "context" + + "github.com/tarantool/go-tarantool" +) + +type ModelStruct interface { + Insert(ctx context.Context) error + Replace(ctx context.Context) error + InsertOrReplace(ctx context.Context) error + Update(ctx context.Context) error + Delete(ctx context.Context) error +} + +type BaseField struct { + Collection []ModelStruct + UpdateOps []tarantool.Op + Exists bool + IsReplica bool + ReadOnly bool + Objects map[string][]ModelStruct +}