diff --git a/go.mod b/go.mod index f53323bb290e119feda2d24ebe935d02cfdc9e38..14a85cd72aebd444fa384bc1788849052a937c4c 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,8 @@ require ( github.com/gosimple/slug v1.13.1 github.com/hashicorp/go-multierror v1.1.1 github.com/hashicorp/golang-lru v0.5.4 + github.com/json-iterator/go v1.1.12 + github.com/mitchellh/mapstructure v1.4.2 github.com/pkg/errors v0.9.1 github.com/rs/xid v1.4.0 github.com/stretchr/testify v1.8.0 @@ -25,13 +27,23 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-kit/log v0.2.0 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect + github.com/golang/snappy v0.0.1 // indirect github.com/google/go-cmp v0.5.7 // indirect github.com/gosimple/unidecode v1.0.1 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/klauspost/compress v1.13.6 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/objx v0.4.0 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.1 // indirect + github.com/xdg-go/stringprep v1.0.3 // indirect + github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.7.0 // indirect + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac // indirect golang.org/x/text v0.3.7 // indirect google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4 // indirect diff --git a/go.sum b/go.sum index 9dea84ab8c222f13374d0a59a405fa290feed61a..511239a6e719ed8e4be2d67f6fc16c582aad70ef 100644 --- a/go.sum +++ b/go.sum @@ -57,6 +57,7 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -67,6 +68,7 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gosimple/slug v1.13.1 h1:bQ+kpX9Qa6tHRaK+fZR0A0M2Kd7Pa5eHPPsb1JpHD+Q= github.com/gosimple/slug v1.13.1/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= @@ -79,6 +81,9 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= @@ -94,6 +99,14 @@ github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1 github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mitchellh/mapstructure v1.4.2 h1:6h7AQ0yhTcIsmFmnAwQls75jp2Gzs4iB8W7pjMO+rqo= +github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -122,9 +135,13 @@ github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PK github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= +github.com/xdg-go/scram v1.1.1 h1:VOMT+81stJgXW3CpHyqHN3AXDYIMsx56mEFrB37Mb/E= github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= +github.com/xdg-go/stringprep v1.0.3 h1:kdwGpVNwPFtjs98xCGkHjQtGKh86rDcRZN17QEMCOIs= github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= +github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA= github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.mongodb.org/mongo-driver v1.11.4 h1:4ayjakA013OdpGyL2K3ZqylTac/rMjrJOMZ1EHizXas= @@ -170,6 +187,7 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/pkg/auth/anonymous.go b/pkg/auth/anonymous.go new file mode 100644 index 0000000000000000000000000000000000000000..62aa8a1ac28c45f09a065906a7735e44c1add05e --- /dev/null +++ b/pkg/auth/anonymous.go @@ -0,0 +1,127 @@ +package auth + +import ( + "context" + "fmt" + + "git.perx.ru/perxis/perxis-go/pkg/environments" + "git.perx.ru/perxis/perxis-go/pkg/errors" + "git.perx.ru/perxis/perxis-go/pkg/members" + "git.perx.ru/perxis/perxis-go/pkg/permission" + "git.perx.ru/perxis/perxis-go/pkg/roles" + "git.perx.ru/perxis/perxis-go/pkg/spaces" +) + +type Anonymous struct { + roles roles.Roles + spaces spaces.Spaces + spaceID string + environments environments.Environments +} + +func (Anonymous) GetID(ctx context.Context) string { return "anonymous" } +func (Anonymous) IsValid(ctx context.Context) bool { return false } +func (Anonymous) IsSystem(ctx context.Context) bool { return false } +func (Anonymous) IsManagementAllowed(ctx context.Context, spaceID string) error { + return ErrAccessDenied +} + +func (a Anonymous) Space(spaceID string) SpaceAccessor { + a.spaceID = spaceID + return &a +} + +func (a *Anonymous) getSpace(ctx context.Context, spaceID string) *spaces.Space { + if spaceID == "" { + return nil + } + space, _ := a.spaces.Get(WithSystem(ctx), spaceID) + return space +} + +func (a *Anonymous) HasSpaceAccess(ctx context.Context, spaceID string) bool { + if a.spaceID == "" || a.spaces == nil { + return false + } + return a.Role(ctx, spaceID) != nil +} + +func (a *Anonymous) Member(ctx context.Context) members.Role { + return members.NotMember +} + +func (a *Anonymous) Role(ctx context.Context, spaceID string) *roles.Role { + if a.spaceID == "" || a.roles == nil { + return nil + } + role, err := a.roles.Get(WithSystem(ctx), spaceID, roles.AnonymousRole) + if err != nil { + return nil + } + return role +} + +func (a *Anonymous) Rules(ctx context.Context, spaceID, envID string) permission.Ruleset { + role := a.Role(WithSystem(ctx), spaceID) + if role == nil { + return nil + } + + if !a.HasEnvironmentAccess(ctx, spaceID, envID) { + return nil + } + + return role.Rules +} + +func (a *Anonymous) HasEnvironmentAccess(ctx context.Context, space, env string) bool { + return hasEnvironmentAccess(ctx, a.environments, a.Role(ctx, space), env) +} + +func (Anonymous) Format(f fmt.State, verb rune) { + f.Write([]byte("AnonymousPrincipal{}")) +} + +func (a Anonymous) HasAccess(ctx context.Context, spaceID, orgID string) error { + if !a.IsValid(ctx) { + return ErrAccessDenied + } + + if a.IsSystem(ctx) { + return nil + } + + if spaceID != "" { + hasAllow, err := a.hasRole(ctx, spaceID) + if err != nil { + return err + } + + if hasAllow { + return nil + } + } + + if a.Member(ctx).IsPrivileged() { + return nil + } + + return ErrAccessDenied +} + +func (a *Anonymous) hasRole(ctx context.Context, spaceID string) (bool, error) { + if a.spaceID == "" || a.roles == nil { + return false, nil + } + _, err := a.roles.Get(WithSystem(ctx), spaceID, roles.AnonymousRole) + if err == nil { + return true, nil + } + + if errors.Is(err, ErrNotFound) { + if sp := a.getSpace(ctx, spaceID); sp == nil { + return false, ErrNotFound + } + } + return false, nil +} diff --git a/pkg/auth/client.go b/pkg/auth/client.go new file mode 100644 index 0000000000000000000000000000000000000000..cf63410f0c2efb4c3d2150ce3956a75c5e1cbfab --- /dev/null +++ b/pkg/auth/client.go @@ -0,0 +1,255 @@ +package auth + +import ( + "context" + "fmt" + + "git.perx.ru/perxis/perxis-go/pkg/clients" + "git.perx.ru/perxis/perxis-go/pkg/collaborators" + "git.perx.ru/perxis/perxis-go/pkg/environments" + "git.perx.ru/perxis/perxis-go/pkg/errors" + "git.perx.ru/perxis/perxis-go/pkg/members" + "git.perx.ru/perxis/perxis-go/pkg/permission" + "git.perx.ru/perxis/perxis-go/pkg/roles" + "git.perx.ru/perxis/perxis-go/pkg/spaces" +) + +type ClientPrincipal struct { + identity *clients.GetByParams + spaceID string + space *spaces.Space + + client *clients.Client + invalid bool + + spaces spaces.Spaces + environments environments.Environments + clients clients.Clients + roles roles.Roles + collaborators collaborators.Collaborators +} + +func NewClientPrincipal(identity *clients.GetByParams) *ClientPrincipal { + return &ClientPrincipal{identity: identity} +} + +func (c ClientPrincipal) Format(f fmt.State, verb rune) { + var identity string + switch { + case c.identity == nil: + identity = "<nil>" + case c.identity.APIKey != "": + identity = fmt.Sprintf("APIKey: '%s'", c.identity.APIKey) + case c.identity.OAuthClientID != "": + identity = fmt.Sprintf("OAuthClientID: '%s'", c.identity.OAuthClientID) + case c.identity.TLSSubject != "": + identity = fmt.Sprintf("TLSSubject: '%s'", c.identity.TLSSubject) + } + + var id string + if c.client != nil { + id = c.client.ID + } + + f.Write([]byte(fmt.Sprintf("ClientPrincipal{ID: '%s', Identity: {%s}}", id, identity))) +} + +func (c *ClientPrincipal) Space(spaceID string) SpaceAccessor { + c.spaceID = spaceID + c.space = nil + c.invalid = false + c.client = nil + return c +} + +func (c *ClientPrincipal) getSpace(ctx context.Context, spaceID string) *spaces.Space { + if spaceID == "" { + return nil + } + space, _ := c.spaces.Get(WithSystem(ctx), spaceID) + return space +} + +func (ClientPrincipal) IsSystem(ctx context.Context) bool { + return false +} + +func (c *ClientPrincipal) IsManagementAllowed(ctx context.Context, spaceID string) error { + if !c.IsValid(ctx) { + return ErrAccessDenied + } + + if role := c.Role(ctx, spaceID); role != nil && role.AllowManagement { + return nil + } + + return ErrAccessDenied +} + +func (c *ClientPrincipal) Member(ctx context.Context) members.Role { + return members.NotMember +} + +func (c *ClientPrincipal) HasSpaceAccess(ctx context.Context, spaceID string) bool { + if c.spaceID == "" { + return false + } + client, _ := c.Client(ctx) + return client != nil && client.SpaceID == spaceID +} + +func (c *ClientPrincipal) GetID(ctx context.Context) string { + client, _ := c.Client(ctx) + if client == nil { + return "" + } + return client.ID +} + +func (c *ClientPrincipal) GetIdentity(ctx context.Context) *clients.GetByParams { + return c.identity +} + +func (c *ClientPrincipal) IsValid(ctx context.Context) bool { + if c == nil { + return false + } + client, _ := c.Client(ctx) + return client != nil +} + +func (c *ClientPrincipal) Client(ctx context.Context) (*clients.Client, error) { + if c.invalid { + return nil, nil + } + + if c.client != nil { + return c.client, nil + } + + if c.clients == nil { + c.invalid = true + return nil, nil + } + + client, err := c.clients.GetBy(WithSystem(ctx), c.spaceID, c.identity) + if err != nil || client == nil || client.IsDisabled() { + c.invalid = true + return nil, err + } + + c.client = client + return c.client, nil +} + +func (c *ClientPrincipal) HasEnvironmentAccess(ctx context.Context, spaceID, envID string) bool { + return hasEnvironmentAccess(ctx, c.environments, c.Role(ctx, spaceID), envID) +} + +func (c *ClientPrincipal) getRoleID(ctx context.Context, spaceID string) (string, bool) { + + if c.spaceID == "" || spaceID == "" { + return "", false + } + + if spaceID == c.spaceID { + cl, _ := c.Client(ctx) + if cl == nil || cl.RoleID == "" { + return "", false + } + + return cl.RoleID, true + } + + rID, err := c.collaborators.Get(WithSystem(ctx), spaceID, c.spaceID) + if err != nil { + rID = roles.ViewRole + } + return rID, true + +} + +func (c *ClientPrincipal) Role(ctx context.Context, spaceID string) *roles.Role { + if c.spaceID == "" { + return nil + } + + rID, ok := c.getRoleID(ctx, spaceID) + if !ok { + return nil + } + + role, err := c.roles.Get(WithSystem(ctx), spaceID, rID) + if err == nil { + //c.hasRole = true + //c.role = role + return role + } + + return nil +} + +func (c *ClientPrincipal) Rules(ctx context.Context, spaceID, envID string) permission.Ruleset { + if c.spaceID == "" || spaceID == "" || envID == "" { + return nil + } + + role := c.Role(ctx, spaceID) + if role == nil { + return nil + } + + if role.AllowManagement { + return permission.PrivilegedRuleset{} + } + + if hasEnvironmentAccess(ctx, c.environments, role, envID) { + return role.Rules + } + return nil +} + +func (c *ClientPrincipal) HasAccess(ctx context.Context, spaceID, orgID string) error { + if !c.IsValid(ctx) { + return ErrAccessDenied + } + + if c.IsSystem(ctx) { + return nil + } + + if spaceID != "" { + if c.spaceID == "" { + return ErrAccessDenied + } + + client, _ := c.Client(ctx) + if client != nil && client.SpaceID == spaceID { + return nil + } + } + + if c.Member(ctx).IsPrivileged() { + return nil + } + + return ErrAccessDenied +} + +func (c *ClientPrincipal) hasRole(ctx context.Context, spaceID string) (bool, error) { + if c.spaceID == "" { + return false, nil + } + + client, err := c.Client(ctx) + if err != nil && errors.Is(err, ErrNotFound) { + if sp := c.getSpace(ctx, spaceID); sp == nil { + return false, ErrNotFound + } + } + if client != nil && client.SpaceID == spaceID { + return true, nil + } + + return false, nil +} diff --git a/pkg/auth/context.go b/pkg/auth/context.go new file mode 100644 index 0000000000000000000000000000000000000000..d447681068fefc974089f351d7654056314796b7 --- /dev/null +++ b/pkg/auth/context.go @@ -0,0 +1,27 @@ +package auth + +import ( + "context" +) + +type principalKey struct{} + +func GetPrincipal(ctx context.Context) Principal { + p, _ := ctx.Value(principalKey{}).(Principal) + if p == nil { + return Anonymous{} + } + return p +} + +func WithPrincipal(ctx context.Context, p Principal) context.Context { + if ctx == nil { + ctx = context.Background() + } + + return context.WithValue(ctx, principalKey{}, p) +} + +func WithSystem(ctx context.Context) context.Context { + return WithPrincipal(ctx, &SystemPrincipal{}) +} diff --git a/pkg/auth/errors.go b/pkg/auth/errors.go new file mode 100644 index 0000000000000000000000000000000000000000..8522ec94ff4ac7e04e6eabfdca561f698645db9d --- /dev/null +++ b/pkg/auth/errors.go @@ -0,0 +1,10 @@ +package auth + +import ( + "git.perx.ru/perxis/perxis-go/pkg/errors" +) + +var ( + ErrAccessDenied = errors.PermissionDenied(errors.New("access denied")) + ErrNotFound = errors.NotFound(errors.New("not found")) +) diff --git a/pkg/auth/factory.go b/pkg/auth/factory.go new file mode 100644 index 0000000000000000000000000000000000000000..2394c62a2f15ca7605959b3f5b31996a5c79a164 --- /dev/null +++ b/pkg/auth/factory.go @@ -0,0 +1,82 @@ +package auth + +import ( + "strings" + + "git.perx.ru/perxis/perxis-go/pkg/clients" + "git.perx.ru/perxis/perxis-go/pkg/collaborators" + "git.perx.ru/perxis/perxis-go/pkg/environments" + "git.perx.ru/perxis/perxis-go/pkg/members" + "git.perx.ru/perxis/perxis-go/pkg/roles" + "git.perx.ru/perxis/perxis-go/pkg/spaces" + "git.perx.ru/perxis/perxis-go/pkg/users" +) + +type PrincipalFactory struct { + users.Users + members.Members + collaborators.Collaborators + roles.Roles + clients.Clients + spaces.Spaces + environments.Environments +} + +func (f PrincipalFactory) User(identity string) Principal { + return &UserPrincipal{ + identity: identity, + users: f.Users, + members: f.Members, + roles: f.Roles, + collaborators: f.Collaborators, + spaces: f.Spaces, + environments: f.Environments, + } +} + +func (f PrincipalFactory) Client(param *clients.GetByParams) Principal { + return &ClientPrincipal{ + identity: param, + //authID: authID, + clients: f.Clients, + environments: f.Environments, + roles: f.Roles, + spaces: f.Spaces, + collaborators: f.Collaborators, + } +} + +func (f PrincipalFactory) Anonymous() Principal { + return &Anonymous{ + roles: f.Roles, + spaces: f.Spaces, + } +} + +func (f PrincipalFactory) System() Principal { + return &SystemPrincipal{} +} + +func (f PrincipalFactory) Principal(principalId string) Principal { + switch { + case strings.Contains(principalId, "Subject="): + return f.Client(&clients.GetByParams{TLSSubject: getSubject(principalId)}) + case strings.HasSuffix(principalId, "@clients"): + return f.Client(&clients.GetByParams{OAuthClientID: strings.TrimSuffix(principalId, "@clients")}) + case strings.HasPrefix(principalId, "API-Key"): + return f.Client(&clients.GetByParams{APIKey: strings.TrimPrefix(principalId, "API-Key ")}) + default: + return f.User(principalId) + } +} + +func getSubject(header string) string { + var p string + for _, part := range strings.Split(header, ";") { + if strings.Contains(part, "Subject") { + p = strings.TrimSuffix(strings.TrimPrefix(part, "Subject=\""), "\"") + break + } + } + return p +} diff --git a/pkg/auth/grpc.go b/pkg/auth/grpc.go new file mode 100644 index 0000000000000000000000000000000000000000..7a566711db76c11b22206d947ba94ecc2bc3b366 --- /dev/null +++ b/pkg/auth/grpc.go @@ -0,0 +1,92 @@ +package auth + +import ( + "context" + + kitgrpc "github.com/go-kit/kit/transport/grpc" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" +) + +const ( + OAuth2IdentityMetadata = "x-perxis-identity" + TLSIdentityMetadata = "x-forwarded-client-cert" + AccessMetadata = "x-perxis-access" + + AuthorizationMetadata = "authorization" +) + +func GRPCToContext(factory *PrincipalFactory) kitgrpc.ServerRequestFunc { + return func(ctx context.Context, md metadata.MD) context.Context { + if identity := md.Get(TLSIdentityMetadata); len(identity) > 0 { + return WithPrincipal(ctx, factory.Principal(identity[0])) + } + + if identity := md.Get(OAuth2IdentityMetadata); len(identity) > 0 { + return WithPrincipal(ctx, factory.Principal(identity[0])) + } + + if identity := md.Get(AuthorizationMetadata); len(identity) > 0 { + return WithPrincipal(ctx, factory.Principal(identity[0])) + } + + if access := md.Get(AccessMetadata); len(access) > 0 { + return WithPrincipal(ctx, factory.System()) + } + + return WithPrincipal(ctx, factory.Anonymous()) + } +} + +func ContextToGRPC() kitgrpc.ClientRequestFunc { + return func(ctx context.Context, md *metadata.MD) context.Context { + p := GetPrincipal(ctx) + + switch p := p.(type) { + case *UserPrincipal: + if p.GetIdentity(ctx) != "" { + (*md)[OAuth2IdentityMetadata] = []string{p.GetIdentity(ctx)} + } + case *ClientPrincipal: + if ident := p.GetIdentity(ctx); ident != nil { + switch { + case ident.OAuthClientID != "": + (*md)[OAuth2IdentityMetadata] = []string{ident.OAuthClientID + "@clients"} + case ident.TLSSubject != "": + (*md)[TLSIdentityMetadata] = []string{ident.TLSSubject} + case ident.APIKey != "": + (*md)[AuthorizationMetadata] = []string{"API-Key " + ident.APIKey} + + } + } + case *SystemPrincipal: + (*md)[AccessMetadata] = []string{p.GetID(ctx)} + } + + return ctx + } +} + +// PrincipalServerInterceptor - grpc-интерÑептор, который иÑпользуетÑÑ Ð´Ð»Ñ Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ð´Ð°Ð½Ð½Ñ‹Ñ… принципала из grpc-метаданы и Ð´Ð¾Ð±Ð°Ð²Ð»ÐµÐ½Ð¸Ñ Ð² контекÑÑ‚ ''. Ð’ Ñлучае, еÑли +// ÑÐµÑ€Ð²Ð¸Ñ Ð½Ðµ иÑпользует проверку прав 'Principal' к ÑиÑтеме, в параметрах передаетÑÑ Ð¿ÑƒÑтой объект '&PrincipalFactory{}' +func PrincipalServerInterceptor(factory *PrincipalFactory) grpc.UnaryServerInterceptor { + return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + if md, ok := metadata.FromIncomingContext(ctx); ok { + ctx = GRPCToContext(factory)(ctx, md) + } + return handler(ctx, req) + } +} + +// PrincipalClientInterceptor - grpc-интерÑептор, который иÑпользуетÑÑ Ð´Ð»Ñ Ð¿Ð¾Ð»ÑƒÑ‡ÐµÐ½Ð¸Ñ Ð´Ð°Ð½Ð½Ñ‹Ñ… принципала. Ð’ Ñлучае, еÑли +// ÑÐµÑ€Ð²Ð¸Ñ Ð½Ðµ иÑпользует проверку прав 'Principal' к ÑиÑтеме, в параметрах передаетÑÑ Ð¿ÑƒÑтой объект '&PrincipalFactory{}' +func PrincipalClientInterceptor() grpc.UnaryClientInterceptor { + return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { + md, ok := metadata.FromOutgoingContext(ctx) + if !ok { + md = metadata.MD{} + } + ctx = metadata.NewOutgoingContext(ContextToGRPC()(ctx, &md), md) + return invoker(ctx, method, req, reply, cc, opts...) + } +} diff --git a/pkg/auth/principal.go b/pkg/auth/principal.go new file mode 100644 index 0000000000000000000000000000000000000000..78da09cb27f493f034720d40d91f33b7b6ce7313 --- /dev/null +++ b/pkg/auth/principal.go @@ -0,0 +1,86 @@ +package auth + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/data" + "git.perx.ru/perxis/perxis-go/pkg/environments" + "git.perx.ru/perxis/perxis-go/pkg/members" + "git.perx.ru/perxis/perxis-go/pkg/permission" + "git.perx.ru/perxis/perxis-go/pkg/roles" +) + +type Principal interface { + GetID(ctx context.Context) string + IsValid(ctx context.Context) bool + IsSystem(ctx context.Context) bool + HasAccess(ctx context.Context, spID, orgID string) error + + IsManagementAllowed(ctx context.Context, spaceID string) error +} + +type SpaceAccessor interface { + Principal + Space(spaceID string) SpaceAccessor + + // HasSpaceAccess проверÑет, еÑть ли у принципала доÑтуп на чтение проÑтранÑтва + // (проÑмотр информации о проÑтранÑтве, окружений, Ñ‚.д. - доÑтуп к запиÑÑм коллекций + // определÑетÑÑ Ð¾Ñ‚Ð´ÐµÐ»ÑŒÐ½Ñ‹Ð¼ набором правил, Ñм. SpaceAccessor.Rules()) + HasSpaceAccess(ctx context.Context, spaceID string) bool + HasEnvironmentAccess(ctx context.Context, spaceID, env string) bool + + // Member возвращает роль принципала в организации + Member(ctx context.Context) members.Role + + Role(ctx context.Context, spaceID string) *roles.Role + + // Rules возвращает набор правил, по которым принципал может получить + // доÑтуп к запиÑÑм коллекций проÑтранÑтва. + Rules(ctx context.Context, spaceID, envID string) permission.Ruleset +} + +type OrganizationAccessor interface { + Principal + Organization(orgID string) OrganizationAccessor + Member(ctx context.Context) members.Role +} + +func hasEnvironmentAccess(ctx context.Context, envsrv environments.Environments, role *roles.Role, envID string) bool { + if role == nil || role.SpaceID == "" || envID == "" { + return false + } + + if role.AllowManagement { + return true + } + + envs := role.Environments + + // ЕÑли Ñвно не указаны доÑтупные Ð¾ÐºÑ€ÑƒÐ¶ÐµÐ½Ð¸Ñ - доÑтуп по умолчанию к окружению master + if len(envs) == 0 { + envs = []string{environments.DefaultEnvironment} + } + + for _, ce := range envs { + if envID == ce || data.GlobMatch(envID, ce) { + return true + } + } + + e, err := envsrv.Get(WithSystem(ctx), role.SpaceID, envID) + if err != nil || e == nil { + return false + } + + aliases := append(e.Aliases, e.ID) + + for _, ce := range envs { + for _, al := range aliases { + if al == ce || data.GlobMatch(al, ce) { + return true + } + } + } + + return false +} diff --git a/pkg/auth/principal_test.go b/pkg/auth/principal_test.go new file mode 100644 index 0000000000000000000000000000000000000000..54e04ee4a8bf96151f9dfa9cf1edff0342ab03dd --- /dev/null +++ b/pkg/auth/principal_test.go @@ -0,0 +1,178 @@ +package auth + +import ( + "context" + "testing" + + "git.perx.ru/perxis/perxis-go/pkg/environments" + mocksenvs "git.perx.ru/perxis/perxis-go/pkg/environments/mocks" + "git.perx.ru/perxis/perxis-go/pkg/roles" + "github.com/stretchr/testify/mock" +) + +func Test_hasEnvironmentAccess(t *testing.T) { + type args struct { + ctx context.Context + envscall func(envsservice *mocksenvs.Environments) + role *roles.Role + envID string + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "simple", + args: args{ + ctx: context.Background(), + role: &roles.Role{ + ID: "1", + SpaceID: "space", + Description: "Current", + Environments: []string{"env1", "env2"}, + }, + envID: "env1", + }, + want: true, + }, + { + name: "glob env in role test: e*", + args: args{ + ctx: context.Background(), + envscall: func(envsservice *mocksenvs.Environments) { + envsservice.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(&environments.Environment{ + ID: "env1", + SpaceID: "space", + Aliases: []string{"master"}, + }, nil).Once() + }, + role: &roles.Role{ + ID: "1", + SpaceID: "space", + Description: "Current", + Environments: []string{"e*"}, + }, + envID: "env", + }, + want: true, + }, + { + name: "glob env in role test: *n*", + args: args{ + ctx: context.Background(), + envscall: func(envsservice *mocksenvs.Environments) { + envsservice.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(&environments.Environment{ + ID: "env1", + SpaceID: "space", + Aliases: []string{"master"}, + }, nil).Once() + }, + role: &roles.Role{ + ID: "1", + SpaceID: "space", + Description: "Current", + Environments: []string{"*n*"}, + }, + envID: "env", + }, + want: true, + }, + { + name: "glob env in role test: *1", + args: args{ + ctx: context.Background(), + envscall: func(envsservice *mocksenvs.Environments) { + envsservice.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(&environments.Environment{ + ID: "env1", + SpaceID: "space", + Aliases: []string{"master"}, + }, nil).Once() + }, + role: &roles.Role{ + ID: "1", + SpaceID: "space", + Description: "Current", + Environments: []string{"*1"}, + }, + envID: "env", + }, + want: true, + }, + { + name: "glob env in role test (alias): ma*", + args: args{ + ctx: context.Background(), + envscall: func(envsservice *mocksenvs.Environments) { + envsservice.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(&environments.Environment{ + ID: "env1", + SpaceID: "space", + Aliases: []string{"master"}, + }, nil).Once() + }, + role: &roles.Role{ + ID: "1", + SpaceID: "space", + Description: "Current", + Environments: []string{"ma*"}, + }, + envID: "env1", + }, + want: true, + }, + { + name: "glob env in role test: *", + args: args{ + ctx: context.Background(), + envscall: func(envsservice *mocksenvs.Environments) { + envsservice.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(&environments.Environment{ + ID: "env1", + SpaceID: "space", + Aliases: []string{"master"}, + }, nil).Once() + }, + role: &roles.Role{ + ID: "1", + SpaceID: "space", + Description: "Current", + Environments: []string{"*"}, + }, + envID: "env1", + }, + want: true, + }, + { + name: "glob env in role test: q*", + args: args{ + ctx: context.Background(), + envscall: func(envsservice *mocksenvs.Environments) { + envsservice.On("Get", mock.Anything, mock.Anything, mock.Anything).Return(&environments.Environment{ + ID: "env1", + SpaceID: "space", + Aliases: []string{"master"}, + }, nil).Once() + }, + role: &roles.Role{ + ID: "1", + SpaceID: "space", + Description: "Current", + Environments: []string{"q*"}, + }, + envID: "env1", + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + envsservice := &mocksenvs.Environments{} + if tt.args.envscall != nil { + tt.args.envscall(envsservice) + } + + if got := hasEnvironmentAccess(tt.args.ctx, envsservice, tt.args.role, tt.args.envID); got != tt.want { + t.Errorf("hasEnvironmentAccess() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/auth/system.go b/pkg/auth/system.go new file mode 100644 index 0000000000000000000000000000000000000000..b602fe114bb53d5f82ae5f6796806dc20ed75188 --- /dev/null +++ b/pkg/auth/system.go @@ -0,0 +1,39 @@ +package auth + +import ( + "context" + "fmt" + + "git.perx.ru/perxis/perxis-go/pkg/members" + "git.perx.ru/perxis/perxis-go/pkg/permission" + "git.perx.ru/perxis/perxis-go/pkg/roles" +) + +type SystemPrincipal struct{} + +const ( + SystemID = "system" +) + +func (p SystemPrincipal) GetID(ctx context.Context) string { return SystemID } +func (SystemPrincipal) IsValid(ctx context.Context) bool { return true } +func (SystemPrincipal) IsSystem(ctx context.Context) bool { return true } +func (SystemPrincipal) IsManagementAllowed(ctx context.Context, spaceID string) error { return nil } + +func (p SystemPrincipal) Organization(_ string) OrganizationAccessor { return p } + +func (p SystemPrincipal) Space(_ string) SpaceAccessor { return p } +func (SystemPrincipal) HasSpaceAccess(_ context.Context, _ string) bool { return true } +func (SystemPrincipal) HasAccess(ctx context.Context, spaceID, orgID string) error { + return nil +} +func (SystemPrincipal) HasEnvironmentAccess(_ context.Context, _, _ string) bool { return true } +func (SystemPrincipal) Member(_ context.Context) members.Role { return members.NotMember } +func (SystemPrincipal) Role(_ context.Context, _ string) *roles.Role { return nil } +func (SystemPrincipal) Rules(_ context.Context, _, _ string) permission.Ruleset { + return &permission.PrivilegedRuleset{} +} + +func (SystemPrincipal) Format(f fmt.State, verb rune) { + f.Write([]byte("SystemPrincipal{}")) +} diff --git a/pkg/auth/user.go b/pkg/auth/user.go new file mode 100644 index 0000000000000000000000000000000000000000..f34693bf93f9b63c5d5eca84398b096af86e52ef --- /dev/null +++ b/pkg/auth/user.go @@ -0,0 +1,334 @@ +package auth + +import ( + "context" + "fmt" + + "git.perx.ru/perxis/perxis-go/pkg/collaborators" + "git.perx.ru/perxis/perxis-go/pkg/environments" + "git.perx.ru/perxis/perxis-go/pkg/errors" + "git.perx.ru/perxis/perxis-go/pkg/members" + "git.perx.ru/perxis/perxis-go/pkg/permission" + "git.perx.ru/perxis/perxis-go/pkg/roles" + "git.perx.ru/perxis/perxis-go/pkg/spaces" + "git.perx.ru/perxis/perxis-go/pkg/users" +) + +type UserPrincipal struct { + id string + identity string + + user *users.User + invalid bool + spaceID string + orgID string + + users users.Users + members members.Members + hasMemberRole bool + memberRole members.Role + + collaborators collaborators.Collaborators + spaces spaces.Spaces + environments environments.Environments + roles roles.Roles +} + +func (u UserPrincipal) Format(f fmt.State, verb rune) { + f.Write([]byte(fmt.Sprintf("UserPrincipal{ID: '%s', Identity: '%s'}", u.id, u.identity))) +} + +func (u *UserPrincipal) Space(spaceID string) SpaceAccessor { + u.spaceID = spaceID + u.orgID = "" + return u +} + +func (u *UserPrincipal) getSpace(ctx context.Context, spaceID string) *spaces.Space { + if spaceID == "" { + return nil + } + space, _ := u.spaces.Get(WithSystem(ctx), spaceID) + return space +} + +func (u UserPrincipal) Organization(orgID string) OrganizationAccessor { + u.orgID = orgID + return &u +} + +func (u *UserPrincipal) GetID(ctx context.Context) string { + user := u.User(ctx) + if user == nil { + return "" + } + return user.ID +} + +func (u *UserPrincipal) GetIdentity(ctx context.Context) string { + return u.identity +} + +func (u *UserPrincipal) IsValid(ctx context.Context) bool { + if u == nil { + return false + } + + return u.User(ctx) != nil +} + +func (u *UserPrincipal) IsSystem(ctx context.Context) bool { + user := u.User(ctx) + if user != nil { + return user.IsSystem() + } + return false +} + +func (u *UserPrincipal) IsManagementAllowed(ctx context.Context, spaceID string) error { + if !u.IsValid(ctx) { + return ErrAccessDenied + } + + if u.IsSystem(ctx) { + return nil + } + + if u.Member(ctx).IsPrivileged() { + return nil + } + + if role := u.Role(ctx, spaceID); role != nil && role.AllowManagement { + return nil + } + + return ErrAccessDenied +} + +func (u *UserPrincipal) User(ctx context.Context) *users.User { + if u.invalid { + return nil + } + + if u.user != nil { + return u.user + } + if u.users == nil { + u.invalid = true + return nil + } + + var user *users.User + var err error + switch { + case u.id != "": + user, err = u.users.Get(WithSystem(ctx), u.id) + case u.identity != "": + ctx = WithSystem(ctx) + user, err = u.users.GetByIdentity(WithSystem(ctx), u.identity) + } + + if err != nil || user == nil { + u.invalid = true + return nil + } + + u.user = user + return u.user +} + +func (u *UserPrincipal) Member(ctx context.Context) members.Role { + if u.hasMemberRole { + return u.memberRole + } + + if u.members == nil || (u.orgID == "" && u.spaceID == "") { + u.hasMemberRole = true + return members.NotMember + } + + if u.orgID == "" && u.spaceID != "" { + sp := u.getSpace(ctx, u.spaceID) + if sp == nil { + u.hasMemberRole = true + return members.NotMember + } + u.orgID = sp.OrgID + } + + role, err := u.members.Get(WithSystem(ctx), u.orgID, u.GetID(ctx)) + if err != nil { + role = members.NotMember + } + + u.memberRole = role + u.hasMemberRole = true + return u.memberRole +} + +// HasSpaceAccess проверÑет, еÑть ли у Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ Ð´Ð¾Ñтуп к проÑтранÑтву +// Пользователь имеет доÑтуп к проÑтранÑтву еÑли: +// - ЯвлÑетÑÑ ÑƒÑ‡Ð°Ñтником проÑтранÑтва (даже еÑли его роль не ÑущеÑтвует) +// - ПроÑтранÑтво позволÑет доÑтуп Ð´Ð»Ñ Ð½Ðµ учаÑтников (еÑть роли AnonymousRole/AuthorizedRole/ViewRole) +// Deprecated :use HasAccess +func (u *UserPrincipal) HasSpaceAccess(ctx context.Context, spaceID string) bool { + res, _ := u.hasRole(ctx, spaceID) + return res +} + +// HasAccess проверÑет, еÑть ли у Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ Ð´Ð¾Ñтуп к проÑтранÑтву +// Пользователь имеет доÑтуп к проÑтранÑтву еÑли: +// - ЯвлÑетÑÑ ÑƒÑ‡Ð°Ñтником проÑтранÑтва (даже еÑли его роль не ÑущеÑтвует) +// - ПроÑтранÑтво позволÑет доÑтуп Ð´Ð»Ñ Ð½Ðµ учаÑтников (еÑть роли AnonymousRole/AuthorizedRole/ViewRole) +func (u *UserPrincipal) HasAccess(ctx context.Context, spaceID, orgID string) error { + if !u.IsValid(ctx) { + return ErrAccessDenied + } + + if u.IsSystem(ctx) { + return nil + } + + if spaceID != "" { + hasAllow, err := u.hasRole(ctx, spaceID) + if err != nil { + return err + } + + if hasAllow { + return nil + } + } + + if orgID != "" { + if u.Organization(orgID).Member(ctx).IsPrivileged() { + return nil + } + } else { + if u.Member(ctx).IsPrivileged() { + return nil + } + } + + return ErrAccessDenied +} + +func (u *UserPrincipal) hasRole(ctx context.Context, spaceID string) (bool, error) { + + if u.spaceID == "" || spaceID == "" { + return false, nil + } + + ctx = WithSystem(ctx) + + if spaceID != u.spaceID { + _, cErr := u.collaborators.Get(ctx, spaceID, u.spaceID) + if cErr == nil { + return true, nil + } + _, rErr := u.roles.Get(ctx, spaceID, roles.ViewRole) + if rErr == nil { + return true, nil + } + if errors.Is(cErr, ErrNotFound) || errors.Is(rErr, ErrNotFound) { + if sp := u.getSpace(ctx, spaceID); sp == nil { + return false, ErrNotFound + } + } + + return false, nil + } + + _, cErr := u.collaborators.Get(ctx, spaceID, u.GetID(ctx)) + if cErr == nil { + return true, nil + } + + _, rErr := u.roles.Get(ctx, spaceID, roles.AuthorizedRole) + if rErr == nil { + return true, nil + } + + if errors.Is(cErr, ErrNotFound) || errors.Is(rErr, ErrNotFound) { + if sp := u.getSpace(ctx, spaceID); sp == nil { + return false, ErrNotFound + } + } + + return false, nil +} + +func (u *UserPrincipal) getRoleID(ctx context.Context, spaceID string) string { + + if u.spaceID == "" || spaceID == "" { + return "" + } + + ctx = WithSystem(ctx) + + if spaceID != u.spaceID { + rID, err := u.collaborators.Get(ctx, spaceID, u.spaceID) + if err != nil { + rID = roles.ViewRole + } + return rID + } + + if roleID, err := u.collaborators.Get(ctx, spaceID, u.GetID(ctx)); err == nil { + return roleID + } + + return roles.AuthorizedRole +} + +func (u *UserPrincipal) Role(ctx context.Context, spaceID string) *roles.Role { + + if roleID := u.getRoleID(ctx, spaceID); roleID != "" { + role, _ := u.roles.Get(WithSystem(ctx), spaceID, roleID) + return role + } + + return nil +} + +func (u *UserPrincipal) Rules(ctx context.Context, spaceID, envID string) permission.Ruleset { + if spaceID == "" || envID == "" { + return nil + } + + if u.spaceID == spaceID && (u.IsSystem(ctx) || u.Member(ctx).IsPrivileged()) { + return permission.PrivilegedRuleset{} + } + + role := u.Role(ctx, spaceID) + if role == nil { + return nil + } + + if !hasEnvironmentAccess(ctx, u.environments, role, envID) { + return nil + } + + return role.Rules +} + +func IsValidUser(ctx context.Context, p Principal) bool { + if p == nil { + return false + } + if u, ok := p.(*UserPrincipal); ok { + return u.IsValid(ctx) + } + return false +} + +func User(ctx context.Context, p Principal) *users.User { + if u, ok := p.(*UserPrincipal); ok { + return u.User(ctx) + } + return nil +} + +func (u *UserPrincipal) HasEnvironmentAccess(ctx context.Context, spaceID, env string) bool { + return hasEnvironmentAccess(ctx, u.environments, u.Role(ctx, spaceID), env) +} diff --git a/pkg/environments/environment.go b/pkg/environments/environment.go new file mode 100644 index 0000000000000000000000000000000000000000..cbd468dc361951e048a0840f4158a42c962e7af1 --- /dev/null +++ b/pkg/environments/environment.go @@ -0,0 +1,114 @@ +package environments + +import "time" + +const ( + DefaultEnvironment = "master" +) + +type State int + +const ( + StateUnknown State = iota + StateNew + StatePreparing + StateReady + StateError + + StateInfoEmpty = "EMPTY" +) + +func (s State) String() string { + switch s { + case StateNew: + return "new" + case StatePreparing: + return "preparing" + case StateReady: + return "ready" + case StateError: + return "error" + default: + return "unknown" + } +} + +type StateInfo struct { + State State `json:"state" bson:"state"` + StartedAt time.Time `json:"started_at,omitempty" bson:"started_at,omitempty"` + Info string `json:"info,omitempty" bson:"info,omitempty"` +} + +type Config struct { + SourceID string + + // Deprecated + Features []string +} + +// Environment - предÑтавлÑет рабочее Ð¾ÐºÑ€ÑƒÐ¶ÐµÐ½Ð¸Ñ Ð´Ð»Ñ Ð¿Ñ€Ð¾ÑтранÑтва +// Каждое окружение может иметь ÑобÑтвенный набор коллекций и данных и +// иÑпользоватьÑÑ Ð½ÐµÐ·Ð°Ð²Ð¸Ñимо друг от друга +type Environment struct { + ID string `json:"id" bson:"_id"` // Идентификатор окружениÑ, задаетÑÑ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»ÐµÐ¼ при Ñоздании. Уникален в рамках проÑтранÑтва `SpaceID` + SpaceID string `json:"spaceID" bson:"-"` + Description string `json:"description" bson:"desc,omitempty"` // ОпиÑание Ð´Ð»Ñ Ð¾ÐºÑ€ÑƒÐ¶ÐµÐ½Ð¸Ñ + //State State `json:"state" bson:"state"` // СоÑтоÑние Ð¾ÐºÑ€ÑƒÐ¶ÐµÐ½Ð¸Ñ (Preparing/Ready/Failed) + //StateInfo string `json:"state_info,omitempty" bson:"state_info,omitempty"` + + // StateInfo отображает ÑоÑтоÑние коллекции: + // - State: идентификатор ÑоÑтоÑÐ½Ð¸Ñ Ð¾ÐºÑ€ÑƒÐ¶ÐµÐ½Ð¸Ñ (unknown/new/preparing/ready/error) + // - Info: Ð´Ð¾Ð¿Ð¾Ð»Ð½Ð¸Ñ‚ÐµÐ»ÑŒÐ½Ð°Ñ Ð¸Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð¾ ÑоÑтоÑнии коллекции (например, еÑли при + // применении Ñхемы к коллекции произошла ошибка) + // - StartedAt: времÑ, в которое ÐºÐ¾Ð»Ð»ÐµÐºÑ†Ð¸Ñ Ð¿ÐµÑ€ÐµÑˆÐ»Ð° в ÑоÑтоÑние `Preparing` + StateInfo *StateInfo `json:"state_info" bson:"state_info,omitempty"` + + Aliases []string `json:"aliases" bson:"aliases,omitempty"` // Синонимы Ð¾ÐºÑ€ÑƒÐ¶ÐµÐ½Ð¸Ñ (только чтение) + Config *Config `json:"config,omitempty" bson:"config,omitempty"` +} + +func (e Environment) Clone() *Environment { + clone := &Environment{ + ID: e.ID, + SpaceID: e.SpaceID, + Description: e.Description, + Aliases: append([]string(nil), e.Aliases...), + Config: nil, + } + + if e.StateInfo != nil { + clone.StateInfo = &StateInfo{ + State: e.StateInfo.State, + Info: e.StateInfo.Info, + StartedAt: e.StateInfo.StartedAt, + } + } + + if e.Config != nil { + clone.Config = &Config{ + SourceID: e.Config.SourceID, + } + } + + return clone +} + +func (e Environment) Fetch(i interface{}) interface{} { + p, _ := i.(string) + switch p { + case "ID": + return e.ID + case "SpaceID": + return e.SpaceID + case "Description": + return e.Description + case "StateInfo": + return e.StateInfo + case "Aliases": + return e.Aliases + case "Config": + return e.Config + default: + panic("unknown parameter") + } +} diff --git a/pkg/environments/mocks/Environments.go b/pkg/environments/mocks/Environments.go new file mode 100644 index 0000000000000000000000000000000000000000..8c8d099f32cbcbd7cd1edf377b3d46e6756a1d72 --- /dev/null +++ b/pkg/environments/mocks/Environments.go @@ -0,0 +1,176 @@ +// Code generated by mockery v2.14.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + environments "git.perx.ru/perxis/perxis-go/pkg/environments" + mock "github.com/stretchr/testify/mock" +) + +// Environments is an autogenerated mock type for the Environments type +type Environments struct { + mock.Mock +} + +// Create provides a mock function with given fields: ctx, env +func (_m *Environments) Create(ctx context.Context, env *environments.Environment) (*environments.Environment, error) { + ret := _m.Called(ctx, env) + + var r0 *environments.Environment + if rf, ok := ret.Get(0).(func(context.Context, *environments.Environment) *environments.Environment); ok { + r0 = rf(ctx, env) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*environments.Environment) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *environments.Environment) error); ok { + r1 = rf(ctx, env) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Delete provides a mock function with given fields: ctx, spaceId, envId +func (_m *Environments) Delete(ctx context.Context, spaceId string, envId string) error { + ret := _m.Called(ctx, spaceId, envId) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, spaceId, envId) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Get provides a mock function with given fields: ctx, spaceId, envId +func (_m *Environments) Get(ctx context.Context, spaceId string, envId string) (*environments.Environment, error) { + ret := _m.Called(ctx, spaceId, envId) + + var r0 *environments.Environment + if rf, ok := ret.Get(0).(func(context.Context, string, string) *environments.Environment); ok { + r0 = rf(ctx, spaceId, envId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*environments.Environment) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, spaceId, envId) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// List provides a mock function with given fields: ctx, spaceId +func (_m *Environments) List(ctx context.Context, spaceId string) ([]*environments.Environment, error) { + ret := _m.Called(ctx, spaceId) + + var r0 []*environments.Environment + if rf, ok := ret.Get(0).(func(context.Context, string) []*environments.Environment); ok { + r0 = rf(ctx, spaceId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*environments.Environment) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, spaceId) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Migrate provides a mock function with given fields: ctx, spaceId, envId, options +func (_m *Environments) Migrate(ctx context.Context, spaceId string, envId string, options ...*environments.MigrateOptions) error { + _va := make([]interface{}, len(options)) + for _i := range options { + _va[_i] = options[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, spaceId, envId) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, ...*environments.MigrateOptions) error); ok { + r0 = rf(ctx, spaceId, envId, options...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RemoveAlias provides a mock function with given fields: ctx, spaceId, envId, alias +func (_m *Environments) RemoveAlias(ctx context.Context, spaceId string, envId string, alias string) error { + ret := _m.Called(ctx, spaceId, envId, alias) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, string) error); ok { + r0 = rf(ctx, spaceId, envId, alias) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SetAlias provides a mock function with given fields: ctx, spaceId, envId, alias +func (_m *Environments) SetAlias(ctx context.Context, spaceId string, envId string, alias string) error { + ret := _m.Called(ctx, spaceId, envId, alias) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, string) error); ok { + r0 = rf(ctx, spaceId, envId, alias) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Update provides a mock function with given fields: ctx, env +func (_m *Environments) Update(ctx context.Context, env *environments.Environment) error { + ret := _m.Called(ctx, env) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *environments.Environment) error); ok { + r0 = rf(ctx, env) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +type mockConstructorTestingTNewEnvironments interface { + mock.TestingT + Cleanup(func()) +} + +// NewEnvironments creates a new instance of Environments. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewEnvironments(t mockConstructorTestingTNewEnvironments) *Environments { + mock := &Environments{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/environments/options.go b/pkg/environments/options.go new file mode 100644 index 0000000000000000000000000000000000000000..02f1a1fea3229ccfe702401c2dfa06e5094d9671 --- /dev/null +++ b/pkg/environments/options.go @@ -0,0 +1,37 @@ +package environments + +type MigrateOptions struct { + + // Ожидать Ð·Ð°Ð²ÐµÑ€ÑˆÐµÐ½Ð¸Ñ Ð¼Ð¸Ð³Ñ€Ð°Ñ†Ð¸Ð¸ в Ñинхронном режиме + Wait bool +} + +func MergeMigrateOptions(opts ...*MigrateOptions) *MigrateOptions { + o := &MigrateOptions{} + for _, opt := range opts { + if opt.Wait { + o.Wait = true + } + } + return o +} + +type UpdateOptions struct { + + // СоÑтоÑние будет обновлено только в том Ñлучае, еÑли выполнÑетÑÑ ÑƒÐºÐ°Ð·Ð°Ð½Ð½Ð¾Ðµ уÑловие + // Cond указываетÑÑ Ñ Ð¸Ñпользованием ÑинтакÑиÑа `expr` + Cond string +} + +func MergeUpdateOptions(opts ...*UpdateOptions) *UpdateOptions { + o := &UpdateOptions{} + for _, opt := range opts { + if opt.Cond != "" { + if o.Cond != "" { + o.Cond += " && " + } + o.Cond += opt.Cond + } + } + return o +} diff --git a/pkg/environments/service.go b/pkg/environments/service.go new file mode 100644 index 0000000000000000000000000000000000000000..cd0a37f449819a4b92239f8d8b38a3a0036ec8d8 --- /dev/null +++ b/pkg/environments/service.go @@ -0,0 +1,20 @@ +package environments + +import ( + "context" +) + +// Environments +// @microgen grpc +// @protobuf git.perx.ru/perxis/perxis-go/proto/environments +// @grpc-addr content.environments.Environments +type Environments interface { + Create(ctx context.Context, env *Environment) (created *Environment, err error) + Get(ctx context.Context, spaceId, envId string) (env *Environment, err error) + List(ctx context.Context, spaceId string) (envs []*Environment, err error) + Update(ctx context.Context, env *Environment) (err error) + Delete(ctx context.Context, spaceId, envId string) (err error) + SetAlias(ctx context.Context, spaceId, envId, alias string) (err error) + RemoveAlias(ctx context.Context, spaceId, envId, alias string) (err error) + Migrate(ctx context.Context, spaceId, envId string, options ...*MigrateOptions) (err error) +} diff --git a/pkg/environments/transport/client.microgen.go b/pkg/environments/transport/client.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..96094fda943e407d0264a8abc738fd5c3afe440e --- /dev/null +++ b/pkg/environments/transport/client.microgen.go @@ -0,0 +1,126 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +package transport + +import ( + "context" + "errors" + + environments "git.perx.ru/perxis/perxis-go/pkg/environments" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +func (set EndpointsSet) Create(arg0 context.Context, arg1 *environments.Environment) (res0 *environments.Environment, res1 error) { + request := CreateRequest{Env: arg1} + response, res1 := set.CreateEndpoint(arg0, &request) + if res1 != nil { + if e, ok := status.FromError(res1); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown { + res1 = errors.New(e.Message()) + } + return + } + return response.(*CreateResponse).Created, res1 +} + +func (set EndpointsSet) Get(arg0 context.Context, arg1 string, arg2 string) (res0 *environments.Environment, res1 error) { + request := GetRequest{ + EnvId: arg2, + SpaceId: arg1, + } + response, res1 := set.GetEndpoint(arg0, &request) + if res1 != nil { + if e, ok := status.FromError(res1); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown { + res1 = errors.New(e.Message()) + } + return + } + return response.(*GetResponse).Env, res1 +} + +func (set EndpointsSet) List(arg0 context.Context, arg1 string) (res0 []*environments.Environment, res1 error) { + request := ListRequest{SpaceId: arg1} + response, res1 := set.ListEndpoint(arg0, &request) + if res1 != nil { + if e, ok := status.FromError(res1); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown { + res1 = errors.New(e.Message()) + } + return + } + return response.(*ListResponse).Envs, res1 +} + +func (set EndpointsSet) Update(arg0 context.Context, arg1 *environments.Environment) (res0 error) { + request := UpdateRequest{Env: arg1} + _, res0 = set.UpdateEndpoint(arg0, &request) + if res0 != nil { + if e, ok := status.FromError(res0); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown { + res0 = errors.New(e.Message()) + } + return + } + return res0 +} + +func (set EndpointsSet) Delete(arg0 context.Context, arg1 string, arg2 string) (res0 error) { + request := DeleteRequest{ + EnvId: arg2, + SpaceId: arg1, + } + _, res0 = set.DeleteEndpoint(arg0, &request) + if res0 != nil { + if e, ok := status.FromError(res0); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown { + res0 = errors.New(e.Message()) + } + return + } + return res0 +} + +func (set EndpointsSet) SetAlias(arg0 context.Context, arg1 string, arg2 string, arg3 string) (res0 error) { + request := SetAliasRequest{ + Alias: arg3, + EnvId: arg2, + SpaceId: arg1, + } + _, res0 = set.SetAliasEndpoint(arg0, &request) + if res0 != nil { + if e, ok := status.FromError(res0); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown { + res0 = errors.New(e.Message()) + } + return + } + return res0 +} + +func (set EndpointsSet) RemoveAlias(arg0 context.Context, arg1 string, arg2 string, arg3 string) (res0 error) { + request := RemoveAliasRequest{ + Alias: arg3, + EnvId: arg2, + SpaceId: arg1, + } + _, res0 = set.RemoveAliasEndpoint(arg0, &request) + if res0 != nil { + if e, ok := status.FromError(res0); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown { + res0 = errors.New(e.Message()) + } + return + } + return res0 +} + +func (set EndpointsSet) Migrate(arg0 context.Context, arg1 string, arg2 string, arg3 ...*environments.MigrateOptions) (res0 error) { + request := MigrateRequest{ + EnvId: arg2, + Options: arg3, + SpaceId: arg1, + } + _, res0 = set.MigrateEndpoint(arg0, &request) + if res0 != nil { + if e, ok := status.FromError(res0); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown { + res0 = errors.New(e.Message()) + } + return + } + return res0 +} diff --git a/pkg/environments/transport/endpoints.microgen.go b/pkg/environments/transport/endpoints.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..5639637d2b479721e21d5c3fe4ea097d2f2a6cde --- /dev/null +++ b/pkg/environments/transport/endpoints.microgen.go @@ -0,0 +1,17 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +package transport + +import endpoint "github.com/go-kit/kit/endpoint" + +// EndpointsSet implements Environments API and used for transport purposes. +type EndpointsSet struct { + CreateEndpoint endpoint.Endpoint + GetEndpoint endpoint.Endpoint + ListEndpoint endpoint.Endpoint + UpdateEndpoint endpoint.Endpoint + DeleteEndpoint endpoint.Endpoint + SetAliasEndpoint endpoint.Endpoint + RemoveAliasEndpoint endpoint.Endpoint + MigrateEndpoint endpoint.Endpoint +} diff --git a/pkg/environments/transport/exchanges.microgen.go b/pkg/environments/transport/exchanges.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..a1a0ab068d575767c543d8db652c5d8c9a7e02f4 --- /dev/null +++ b/pkg/environments/transport/exchanges.microgen.go @@ -0,0 +1,66 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +package transport + +import environments "git.perx.ru/perxis/perxis-go/pkg/environments" + +type ( + CreateRequest struct { + Env *environments.Environment `json:"env"` + } + CreateResponse struct { + Created *environments.Environment `json:"created"` + } + + GetRequest struct { + SpaceId string `json:"space_id"` + EnvId string `json:"env_id"` + } + GetResponse struct { + Env *environments.Environment `json:"env"` + } + + ListRequest struct { + SpaceId string `json:"space_id"` + } + ListResponse struct { + Envs []*environments.Environment `json:"envs"` + } + + UpdateRequest struct { + Env *environments.Environment `json:"env"` + } + // Formal exchange type, please do not delete. + UpdateResponse struct{} + + DeleteRequest struct { + SpaceId string `json:"space_id"` + EnvId string `json:"env_id"` + } + // Formal exchange type, please do not delete. + DeleteResponse struct{} + + SetAliasRequest struct { + SpaceId string `json:"space_id"` + EnvId string `json:"env_id"` + Alias string `json:"alias"` + } + // Formal exchange type, please do not delete. + SetAliasResponse struct{} + + RemoveAliasRequest struct { + SpaceId string `json:"space_id"` + EnvId string `json:"env_id"` + Alias string `json:"alias"` + } + // Formal exchange type, please do not delete. + RemoveAliasResponse struct{} + + MigrateRequest struct { + SpaceId string `json:"space_id"` + EnvId string `json:"env_id"` + Options []*environments.MigrateOptions `json:"options"` // This field was defined with ellipsis (...). + } + // Formal exchange type, please do not delete. + MigrateResponse struct{} +) diff --git a/pkg/environments/transport/grpc/client.microgen.go b/pkg/environments/transport/grpc/client.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..34e0fa4db1cf8149475dde562b3179091089f70f --- /dev/null +++ b/pkg/environments/transport/grpc/client.microgen.go @@ -0,0 +1,75 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +package transportgrpc + +import ( + transport "git.perx.ru/perxis/perxis-go/pkg/environments/transport" + pb "git.perx.ru/perxis/perxis-go/proto/environments" + grpckit "github.com/go-kit/kit/transport/grpc" + empty "github.com/golang/protobuf/ptypes/empty" + grpc "google.golang.org/grpc" +) + +func NewGRPCClient(conn *grpc.ClientConn, addr string, opts ...grpckit.ClientOption) transport.EndpointsSet { + if addr == "" { + addr = "content.environments.Environments" + } + return transport.EndpointsSet{ + CreateEndpoint: grpckit.NewClient( + conn, addr, "Create", + _Encode_Create_Request, + _Decode_Create_Response, + pb.CreateResponse{}, + opts..., + ).Endpoint(), + DeleteEndpoint: grpckit.NewClient( + conn, addr, "Delete", + _Encode_Delete_Request, + _Decode_Delete_Response, + empty.Empty{}, + opts..., + ).Endpoint(), + GetEndpoint: grpckit.NewClient( + conn, addr, "Get", + _Encode_Get_Request, + _Decode_Get_Response, + pb.GetResponse{}, + opts..., + ).Endpoint(), + ListEndpoint: grpckit.NewClient( + conn, addr, "List", + _Encode_List_Request, + _Decode_List_Response, + pb.ListResponse{}, + opts..., + ).Endpoint(), + MigrateEndpoint: grpckit.NewClient( + conn, addr, "Migrate", + _Encode_Migrate_Request, + _Decode_Migrate_Response, + empty.Empty{}, + opts..., + ).Endpoint(), + RemoveAliasEndpoint: grpckit.NewClient( + conn, addr, "RemoveAlias", + _Encode_RemoveAlias_Request, + _Decode_RemoveAlias_Response, + empty.Empty{}, + opts..., + ).Endpoint(), + SetAliasEndpoint: grpckit.NewClient( + conn, addr, "SetAlias", + _Encode_SetAlias_Request, + _Decode_SetAlias_Response, + empty.Empty{}, + opts..., + ).Endpoint(), + UpdateEndpoint: grpckit.NewClient( + conn, addr, "Update", + _Encode_Update_Request, + _Decode_Update_Response, + empty.Empty{}, + opts..., + ).Endpoint(), + } +} diff --git a/pkg/environments/transport/grpc/protobuf_endpoint_converters.microgen.go b/pkg/environments/transport/grpc/protobuf_endpoint_converters.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..6411216cd4f54cedd9bf2a3c61ff77b98d70cc35 --- /dev/null +++ b/pkg/environments/transport/grpc/protobuf_endpoint_converters.microgen.go @@ -0,0 +1,307 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +// Please, do not change functions names! +package transportgrpc + +import ( + "context" + "errors" + + transport "git.perx.ru/perxis/perxis-go/pkg/environments/transport" + pb "git.perx.ru/perxis/perxis-go/proto/environments" + empty "github.com/golang/protobuf/ptypes/empty" +) + +func _Encode_Create_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil CreateRequest") + } + req := request.(*transport.CreateRequest) + reqEnv, err := PtrEnvironmentToProto(req.Env) + if err != nil { + return nil, err + } + return &pb.CreateRequest{Env: reqEnv}, nil +} + +func _Encode_Get_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil GetRequest") + } + req := request.(*transport.GetRequest) + return &pb.GetRequest{ + EnvId: req.EnvId, + SpaceId: req.SpaceId, + }, nil +} + +func _Encode_List_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil ListRequest") + } + req := request.(*transport.ListRequest) + return &pb.ListRequest{SpaceId: req.SpaceId}, nil +} + +func _Encode_Update_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil UpdateRequest") + } + req := request.(*transport.UpdateRequest) + reqEnv, err := PtrEnvironmentToProto(req.Env) + if err != nil { + return nil, err + } + return &pb.UpdateRequest{Env: reqEnv}, nil +} + +func _Encode_Delete_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil DeleteRequest") + } + req := request.(*transport.DeleteRequest) + return &pb.DeleteRequest{ + EnvId: req.EnvId, + SpaceId: req.SpaceId, + }, nil +} + +func _Encode_SetAlias_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil SetAliasRequest") + } + req := request.(*transport.SetAliasRequest) + return &pb.SetAliasRequest{ + Alias: req.Alias, + EnvId: req.EnvId, + SpaceId: req.SpaceId, + }, nil +} + +func _Encode_RemoveAlias_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil RemoveAliasRequest") + } + req := request.(*transport.RemoveAliasRequest) + return &pb.RemoveAliasRequest{ + Alias: req.Alias, + EnvId: req.EnvId, + SpaceId: req.SpaceId, + }, nil +} + +func _Encode_Create_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil CreateResponse") + } + resp := response.(*transport.CreateResponse) + respCreated, err := PtrEnvironmentToProto(resp.Created) + if err != nil { + return nil, err + } + return &pb.CreateResponse{Created: respCreated}, nil +} + +func _Encode_Get_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil GetResponse") + } + resp := response.(*transport.GetResponse) + respEnv, err := PtrEnvironmentToProto(resp.Env) + if err != nil { + return nil, err + } + return &pb.GetResponse{Env: respEnv}, nil +} + +func _Encode_List_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil ListResponse") + } + resp := response.(*transport.ListResponse) + respEnvs, err := ListPtrEnvironmentToProto(resp.Envs) + if err != nil { + return nil, err + } + return &pb.ListResponse{Envs: respEnvs}, nil +} + +func _Encode_Update_Response(ctx context.Context, response interface{}) (interface{}, error) { + return &empty.Empty{}, nil +} + +func _Encode_Delete_Response(ctx context.Context, response interface{}) (interface{}, error) { + return &empty.Empty{}, nil +} + +func _Encode_SetAlias_Response(ctx context.Context, response interface{}) (interface{}, error) { + return &empty.Empty{}, nil +} + +func _Encode_RemoveAlias_Response(ctx context.Context, response interface{}) (interface{}, error) { + return &empty.Empty{}, nil +} + +func _Decode_Create_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil CreateRequest") + } + req := request.(*pb.CreateRequest) + reqEnv, err := ProtoToPtrEnvironment(req.Env) + if err != nil { + return nil, err + } + return &transport.CreateRequest{Env: reqEnv}, nil +} + +func _Decode_Get_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil GetRequest") + } + req := request.(*pb.GetRequest) + return &transport.GetRequest{ + EnvId: string(req.EnvId), + SpaceId: string(req.SpaceId), + }, nil +} + +func _Decode_List_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil ListRequest") + } + req := request.(*pb.ListRequest) + return &transport.ListRequest{SpaceId: string(req.SpaceId)}, nil +} + +func _Decode_Update_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil UpdateRequest") + } + req := request.(*pb.UpdateRequest) + reqEnv, err := ProtoToPtrEnvironment(req.Env) + if err != nil { + return nil, err + } + return &transport.UpdateRequest{Env: reqEnv}, nil +} + +func _Decode_Delete_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil DeleteRequest") + } + req := request.(*pb.DeleteRequest) + return &transport.DeleteRequest{ + EnvId: string(req.EnvId), + SpaceId: string(req.SpaceId), + }, nil +} + +func _Decode_SetAlias_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil SetAliasRequest") + } + req := request.(*pb.SetAliasRequest) + return &transport.SetAliasRequest{ + Alias: string(req.Alias), + EnvId: string(req.EnvId), + SpaceId: string(req.SpaceId), + }, nil +} + +func _Decode_RemoveAlias_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil RemoveAliasRequest") + } + req := request.(*pb.RemoveAliasRequest) + return &transport.RemoveAliasRequest{ + Alias: string(req.Alias), + EnvId: string(req.EnvId), + SpaceId: string(req.SpaceId), + }, nil +} + +func _Decode_Create_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil CreateResponse") + } + resp := response.(*pb.CreateResponse) + respCreated, err := ProtoToPtrEnvironment(resp.Created) + if err != nil { + return nil, err + } + return &transport.CreateResponse{Created: respCreated}, nil +} + +func _Decode_Get_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil GetResponse") + } + resp := response.(*pb.GetResponse) + respEnv, err := ProtoToPtrEnvironment(resp.Env) + if err != nil { + return nil, err + } + return &transport.GetResponse{Env: respEnv}, nil +} + +func _Decode_List_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil ListResponse") + } + resp := response.(*pb.ListResponse) + respEnvs, err := ProtoToListPtrEnvironment(resp.Envs) + if err != nil { + return nil, err + } + return &transport.ListResponse{Envs: respEnvs}, nil +} + +func _Decode_Update_Response(ctx context.Context, response interface{}) (interface{}, error) { + return &empty.Empty{}, nil +} + +func _Decode_Delete_Response(ctx context.Context, response interface{}) (interface{}, error) { + return &empty.Empty{}, nil +} + +func _Decode_SetAlias_Response(ctx context.Context, response interface{}) (interface{}, error) { + return &empty.Empty{}, nil +} + +func _Decode_RemoveAlias_Response(ctx context.Context, response interface{}) (interface{}, error) { + return &empty.Empty{}, nil +} + +func _Encode_Migrate_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil MigrateRequest") + } + req := request.(*transport.MigrateRequest) + opts, _ := ElPtrMigrateOptionsToProto(req.Options) + return &pb.MigrateRequest{ + EnvId: req.EnvId, + SpaceId: req.SpaceId, + Options: opts, + }, nil +} + +func _Encode_Migrate_Response(ctx context.Context, response interface{}) (interface{}, error) { + return &empty.Empty{}, nil +} + +func _Decode_Migrate_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil MigrateRequest") + } + req := request.(*pb.MigrateRequest) + opts, _ := ProtoToElPtrMigrateOptions(req.Options) + return &transport.MigrateRequest{ + EnvId: string(req.EnvId), + SpaceId: string(req.SpaceId), + Options: opts, + }, nil +} + +func _Decode_Migrate_Response(ctx context.Context, response interface{}) (interface{}, error) { + return &empty.Empty{}, nil +} diff --git a/pkg/environments/transport/grpc/protobuf_type_converters.microgen.go b/pkg/environments/transport/grpc/protobuf_type_converters.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..5402c513bc7ecbf3a7dc3b4a291282eed03f554e --- /dev/null +++ b/pkg/environments/transport/grpc/protobuf_type_converters.microgen.go @@ -0,0 +1,99 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +// It is better for you if you do not change functions names! +// This file will never be overwritten. +package transportgrpc + +import ( + pb "git.perx.ru/perxis/perxis-go/proto/environments" + service "git.perx.ru/perxis/perxis-go/pkg/environments" + "github.com/golang/protobuf/ptypes" +) + +func PtrEnvironmentToProto(env *service.Environment) (*pb.Environment, error) { + if env == nil { + return nil, nil + } + protoEnvironment := &pb.Environment{ + Id: env.ID, + SpaceId: env.SpaceID, + Description: env.Description, + Aliases: env.Aliases, + } + if env.StateInfo != nil { + protoEnvironment.StateInfo = &pb.StateInfo{ + State: pb.StateInfo_State(env.StateInfo.State), + Info: env.StateInfo.Info, + } + protoEnvironment.StateInfo.StartedAt, _ = ptypes.TimestampProto(env.StateInfo.StartedAt) + } + if env.Config != nil { + protoEnvironment.Config = &pb.Config{ + SourceId: env.Config.SourceID, + Features: env.Config.Features, + } + } + return protoEnvironment, nil +} + +func ProtoToPtrEnvironment(protoEnv *pb.Environment) (*service.Environment, error) { + if protoEnv == nil { + return nil, nil + } + env := &service.Environment{ + ID: protoEnv.Id, + SpaceID: protoEnv.SpaceId, + Description: protoEnv.Description, + Aliases: protoEnv.Aliases, + } + if protoEnv.StateInfo != nil { + env.StateInfo = &service.StateInfo{ + State: service.State(protoEnv.StateInfo.State), + Info: protoEnv.StateInfo.Info, + } + env.StateInfo.StartedAt, _ = ptypes.Timestamp(protoEnv.StateInfo.StartedAt) + } + if protoEnv.Config != nil { + env.Config = &service.Config{ + SourceID: protoEnv.Config.SourceId, + Features: protoEnv.Config.Features, + } + } + return env, nil +} + +func ListPtrEnvironmentToProto(envs []*service.Environment) ([]*pb.Environment, error) { + protoEnvironments := make([]*pb.Environment, 0, len(envs)) + for _, environment := range envs { + protoEnvironment, err := PtrEnvironmentToProto(environment) + if err != nil { + return nil, err + } + protoEnvironments = append(protoEnvironments, protoEnvironment) + } + return protoEnvironments, nil +} + +func ProtoToListPtrEnvironment(protoEnvs []*pb.Environment) ([]*service.Environment, error) { + environments := make([]*service.Environment, 0, len(protoEnvs)) + for _, protoEnvironment := range protoEnvs { + environment, err := ProtoToPtrEnvironment(protoEnvironment) + if err != nil { + return nil, err + } + environments = append(environments, environment) + } + return environments, nil +} + +func ElPtrMigrateOptionsToProto(options []*service.MigrateOptions) (*pb.MigrateOptions, error) { + opts := service.MergeMigrateOptions(options...) + return &pb.MigrateOptions{Wait: opts.Wait}, nil +} + +func ProtoToElPtrMigrateOptions(protoOptions *pb.MigrateOptions) ([]*service.MigrateOptions, error) { + if protoOptions == nil { + return nil, nil + } + return []*service.MigrateOptions{{Wait: protoOptions.Wait}}, nil +} diff --git a/pkg/environments/transport/grpc/server.microgen.go b/pkg/environments/transport/grpc/server.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..1389d32692eaf27f5068073d330696d1ad221bd3 --- /dev/null +++ b/pkg/environments/transport/grpc/server.microgen.go @@ -0,0 +1,142 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +// DO NOT EDIT. +package transportgrpc + +import ( + transport "git.perx.ru/perxis/perxis-go/pkg/environments/transport" + pb "git.perx.ru/perxis/perxis-go/proto/environments" + grpc "github.com/go-kit/kit/transport/grpc" + empty "github.com/golang/protobuf/ptypes/empty" + context "golang.org/x/net/context" +) + +type environmentsServer struct { + create grpc.Handler + get grpc.Handler + list grpc.Handler + update grpc.Handler + delete grpc.Handler + setAlias grpc.Handler + removeAlias grpc.Handler + migrate grpc.Handler + + pb.UnimplementedEnvironmentsServer +} + +func NewGRPCServer(endpoints *transport.EndpointsSet, opts ...grpc.ServerOption) pb.EnvironmentsServer { + return &environmentsServer{ + create: grpc.NewServer( + endpoints.CreateEndpoint, + _Decode_Create_Request, + _Encode_Create_Response, + opts..., + ), + delete: grpc.NewServer( + endpoints.DeleteEndpoint, + _Decode_Delete_Request, + _Encode_Delete_Response, + opts..., + ), + get: grpc.NewServer( + endpoints.GetEndpoint, + _Decode_Get_Request, + _Encode_Get_Response, + opts..., + ), + list: grpc.NewServer( + endpoints.ListEndpoint, + _Decode_List_Request, + _Encode_List_Response, + opts..., + ), + migrate: grpc.NewServer( + endpoints.MigrateEndpoint, + _Decode_Migrate_Request, + _Encode_Migrate_Response, + opts..., + ), + removeAlias: grpc.NewServer( + endpoints.RemoveAliasEndpoint, + _Decode_RemoveAlias_Request, + _Encode_RemoveAlias_Response, + opts..., + ), + setAlias: grpc.NewServer( + endpoints.SetAliasEndpoint, + _Decode_SetAlias_Request, + _Encode_SetAlias_Response, + opts..., + ), + update: grpc.NewServer( + endpoints.UpdateEndpoint, + _Decode_Update_Request, + _Encode_Update_Response, + opts..., + ), + } +} + +func (S *environmentsServer) Create(ctx context.Context, req *pb.CreateRequest) (*pb.CreateResponse, error) { + _, resp, err := S.create.ServeGRPC(ctx, req) + if err != nil { + return nil, err + } + return resp.(*pb.CreateResponse), nil +} + +func (S *environmentsServer) Get(ctx context.Context, req *pb.GetRequest) (*pb.GetResponse, error) { + _, resp, err := S.get.ServeGRPC(ctx, req) + if err != nil { + return nil, err + } + return resp.(*pb.GetResponse), nil +} + +func (S *environmentsServer) List(ctx context.Context, req *pb.ListRequest) (*pb.ListResponse, error) { + _, resp, err := S.list.ServeGRPC(ctx, req) + if err != nil { + return nil, err + } + return resp.(*pb.ListResponse), nil +} + +func (S *environmentsServer) Update(ctx context.Context, req *pb.UpdateRequest) (*empty.Empty, error) { + _, resp, err := S.update.ServeGRPC(ctx, req) + if err != nil { + return nil, err + } + return resp.(*empty.Empty), nil +} + +func (S *environmentsServer) Delete(ctx context.Context, req *pb.DeleteRequest) (*empty.Empty, error) { + _, resp, err := S.delete.ServeGRPC(ctx, req) + if err != nil { + return nil, err + } + return resp.(*empty.Empty), nil +} + +func (S *environmentsServer) SetAlias(ctx context.Context, req *pb.SetAliasRequest) (*empty.Empty, error) { + _, resp, err := S.setAlias.ServeGRPC(ctx, req) + if err != nil { + return nil, err + } + return resp.(*empty.Empty), nil +} + +func (S *environmentsServer) RemoveAlias(ctx context.Context, req *pb.RemoveAliasRequest) (*empty.Empty, error) { + _, resp, err := S.removeAlias.ServeGRPC(ctx, req) + if err != nil { + return nil, err + } + return resp.(*empty.Empty), nil +} + +func (S *environmentsServer) Migrate(ctx context.Context, req *pb.MigrateRequest) (*empty.Empty, error) { + _, resp, err := S.migrate.ServeGRPC(ctx, req) + if err != nil { + return nil, err + } + return resp.(*empty.Empty), nil +} diff --git a/pkg/environments/transport/server.microgen.go b/pkg/environments/transport/server.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..3a0b20a55cdf3aff82beb803efb59c5833e5afd7 --- /dev/null +++ b/pkg/environments/transport/server.microgen.go @@ -0,0 +1,88 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +package transport + +import ( + +"context" + + "git.perx.ru/perxis/perxis-go/pkg/environments" + endpoint "github.com/go-kit/kit/endpoint" +) + +func Endpoints(svc environments.Environments) EndpointsSet { + return EndpointsSet{ + CreateEndpoint: CreateEndpoint(svc), + DeleteEndpoint: DeleteEndpoint(svc), + GetEndpoint: GetEndpoint(svc), + ListEndpoint: ListEndpoint(svc), + MigrateEndpoint: MigrateEndpoint(svc), + RemoveAliasEndpoint: RemoveAliasEndpoint(svc), + SetAliasEndpoint: SetAliasEndpoint(svc), + UpdateEndpoint: UpdateEndpoint(svc), + } +} + +func CreateEndpoint(svc environments.Environments) endpoint.Endpoint { + return func(arg0 context.Context, request interface{}) (interface{}, error) { + req := request.(*CreateRequest) + res0, res1 := svc.Create(arg0, req.Env) + return &CreateResponse{Created: res0}, res1 + } +} + +func GetEndpoint(svc environments.Environments) endpoint.Endpoint { + return func(arg0 context.Context, request interface{}) (interface{}, error) { + req := request.(*GetRequest) + res0, res1 := svc.Get(arg0, req.SpaceId, req.EnvId) + return &GetResponse{Env: res0}, res1 + } +} + +func ListEndpoint(svc environments.Environments) endpoint.Endpoint { + return func(arg0 context.Context, request interface{}) (interface{}, error) { + req := request.(*ListRequest) + res0, res1 := svc.List(arg0, req.SpaceId) + return &ListResponse{Envs: res0}, res1 + } +} + +func UpdateEndpoint(svc environments.Environments) endpoint.Endpoint { + return func(arg0 context.Context, request interface{}) (interface{}, error) { + req := request.(*UpdateRequest) + res0 := svc.Update(arg0, req.Env) + return &UpdateResponse{}, res0 + } +} + +func DeleteEndpoint(svc environments.Environments) endpoint.Endpoint { + return func(arg0 context.Context, request interface{}) (interface{}, error) { + req := request.(*DeleteRequest) + res0 := svc.Delete(arg0, req.SpaceId, req.EnvId) + return &DeleteResponse{}, res0 + } +} + +func SetAliasEndpoint(svc environments.Environments) endpoint.Endpoint { + return func(arg0 context.Context, request interface{}) (interface{}, error) { + req := request.(*SetAliasRequest) + res0 := svc.SetAlias(arg0, req.SpaceId, req.EnvId, req.Alias) + return &SetAliasResponse{}, res0 + } +} + +func RemoveAliasEndpoint(svc environments.Environments) endpoint.Endpoint { + return func(arg0 context.Context, request interface{}) (interface{}, error) { + req := request.(*RemoveAliasRequest) + res0 := svc.RemoveAlias(arg0, req.SpaceId, req.EnvId, req.Alias) + return &RemoveAliasResponse{}, res0 + } +} + +func MigrateEndpoint(svc environments.Environments) endpoint.Endpoint { + return func(arg0 context.Context, request interface{}) (interface{}, error) { + req := request.(*MigrateRequest) + res0 := svc.Migrate(arg0, req.SpaceId, req.EnvId, req.Options...) + return &MigrateResponse{}, res0 + } +} diff --git a/pkg/invitations/invitation.go b/pkg/invitations/invitation.go new file mode 100644 index 0000000000000000000000000000000000000000..5dc5913774fc5774076823309abdf52b195c87c2 --- /dev/null +++ b/pkg/invitations/invitation.go @@ -0,0 +1,16 @@ +package invitations + +import "time" + +const InvitationTTL = 7 * 24 * time.Hour + +type Invitation struct { + ID string `bson:"_id"` + Email string `bson:"email"` + OrgID string `bson:"orgId"` + SpaceID string `bson:"spaceId"` + OwnerID string `bson:"ownerId"` // Invitation owner + Role string `bson:"role"` + CreatedAt *time.Time `bson:"createdAt"` + ValidUntil *time.Time `bson:"validUntil"` +} diff --git a/pkg/invitations/mocks/Invitations.go b/pkg/invitations/mocks/Invitations.go new file mode 100644 index 0000000000000000000000000000000000000000..610f9fff80d4867b35b80ff464bcf2a8a9d0e764 --- /dev/null +++ b/pkg/invitations/mocks/Invitations.go @@ -0,0 +1,120 @@ +// Code generated by mockery v2.7.4. DO NOT EDIT. + +package mocks + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/invitations" + "git.perx.ru/perxis/perxis-go/pkg/options" + "github.com/stretchr/testify/mock" +) + +// Invitations is an autogenerated mock type for the Invitations type +type Invitations struct { + mock.Mock +} + +// Accept provides a mock function with given fields: ctx, invitationId, userId +func (_m *Invitations) Accept(ctx context.Context, invitationId string, userId string) error { + ret := _m.Called(ctx, invitationId, userId) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, invitationId, userId) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Create provides a mock function with given fields: ctx, invitation +func (_m *Invitations) Create(ctx context.Context, invitation *invitations.Invitation) (*invitations.Invitation, error) { + ret := _m.Called(ctx, invitation) + + var r0 *invitations.Invitation + if rf, ok := ret.Get(0).(func(context.Context, *invitations.Invitation) *invitations.Invitation); ok { + r0 = rf(ctx, invitation) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*invitations.Invitation) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *invitations.Invitation) error); ok { + r1 = rf(ctx, invitation) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Delete provides a mock function with given fields: ctx, invitationId +func (_m *Invitations) Delete(ctx context.Context, invitationId string) error { + ret := _m.Called(ctx, invitationId) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, invitationId) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Find provides a mock function with given fields: ctx, filter, opts +func (_m *Invitations) Find(ctx context.Context, filter *invitations.Filter, opts *options.FindOptions) ([]*invitations.Invitation, int, error) { + ret := _m.Called(ctx, filter, opts) + + var r0 []*invitations.Invitation + if rf, ok := ret.Get(0).(func(context.Context, *invitations.Filter, *options.FindOptions) []*invitations.Invitation); ok { + r0 = rf(ctx, filter, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*invitations.Invitation) + } + } + + var r1 int + if rf, ok := ret.Get(1).(func(context.Context, *invitations.Filter, *options.FindOptions) int); ok { + r1 = rf(ctx, filter, opts) + } else { + r1 = ret.Get(1).(int) + } + + var r2 error + if rf, ok := ret.Get(2).(func(context.Context, *invitations.Filter, *options.FindOptions) error); ok { + r2 = rf(ctx, filter, opts) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// Get provides a mock function with given fields: ctx, invitationId +func (_m *Invitations) Get(ctx context.Context, invitationId string) (*invitations.Invitation, error) { + ret := _m.Called(ctx, invitationId) + + var r0 *invitations.Invitation + if rf, ok := ret.Get(0).(func(context.Context, string) *invitations.Invitation); ok { + r0 = rf(ctx, invitationId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*invitations.Invitation) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, invitationId) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/pkg/invitations/service.go b/pkg/invitations/service.go new file mode 100644 index 0000000000000000000000000000000000000000..26426d67462cdcb51fc7fe8f1bd60dc1fde69260 --- /dev/null +++ b/pkg/invitations/service.go @@ -0,0 +1,27 @@ +package invitations + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/options" +) + +type Filter struct { + ID []string + Email []string + OrgID []string + SpaceID []string + OwnerID []string + Role []string +} + +// @microgen grpc +// @protobuf git.perx.ru/perxis/perxis-go/proto/invitations +// @grpc-addr content.invitations.Invitations +type Invitations interface { + Create(ctx context.Context, invitation *Invitation) (created *Invitation, err error) + Get(ctx context.Context, invitationId string) (invitation *Invitation, err error) + Accept(ctx context.Context, invitationId, userId string) (err error) + Find(ctx context.Context, filter *Filter, opts *options.FindOptions) (invitations []*Invitation, total int, err error) + Delete(ctx context.Context, invitationId string) (err error) +} diff --git a/pkg/invitations/transport/client.microgen.go b/pkg/invitations/transport/client.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..6f896b0e67fa88a1eaa539c305a47fada11c9c8b --- /dev/null +++ b/pkg/invitations/transport/client.microgen.go @@ -0,0 +1,79 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +package transport + +import ( + "context" + "errors" + + invitations "git.perx.ru/perxis/perxis-go/pkg/invitations" + "git.perx.ru/perxis/perxis-go/pkg/options" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +func (set EndpointsSet) Create(arg0 context.Context, arg1 *invitations.Invitation) (res0 *invitations.Invitation, res1 error) { + request := CreateRequest{Invitation: arg1} + response, res1 := set.CreateEndpoint(arg0, &request) + if res1 != nil { + if e, ok := status.FromError(res1); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown { + res1 = errors.New(e.Message()) + } + return + } + return response.(*CreateResponse).Created, res1 +} + +func (set EndpointsSet) Get(arg0 context.Context, arg1 string) (res0 *invitations.Invitation, res1 error) { + request := GetRequest{InvitationId: arg1} + response, res1 := set.GetEndpoint(arg0, &request) + if res1 != nil { + if e, ok := status.FromError(res1); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown { + res1 = errors.New(e.Message()) + } + return + } + return response.(*GetResponse).Invitation, res1 +} + +func (set EndpointsSet) Accept(arg0 context.Context, arg1 string, arg2 string) (res0 error) { + request := AcceptRequest{ + InvitationId: arg1, + UserId: arg2, + } + _, res0 = set.AcceptEndpoint(arg0, &request) + if res0 != nil { + if e, ok := status.FromError(res0); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown { + res0 = errors.New(e.Message()) + } + return + } + return res0 +} + +func (set EndpointsSet) Find(arg0 context.Context, arg1 *invitations.Filter, arg2 *options.FindOptions) (res0 []*invitations.Invitation, res1 int, res2 error) { + request := FindRequest{ + Filter: arg1, + Opts: arg2, + } + response, res2 := set.FindEndpoint(arg0, &request) + if res2 != nil { + if e, ok := status.FromError(res2); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown { + res2 = errors.New(e.Message()) + } + return + } + return response.(*FindResponse).Invitations, response.(*FindResponse).Total, res2 +} + +func (set EndpointsSet) Delete(arg0 context.Context, arg1 string) (res0 error) { + request := DeleteRequest{InvitationId: arg1} + _, res0 = set.DeleteEndpoint(arg0, &request) + if res0 != nil { + if e, ok := status.FromError(res0); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown { + res0 = errors.New(e.Message()) + } + return + } + return res0 +} diff --git a/pkg/invitations/transport/endpoints.microgen.go b/pkg/invitations/transport/endpoints.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..70d78bbcf04e68ad97c36d90b4f66f3287f108d2 --- /dev/null +++ b/pkg/invitations/transport/endpoints.microgen.go @@ -0,0 +1,14 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +package transport + +import endpoint "github.com/go-kit/kit/endpoint" + +// EndpointsSet implements Invitations API and used for transport purposes. +type EndpointsSet struct { + CreateEndpoint endpoint.Endpoint + GetEndpoint endpoint.Endpoint + AcceptEndpoint endpoint.Endpoint + FindEndpoint endpoint.Endpoint + DeleteEndpoint endpoint.Endpoint +} diff --git a/pkg/invitations/transport/exchanges.microgen.go b/pkg/invitations/transport/exchanges.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..bb36e466cbcb25584c462931a3eab55ffd90732c --- /dev/null +++ b/pkg/invitations/transport/exchanges.microgen.go @@ -0,0 +1,46 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +package transport + +import ( + "git.perx.ru/perxis/perxis-go/pkg/invitations" + "git.perx.ru/perxis/perxis-go/pkg/options" +) + +type ( + CreateRequest struct { + Invitation *invitations.Invitation `json:"invitation"` + } + CreateResponse struct { + Created *invitations.Invitation `json:"created"` + } + + GetRequest struct { + InvitationId string `json:"invitation_id"` + } + GetResponse struct { + Invitation *invitations.Invitation `json:"invitation"` + } + + AcceptRequest struct { + InvitationId string `json:"invitation_id"` + UserId string `json:"user_id"` + } + // Formal exchange type, please do not delete. + AcceptResponse struct{} + + FindRequest struct { + Filter *invitations.Filter `json:"filter"` + Opts *options.FindOptions `json:"opts"` + } + FindResponse struct { + Invitations []*invitations.Invitation `json:"invitations"` + Total int `json:"total"` + } + + DeleteRequest struct { + InvitationId string `json:"invitation_id"` + } + // Formal exchange type, please do not delete. + DeleteResponse struct{} +) diff --git a/pkg/invitations/transport/grpc/client.microgen.go b/pkg/invitations/transport/grpc/client.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..e319e19f9d32b9d3efa1e0d6fe485372e177efa2 --- /dev/null +++ b/pkg/invitations/transport/grpc/client.microgen.go @@ -0,0 +1,54 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +package transportgrpc + +import ( + transport "git.perx.ru/perxis/perxis-go/pkg/invitations/transport" + pb "git.perx.ru/perxis/perxis-go/proto/invitations" + grpckit "github.com/go-kit/kit/transport/grpc" + empty "github.com/golang/protobuf/ptypes/empty" + grpc "google.golang.org/grpc" +) + +func NewGRPCClient(conn *grpc.ClientConn, addr string, opts ...grpckit.ClientOption) transport.EndpointsSet { + if addr == "" { + addr = "content.invitations.Invitations" + } + return transport.EndpointsSet{ + AcceptEndpoint: grpckit.NewClient( + conn, addr, "Accept", + _Encode_Accept_Request, + _Decode_Accept_Response, + empty.Empty{}, + opts..., + ).Endpoint(), + CreateEndpoint: grpckit.NewClient( + conn, addr, "Create", + _Encode_Create_Request, + _Decode_Create_Response, + pb.CreateResponse{}, + opts..., + ).Endpoint(), + DeleteEndpoint: grpckit.NewClient( + conn, addr, "Delete", + _Encode_Delete_Request, + _Decode_Delete_Response, + empty.Empty{}, + opts..., + ).Endpoint(), + FindEndpoint: grpckit.NewClient( + conn, addr, "Find", + _Encode_Find_Request, + _Decode_Find_Response, + pb.FindResponse{}, + opts..., + ).Endpoint(), + GetEndpoint: grpckit.NewClient( + conn, addr, "Get", + _Encode_Get_Request, + _Decode_Get_Response, + pb.GetResponse{}, + opts..., + ).Endpoint(), + } +} diff --git a/pkg/invitations/transport/grpc/protobuf_endpoint_converters.microgen.go b/pkg/invitations/transport/grpc/protobuf_endpoint_converters.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..85403cab8c13fb71612e275a2851c02a904d8abc --- /dev/null +++ b/pkg/invitations/transport/grpc/protobuf_endpoint_converters.microgen.go @@ -0,0 +1,223 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +// Please, do not change functions names! +package transportgrpc + +import ( + "context" + "errors" + + transport "git.perx.ru/perxis/perxis-go/pkg/invitations/transport" + pb "git.perx.ru/perxis/perxis-go/proto/invitations" + empty "github.com/golang/protobuf/ptypes/empty" +) + +func _Encode_Create_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil CreateRequest") + } + req := request.(*transport.CreateRequest) + pbInvitation, err := PtrInvitationToProto(req.Invitation) + if err != nil { + return nil, err + } + return &pb.CreateRequest{Invitation: pbInvitation}, nil +} + +func _Encode_Get_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil GetRequest") + } + req := request.(*transport.GetRequest) + return &pb.GetRequest{InvitationId: req.InvitationId}, nil +} + +func _Encode_Accept_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil AcceptRequest") + } + req := request.(*transport.AcceptRequest) + return &pb.AcceptRequest{ + InvitationId: req.InvitationId, + UserId: req.UserId, + }, nil +} + +func _Encode_Find_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil FindRequest") + } + req := request.(*transport.FindRequest) + reqFilter, err := PtrFilterToProto(req.Filter) + if err != nil { + return nil, err + } + reqOpts, err := PtrServicesFindOptionsToProto(req.Opts) + if err != nil { + return nil, err + } + return &pb.FindRequest{ + Filter: reqFilter, + Opts: reqOpts, + }, nil +} + +func _Encode_Delete_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil DeleteRequest") + } + req := request.(*transport.DeleteRequest) + return &pb.DeleteRequest{InvitationId: req.InvitationId}, nil +} + +func _Encode_Create_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil CreateResponse") + } + resp := response.(*transport.CreateResponse) + respInvitation, err := PtrInvitationToProto(resp.Created) + if err != nil { + return nil, err + } + return &pb.CreateResponse{Invitation: respInvitation}, nil +} + +func _Encode_Get_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil GetResponse") + } + resp := response.(*transport.GetResponse) + respInvitation, err := PtrInvitationToProto(resp.Invitation) + if err != nil { + return nil, err + } + return &pb.GetResponse{Invitation: respInvitation}, nil +} + +func _Encode_Accept_Response(ctx context.Context, response interface{}) (interface{}, error) { + return &empty.Empty{}, nil +} + +func _Encode_Find_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil FindResponse") + } + resp := response.(*transport.FindResponse) + respInvitations, err := ListPtrInvitationToProto(resp.Invitations) + if err != nil { + return nil, err + } + return &pb.FindResponse{ + Invitations: respInvitations, + Total: int64(resp.Total), + }, nil +} + +func _Encode_Delete_Response(ctx context.Context, response interface{}) (interface{}, error) { + return &empty.Empty{}, nil +} + +func _Decode_Create_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil CreateRequest") + } + req := request.(*pb.CreateRequest) + invitation, err := ProtoToPtrInvitation(req.Invitation) + if err != nil { + return nil, err + } + return &transport.CreateRequest{Invitation: invitation}, nil +} + +func _Decode_Get_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil GetRequest") + } + req := request.(*pb.GetRequest) + return &transport.GetRequest{InvitationId: string(req.InvitationId)}, nil +} + +func _Decode_Accept_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil AcceptRequest") + } + req := request.(*pb.AcceptRequest) + return &transport.AcceptRequest{ + InvitationId: string(req.InvitationId), + UserId: string(req.UserId), + }, nil +} + +func _Decode_Find_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil FindRequest") + } + req := request.(*pb.FindRequest) + reqFilter, err := ProtoToPtrFilter(req.Filter) + if err != nil { + return nil, err + } + reqOpts, err := ProtoToPtrServicesFindOptions(req.Opts) + if err != nil { + return nil, err + } + return &transport.FindRequest{ + Filter: reqFilter, + Opts: reqOpts, + }, nil +} + +func _Decode_Delete_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil DeleteRequest") + } + req := request.(*pb.DeleteRequest) + return &transport.DeleteRequest{InvitationId: string(req.InvitationId)}, nil +} + +func _Decode_Create_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil CreateResponse") + } + resp := response.(*pb.CreateResponse) + respInvitation, err := ProtoToPtrInvitation(resp.Invitation) + if err != nil { + return nil, err + } + return &transport.CreateResponse{Created: respInvitation}, nil +} + +func _Decode_Get_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil GetResponse") + } + resp := response.(*pb.GetResponse) + respInvitation, err := ProtoToPtrInvitation(resp.Invitation) + if err != nil { + return nil, err + } + return &transport.GetResponse{Invitation: respInvitation}, nil +} + +func _Decode_Accept_Response(ctx context.Context, response interface{}) (interface{}, error) { + return &empty.Empty{}, nil +} + +func _Decode_Find_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil FindResponse") + } + resp := response.(*pb.FindResponse) + respInvitations, err := ProtoToListPtrInvitation(resp.Invitations) + if err != nil { + return nil, err + } + return &transport.FindResponse{ + Invitations: respInvitations, + Total: int(resp.Total), + }, nil +} + +func _Decode_Delete_Response(ctx context.Context, response interface{}) (interface{}, error) { + return &empty.Empty{}, nil +} diff --git a/pkg/invitations/transport/grpc/protobuf_type_converters.microgen.go b/pkg/invitations/transport/grpc/protobuf_type_converters.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..9899e39885a8ca07de15f74f2eeda24d65296928 --- /dev/null +++ b/pkg/invitations/transport/grpc/protobuf_type_converters.microgen.go @@ -0,0 +1,161 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +// It is better for you if you do not change functions names! +// This file will never be overwritten. +package transportgrpc + +import ( + "time" + + service "git.perx.ru/perxis/perxis-go/pkg/invitations" + "git.perx.ru/perxis/perxis-go/pkg/options" + pb "git.perx.ru/perxis/perxis-go/proto/invitations" + "github.com/golang/protobuf/ptypes" + timestamp "github.com/golang/protobuf/ptypes/timestamp" +) + +func PtrTimeTimeToProto(validUntil *time.Time) (*timestamp.Timestamp, error) { + if validUntil == nil { + return nil, nil + } + t, err := ptypes.TimestampProto(*validUntil) + if err != nil { + return nil, err + } + return t, err +} + +func ProtoToPtrTimeTime(protoValidUntil *timestamp.Timestamp) (*time.Time, error) { + if protoValidUntil == nil { + return nil, nil + } + t, err := ptypes.Timestamp(protoValidUntil) + if err != nil { + return nil, err + } + return &t, nil +} + +func PtrInvitationToProto(invitation *service.Invitation) (*pb.Invitation, error) { + if invitation == nil { + return nil, nil + } + pi := &pb.Invitation{ + Id: invitation.ID, + Email: invitation.Email, + OrgId: invitation.OrgID, + SpaceId: invitation.SpaceID, + OwnerId: invitation.OwnerID, + Role: invitation.Role, + } + if invitation.CreatedAt != nil && !invitation.CreatedAt.IsZero() { + t, _ := ptypes.TimestampProto(*invitation.CreatedAt) + pi.CreatedAt = t + } + if invitation.ValidUntil != nil && !invitation.ValidUntil.IsZero() { + t, _ := ptypes.TimestampProto(*invitation.ValidUntil) + pi.ValidUntil = t + } + return pi, nil +} + +func ProtoToPtrInvitation(protoInvitation *pb.Invitation) (*service.Invitation, error) { + if protoInvitation == nil { + return nil, nil + } + i := &service.Invitation{ + ID: protoInvitation.Id, + Email: protoInvitation.Email, + OrgID: protoInvitation.OrgId, + SpaceID: protoInvitation.SpaceId, + OwnerID: protoInvitation.OwnerId, + Role: protoInvitation.Role, + } + if protoInvitation.CreatedAt != nil { + t, _ := ptypes.Timestamp(protoInvitation.CreatedAt) + i.CreatedAt = &t + } + if protoInvitation.ValidUntil != nil { + t, _ := ptypes.Timestamp(protoInvitation.ValidUntil) + i.ValidUntil = &t + } + return i, nil +} + +func PtrFilterToProto(filter *service.Filter) (*pb.Filter, error) { + if filter == nil { + return nil, nil + } + return &pb.Filter{ + Id: filter.ID, + Email: filter.Email, + OrgId: filter.OrgID, + SpaceId: filter.SpaceID, + OwnerId: filter.OwnerID, + Role: filter.Role, + }, nil +} + +func ProtoToPtrFilter(protoFilter *pb.Filter) (*service.Filter, error) { + if protoFilter == nil { + return nil, nil + } + return &service.Filter{ + ID: protoFilter.Id, + Email: protoFilter.Email, + OrgID: protoFilter.OrgId, + SpaceID: protoFilter.SpaceId, + OwnerID: protoFilter.OwnerId, + Role: protoFilter.Role, + }, nil +} + +func PtrServicesFindOptionsToProto(opts *options.FindOptions) (*pb.FindOptions, error) { + if opts == nil { + return nil, nil + } + return &pb.FindOptions{ + Sort: opts.Sort, + PageNum: int32(opts.PageNum), + PageSize: int32(opts.PageSize), + }, nil +} + +func ProtoToPtrServicesFindOptions(protoOpts *pb.FindOptions) (*options.FindOptions, error) { + if protoOpts == nil { + return nil, nil + } + return &options.FindOptions{ + SortOptions: options.SortOptions{ + Sort: protoOpts.Sort, + }, + PaginationOptions: options.PaginationOptions{ + PageNum: int(protoOpts.PageNum), + PageSize: int(protoOpts.PageSize), + }, + }, nil +} + +func ListPtrInvitationToProto(invitations []*service.Invitation) ([]*pb.Invitation, error) { + protoInvitations := make([]*pb.Invitation, 0, len(invitations)) + for _, i := range invitations { + pi, err := PtrInvitationToProto(i) + if err != nil { + return nil, err + } + protoInvitations = append(protoInvitations, pi) + } + return protoInvitations, nil +} + +func ProtoToListPtrInvitation(protoInvitations []*pb.Invitation) ([]*service.Invitation, error) { + invitations := make([]*service.Invitation, 0, len(protoInvitations)) + for _, pi := range protoInvitations { + p, err := ProtoToPtrInvitation(pi) + if err != nil { + return nil, err + } + invitations = append(invitations, p) + } + return invitations, nil +} diff --git a/pkg/invitations/transport/grpc/server.microgen.go b/pkg/invitations/transport/grpc/server.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..fa7cf241737173d3c41cadaa2ae9fa17ecb43fe7 --- /dev/null +++ b/pkg/invitations/transport/grpc/server.microgen.go @@ -0,0 +1,97 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +// DO NOT EDIT. +package transportgrpc + +import ( + transport "git.perx.ru/perxis/perxis-go/pkg/invitations/transport" + pb "git.perx.ru/perxis/perxis-go/proto/invitations" + grpc "github.com/go-kit/kit/transport/grpc" + empty "github.com/golang/protobuf/ptypes/empty" + context "golang.org/x/net/context" +) + +type invitationsServer struct { + create grpc.Handler + get grpc.Handler + accept grpc.Handler + find grpc.Handler + delete grpc.Handler + + pb.UnimplementedInvitationsServer +} + +func NewGRPCServer(endpoints *transport.EndpointsSet, opts ...grpc.ServerOption) pb.InvitationsServer { + return &invitationsServer{ + accept: grpc.NewServer( + endpoints.AcceptEndpoint, + _Decode_Accept_Request, + _Encode_Accept_Response, + opts..., + ), + create: grpc.NewServer( + endpoints.CreateEndpoint, + _Decode_Create_Request, + _Encode_Create_Response, + opts..., + ), + delete: grpc.NewServer( + endpoints.DeleteEndpoint, + _Decode_Delete_Request, + _Encode_Delete_Response, + opts..., + ), + find: grpc.NewServer( + endpoints.FindEndpoint, + _Decode_Find_Request, + _Encode_Find_Response, + opts..., + ), + get: grpc.NewServer( + endpoints.GetEndpoint, + _Decode_Get_Request, + _Encode_Get_Response, + opts..., + ), + } +} + +func (S *invitationsServer) Create(ctx context.Context, req *pb.CreateRequest) (*pb.CreateResponse, error) { + _, resp, err := S.create.ServeGRPC(ctx, req) + if err != nil { + return nil, err + } + return resp.(*pb.CreateResponse), nil +} + +func (S *invitationsServer) Get(ctx context.Context, req *pb.GetRequest) (*pb.GetResponse, error) { + _, resp, err := S.get.ServeGRPC(ctx, req) + if err != nil { + return nil, err + } + return resp.(*pb.GetResponse), nil +} + +func (S *invitationsServer) Accept(ctx context.Context, req *pb.AcceptRequest) (*empty.Empty, error) { + _, resp, err := S.accept.ServeGRPC(ctx, req) + if err != nil { + return nil, err + } + return resp.(*empty.Empty), nil +} + +func (S *invitationsServer) Find(ctx context.Context, req *pb.FindRequest) (*pb.FindResponse, error) { + _, resp, err := S.find.ServeGRPC(ctx, req) + if err != nil { + return nil, err + } + return resp.(*pb.FindResponse), nil +} + +func (S *invitationsServer) Delete(ctx context.Context, req *pb.DeleteRequest) (*empty.Empty, error) { + _, resp, err := S.delete.ServeGRPC(ctx, req) + if err != nil { + return nil, err + } + return resp.(*empty.Empty), nil +} diff --git a/pkg/invitations/transport/server.microgen.go b/pkg/invitations/transport/server.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..326659f2b813d8f6b306d8b9498c0590c6434011 --- /dev/null +++ b/pkg/invitations/transport/server.microgen.go @@ -0,0 +1,63 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +package transport + +import ( + "context" + + invitations "git.perx.ru/perxis/perxis-go/pkg/invitations" + endpoint "github.com/go-kit/kit/endpoint" +) + +func Endpoints(svc invitations.Invitations) EndpointsSet { + return EndpointsSet{ + AcceptEndpoint: AcceptEndpoint(svc), + CreateEndpoint: CreateEndpoint(svc), + DeleteEndpoint: DeleteEndpoint(svc), + FindEndpoint: FindEndpoint(svc), + GetEndpoint: GetEndpoint(svc), + } +} + +func CreateEndpoint(svc invitations.Invitations) endpoint.Endpoint { + return func(arg0 context.Context, request interface{}) (interface{}, error) { + req := request.(*CreateRequest) + res0, res1 := svc.Create(arg0, req.Invitation) + return &CreateResponse{Created: res0}, res1 + } +} + +func GetEndpoint(svc invitations.Invitations) endpoint.Endpoint { + return func(arg0 context.Context, request interface{}) (interface{}, error) { + req := request.(*GetRequest) + res0, res1 := svc.Get(arg0, req.InvitationId) + return &GetResponse{Invitation: res0}, res1 + } +} + +func AcceptEndpoint(svc invitations.Invitations) endpoint.Endpoint { + return func(arg0 context.Context, request interface{}) (interface{}, error) { + req := request.(*AcceptRequest) + res0 := svc.Accept(arg0, req.InvitationId, req.UserId) + return &AcceptResponse{}, res0 + } +} + +func FindEndpoint(svc invitations.Invitations) endpoint.Endpoint { + return func(arg0 context.Context, request interface{}) (interface{}, error) { + req := request.(*FindRequest) + res0, res1, res2 := svc.Find(arg0, req.Filter, req.Opts) + return &FindResponse{ + Invitations: res0, + Total: res1, + }, res2 + } +} + +func DeleteEndpoint(svc invitations.Invitations) endpoint.Endpoint { + return func(arg0 context.Context, request interface{}) (interface{}, error) { + req := request.(*DeleteRequest) + res0 := svc.Delete(arg0, req.InvitationId) + return &DeleteResponse{}, res0 + } +} diff --git a/pkg/members/members.go b/pkg/members/members.go new file mode 100644 index 0000000000000000000000000000000000000000..0993b6fddfe34cf7f257e6b70433893485a60dba --- /dev/null +++ b/pkg/members/members.go @@ -0,0 +1,35 @@ +package members + +import ( + "fmt" +) + +type Member struct { + OrgId string `bson:"orgId"` + UserId string `bson:"userId"` + Role Role `bson:"role"` +} + +type Role uint + +const ( + NotMember Role = iota + RoleMember + RoleOwner + RoleAdmin +) + +func (r Role) IsPrivileged() bool { + return r == RoleOwner || r == RoleAdmin +} + +func (r Role) Format(s fmt.State, verb rune) { + switch r { + case RoleOwner: + fmt.Fprint(s, "owner") + case RoleAdmin: + fmt.Fprint(s, "admin") + case RoleMember: + fmt.Fprint(s, "member") + } +} diff --git a/pkg/members/mocks/Members.go b/pkg/members/mocks/Members.go new file mode 100644 index 0000000000000000000000000000000000000000..2adeaac705bf18a17eb06f7932d5d3769de1b50f --- /dev/null +++ b/pkg/members/mocks/Members.go @@ -0,0 +1,124 @@ +// Code generated by mockery v2.7.4. DO NOT EDIT. + +package mocks + +import ( + context "context" + + members "git.perx.ru/perxis/perxis-go/pkg/members" + mock "github.com/stretchr/testify/mock" +) + +// Members is an autogenerated mock type for the Members type +type Members struct { + mock.Mock +} + +// Get provides a mock function with given fields: ctx, orgId, userId +func (_m *Members) Get(ctx context.Context, orgId string, userId string) (members.Role, error) { + ret := _m.Called(ctx, orgId, userId) + + var r0 members.Role + if rf, ok := ret.Get(0).(func(context.Context, string, string) members.Role); ok { + r0 = rf(ctx, orgId, userId) + } else { + r0 = ret.Get(0).(members.Role) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, orgId, userId) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListMembers provides a mock function with given fields: ctx, orgId +func (_m *Members) ListMembers(ctx context.Context, orgId string) ([]*members.Member, error) { + ret := _m.Called(ctx, orgId) + + var r0 []*members.Member + if rf, ok := ret.Get(0).(func(context.Context, string) []*members.Member); ok { + r0 = rf(ctx, orgId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*members.Member) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, orgId) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListOrganizations provides a mock function with given fields: ctx, userId +func (_m *Members) ListOrganizations(ctx context.Context, userId string) ([]*members.Member, error) { + ret := _m.Called(ctx, userId) + + var r0 []*members.Member + if rf, ok := ret.Get(0).(func(context.Context, string) []*members.Member); ok { + r0 = rf(ctx, userId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*members.Member) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, userId) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Remove provides a mock function with given fields: ctx, orgId, userId +func (_m *Members) Remove(ctx context.Context, orgId string, userId string) error { + ret := _m.Called(ctx, orgId, userId) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, orgId, userId) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// RemoveAll provides a mock function with given fields: ctx, orgId +func (_m *Members) RemoveAll(ctx context.Context, orgId string) error { + ret := _m.Called(ctx, orgId) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, orgId) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Set provides a mock function with given fields: ctx, orgId, userId, role +func (_m *Members) Set(ctx context.Context, orgId string, userId string, role members.Role) error { + ret := _m.Called(ctx, orgId, userId, role) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, members.Role) error); ok { + r0 = rf(ctx, orgId, userId, role) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/pkg/members/service.go b/pkg/members/service.go new file mode 100644 index 0000000000000000000000000000000000000000..5b88239b9b8509d8c1f65ca1a1a353cfe7281503 --- /dev/null +++ b/pkg/members/service.go @@ -0,0 +1,23 @@ +package members + +import ( + "context" +) + +// @microgen grpc +// @protobuf git.perx.ru/perxis/perxis-go/proto/members +// @grpc-addr account.members.Members +type Members interface { + Set(ctx context.Context, orgId, userId string, role Role) (err error) + + Get(ctx context.Context, orgId, userId string) (role Role, err error) + + Remove(ctx context.Context, orgId, userId string) (err error) + + // @microgen - + RemoveAll(ctx context.Context, orgId string) (err error) + + ListMembers(ctx context.Context, orgId string) (members []*Member, err error) + + ListOrganizations(ctx context.Context, userId string) (organizations []*Member, err error) +} diff --git a/pkg/members/transport/client.microgen.go b/pkg/members/transport/client.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..4cd10cad5b1b970f30a9380eae77a5d706748a08 --- /dev/null +++ b/pkg/members/transport/client.microgen.go @@ -0,0 +1,86 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +package transport + +import ( + "context" + "errors" + + members "git.perx.ru/perxis/perxis-go/pkg/members" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +func (set EndpointsSet) Set(arg0 context.Context, arg1 string, arg2 string, arg3 members.Role) (res0 error) { + request := SetRequest{ + OrgId: arg1, + Role: arg3, + UserId: arg2, + } + _, res0 = set.SetEndpoint(arg0, &request) + if res0 != nil { + if e, ok := status.FromError(res0); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown { + res0 = errors.New(e.Message()) + } + return + } + return res0 +} + +func (set EndpointsSet) Get(arg0 context.Context, arg1 string, arg2 string) (res0 members.Role, res1 error) { + request := GetRequest{ + OrgId: arg1, + UserId: arg2, + } + response, res1 := set.GetEndpoint(arg0, &request) + if res1 != nil { + if e, ok := status.FromError(res1); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown { + res1 = errors.New(e.Message()) + } + return + } + return response.(*GetResponse).Role, res1 +} + +func (set EndpointsSet) Remove(arg0 context.Context, arg1 string, arg2 string) (res0 error) { + request := RemoveRequest{ + OrgId: arg1, + UserId: arg2, + } + _, res0 = set.RemoveEndpoint(arg0, &request) + if res0 != nil { + if e, ok := status.FromError(res0); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown { + res0 = errors.New(e.Message()) + } + return + } + return res0 +} + +func (set EndpointsSet) RemoveAll(arg0 context.Context, arg1 string) (res0 error) { + return +} + +func (set EndpointsSet) ListMembers(arg0 context.Context, arg1 string) (res0 []*members.Member, res1 error) { + request := ListMembersRequest{OrgId: arg1} + response, res1 := set.ListMembersEndpoint(arg0, &request) + if res1 != nil { + if e, ok := status.FromError(res1); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown { + res1 = errors.New(e.Message()) + } + return + } + return response.(*ListMembersResponse).Members, res1 +} + +func (set EndpointsSet) ListOrganizations(arg0 context.Context, arg1 string) (res0 []*members.Member, res1 error) { + request := ListOrganizationsRequest{UserId: arg1} + response, res1 := set.ListOrganizationsEndpoint(arg0, &request) + if res1 != nil { + if e, ok := status.FromError(res1); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown { + res1 = errors.New(e.Message()) + } + return + } + return response.(*ListOrganizationsResponse).Organizations, res1 +} diff --git a/pkg/members/transport/endpoints.microgen.go b/pkg/members/transport/endpoints.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..88740bb07c63a46ff111527fdeae40dfc89a6167 --- /dev/null +++ b/pkg/members/transport/endpoints.microgen.go @@ -0,0 +1,14 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +package transport + +import endpoint "github.com/go-kit/kit/endpoint" + +// EndpointsSet implements Members API and used for transport purposes. +type EndpointsSet struct { + SetEndpoint endpoint.Endpoint + GetEndpoint endpoint.Endpoint + RemoveEndpoint endpoint.Endpoint + ListMembersEndpoint endpoint.Endpoint + ListOrganizationsEndpoint endpoint.Endpoint +} diff --git a/pkg/members/transport/exchanges.microgen.go b/pkg/members/transport/exchanges.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..afa03b65ed508022e89565e2f0881e6afb3c5678 --- /dev/null +++ b/pkg/members/transport/exchanges.microgen.go @@ -0,0 +1,44 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +package transport + +import members "git.perx.ru/perxis/perxis-go/pkg/members" + +type ( + SetRequest struct { + OrgId string `json:"org_id"` + UserId string `json:"user_id"` + Role members.Role `json:"role"` + } + // Formal exchange type, please do not delete. + SetResponse struct{} + + GetRequest struct { + OrgId string `json:"org_id"` + UserId string `json:"user_id"` + } + GetResponse struct { + Role members.Role `json:"role"` + } + + RemoveRequest struct { + OrgId string `json:"org_id"` + UserId string `json:"user_id"` + } + // Formal exchange type, please do not delete. + RemoveResponse struct{} + + ListMembersRequest struct { + OrgId string `json:"org_id"` + } + ListMembersResponse struct { + Members []*members.Member `json:"members"` + } + + ListOrganizationsRequest struct { + UserId string `json:"user_id"` + } + ListOrganizationsResponse struct { + Organizations []*members.Member `json:"organizations"` + } +) diff --git a/pkg/members/transport/grpc/client.microgen.go b/pkg/members/transport/grpc/client.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..6df6b7951b031d7125b2a93174cf38117671ec7a --- /dev/null +++ b/pkg/members/transport/grpc/client.microgen.go @@ -0,0 +1,54 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +package transportgrpc + +import ( + transport "git.perx.ru/perxis/perxis-go/pkg/members/transport" + pb "git.perx.ru/perxis/perxis-go/proto/members" + grpckit "github.com/go-kit/kit/transport/grpc" + empty "github.com/golang/protobuf/ptypes/empty" + grpc "google.golang.org/grpc" +) + +func NewGRPCClient(conn *grpc.ClientConn, addr string, opts ...grpckit.ClientOption) transport.EndpointsSet { + if addr == "" { + addr = "account.members.Members" + } + return transport.EndpointsSet{ + GetEndpoint: grpckit.NewClient( + conn, addr, "Get", + _Encode_Get_Request, + _Decode_Get_Response, + pb.GetResponse{}, + opts..., + ).Endpoint(), + ListMembersEndpoint: grpckit.NewClient( + conn, addr, "ListMembers", + _Encode_ListMembers_Request, + _Decode_ListMembers_Response, + pb.ListMembersResponse{}, + opts..., + ).Endpoint(), + ListOrganizationsEndpoint: grpckit.NewClient( + conn, addr, "ListOrganizations", + _Encode_ListOrganizations_Request, + _Decode_ListOrganizations_Response, + pb.ListOrganizationsResponse{}, + opts..., + ).Endpoint(), + RemoveEndpoint: grpckit.NewClient( + conn, addr, "Remove", + _Encode_Remove_Request, + _Decode_Remove_Response, + empty.Empty{}, + opts..., + ).Endpoint(), + SetEndpoint: grpckit.NewClient( + conn, addr, "Set", + _Encode_Set_Request, + _Decode_Set_Response, + empty.Empty{}, + opts..., + ).Endpoint(), + } +} diff --git a/pkg/members/transport/grpc/protobuf_endpoint_converters.microgen.go b/pkg/members/transport/grpc/protobuf_endpoint_converters.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..9ee491a71335a89beb14582f02b8b26fd3a264d6 --- /dev/null +++ b/pkg/members/transport/grpc/protobuf_endpoint_converters.microgen.go @@ -0,0 +1,209 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +// Please, do not change functions names! +package transportgrpc + +import ( + "context" + "errors" + + "git.perx.ru/perxis/perxis-go/pkg/members/transport" + "git.perx.ru/perxis/perxis-go/proto/members" + "github.com/golang/protobuf/ptypes/empty" +) + +func _Encode_Set_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil SetRequest") + } + req := request.(*transport.SetRequest) + reqRole, err := RoleToProto(req.Role) + if err != nil { + return nil, err + } + return &members.SetRequest{ + OrgId: req.OrgId, + Role: reqRole, + UserId: req.UserId, + }, nil +} + +func _Encode_Get_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil GetRequest") + } + req := request.(*transport.GetRequest) + return &members.GetRequest{ + OrgId: req.OrgId, + UserId: req.UserId, + }, nil +} + +func _Encode_Remove_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil RemoveRequest") + } + req := request.(*transport.RemoveRequest) + return &members.RemoveRequest{ + OrgId: req.OrgId, + UserId: req.UserId, + }, nil +} + +func _Encode_ListMembers_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil ListMembersRequest") + } + req := request.(*transport.ListMembersRequest) + return &members.ListMembersRequest{OrgId: req.OrgId}, nil +} + +func _Encode_ListOrganizations_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil ListOrganizationsRequest") + } + req := request.(*transport.ListOrganizationsRequest) + return &members.ListOrganizationsRequest{UserId: req.UserId}, nil +} + +func _Encode_Set_Response(ctx context.Context, response interface{}) (interface{}, error) { + return &empty.Empty{}, nil +} + +func _Encode_Get_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil GetResponse") + } + resp := response.(*transport.GetResponse) + respRole, err := RoleToProto(resp.Role) + if err != nil { + return nil, err + } + return &members.GetResponse{Role: respRole}, nil +} + +func _Encode_Remove_Response(ctx context.Context, response interface{}) (interface{}, error) { + return &empty.Empty{}, nil +} + +func _Encode_ListMembers_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil ListMembersResponse") + } + resp := response.(*transport.ListMembersResponse) + respMembers, err := ListPtrMemberToProto(resp.Members) + if err != nil { + return nil, err + } + return &members.ListMembersResponse{Members: respMembers}, nil +} + +func _Encode_ListOrganizations_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil ListOrganizationsResponse") + } + resp := response.(*transport.ListOrganizationsResponse) + respOrganizations, err := ListPtrMemberToProto(resp.Organizations) + if err != nil { + return nil, err + } + return &members.ListOrganizationsResponse{Organizations: respOrganizations}, nil +} + +func _Decode_Set_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil SetRequest") + } + req := request.(*members.SetRequest) + reqRole, err := ProtoToRole(req.Role) + if err != nil { + return nil, err + } + return &transport.SetRequest{ + OrgId: string(req.OrgId), + Role: reqRole, + UserId: string(req.UserId), + }, nil +} + +func _Decode_Get_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil GetRequest") + } + req := request.(*members.GetRequest) + return &transport.GetRequest{ + OrgId: string(req.OrgId), + UserId: string(req.UserId), + }, nil +} + +func _Decode_Remove_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil RemoveRequest") + } + req := request.(*members.RemoveRequest) + return &transport.RemoveRequest{ + OrgId: string(req.OrgId), + UserId: string(req.UserId), + }, nil +} + +func _Decode_ListMembers_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil ListMembersRequest") + } + req := request.(*members.ListMembersRequest) + return &transport.ListMembersRequest{OrgId: string(req.OrgId)}, nil +} + +func _Decode_ListOrganizations_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil ListOrganizationsRequest") + } + req := request.(*members.ListOrganizationsRequest) + return &transport.ListOrganizationsRequest{UserId: string(req.UserId)}, nil +} + +func _Decode_Set_Response(ctx context.Context, response interface{}) (interface{}, error) { + return &empty.Empty{}, nil +} + +func _Decode_Get_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil GetResponse") + } + resp := response.(*members.GetResponse) + respRole, err := ProtoToRole(resp.Role) + if err != nil { + return nil, err + } + return &transport.GetResponse{Role: respRole}, nil +} + +func _Decode_Remove_Response(ctx context.Context, response interface{}) (interface{}, error) { + return &empty.Empty{}, nil +} + +func _Decode_ListMembers_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil ListMembersResponse") + } + resp := response.(*members.ListMembersResponse) + respMembers, err := ProtoToListPtrMember(resp.Members) + if err != nil { + return nil, err + } + return &transport.ListMembersResponse{Members: respMembers}, nil +} + +func _Decode_ListOrganizations_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil ListOrganizationsResponse") + } + resp := response.(*members.ListOrganizationsResponse) + respOrganizations, err := ProtoToListPtrMember(resp.Organizations) + if err != nil { + return nil, err + } + return &transport.ListOrganizationsResponse{Organizations: respOrganizations}, nil +} diff --git a/pkg/members/transport/grpc/protobuf_type_converters.microgen.go b/pkg/members/transport/grpc/protobuf_type_converters.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..70d4628a570042ecad26a9adc557f948a8098778 --- /dev/null +++ b/pkg/members/transport/grpc/protobuf_type_converters.microgen.go @@ -0,0 +1,42 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +// It is better for you if you do not change functions names! +// This file will never be overwritten. +package transportgrpc + +import ( + service "git.perx.ru/perxis/perxis-go/pkg/members" + pb "git.perx.ru/perxis/perxis-go/proto/members" +) + +func RoleToProto(role service.Role) (pb.Role, error) { + return pb.Role(role), nil +} + +func ProtoToRole(protoRole pb.Role) (service.Role, error) { + return service.Role(protoRole), nil +} + +func ListPtrMemberToProto(members []*service.Member) ([]*pb.Member, error) { + res := make([]*pb.Member, 0, len(members)) + for _, m := range members { + res = append(res, &pb.Member{ + OrgId: m.OrgId, + UserId: m.UserId, + Role: pb.Role(m.Role), + }) + } + return res, nil +} + +func ProtoToListPtrMember(protoMembers []*pb.Member) ([]*service.Member, error) { + res := make([]*service.Member, 0, len(protoMembers)) + for _, m := range protoMembers { + res = append(res, &service.Member{ + OrgId: m.OrgId, + UserId: m.UserId, + Role: service.Role(m.Role), + }) + } + return res, nil +} diff --git a/pkg/members/transport/grpc/server.microgen.go b/pkg/members/transport/grpc/server.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..1d11e1c0990df144d99f511d20b4f844d4431b7a --- /dev/null +++ b/pkg/members/transport/grpc/server.microgen.go @@ -0,0 +1,97 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +// DO NOT EDIT. +package transportgrpc + +import ( + transport "git.perx.ru/perxis/perxis-go/pkg/members/transport" + pb "git.perx.ru/perxis/perxis-go/proto/members" + grpc "github.com/go-kit/kit/transport/grpc" + empty "github.com/golang/protobuf/ptypes/empty" + context "golang.org/x/net/context" +) + +type membersServer struct { + set grpc.Handler + get grpc.Handler + remove grpc.Handler + listMembers grpc.Handler + listOrganizations grpc.Handler + + pb.UnimplementedMembersServer +} + +func NewGRPCServer(endpoints *transport.EndpointsSet, opts ...grpc.ServerOption) pb.MembersServer { + return &membersServer{ + get: grpc.NewServer( + endpoints.GetEndpoint, + _Decode_Get_Request, + _Encode_Get_Response, + opts..., + ), + listMembers: grpc.NewServer( + endpoints.ListMembersEndpoint, + _Decode_ListMembers_Request, + _Encode_ListMembers_Response, + opts..., + ), + listOrganizations: grpc.NewServer( + endpoints.ListOrganizationsEndpoint, + _Decode_ListOrganizations_Request, + _Encode_ListOrganizations_Response, + opts..., + ), + remove: grpc.NewServer( + endpoints.RemoveEndpoint, + _Decode_Remove_Request, + _Encode_Remove_Response, + opts..., + ), + set: grpc.NewServer( + endpoints.SetEndpoint, + _Decode_Set_Request, + _Encode_Set_Response, + opts..., + ), + } +} + +func (S *membersServer) Set(ctx context.Context, req *pb.SetRequest) (*empty.Empty, error) { + _, resp, err := S.set.ServeGRPC(ctx, req) + if err != nil { + return nil, err + } + return resp.(*empty.Empty), nil +} + +func (S *membersServer) Get(ctx context.Context, req *pb.GetRequest) (*pb.GetResponse, error) { + _, resp, err := S.get.ServeGRPC(ctx, req) + if err != nil { + return nil, err + } + return resp.(*pb.GetResponse), nil +} + +func (S *membersServer) Remove(ctx context.Context, req *pb.RemoveRequest) (*empty.Empty, error) { + _, resp, err := S.remove.ServeGRPC(ctx, req) + if err != nil { + return nil, err + } + return resp.(*empty.Empty), nil +} + +func (S *membersServer) ListMembers(ctx context.Context, req *pb.ListMembersRequest) (*pb.ListMembersResponse, error) { + _, resp, err := S.listMembers.ServeGRPC(ctx, req) + if err != nil { + return nil, err + } + return resp.(*pb.ListMembersResponse), nil +} + +func (S *membersServer) ListOrganizations(ctx context.Context, req *pb.ListOrganizationsRequest) (*pb.ListOrganizationsResponse, error) { + _, resp, err := S.listOrganizations.ServeGRPC(ctx, req) + if err != nil { + return nil, err + } + return resp.(*pb.ListOrganizationsResponse), nil +} diff --git a/pkg/members/transport/server.microgen.go b/pkg/members/transport/server.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..b1c40ac3039b2ce46685880e93651f20d4fe7d16 --- /dev/null +++ b/pkg/members/transport/server.microgen.go @@ -0,0 +1,60 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +package transport + +import ( + "context" + + members "git.perx.ru/perxis/perxis-go/pkg/members" + endpoint "github.com/go-kit/kit/endpoint" +) + +func Endpoints(svc members.Members) EndpointsSet { + return EndpointsSet{ + GetEndpoint: GetEndpoint(svc), + ListMembersEndpoint: ListMembersEndpoint(svc), + ListOrganizationsEndpoint: ListOrganizationsEndpoint(svc), + RemoveEndpoint: RemoveEndpoint(svc), + SetEndpoint: SetEndpoint(svc), + } +} + +func SetEndpoint(svc members.Members) endpoint.Endpoint { + return func(arg0 context.Context, request interface{}) (interface{}, error) { + req := request.(*SetRequest) + res0 := svc.Set(arg0, req.OrgId, req.UserId, req.Role) + return &SetResponse{}, res0 + } +} + +func GetEndpoint(svc members.Members) endpoint.Endpoint { + return func(arg0 context.Context, request interface{}) (interface{}, error) { + req := request.(*GetRequest) + res0, res1 := svc.Get(arg0, req.OrgId, req.UserId) + return &GetResponse{Role: res0}, res1 + } +} + +func RemoveEndpoint(svc members.Members) endpoint.Endpoint { + return func(arg0 context.Context, request interface{}) (interface{}, error) { + req := request.(*RemoveRequest) + res0 := svc.Remove(arg0, req.OrgId, req.UserId) + return &RemoveResponse{}, res0 + } +} + +func ListMembersEndpoint(svc members.Members) endpoint.Endpoint { + return func(arg0 context.Context, request interface{}) (interface{}, error) { + req := request.(*ListMembersRequest) + res0, res1 := svc.ListMembers(arg0, req.OrgId) + return &ListMembersResponse{Members: res0}, res1 + } +} + +func ListOrganizationsEndpoint(svc members.Members) endpoint.Endpoint { + return func(arg0 context.Context, request interface{}) (interface{}, error) { + req := request.(*ListOrganizationsRequest) + res0, res1 := svc.ListOrganizations(arg0, req.UserId) + return &ListOrganizationsResponse{Organizations: res0}, res1 + } +} diff --git a/pkg/organizations/mocks/Organizations.go b/pkg/organizations/mocks/Organizations.go new file mode 100644 index 0000000000000000000000000000000000000000..7b7255897b5f33f44202ef8e79255a93ea7ee429 --- /dev/null +++ b/pkg/organizations/mocks/Organizations.go @@ -0,0 +1,120 @@ +// Code generated by mockery v2.7.4. DO NOT EDIT. + +package mocks + +import ( + context "context" + + organizations "git.perx.ru/perxis/perxis-go/pkg/organizations" + services "git.perx.ru/perxis/perxis-go/pkg/options" + mock "github.com/stretchr/testify/mock" +) + +// Organizations is an autogenerated mock type for the Organizations type +type Organizations struct { + mock.Mock +} + +// Create provides a mock function with given fields: ctx, org +func (_m *Organizations) Create(ctx context.Context, org *organizations.Organization) (*organizations.Organization, error) { + ret := _m.Called(ctx, org) + + var r0 *organizations.Organization + if rf, ok := ret.Get(0).(func(context.Context, *organizations.Organization) *organizations.Organization); ok { + r0 = rf(ctx, org) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*organizations.Organization) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *organizations.Organization) error); ok { + r1 = rf(ctx, org) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Delete provides a mock function with given fields: ctx, orgId +func (_m *Organizations) Delete(ctx context.Context, orgId string) error { + ret := _m.Called(ctx, orgId) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, orgId) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Find provides a mock function with given fields: ctx, filter, opts +func (_m *Organizations) Find(ctx context.Context, filter *organizations.Filter, opts *services.FindOptions) ([]*organizations.Organization, int, error) { + ret := _m.Called(ctx, filter, opts) + + var r0 []*organizations.Organization + if rf, ok := ret.Get(0).(func(context.Context, *organizations.Filter, *services.FindOptions) []*organizations.Organization); ok { + r0 = rf(ctx, filter, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*organizations.Organization) + } + } + + var r1 int + if rf, ok := ret.Get(1).(func(context.Context, *organizations.Filter, *services.FindOptions) int); ok { + r1 = rf(ctx, filter, opts) + } else { + r1 = ret.Get(1).(int) + } + + var r2 error + if rf, ok := ret.Get(2).(func(context.Context, *organizations.Filter, *services.FindOptions) error); ok { + r2 = rf(ctx, filter, opts) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// Get provides a mock function with given fields: ctx, orgId +func (_m *Organizations) Get(ctx context.Context, orgId string) (*organizations.Organization, error) { + ret := _m.Called(ctx, orgId) + + var r0 *organizations.Organization + if rf, ok := ret.Get(0).(func(context.Context, string) *organizations.Organization); ok { + r0 = rf(ctx, orgId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*organizations.Organization) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, orgId) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Update provides a mock function with given fields: ctx, org +func (_m *Organizations) Update(ctx context.Context, org *organizations.Organization) error { + ret := _m.Called(ctx, org) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *organizations.Organization) error); ok { + r0 = rf(ctx, org) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/pkg/organizations/organization.go b/pkg/organizations/organization.go new file mode 100644 index 0000000000000000000000000000000000000000..b95daa6988710d26fcda200eebef8afeff9158df --- /dev/null +++ b/pkg/organizations/organization.go @@ -0,0 +1,14 @@ +package organizations + +type Organization struct { + ID string `bson:"_id"` + Name string `bson:"name"` + Description string `bson:"description"` + LogoURL string `bson:"logoUrl"` + OwnerID *string `bson:"-"` +} + +func (o *Organization) SetOwnerID(s string) *Organization { + o.OwnerID = &s + return o +} diff --git a/pkg/organizations/service.go b/pkg/organizations/service.go new file mode 100644 index 0000000000000000000000000000000000000000..9ba3b9a91e6acd2353b1cebff342fa6a329547c9 --- /dev/null +++ b/pkg/organizations/service.go @@ -0,0 +1,25 @@ +package organizations + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/options" +) + +// @microgen grpc +// @protobuf git.perx.ru/perxis/perxis-go/proto/organizations +// @grpc-addr account.organizations.Organizations +type Organizations interface { + Create(ctx context.Context, org *Organization) (created *Organization, err error) + + Get(ctx context.Context, orgId string) (org *Organization, err error) + Update(ctx context.Context, org *Organization) (err error) + Delete(ctx context.Context, orgId string) (err error) + Find(ctx context.Context, filter *Filter, opts *options.FindOptions) (orgs []*Organization, total int, err error) +} + +// Organizations +type Filter struct { + ID []string + Name []string +} diff --git a/pkg/organizations/transport/client.microgen.go b/pkg/organizations/transport/client.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..00b14af754877e02fe3936269d72cb13c7b138da --- /dev/null +++ b/pkg/organizations/transport/client.microgen.go @@ -0,0 +1,76 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +package transport + +import ( + "context" + "errors" + + options "git.perx.ru/perxis/perxis-go/pkg/options" + organizations "git.perx.ru/perxis/perxis-go/pkg/organizations" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +func (set EndpointsSet) Create(arg0 context.Context, arg1 *organizations.Organization) (res0 *organizations.Organization, res1 error) { + request := CreateRequest{Org: arg1} + response, res1 := set.CreateEndpoint(arg0, &request) + if res1 != nil { + if e, ok := status.FromError(res1); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown { + res1 = errors.New(e.Message()) + } + return + } + return response.(*CreateResponse).Created, res1 +} + +func (set EndpointsSet) Get(arg0 context.Context, arg1 string) (res0 *organizations.Organization, res1 error) { + request := GetRequest{OrgId: arg1} + response, res1 := set.GetEndpoint(arg0, &request) + if res1 != nil { + if e, ok := status.FromError(res1); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown { + res1 = errors.New(e.Message()) + } + return + } + return response.(*GetResponse).Org, res1 +} + +func (set EndpointsSet) Update(arg0 context.Context, arg1 *organizations.Organization) (res0 error) { + request := UpdateRequest{Org: arg1} + _, res0 = set.UpdateEndpoint(arg0, &request) + if res0 != nil { + if e, ok := status.FromError(res0); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown { + res0 = errors.New(e.Message()) + } + return + } + return res0 +} + +func (set EndpointsSet) Delete(arg0 context.Context, arg1 string) (res0 error) { + request := DeleteRequest{OrgId: arg1} + _, res0 = set.DeleteEndpoint(arg0, &request) + if res0 != nil { + if e, ok := status.FromError(res0); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown { + res0 = errors.New(e.Message()) + } + return + } + return res0 +} + +func (set EndpointsSet) Find(arg0 context.Context, arg1 *organizations.Filter, arg2 *options.FindOptions) (res0 []*organizations.Organization, res1 int, res2 error) { + request := FindRequest{ + Filter: arg1, + Opts: arg2, + } + response, res2 := set.FindEndpoint(arg0, &request) + if res2 != nil { + if e, ok := status.FromError(res2); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown { + res2 = errors.New(e.Message()) + } + return + } + return response.(*FindResponse).Orgs, response.(*FindResponse).Total, res2 +} diff --git a/pkg/organizations/transport/endpoints.microgen.go b/pkg/organizations/transport/endpoints.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..7e15457fef975b2f07611ef46786cf7b73f5bbee --- /dev/null +++ b/pkg/organizations/transport/endpoints.microgen.go @@ -0,0 +1,14 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +package transport + +import endpoint "github.com/go-kit/kit/endpoint" + +// EndpointsSet implements Organizations API and used for transport purposes. +type EndpointsSet struct { + CreateEndpoint endpoint.Endpoint + GetEndpoint endpoint.Endpoint + UpdateEndpoint endpoint.Endpoint + DeleteEndpoint endpoint.Endpoint + FindEndpoint endpoint.Endpoint +} diff --git a/pkg/organizations/transport/exchanges.microgen.go b/pkg/organizations/transport/exchanges.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..7ae098e5b5b555cfe834c55e2fe73c79805fdbfd --- /dev/null +++ b/pkg/organizations/transport/exchanges.microgen.go @@ -0,0 +1,45 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +package transport + +import ( + options "git.perx.ru/perxis/perxis-go/pkg/options" + organizations "git.perx.ru/perxis/perxis-go/pkg/organizations" +) + +type ( + CreateRequest struct { + Org *organizations.Organization `json:"org"` + } + CreateResponse struct { + Created *organizations.Organization `json:"created"` + } + + GetRequest struct { + OrgId string `json:"org_id"` + } + GetResponse struct { + Org *organizations.Organization `json:"org"` + } + + UpdateRequest struct { + Org *organizations.Organization `json:"org"` + } + // Formal exchange type, please do not delete. + UpdateResponse struct{} + + DeleteRequest struct { + OrgId string `json:"org_id"` + } + // Formal exchange type, please do not delete. + DeleteResponse struct{} + + FindRequest struct { + Filter *organizations.Filter `json:"filter"` + Opts *options.FindOptions `json:"opts"` + } + FindResponse struct { + Orgs []*organizations.Organization `json:"orgs"` + Total int `json:"total"` + } +) diff --git a/pkg/organizations/transport/grpc/client.microgen.go b/pkg/organizations/transport/grpc/client.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..92918a88884de0a621858b7e09c49c66d83e5cab --- /dev/null +++ b/pkg/organizations/transport/grpc/client.microgen.go @@ -0,0 +1,54 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +package transportgrpc + +import ( + transport "git.perx.ru/perxis/perxis-go/pkg/organizations/transport" + pb "git.perx.ru/perxis/perxis-go/proto/organizations" + grpckit "github.com/go-kit/kit/transport/grpc" + empty "github.com/golang/protobuf/ptypes/empty" + grpc "google.golang.org/grpc" +) + +func NewGRPCClient(conn *grpc.ClientConn, addr string, opts ...grpckit.ClientOption) transport.EndpointsSet { + if addr == "" { + addr = "account.organizations.Organizations" + } + return transport.EndpointsSet{ + CreateEndpoint: grpckit.NewClient( + conn, addr, "Create", + _Encode_Create_Request, + _Decode_Create_Response, + pb.CreateResponse{}, + opts..., + ).Endpoint(), + DeleteEndpoint: grpckit.NewClient( + conn, addr, "Delete", + _Encode_Delete_Request, + _Decode_Delete_Response, + empty.Empty{}, + opts..., + ).Endpoint(), + FindEndpoint: grpckit.NewClient( + conn, addr, "Find", + _Encode_Find_Request, + _Decode_Find_Response, + pb.FindResponse{}, + opts..., + ).Endpoint(), + GetEndpoint: grpckit.NewClient( + conn, addr, "Get", + _Encode_Get_Request, + _Decode_Get_Response, + pb.GetResponse{}, + opts..., + ).Endpoint(), + UpdateEndpoint: grpckit.NewClient( + conn, addr, "Update", + _Encode_Update_Request, + _Decode_Update_Response, + empty.Empty{}, + opts..., + ).Endpoint(), + } +} diff --git a/pkg/organizations/transport/grpc/protobuf_endpoint_converters.microgen.go b/pkg/organizations/transport/grpc/protobuf_endpoint_converters.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..966189093b756b558406ebacf758da6c2a3f4839 --- /dev/null +++ b/pkg/organizations/transport/grpc/protobuf_endpoint_converters.microgen.go @@ -0,0 +1,225 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +// Please, do not change functions names! +package transportgrpc + +import ( + "context" + "errors" + + transport "git.perx.ru/perxis/perxis-go/pkg/organizations/transport" + pb "git.perx.ru/perxis/perxis-go/proto/organizations" + empty "github.com/golang/protobuf/ptypes/empty" +) + +func _Encode_Create_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil CreateRequest") + } + req := request.(*transport.CreateRequest) + reqOrg, err := PtrOrganizationToProto(req.Org) + if err != nil { + return nil, err + } + return &pb.CreateRequest{Org: reqOrg}, nil +} + +func _Encode_Get_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil GetRequest") + } + req := request.(*transport.GetRequest) + return &pb.GetRequest{OrgId: req.OrgId}, nil +} + +func _Encode_Delete_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil DeleteRequest") + } + req := request.(*transport.DeleteRequest) + return &pb.DeleteRequest{OrgId: req.OrgId}, nil +} + +func _Encode_Find_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil FindRequest") + } + req := request.(*transport.FindRequest) + reqFilter, err := PtrFilterToProto(req.Filter) + if err != nil { + return nil, err + } + reqOpts, err := PtrServicesFindOptionsToProto(req.Opts) + if err != nil { + return nil, err + } + return &pb.FindRequest{ + Filter: reqFilter, + Opts: reqOpts, + }, nil +} + +func _Encode_Create_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil CreateResponse") + } + resp := response.(*transport.CreateResponse) + respCreated, err := PtrOrganizationToProto(resp.Created) + if err != nil { + return nil, err + } + return &pb.CreateResponse{Created: respCreated}, nil +} + +func _Encode_Get_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil GetResponse") + } + resp := response.(*transport.GetResponse) + respOrg, err := PtrOrganizationToProto(resp.Org) + if err != nil { + return nil, err + } + return &pb.GetResponse{Org: respOrg}, nil +} + +func _Encode_Update_Response(ctx context.Context, response interface{}) (interface{}, error) { + return &empty.Empty{}, nil +} + +func _Encode_Delete_Response(ctx context.Context, response interface{}) (interface{}, error) { + return &empty.Empty{}, nil +} + +func _Encode_Find_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil FindResponse") + } + resp := response.(*transport.FindResponse) + respOrgs, err := ListPtrOrganizationToProto(resp.Orgs) + if err != nil { + return nil, err + } + return &pb.FindResponse{ + Orgs: respOrgs, + Total: int64(resp.Total), + }, nil +} + +func _Decode_Create_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil CreateRequest") + } + req := request.(*pb.CreateRequest) + reqOrg, err := ProtoToPtrOrganization(req.Org) + if err != nil { + return nil, err + } + return &transport.CreateRequest{Org: reqOrg}, nil +} + +func _Decode_Get_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil GetRequest") + } + req := request.(*pb.GetRequest) + return &transport.GetRequest{OrgId: string(req.OrgId)}, nil +} + +func _Decode_Delete_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil DeleteRequest") + } + req := request.(*pb.DeleteRequest) + return &transport.DeleteRequest{OrgId: string(req.OrgId)}, nil +} + +func _Decode_Find_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil FindRequest") + } + req := request.(*pb.FindRequest) + reqFilter, err := ProtoToPtrFilter(req.Filter) + if err != nil { + return nil, err + } + reqOpts, err := ProtoToPtrServicesFindOptions(req.Opts) + if err != nil { + return nil, err + } + return &transport.FindRequest{ + Filter: reqFilter, + Opts: reqOpts, + }, nil +} + +func _Decode_Create_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil CreateResponse") + } + resp := response.(*pb.CreateResponse) + respCreated, err := ProtoToPtrOrganization(resp.Created) + if err != nil { + return nil, err + } + return &transport.CreateResponse{Created: respCreated}, nil +} + +func _Decode_Get_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil GetResponse") + } + resp := response.(*pb.GetResponse) + respOrg, err := ProtoToPtrOrganization(resp.Org) + if err != nil { + return nil, err + } + return &transport.GetResponse{Org: respOrg}, nil +} + +func _Decode_Update_Response(ctx context.Context, response interface{}) (interface{}, error) { + return &empty.Empty{}, nil +} + +func _Decode_Delete_Response(ctx context.Context, response interface{}) (interface{}, error) { + return &empty.Empty{}, nil +} + +func _Decode_Find_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil FindResponse") + } + resp := response.(*pb.FindResponse) + respOrgs, err := ProtoToListPtrOrganization(resp.Orgs) + if err != nil { + return nil, err + } + return &transport.FindResponse{ + Orgs: respOrgs, + Total: int(resp.Total), + }, nil +} + +func _Encode_Update_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil UpdateRequest") + } + req := request.(*transport.UpdateRequest) + reqOrg, err := PtrOrganizationToProto(req.Org) + if err != nil { + return nil, err + } + return &pb.UpdateRequest{Org: reqOrg}, nil +} + +func _Decode_Update_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil UpdateRequest") + } + req := request.(*pb.UpdateRequest) + reqOrg, err := ProtoToPtrOrganization(req.Org) + if err != nil { + return nil, err + } + return &transport.UpdateRequest{Org: reqOrg}, nil +} diff --git a/pkg/organizations/transport/grpc/protobuf_type_converters.microgen.go b/pkg/organizations/transport/grpc/protobuf_type_converters.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..15fce8f5a7966f8fa2958fef30f62805923f512b --- /dev/null +++ b/pkg/organizations/transport/grpc/protobuf_type_converters.microgen.go @@ -0,0 +1,111 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +// It is better for you if you do not change functions names! +// This file will never be overwritten. +package transportgrpc + +import ( + "git.perx.ru/perxis/perxis-go/pkg/options" + "git.perx.ru/perxis/perxis-go/pkg/organizations" + "git.perx.ru/perxis/perxis-go/proto/common" + pb "git.perx.ru/perxis/perxis-go/proto/organizations" +) + +func PtrOrganizationToProto(org *organizations.Organization) (*pb.Organization, error) { + if org == nil { + return nil, nil + } + po := &pb.Organization{ + Id: org.ID, + Name: org.Name, + Description: org.Description, + LogoUrl: org.LogoURL, + OwnerId: org.OwnerID, + } + + return po, nil +} + +func ProtoToPtrOrganization(protoOrg *pb.Organization) (*organizations.Organization, error) { + if protoOrg == nil { + return nil, nil + } + o := &organizations.Organization{ + ID: protoOrg.Id, + Name: protoOrg.Name, + Description: protoOrg.Description, + LogoURL: protoOrg.LogoUrl, + OwnerID: protoOrg.OwnerId, + } + return o, nil +} + +func PtrFilterToProto(filter *organizations.Filter) (*pb.Filter, error) { + if filter == nil { + return nil, nil + } + return &pb.Filter{ + Ids: filter.ID, + Names: filter.Name, + }, nil +} + +func ProtoToPtrFilter(protoFilter *pb.Filter) (*organizations.Filter, error) { + if protoFilter == nil { + return nil, nil + } + return &organizations.Filter{ + ID: protoFilter.Ids, + Name: protoFilter.Names, + }, nil +} + +func PtrServicesFindOptionsToProto(opts *options.FindOptions) (*common.FindOptions, error) { + if opts == nil { + return nil, nil + } + return &common.FindOptions{ + Sort: opts.Sort, + PageNum: int32(opts.PageNum), + PageSize: int32(opts.PageSize), + }, nil +} + +func ProtoToPtrServicesFindOptions(protoOpts *common.FindOptions) (*options.FindOptions, error) { + if protoOpts == nil { + return nil, nil + } + return &options.FindOptions{ + SortOptions: options.SortOptions{ + Sort: protoOpts.Sort, + }, + PaginationOptions: options.PaginationOptions{ + PageNum: int(protoOpts.PageNum), + PageSize: int(protoOpts.PageSize), + }, + }, nil +} + +func ListPtrOrganizationToProto(orgs []*organizations.Organization) ([]*pb.Organization, error) { + protoOrgs := make([]*pb.Organization, 0, len(orgs)) + for _, o := range orgs { + op, err := PtrOrganizationToProto(o) + if err != nil { + return nil, err + } + protoOrgs = append(protoOrgs, op) + } + return protoOrgs, nil +} + +func ProtoToListPtrOrganization(protoOrgs []*pb.Organization) ([]*organizations.Organization, error) { + orgs := make([]*organizations.Organization, 0, len(protoOrgs)) + for _, op := range protoOrgs { + o, err := ProtoToPtrOrganization(op) + if err != nil { + return nil, err + } + orgs = append(orgs, o) + } + return orgs, nil +} diff --git a/pkg/organizations/transport/grpc/server.microgen.go b/pkg/organizations/transport/grpc/server.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..1cf24d998b6422ad418cfbb0a47a51eba6fe801d --- /dev/null +++ b/pkg/organizations/transport/grpc/server.microgen.go @@ -0,0 +1,97 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +// DO NOT EDIT. +package transportgrpc + +import ( + transport "git.perx.ru/perxis/perxis-go/pkg/organizations/transport" + pb "git.perx.ru/perxis/perxis-go/proto/organizations" + grpc "github.com/go-kit/kit/transport/grpc" + empty "github.com/golang/protobuf/ptypes/empty" + context "golang.org/x/net/context" +) + +type organizationsServer struct { + create grpc.Handler + get grpc.Handler + update grpc.Handler + delete grpc.Handler + find grpc.Handler + + pb.UnimplementedOrganizationsServer +} + +func NewGRPCServer(endpoints *transport.EndpointsSet, opts ...grpc.ServerOption) pb.OrganizationsServer { + return &organizationsServer{ + create: grpc.NewServer( + endpoints.CreateEndpoint, + _Decode_Create_Request, + _Encode_Create_Response, + opts..., + ), + delete: grpc.NewServer( + endpoints.DeleteEndpoint, + _Decode_Delete_Request, + _Encode_Delete_Response, + opts..., + ), + find: grpc.NewServer( + endpoints.FindEndpoint, + _Decode_Find_Request, + _Encode_Find_Response, + opts..., + ), + get: grpc.NewServer( + endpoints.GetEndpoint, + _Decode_Get_Request, + _Encode_Get_Response, + opts..., + ), + update: grpc.NewServer( + endpoints.UpdateEndpoint, + _Decode_Update_Request, + _Encode_Update_Response, + opts..., + ), + } +} + +func (S *organizationsServer) Create(ctx context.Context, req *pb.CreateRequest) (*pb.CreateResponse, error) { + _, resp, err := S.create.ServeGRPC(ctx, req) + if err != nil { + return nil, err + } + return resp.(*pb.CreateResponse), nil +} + +func (S *organizationsServer) Get(ctx context.Context, req *pb.GetRequest) (*pb.GetResponse, error) { + _, resp, err := S.get.ServeGRPC(ctx, req) + if err != nil { + return nil, err + } + return resp.(*pb.GetResponse), nil +} + +func (S *organizationsServer) Update(ctx context.Context, req *pb.UpdateRequest) (*empty.Empty, error) { + _, resp, err := S.update.ServeGRPC(ctx, req) + if err != nil { + return nil, err + } + return resp.(*empty.Empty), nil +} + +func (S *organizationsServer) Delete(ctx context.Context, req *pb.DeleteRequest) (*empty.Empty, error) { + _, resp, err := S.delete.ServeGRPC(ctx, req) + if err != nil { + return nil, err + } + return resp.(*empty.Empty), nil +} + +func (S *organizationsServer) Find(ctx context.Context, req *pb.FindRequest) (*pb.FindResponse, error) { + _, resp, err := S.find.ServeGRPC(ctx, req) + if err != nil { + return nil, err + } + return resp.(*pb.FindResponse), nil +} diff --git a/pkg/organizations/transport/server.microgen.go b/pkg/organizations/transport/server.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..2e08f8f534f862552ecc5008b2f76277e702a9ab --- /dev/null +++ b/pkg/organizations/transport/server.microgen.go @@ -0,0 +1,63 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +package transport + +import ( + "context" + + organizations "git.perx.ru/perxis/perxis-go/pkg/organizations" + endpoint "github.com/go-kit/kit/endpoint" +) + +func Endpoints(svc organizations.Organizations) EndpointsSet { + return EndpointsSet{ + CreateEndpoint: CreateEndpoint(svc), + DeleteEndpoint: DeleteEndpoint(svc), + FindEndpoint: FindEndpoint(svc), + GetEndpoint: GetEndpoint(svc), + UpdateEndpoint: UpdateEndpoint(svc), + } +} + +func CreateEndpoint(svc organizations.Organizations) endpoint.Endpoint { + return func(arg0 context.Context, request interface{}) (interface{}, error) { + req := request.(*CreateRequest) + res0, res1 := svc.Create(arg0, req.Org) + return &CreateResponse{Created: res0}, res1 + } +} + +func GetEndpoint(svc organizations.Organizations) endpoint.Endpoint { + return func(arg0 context.Context, request interface{}) (interface{}, error) { + req := request.(*GetRequest) + res0, res1 := svc.Get(arg0, req.OrgId) + return &GetResponse{Org: res0}, res1 + } +} + +func UpdateEndpoint(svc organizations.Organizations) endpoint.Endpoint { + return func(arg0 context.Context, request interface{}) (interface{}, error) { + req := request.(*UpdateRequest) + res0 := svc.Update(arg0, req.Org) + return &UpdateResponse{}, res0 + } +} + +func DeleteEndpoint(svc organizations.Organizations) endpoint.Endpoint { + return func(arg0 context.Context, request interface{}) (interface{}, error) { + req := request.(*DeleteRequest) + res0 := svc.Delete(arg0, req.OrgId) + return &DeleteResponse{}, res0 + } +} + +func FindEndpoint(svc organizations.Organizations) endpoint.Endpoint { + return func(arg0 context.Context, request interface{}) (interface{}, error) { + req := request.(*FindRequest) + res0, res1, res2 := svc.Find(arg0, req.Filter, req.Opts) + return &FindResponse{ + Orgs: res0, + Total: res1, + }, res2 + } +} diff --git a/pkg/roles/mocks/Roles.go b/pkg/roles/mocks/Roles.go new file mode 100644 index 0000000000000000000000000000000000000000..d7e61236e36d2a5f544950f0634c5204d4a30a3e --- /dev/null +++ b/pkg/roles/mocks/Roles.go @@ -0,0 +1,112 @@ +// Code generated by mockery v2.7.4. DO NOT EDIT. + +package mocks + +import ( + "context" + + roles "git.perx.ru/perxis/perxis-go/pkg/roles" + "github.com/stretchr/testify/mock" +) + +// Roles is an autogenerated mock type for the Roles type +type Roles struct { + mock.Mock +} + +// Create provides a mock function with given fields: ctx, role +func (_m *Roles) Create(ctx context.Context, role *roles.Role) (*roles.Role, error) { + ret := _m.Called(ctx, role) + + var r0 *roles.Role + if rf, ok := ret.Get(0).(func(context.Context, *roles.Role) *roles.Role); ok { + r0 = rf(ctx, role) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*roles.Role) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *roles.Role) error); ok { + r1 = rf(ctx, role) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Delete provides a mock function with given fields: ctx, spaceId, roleId +func (_m *Roles) Delete(ctx context.Context, spaceId string, roleId string) error { + ret := _m.Called(ctx, spaceId, roleId) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, spaceId, roleId) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Get provides a mock function with given fields: ctx, spaceId, roleId +func (_m *Roles) Get(ctx context.Context, spaceId string, roleId string) (*roles.Role, error) { + ret := _m.Called(ctx, spaceId, roleId) + + var r0 *roles.Role + if rf, ok := ret.Get(0).(func(context.Context, string, string) *roles.Role); ok { + r0 = rf(ctx, spaceId, roleId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*roles.Role) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, spaceId, roleId) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// List provides a mock function with given fields: ctx, spaceId +func (_m *Roles) List(ctx context.Context, spaceId string) ([]*roles.Role, error) { + ret := _m.Called(ctx, spaceId) + + var r0 []*roles.Role + if rf, ok := ret.Get(0).(func(context.Context, string) []*roles.Role); ok { + r0 = rf(ctx, spaceId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*roles.Role) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, spaceId) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Update provides a mock function with given fields: ctx, role +func (_m *Roles) Update(ctx context.Context, role *roles.Role) error { + ret := _m.Called(ctx, role) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *roles.Role) error); ok { + r0 = rf(ctx, role) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/pkg/roles/role.go b/pkg/roles/role.go new file mode 100644 index 0000000000000000000000000000000000000000..eecafb64135a8b4545fe7ef894529641ddbab69a --- /dev/null +++ b/pkg/roles/role.go @@ -0,0 +1,73 @@ +package roles + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/data" + "git.perx.ru/perxis/perxis-go/pkg/environments" + "git.perx.ru/perxis/perxis-go/pkg/permission" +) + +const ( + AnonymousRole = "anonymous" + AuthorizedRole = "authorized" + ViewRole = "view" +) + +type Role struct { + // Внутренний идентификатор роли + ID string `json:"id" bson:"_id"` + + // Идентификатор проÑтранÑтва + SpaceID string `json:"spaceId" bson:"-"` + + // ОпиÑание роли, назначение + Description string `json:"description" bson:"description"` + + // СпиÑок доÑтупных окружений (ID или Alias) + Environments []string `json:"environments" bson:"environments"` + + // СпиÑок правил доÑтупа к коллекциÑм + Rules permission.Rules `json:"rules" bson:"rules"` + + // Разрешить доÑтуп API ÑƒÐ¿Ñ€Ð°Ð²Ð»ÐµÐ½Ð¸Ñ + AllowManagement bool `json:"allow_management" bson:"allow_management"` +} + +func (r Role) CanAccessEnvironment(ctx context.Context, service environments.Environments, spaceID, envID string) bool { + if spaceID == "" || envID == "" { + return false + } + + if r.AllowManagement { + return true + } + + // ЕÑли Ñвно не указаны доÑтупные Ð¾ÐºÑ€ÑƒÐ¶ÐµÐ½Ð¸Ñ - доÑтуп по умолчанию к окружению master + if len(r.Environments) == 0 { + r.Environments = []string{environments.DefaultEnvironment} + } + + for _, e := range r.Environments { + if envID == e || data.GlobMatch(envID, e) { + return true + } + } + + env, err := service.Get(ctx, spaceID, envID) + if err != nil || env == nil { + return false + } + + aliases := append(env.Aliases, env.ID) + + for _, e := range r.Environments { + for _, a := range aliases { + if a == e || data.GlobMatch(a, e) { + return true + } + } + } + + return false +} diff --git a/pkg/roles/service.go b/pkg/roles/service.go new file mode 100644 index 0000000000000000000000000000000000000000..b003008b10c7c0a0de6549c60262741fdf441f65 --- /dev/null +++ b/pkg/roles/service.go @@ -0,0 +1,16 @@ +package roles + +import ( + "context" +) + +// @microgen grpc +// @protobuf git.perx.ru/perxis/perxis-go/proto/roles +// @grpc-addr content.roles.Roles +type Roles interface { + Create(ctx context.Context, role *Role) (created *Role, err error) + Get(ctx context.Context, spaceId, roleId string) (role *Role, err error) + List(ctx context.Context, spaceId string) (roles []*Role, err error) + Update(ctx context.Context, role *Role) (err error) + Delete(ctx context.Context, spaceId, roleId string) (err error) +} diff --git a/pkg/roles/transport/client.microgen.go b/pkg/roles/transport/client.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..71b1de9e2b1b746962effe61dc63c37cf7977e69 --- /dev/null +++ b/pkg/roles/transport/client.microgen.go @@ -0,0 +1,78 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +package transport + +import ( + "context" + "errors" + + roles "git.perx.ru/perxis/perxis-go/pkg/roles" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +func (set EndpointsSet) Create(arg0 context.Context, arg1 *roles.Role) (res0 *roles.Role, res1 error) { + request := CreateRequest{Role: arg1} + response, res1 := set.CreateEndpoint(arg0, &request) + if res1 != nil { + if e, ok := status.FromError(res1); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown { + res1 = errors.New(e.Message()) + } + return + } + return response.(*CreateResponse).Created, res1 +} + +func (set EndpointsSet) Get(arg0 context.Context, arg1 string, arg2 string) (res0 *roles.Role, res1 error) { + request := GetRequest{ + RoleId: arg2, + SpaceId: arg1, + } + response, res1 := set.GetEndpoint(arg0, &request) + if res1 != nil { + if e, ok := status.FromError(res1); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown { + res1 = errors.New(e.Message()) + } + return + } + return response.(*GetResponse).Role, res1 +} + +func (set EndpointsSet) List(arg0 context.Context, arg1 string) (res0 []*roles.Role, res1 error) { + request := ListRequest{SpaceId: arg1} + response, res1 := set.ListEndpoint(arg0, &request) + if res1 != nil { + if e, ok := status.FromError(res1); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown { + res1 = errors.New(e.Message()) + } + return + } + return response.(*ListResponse).Roles, res1 +} + +func (set EndpointsSet) Update(arg0 context.Context, arg1 *roles.Role) (res0 error) { + request := UpdateRequest{Role: arg1} + _, res0 = set.UpdateEndpoint(arg0, &request) + if res0 != nil { + if e, ok := status.FromError(res0); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown { + res0 = errors.New(e.Message()) + } + return + } + return res0 +} + +func (set EndpointsSet) Delete(arg0 context.Context, arg1 string, arg2 string) (res0 error) { + request := DeleteRequest{ + RoleId: arg2, + SpaceId: arg1, + } + _, res0 = set.DeleteEndpoint(arg0, &request) + if res0 != nil { + if e, ok := status.FromError(res0); ok || e.Code() == codes.Internal || e.Code() == codes.Unknown { + res0 = errors.New(e.Message()) + } + return + } + return res0 +} diff --git a/pkg/roles/transport/endpoints.microgen.go b/pkg/roles/transport/endpoints.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..06fe337c3c48656eff8074237e5714c29eac8bb2 --- /dev/null +++ b/pkg/roles/transport/endpoints.microgen.go @@ -0,0 +1,14 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +package transport + +import endpoint "github.com/go-kit/kit/endpoint" + +// EndpointsSet implements Roles API and used for transport purposes. +type EndpointsSet struct { + CreateEndpoint endpoint.Endpoint + GetEndpoint endpoint.Endpoint + ListEndpoint endpoint.Endpoint + UpdateEndpoint endpoint.Endpoint + DeleteEndpoint endpoint.Endpoint +} diff --git a/pkg/roles/transport/exchanges.microgen.go b/pkg/roles/transport/exchanges.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..bb7d8e5010877efb628ce34b4f89ae66857476eb --- /dev/null +++ b/pkg/roles/transport/exchanges.microgen.go @@ -0,0 +1,42 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +package transport + +import roles "git.perx.ru/perxis/perxis-go/pkg/roles" + +type ( + CreateRequest struct { + Role *roles.Role `json:"role"` + } + CreateResponse struct { + Created *roles.Role `json:"created"` + } + + GetRequest struct { + SpaceId string `json:"space_id"` + RoleId string `json:"role_id"` + } + GetResponse struct { + Role *roles.Role `json:"role"` + } + + ListRequest struct { + SpaceId string `json:"space_id"` + } + ListResponse struct { + Roles []*roles.Role `json:"roles"` + } + + UpdateRequest struct { + Role *roles.Role `json:"role"` + } + // Formal exchange type, please do not delete. + UpdateResponse struct{} + + DeleteRequest struct { + SpaceId string `json:"space_id"` + RoleId string `json:"role_id"` + } + // Formal exchange type, please do not delete. + DeleteResponse struct{} +) diff --git a/pkg/roles/transport/grpc/client.microgen.go b/pkg/roles/transport/grpc/client.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..747b1a8cada8b5e189e5e4c7994c79cb36df8b02 --- /dev/null +++ b/pkg/roles/transport/grpc/client.microgen.go @@ -0,0 +1,54 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +package transportgrpc + +import ( + transport "git.perx.ru/perxis/perxis-go/pkg/roles/transport" + pb "git.perx.ru/perxis/perxis-go/proto/roles" + grpckit "github.com/go-kit/kit/transport/grpc" + empty "github.com/golang/protobuf/ptypes/empty" + grpc "google.golang.org/grpc" +) + +func NewGRPCClient(conn *grpc.ClientConn, addr string, opts ...grpckit.ClientOption) transport.EndpointsSet { + if addr == "" { + addr = "content.roles.Roles" + } + return transport.EndpointsSet{ + CreateEndpoint: grpckit.NewClient( + conn, addr, "Create", + _Encode_Create_Request, + _Decode_Create_Response, + pb.CreateResponse{}, + opts..., + ).Endpoint(), + DeleteEndpoint: grpckit.NewClient( + conn, addr, "Delete", + _Encode_Delete_Request, + _Decode_Delete_Response, + empty.Empty{}, + opts..., + ).Endpoint(), + GetEndpoint: grpckit.NewClient( + conn, addr, "Get", + _Encode_Get_Request, + _Decode_Get_Response, + pb.GetResponse{}, + opts..., + ).Endpoint(), + ListEndpoint: grpckit.NewClient( + conn, addr, "List", + _Encode_List_Request, + _Decode_List_Response, + pb.ListResponse{}, + opts..., + ).Endpoint(), + UpdateEndpoint: grpckit.NewClient( + conn, addr, "Update", + _Encode_Update_Request, + _Decode_Update_Response, + empty.Empty{}, + opts..., + ).Endpoint(), + } +} diff --git a/pkg/roles/transport/grpc/protobuf_endpoint_converters.microgen.go b/pkg/roles/transport/grpc/protobuf_endpoint_converters.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..aa66bf57105f290aae04a763c5a8e931a410fc53 --- /dev/null +++ b/pkg/roles/transport/grpc/protobuf_endpoint_converters.microgen.go @@ -0,0 +1,209 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +// Please, do not change functions names! +package transportgrpc + +import ( + "context" + "errors" + + transport "git.perx.ru/perxis/perxis-go/pkg/roles/transport" + pb "git.perx.ru/perxis/perxis-go/proto/roles" + empty "github.com/golang/protobuf/ptypes/empty" +) + +func _Encode_Create_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil CreateRequest") + } + req := request.(*transport.CreateRequest) + reqRole, err := PtrRoleToProto(req.Role) + if err != nil { + return nil, err + } + return &pb.CreateRequest{Role: reqRole}, nil +} + +func _Encode_Get_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil GetRequest") + } + req := request.(*transport.GetRequest) + return &pb.GetRequest{ + RoleId: req.RoleId, + SpaceId: req.SpaceId, + }, nil +} + +func _Encode_List_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil ListRequest") + } + req := request.(*transport.ListRequest) + return &pb.ListRequest{SpaceId: req.SpaceId}, nil +} + +func _Encode_Update_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil UpdateRequest") + } + req := request.(*transport.UpdateRequest) + reqRole, err := PtrRoleToProto(req.Role) + if err != nil { + return nil, err + } + return &pb.UpdateRequest{Role: reqRole}, nil +} + +func _Encode_Delete_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil DeleteRequest") + } + req := request.(*transport.DeleteRequest) + return &pb.DeleteRequest{ + RoleId: req.RoleId, + SpaceId: req.SpaceId, + }, nil +} + +func _Encode_Get_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil GetResponse") + } + resp := response.(*transport.GetResponse) + respRole, err := PtrRoleToProto(resp.Role) + if err != nil { + return nil, err + } + return &pb.GetResponse{Role: respRole}, nil +} + +func _Encode_List_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil ListResponse") + } + resp := response.(*transport.ListResponse) + respRoles, err := ListPtrRoleToProto(resp.Roles) + if err != nil { + return nil, err + } + return &pb.ListResponse{Roles: respRoles}, nil +} + +func _Encode_Update_Response(ctx context.Context, response interface{}) (interface{}, error) { + return &empty.Empty{}, nil +} + +func _Encode_Delete_Response(ctx context.Context, response interface{}) (interface{}, error) { + return &empty.Empty{}, nil +} + +func _Decode_Create_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil CreateRequest") + } + req := request.(*pb.CreateRequest) + reqRole, err := ProtoToPtrRole(req.Role) + if err != nil { + return nil, err + } + return &transport.CreateRequest{Role: reqRole}, nil +} + +func _Decode_Get_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil GetRequest") + } + req := request.(*pb.GetRequest) + return &transport.GetRequest{ + RoleId: string(req.RoleId), + SpaceId: string(req.SpaceId), + }, nil +} + +func _Decode_List_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil ListRequest") + } + req := request.(*pb.ListRequest) + return &transport.ListRequest{SpaceId: string(req.SpaceId)}, nil +} + +func _Decode_Update_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil UpdateRequest") + } + req := request.(*pb.UpdateRequest) + reqRole, err := ProtoToPtrRole(req.Role) + if err != nil { + return nil, err + } + return &transport.UpdateRequest{Role: reqRole}, nil +} + +func _Decode_Delete_Request(ctx context.Context, request interface{}) (interface{}, error) { + if request == nil { + return nil, errors.New("nil DeleteRequest") + } + req := request.(*pb.DeleteRequest) + return &transport.DeleteRequest{ + RoleId: string(req.RoleId), + SpaceId: string(req.SpaceId), + }, nil +} + +func _Decode_Get_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil GetResponse") + } + resp := response.(*pb.GetResponse) + respRole, err := ProtoToPtrRole(resp.Role) + if err != nil { + return nil, err + } + return &transport.GetResponse{Role: respRole}, nil +} + +func _Decode_List_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil ListResponse") + } + resp := response.(*pb.ListResponse) + respRoles, err := ProtoToListPtrRole(resp.Roles) + if err != nil { + return nil, err + } + return &transport.ListResponse{Roles: respRoles}, nil +} + +func _Decode_Update_Response(ctx context.Context, response interface{}) (interface{}, error) { + return &empty.Empty{}, nil +} + +func _Decode_Delete_Response(ctx context.Context, response interface{}) (interface{}, error) { + return &empty.Empty{}, nil +} + +func _Encode_Create_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil CreateResponse") + } + resp := response.(*transport.CreateResponse) + respCreated, err := PtrRoleToProto(resp.Created) + if err != nil { + return nil, err + } + return &pb.CreateResponse{Created: respCreated}, nil +} + +func _Decode_Create_Response(ctx context.Context, response interface{}) (interface{}, error) { + if response == nil { + return nil, errors.New("nil CreateResponse") + } + resp := response.(*pb.CreateResponse) + respCreated, err := ProtoToPtrRole(resp.Created) + if err != nil { + return nil, err + } + return &transport.CreateResponse{Created: respCreated}, nil +} diff --git a/pkg/roles/transport/grpc/protobuf_type_converters.microgen.go b/pkg/roles/transport/grpc/protobuf_type_converters.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..10f207e5ebf3e07804009bf0bbb62401e0bdbf9a --- /dev/null +++ b/pkg/roles/transport/grpc/protobuf_type_converters.microgen.go @@ -0,0 +1,110 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +// It is better for you if you do not change functions names! +// This file will never be overwritten. +package transportgrpc + +import ( + permission "git.perx.ru/perxis/perxis-go/pkg/permission" + service "git.perx.ru/perxis/perxis-go/pkg/roles" + commonpb "git.perx.ru/perxis/perxis-go/proto/common" + pb "git.perx.ru/perxis/perxis-go/proto/roles" +) + +func PtrRoleToProto(role *service.Role) (*pb.Role, error) { + if role == nil { + return nil, nil + } + rules := make([]*commonpb.Rule, 0, len(role.Rules)) + for _, r := range role.Rules { + pr, _ := PtrPermissionRuleToProto(r) + rules = append(rules, pr) + } + return &pb.Role{ + Id: role.ID, + SpaceId: role.SpaceID, + Description: role.Description, + Rules: rules, + Environments: role.Environments, + AllowManagement: role.AllowManagement, + }, nil +} + +func ProtoToPtrRole(protoRole *pb.Role) (*service.Role, error) { + if protoRole == nil { + return nil, nil + } + + rules := make([]*permission.Rule, 0, len(protoRole.Rules)) + for _, pr := range protoRole.Rules { + r, _ := ProtoToPtrPermissionRule(pr) + rules = append(rules, r) + } + + return &service.Role{ + ID: protoRole.Id, + SpaceID: protoRole.SpaceId, + Description: protoRole.Description, + Rules: rules, + Environments: protoRole.Environments, + AllowManagement: protoRole.AllowManagement, + }, nil +} + +func ListPtrRoleToProto(roles []*service.Role) ([]*pb.Role, error) { + protoRoles := make([]*pb.Role, 0, len(roles)) + for _, r := range roles { + protoRole, _ := PtrRoleToProto(r) + protoRoles = append(protoRoles, protoRole) + } + return protoRoles, nil +} + +func ProtoToListPtrRole(protoRoles []*pb.Role) ([]*service.Role, error) { + roles := make([]*service.Role, 0, len(protoRoles)) + for _, r := range protoRoles { + role, _ := ProtoToPtrRole(r) + roles = append(roles, role) + } + return roles, nil +} + +func PtrPermissionRuleToProto(rule *permission.Rule) (*commonpb.Rule, error) { + if rule == nil { + return nil, nil + } + actions := make([]commonpb.Action, 0, len(rule.Actions)) + for _, a := range rule.Actions { + actions = append(actions, commonpb.Action(a)) + } + return &commonpb.Rule{ + CollectionId: rule.CollectionID, + Actions: actions, + Access: commonpb.Access(rule.Access), + HiddenFields: rule.HiddenFields, + ReadonlyFields: rule.ReadonlyFields, + WriteonlyFields: rule.WriteonlyFields, + ReadFilter: rule.ReadFilter, + WriteFilter: rule.WriteFilter, + }, nil +} + +func ProtoToPtrPermissionRule(protoRule *commonpb.Rule) (*permission.Rule, error) { + if protoRule == nil { + return nil, nil + } + actions := make([]permission.Action, 0, len(protoRule.Actions)) + for _, a := range protoRule.Actions { + actions = append(actions, permission.Action(a)) + } + return &permission.Rule{ + CollectionID: protoRule.CollectionId, + Actions: actions, + Access: permission.Access(protoRule.Access), + HiddenFields: protoRule.HiddenFields, + ReadonlyFields: protoRule.ReadonlyFields, + WriteonlyFields: protoRule.WriteonlyFields, + ReadFilter: protoRule.ReadFilter, + WriteFilter: protoRule.WriteFilter, + }, nil +} diff --git a/pkg/roles/transport/grpc/server.microgen.go b/pkg/roles/transport/grpc/server.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..dc012c75f1ff3a6965d9152f204c7d7f6b61d285 --- /dev/null +++ b/pkg/roles/transport/grpc/server.microgen.go @@ -0,0 +1,97 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +// DO NOT EDIT. +package transportgrpc + +import ( + transport "git.perx.ru/perxis/perxis-go/pkg/roles/transport" + pb "git.perx.ru/perxis/perxis-go/proto/roles" + grpc "github.com/go-kit/kit/transport/grpc" + empty "github.com/golang/protobuf/ptypes/empty" + context "golang.org/x/net/context" +) + +type rolesServer struct { + create grpc.Handler + get grpc.Handler + list grpc.Handler + update grpc.Handler + delete grpc.Handler + + pb.UnimplementedRolesServer +} + +func NewGRPCServer(endpoints *transport.EndpointsSet, opts ...grpc.ServerOption) pb.RolesServer { + return &rolesServer{ + create: grpc.NewServer( + endpoints.CreateEndpoint, + _Decode_Create_Request, + _Encode_Create_Response, + opts..., + ), + delete: grpc.NewServer( + endpoints.DeleteEndpoint, + _Decode_Delete_Request, + _Encode_Delete_Response, + opts..., + ), + get: grpc.NewServer( + endpoints.GetEndpoint, + _Decode_Get_Request, + _Encode_Get_Response, + opts..., + ), + list: grpc.NewServer( + endpoints.ListEndpoint, + _Decode_List_Request, + _Encode_List_Response, + opts..., + ), + update: grpc.NewServer( + endpoints.UpdateEndpoint, + _Decode_Update_Request, + _Encode_Update_Response, + opts..., + ), + } +} + +func (S *rolesServer) Create(ctx context.Context, req *pb.CreateRequest) (*pb.CreateResponse, error) { + _, resp, err := S.create.ServeGRPC(ctx, req) + if err != nil { + return nil, err + } + return resp.(*pb.CreateResponse), nil +} + +func (S *rolesServer) Get(ctx context.Context, req *pb.GetRequest) (*pb.GetResponse, error) { + _, resp, err := S.get.ServeGRPC(ctx, req) + if err != nil { + return nil, err + } + return resp.(*pb.GetResponse), nil +} + +func (S *rolesServer) List(ctx context.Context, req *pb.ListRequest) (*pb.ListResponse, error) { + _, resp, err := S.list.ServeGRPC(ctx, req) + if err != nil { + return nil, err + } + return resp.(*pb.ListResponse), nil +} + +func (S *rolesServer) Update(ctx context.Context, req *pb.UpdateRequest) (*empty.Empty, error) { + _, resp, err := S.update.ServeGRPC(ctx, req) + if err != nil { + return nil, err + } + return resp.(*empty.Empty), nil +} + +func (S *rolesServer) Delete(ctx context.Context, req *pb.DeleteRequest) (*empty.Empty, error) { + _, resp, err := S.delete.ServeGRPC(ctx, req) + if err != nil { + return nil, err + } + return resp.(*empty.Empty), nil +} diff --git a/pkg/roles/transport/server.microgen.go b/pkg/roles/transport/server.microgen.go new file mode 100644 index 0000000000000000000000000000000000000000..119aae999a13bab884d4f98d09346b1ef2d13071 --- /dev/null +++ b/pkg/roles/transport/server.microgen.go @@ -0,0 +1,60 @@ +// Code generated by microgen 0.9.1. DO NOT EDIT. + +package transport + +import ( + "context" + + roles "git.perx.ru/perxis/perxis-go/pkg/roles" + endpoint "github.com/go-kit/kit/endpoint" +) + +func Endpoints(svc roles.Roles) EndpointsSet { + return EndpointsSet{ + CreateEndpoint: CreateEndpoint(svc), + DeleteEndpoint: DeleteEndpoint(svc), + GetEndpoint: GetEndpoint(svc), + ListEndpoint: ListEndpoint(svc), + UpdateEndpoint: UpdateEndpoint(svc), + } +} + +func CreateEndpoint(svc roles.Roles) endpoint.Endpoint { + return func(arg0 context.Context, request interface{}) (interface{}, error) { + req := request.(*CreateRequest) + res0, res1 := svc.Create(arg0, req.Role) + return &CreateResponse{Created: res0}, res1 + } +} + +func GetEndpoint(svc roles.Roles) endpoint.Endpoint { + return func(arg0 context.Context, request interface{}) (interface{}, error) { + req := request.(*GetRequest) + res0, res1 := svc.Get(arg0, req.SpaceId, req.RoleId) + return &GetResponse{Role: res0}, res1 + } +} + +func ListEndpoint(svc roles.Roles) endpoint.Endpoint { + return func(arg0 context.Context, request interface{}) (interface{}, error) { + req := request.(*ListRequest) + res0, res1 := svc.List(arg0, req.SpaceId) + return &ListResponse{Roles: res0}, res1 + } +} + +func UpdateEndpoint(svc roles.Roles) endpoint.Endpoint { + return func(arg0 context.Context, request interface{}) (interface{}, error) { + req := request.(*UpdateRequest) + res0 := svc.Update(arg0, req.Role) + return &UpdateResponse{}, res0 + } +} + +func DeleteEndpoint(svc roles.Roles) endpoint.Endpoint { + return func(arg0 context.Context, request interface{}) (interface{}, error) { + req := request.(*DeleteRequest) + res0 := svc.Delete(arg0, req.SpaceId, req.RoleId) + return &DeleteResponse{}, res0 + } +} diff --git a/pkg/schema/field/array.go b/pkg/schema/field/array.go new file mode 100644 index 0000000000000000000000000000000000000000..6700eb9b09af7bb714566dca8c82e90d026c7d57 --- /dev/null +++ b/pkg/schema/field/array.go @@ -0,0 +1,161 @@ +package field + +import ( + "context" + "fmt" + "reflect" + "strconv" + + "git.perx.ru/perxis/perxis-go/pkg/errors" + "github.com/hashicorp/go-multierror" +) + +var arrayType = &ArrayType{} + +type ArrayParameters struct { + Item *Field `json:"item"` +} + +func (ArrayParameters) Type() Type { return arrayType } + +func (p ArrayParameters) Clone(reset bool) Parameters { + return &ArrayParameters{Item: p.Item.Clone(reset)} +} + +type ArrayType struct{} + +func (ArrayType) Name() string { + return "array" +} + +func (ArrayType) NewParameters() Parameters { + return &ArrayParameters{} +} + +func (ArrayType) IsEmpty(v interface{}) bool { + arr, _ := v.([]interface{}) // todo: нужно возвращать ошибку? + return len(arr) == 0 +} + +//func (ArrayType) Decode(ctx *Context.Context, field *Field, v interface{}) (interface{}, error) { +// params, ok := field.Params.(*ArrayParameters) +// if !ok { +// return nil, errors.New("field parameters required") +// } +// +// arr, ok := v.([]interface{}) +// if !ok { +// return nil, fmt.Errorf("[]interface{} required") +// } +// +// m := make([]interface{}, 0, len(arr)) +// +// for _, i := range arr { +// item, err := Decode(ctx, params.Item, i) +// if err != nil { +// return nil, err +// } +// m = append(m, item) +// } +// +// return m, nil +//} +// +//func (ArrayType) Encode(ctx *Context.Context, field *Field, v interface{}) (interface{}, error) { +// params, ok := field.Params.(*ArrayParameters) +// if !ok { +// return nil, errors.New("field parameters required") +// } +// +// arr, ok := v.([]interface{}) +// if !ok { +// return nil, fmt.Errorf("[]interface{} required") +// } +// +// m := make([]interface{}, 0, len(arr)) +// +// for _, i := range arr { +// item, err := params.Item.Encode(ctx, i) +// if err != nil { +// return nil, err +// } +// m = append(m, item) +// } +// +// return m, nil +//} + +//func (ArrayType) Validate(ctx *Context.Context, field *Field, v interface{}) error { +// params, ok := field.Params.(*ArrayParameters) +// if !ok { +// return errors.New("field parameters required") +// } +// +// m, ok := v.([]interface{}) +// if !ok { +// return errors.New("[]interface{} is required") +// } +// for _, i := range m { +// err := params.Item.Validate(ctx, i) +// if err != nil { +// return err +// } +// } +// return nil +//} + +func (ArrayType) Walk(ctx context.Context, field *Field, v interface{}, fn WalkFunc, opts *WalkOptions) (interface{}, bool, error) { + var changed bool + params, ok := field.Params.(*ArrayParameters) + if !ok { + return nil, false, errors.New("field parameters required") + } + + // Ð’ маÑÑиве нет в данных и не выполнÑетÑÑ Ð¾Ð±Ñ…Ð¾Ð´ по Ñхеме + if !opts.WalkSchema && v == nil { + return nil, false, nil + } + + // ВыполнÑетÑÑ Ð¾Ð±Ñ…Ð¾Ð´ по Ñхеме + if opts.WalkSchema && v == nil { + params.Item.Walk(ctx, v, fn, WalkOpts(opts)) + return nil, false, nil + } + + arr, ok := v.([]interface{}) + if !ok { + return nil, false, fmt.Errorf("incorrect type: \"%s\", expected \"[]interface{}\"", reflect.ValueOf(v).Kind()) + } + + m := make([]interface{}, 0, len(arr)) + + var merr *multierror.Error + for i, value := range arr { + + valueNew, valueChanged, err := params.Item.Walk(ctx, value, fn, WalkOpts(opts)) + + if err != nil { + merr = multierror.Append(merr, errors.WithField(err, strconv.Itoa(i))) + } + + if valueChanged { + m = append(m, valueNew) + changed = true + } else { + m = append(m, value) + } + } + + if merr != nil { + merr.ErrorFormat = func(i []error) string { + return fmt.Sprintf("%d error(s)", len(i)) + } + return nil, false, merr + } + + return m, changed, nil +} + +func Array(item *Field, o ...interface{}) *Field { + return NewField(&ArrayParameters{Item: item}, o...) +} diff --git a/pkg/schema/field/array_test.go b/pkg/schema/field/array_test.go new file mode 100644 index 0000000000000000000000000000000000000000..94e60258661932dc72c2c34c27ea7cecb5c6b33d --- /dev/null +++ b/pkg/schema/field/array_test.go @@ -0,0 +1,85 @@ +package field + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestArrayField_Decode(t *testing.T) { + tests := []struct { + name string + field *Field + data interface{} + want interface{} + wantErr bool + }{ + { + "Correct", + Array(Number("float")), + []interface{}{1.0, 2.0}, + []interface{}{1.0, 2.0}, + false, + }, + { + "Incorrect type", + Array(Number("int")), + "1 2 3", + "decode error: incorrect type: \"string\", expected \"[]interface{}\"", + true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Decode(nil, tt.field, tt.data) + if tt.wantErr { + require.Error(t, err) + assert.EqualError(t, err, tt.want.(string), fmt.Sprintf("Decode() error = %v, want %v", err, tt.want.(string))) + } + if !tt.wantErr { + require.NoError(t, err) + assert.ElementsMatch(t, got, tt.want, fmt.Sprintf("Decode() got = %v, want %v", got, tt.want)) + } + }) + } +} + +func TestArrayField_Encode(t *testing.T) { + tests := []struct { + name string + field *Field + data interface{} + want interface{} + wantErr bool + }{ + { + "Correct", + Array(Number("float")), + []interface{}{1.0, 2.0}, + []interface{}{1.0, 2.0}, + false, + }, + { + "Incorrect type", + Array(Number("int")), + "1 2 3", + "encode error: incorrect type: \"string\", expected \"[]interface{}\"", + true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Encode(nil, tt.field, tt.data) + if tt.wantErr { + require.Error(t, err) + assert.EqualError(t, err, tt.want.(string), fmt.Sprintf("Decode() error = %v, want %v", err, tt.want.(string))) + } + if !tt.wantErr { + require.NoError(t, err) + assert.ElementsMatch(t, got, tt.want, fmt.Sprintf("Decode() got = %v, want %v", got, tt.want)) + } + }) + } +} diff --git a/pkg/schema/field/boolean.go b/pkg/schema/field/boolean.go new file mode 100644 index 0000000000000000000000000000000000000000..94580541f8e33f9930adad94bff6a5c18915d05b --- /dev/null +++ b/pkg/schema/field/boolean.go @@ -0,0 +1,51 @@ +package field + +import ( + "context" + "fmt" + "reflect" +) + +var boolType = &BoolType{} + +type BoolParameters struct{} + +func (b BoolParameters) Type() Type { return boolType } +func (b *BoolParameters) Clone(reset bool) Parameters { return b } + +type BoolType struct{} + +func (b BoolType) Name() string { + return "bool" +} + +func (b BoolType) NewParameters() Parameters { + return &BoolParameters{} +} +func (BoolType) IsEmpty(v interface{}) bool { + return v == nil +} + +func (b BoolType) Decode(_ context.Context, field *Field, v interface{}) (interface{}, error) { + if v == nil { + return v, nil + } + if _, ok := v.(bool); ok { + return v, nil + } + return nil, fmt.Errorf("incorrect type: \"%s\", expected \"boolean\"", reflect.ValueOf(v).Kind()) +} + +func (b BoolType) Encode(_ context.Context, field *Field, v interface{}) (interface{}, error) { + if v == nil { + return v, nil + } + if _, ok := v.(bool); ok { + return v, nil + } + return nil, fmt.Errorf("incorrect type: \"%s\", expected \"boolean\"", reflect.ValueOf(v).Kind()) +} + +func Bool(o ...interface{}) *Field { + return NewField(&BoolParameters{}, o...) +} diff --git a/pkg/schema/field/boolean_test.go b/pkg/schema/field/boolean_test.go new file mode 100644 index 0000000000000000000000000000000000000000..aaa09dcec353d7648a976759bb4fcc98d0fe0d7a --- /dev/null +++ b/pkg/schema/field/boolean_test.go @@ -0,0 +1,73 @@ +package field + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBooleanField_Decode(t *testing.T) { + tests := []struct { + name string + field *Field + data interface{} + want interface{} + wantErr bool + }{ + {"Correct data bool", Bool(), true, true, false}, + {"Correct data nil", Bool(), nil, nil, false}, + {"Wrong data int", Bool(), 1, "decode error: incorrect type: \"int\", expected \"boolean\"", true}, + {"Wrong data float", Bool(), 0.0, "decode error: incorrect type: \"float64\", expected \"boolean\"", true}, + {"Wrong data array", Bool(), [2]bool{true}, "decode error: incorrect type: \"array\", expected \"boolean\"", true}, + {"Wrong data slice", Bool(), []bool{true}, "decode error: incorrect type: \"slice\", expected \"boolean\"", true}, + {"Wrong data map", Bool(), map[bool]int{true: 1}, "decode error: incorrect type: \"map\", expected \"boolean\"", true}, + {"Wrong data string", Bool(), "2", "decode error: incorrect type: \"string\", expected \"boolean\"", true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Decode(nil, tt.field, tt.data) + if tt.wantErr { + require.Error(t, err) + assert.EqualError(t, err, tt.want.(string), fmt.Sprintf("Decode() error = %v, want %v", err, tt.want.(string))) + } + if !tt.wantErr { + require.NoError(t, err) + assert.Equal(t, got, tt.want, fmt.Sprintf("Decode() got = %v, want %v", got, tt.want)) + } + }) + } +} + +func TestBooleanField_Encode(t *testing.T) { + tests := []struct { + name string + field *Field + data interface{} + want interface{} + wantErr bool + }{ + {"Correct", Bool(), false, false, false}, + {"Correct data nil", Bool(), nil, nil, false}, + {"Wrong data int", Bool(), 1, "encode error: incorrect type: \"int\", expected \"boolean\"", true}, + {"Wrong data float", Bool(), 0.0, "encode error: incorrect type: \"float64\", expected \"boolean\"", true}, + {"Wrong data array", Bool(), [2]bool{true}, "encode error: incorrect type: \"array\", expected \"boolean\"", true}, + {"Wrong data slice", Bool(), []bool{true}, "encode error: incorrect type: \"slice\", expected \"boolean\"", true}, + {"Wrong data map", Bool(), map[bool]int{true: 1}, "encode error: incorrect type: \"map\", expected \"boolean\"", true}, + {"Wrong data string", Bool(), "2", "encode error: incorrect type: \"string\", expected \"boolean\"", true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Encode(nil, tt.field, tt.data) + if tt.wantErr { + require.Error(t, err) + assert.EqualError(t, err, tt.want.(string), fmt.Sprintf("Encode() error = %v, want %v", err, tt.want.(string))) + } + if !tt.wantErr { + require.NoError(t, err) + assert.Equal(t, got, tt.want, fmt.Sprintf("Encode() got = %v, want %v", got, tt.want)) + } + }) + } +} diff --git a/pkg/schema/field/encode.go b/pkg/schema/field/encode.go new file mode 100644 index 0000000000000000000000000000000000000000..d891b41228e4ff70142cff9bdf3bca8cc99506dd --- /dev/null +++ b/pkg/schema/field/encode.go @@ -0,0 +1,66 @@ +package field + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/errors" +) + +type Decoder interface { + Decode(ctx context.Context, field *Field, v interface{}) (interface{}, error) +} + +type Encoder interface { + Encode(ctx context.Context, field *Field, v interface{}) (interface{}, error) +} + +func Decode(ctx context.Context, w Walker, v interface{}) (interface{}, error) { + var err error + //if ctx == nil { + // ctx = NewContext() + //} + // + //if m, ok := v.(map[string]interface{}); ok { + // ctx = ctx.ExtendEnv(m) + // ctx.DisableConditions = true + //} + + val, _, err := w.Walk(ctx, v, func(ctx context.Context, f *Field, v interface{}) (res WalkFuncResult, err error) { + if decoder, ok := f.GetType().(Decoder); ok { + if v, err = decoder.Decode(ctx, f, v); err != nil { + return + } + res.Value = v + res.Changed = true + return + } + res.Value = v + return + }) + + if err != nil { + return nil, errors.Wrap(err, "decode error") + } + + return val, nil +} + +func Encode(ctx context.Context, w Walker, v interface{}) (interface{}, error) { + val, _, err := w.Walk(ctx, v, func(ctx context.Context, f *Field, v interface{}) (res WalkFuncResult, err error) { + if encode, ok := f.GetType().(Encoder); ok { + if v, err = encode.Encode(ctx, f, v); err != nil { + return + } + res.Value = v + res.Changed = true + return + } + res.Value = v + return + }) + if err != nil { + return nil, errors.Wrap(err, "encode error") + } + return val, nil + +} diff --git a/pkg/schema/field/errors.go b/pkg/schema/field/errors.go new file mode 100644 index 0000000000000000000000000000000000000000..8ee095285a7493cf177cd9185d7235a344c49e67 --- /dev/null +++ b/pkg/schema/field/errors.go @@ -0,0 +1,7 @@ +package field + +import "errors" + +var ( + ErrSkipOption = errors.New("option invalid, skipped") +) diff --git a/pkg/schema/field/evaluate.go b/pkg/schema/field/evaluate.go new file mode 100644 index 0000000000000000000000000000000000000000..ad273819651a434a93e810564c0de5444579035f --- /dev/null +++ b/pkg/schema/field/evaluate.go @@ -0,0 +1,49 @@ +package field + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/errors" + "git.perx.ru/perxis/perxis-go/pkg/expr" +) + +const EvaluatePassesLimit = 10 + +func Evaluate(ctx context.Context, w Walker, v interface{}) (interface{}, error) { + var err error + + chg := true + val := v + i := 0 + + for chg { + m, _ := val.(map[string]interface{}) + + val, chg, err = w.Walk(expr.WithEnv(ctx, m), val, func(ctx context.Context, f *Field, v interface{}) (res WalkFuncResult, err error) { + enabled, _ := f.IsEnabled(ctx) + + if !enabled { + res.Stop = true + if v != nil { + res.Changed = true // Значение изменилоÑÑŒ на пуÑтое + } + return + } + + res.Value = v + return + }) + + if err != nil { + return nil, errors.Wrap(err, "evaluation error") + } + + i += 1 + + if i > EvaluatePassesLimit { + return nil, errors.New("fail to evaluate data conditions") + } + } + + return val, nil +} diff --git a/pkg/schema/field/field.go b/pkg/schema/field/field.go new file mode 100644 index 0000000000000000000000000000000000000000..45ddb40d128ab3f87ff7c41e3cedf75009ba3f98 --- /dev/null +++ b/pkg/schema/field/field.go @@ -0,0 +1,528 @@ +package field + +import ( + "context" + "strings" + + "git.perx.ru/perxis/perxis-go/pkg/data" + "git.perx.ru/perxis/perxis-go/pkg/errors" + "git.perx.ru/perxis/perxis-go/pkg/expr" +) + +const ( + FieldSeparator = "." + IncludeLimit = 10 +) + +type ( + Preparer interface { + Prepare(f *Field) error + } + + Fielder interface { + GetField(path string) *Field + } +) + +type Translation struct { + Locale string `json:"locale,omitempty"` + Title string `json:"title,omitempty"` + Description string `json:"description,omitempty"` +} + +type View struct { + Widget string `json:"widget,omitempty"` // Виджет Ð´Ð»Ñ Ð¾Ñ‚Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ Ð¿Ð¾Ð»Ñ Ð² ÑпиÑке + Options map[string]interface{} `json:"options,omitempty"` // Опции виджета, на уÑÐ¼Ð¾Ñ‚Ñ€ÐµÐ½Ð¸Ñ Ð²Ð¸Ð´Ð¶ÐµÑ‚Ð° +} + +type UI struct { + Widget string `json:"widget,omitempty"` // Ð˜Ð¼Ñ Ð²Ð¸Ð´Ð¶ÐµÑ‚Ð° Ð´Ð»Ñ Ð¾Ñ‚Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ Ð¿Ð¾Ð»Ñ Ð² пользовательÑком интерфейÑе + Placeholder string `json:"placeholder,omitempty"` // ПодÑказка Ð´Ð»Ñ Ð·Ð°Ð¿Ð¾Ð»Ð½ÐµÐ½Ð¸Ñ Ð·Ð½Ð°Ñ‡ÐµÐ½Ð¸Ñ + Options map[string]interface{} `json:"options,omitempty"` // Опции виджета Ð´Ð»Ñ Ð¾Ñ‚Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ + ReadView *View `json:"read_view,omitempty"` // ÐаÑтройки Ð´Ð»Ñ Ð¾Ñ‚Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ Ñкрана в режиме проÑмотра Ñлемента + EditView *View `json:"edit_view,omitempty"` // ÐаÑтройки Ð´Ð»Ñ Ð¾Ñ‚Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ Ñкрана в режиме Ñ€ÐµÐ´Ð°ÐºÑ‚Ð¸Ñ€Ð¾Ð²Ð°Ð½Ð¸Ñ Ñлемента + ListView *View `json:"list_view,omitempty"` // ÐаÑтройки Ð´Ð»Ñ Ð¾Ñ‚Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ Ñкрана в режиме ÑпиÑке Ñлементов +} + +type Include struct { + Ref string `json:"ref,omitempty"` + Optional bool `json:"optional,omitempty"` +} + +type Field struct { + Title string `json:"title,omitempty"` // Ðазвание Ð¿Ð¾Ð»Ñ (Ðапример: name) + Description string `json:"description,omitempty"` // ОпиÑание поле (Ðапример: User name) + Translations []Translation `json:"translations,omitempty"` // Переводы данных на разных Ñзыках + UI *UI `json:"ui,omitempty"` // Опции пользовательÑкого интерфейÑа + Includes []Include `json:"includes,omitempty"` // Импорт Ñхем + SingleLocale bool `json:"singleLocale,omitempty"` // Без перевода + Indexed bool `json:"indexed,omitempty"` // ПоÑтроить Ð¸Ð½Ð´ÐµÐºÑ Ð´Ð»Ñ Ð¿Ð¾Ð»Ñ + Unique bool `json:"unique,omitempty"` // Значение Ð¿Ð¾Ð»Ñ Ð´Ð¾Ð»Ð¶Ð½Ñ‹ быть уникальными + TextSearch bool `json:"text_search,omitempty"` // Значение Ð¿Ð¾Ð»Ñ Ð´Ð¾Ñтупны Ð´Ð»Ñ Ð¿Ð¾Ð»Ð½Ð¾Ñ‚ÐµÐºÑтового поиÑка + Params Parameters `json:"-"` // Параметры полÑ, определÑет так же тип Ð¿Ð¾Ð»Ñ + Options Options `json:"options,omitempty"` // Дополнительные опции + Condition string `json:"condition,omitempty"` // УÑловие Ð¾Ñ‚Ð¾Ð±Ñ€Ð°Ð¶ÐµÐ½Ð¸Ñ Ð¿Ð¾Ð»Ñ + AdditionalValues bool `json:"additional_values,omitempty"` // Разрешает дополнительные Ð·Ð½Ð°Ñ‡ÐµÐ½Ð¸Ñ Ð²Ð½Ðµ ограничений правил + + prepared bool +} + +// TODO: Replace with Named field??? +type PathField struct { + Field + Name string + Path string +} + +type NamedField struct { + *Field + Name string +} + +func NewField(params Parameters, opts ...interface{}) *Field { + f := &Field{} + f.Params = params + f.Options.Add(opts...) + return f +} + +func (f Field) GetType() Type { + return f.Params.Type() +} + +func (f *Field) AddOptions(t ...interface{}) *Field { + f.Options.Add(t...) + return f +} + +func (f Field) WithUI(ui *UI) *Field { + f.UI = ui + return &f +} + +func (f *Field) SetIncludes(includes ...interface{}) { + f.Includes = make([]Include, 0, len(includes)) + for _, i := range includes { + switch v := i.(type) { + case string: + f.Includes = append(f.Includes, Include{Ref: v}) + case Include: + f.Includes = append(f.Includes, v) + default: + panic("incorrect import type") + } + } +} + +func (f Field) WithIncludes(includes ...interface{}) *Field { + f.SetIncludes(includes...) + return &f +} + +func (f Field) GetIncludes() []string { + return f.getIncludes() +} + +func (f Field) getIncludes() []string { + res := make([]string, len(f.Includes)) + for i, inc := range f.Includes { + res[i] = inc.Ref + } + nested := f.GetNestedFields() + for _, fld := range nested { + res = append(res, fld.getIncludes()...) + } + return res +} + +func (f Field) IsIncluded(name string) bool { + return data.GlobMatch(name, f.GetIncludes()...) +} + +func (f Field) SetTitle(title string) *Field { + f.Title = title + return &f +} + +func (f Field) SetDescription(desc string) *Field { + f.Description = desc + return &f +} + +func (f Field) AddTranslation(locale, title, desc string) *Field { + for i, t := range f.Translations { + if t.Locale == locale { + f.Translations[i] = Translation{Locale: locale, Title: title, Description: desc} + return &f + } + } + + f.Translations = append(f.Translations, Translation{Locale: locale, Title: title, Description: desc}) + return &f +} + +func (f Field) SetSingleLocale(r bool) *Field { + f.SingleLocale = r + return &f +} + +func (f Field) SetIndexed(r bool) *Field { + f.Indexed = r + return &f +} + +func (f Field) SetAdditionalValues() *Field { + f.AdditionalValues = true + return &f +} + +func (f Field) SetUnique(r bool) *Field { + f.Unique = r + return &f +} + +func (f Field) SetTextSearch(r bool) *Field { + f.TextSearch = r + return &f +} + +func (f Field) SetCondition(c string) *Field { + f.Condition = c + return &f +} + +func (f *Field) MustEnabled(ctx context.Context) bool { + if enabled, err := f.IsEnabled(ctx); !enabled || err != nil { + return false + } + return true +} + +func (f *Field) IsEnabled(ctx context.Context) (bool, error) { + if f.Condition != "" { + out, err := expr.Eval(ctx, f.Condition, nil) + if err != nil { + return false, err + } + + if enabled, ok := out.(bool); ok { + return enabled, nil + } + + return false, errors.New("condition returns non-boolean value") + } + + return true, nil +} + +// Walk - выполнÑет обход данных по Ñхеме и выполнÑет функцию, ÐºÐ¾Ñ‚Ð¾Ñ€Ð°Ñ Ð¼Ð¾Ð¶ÐµÑ‚ модифицировать данные при необходимоÑти +func (f *Field) Walk(ctx context.Context, v interface{}, fn WalkFunc, opt ...WalkOption) (interface{}, bool, error) { + res, err := fn(ctx, f, v) + + if err != nil { + return nil, false, err + } + + if res.Changed || res.Stop { + return res.Value, res.Changed, err + } + + if res.Context != nil { + ctx = res.Context + } + + if walker, ok := f.GetType().(FieldWalker); ok { + val, changed, err := walker.Walk(ctx, f, v, fn, NewWalkOptions(opt...)) + if err != nil { + return nil, false, err + } + return val, changed, err + } + + return v, false, nil +} + +// DEPRECATED +func (f *Field) Prepare() error { + if preparer, ok := f.GetType().(Preparer); ok { + if err := preparer.Prepare(f); err != nil { + return err + } + } + for _, o := range f.Options { + if preparer, ok := o.(Preparer); ok { + if err := preparer.Prepare(f); err != nil { + return err + } + } + } + return nil +} + +// GetField возвращает поле по Ñтроковому пути +func (f *Field) GetField(path string) *Field { + if path == "" { + switch params := f.Params.(type) { + case *ArrayParameters: + // Возвращаем поле Item еÑли путь указан как "arr." + return params.Item + } + return nil + } + + switch params := f.Params.(type) { + case *ObjectParameters: + pp := strings.SplitN(path, FieldSeparator, 2) + + for k, v := range params.Fields { + + p, ok := v.Params.(*ObjectParameters) + if ok && p.Inline { + f := v.GetField(path) + if f != nil { + return f + } + } + + if k == pp[0] { + if len(pp) == 1 { + return v + } + return v.GetField(pp[1]) + } + } + case Fielder: + return params.GetField(path) + + case *ArrayParameters: + return params.Item.GetField(path) + } + + return nil +} + +// GetFieldsPath возвращает полный путь Ð´Ð»Ñ Ð¼Ð°ÑÑива полей +func GetFieldsPath(flds []PathField) (res []string) { + for _, f := range flds { + res = append(res, f.Path) + } + return res +} + +type FilterFunc func(*Field, string) bool + +func GetAll(field *Field, path string) bool { return true } + +func (f *Field) GetFields(filterFunc FilterFunc, pathPrefix ...string) (res []PathField) { + var path string + + if len(pathPrefix) > 0 { + path = pathPrefix[0] + } + + // добавление корневого объекта Ð´Ð»Ñ Ñ‡ÐµÐ³Ð¾-то нужно? + if path != "" && filterFunc(f, path) { + res = append(res, PathField{ + Field: *f, + Path: path, + }) + } + + switch params := f.Params.(type) { + case *ObjectParameters: + res = append(res, getFieldsObject(path, params, filterFunc, false)...) + case *ArrayParameters: + res = append(res, getFieldsArray(path, params, filterFunc)...) + } + + //if len(pathPrefix) > 0 { + // for _, r := range res { + // r.Path = strings.Join([]string{pathPrefix[0], r.Path}, FieldSeparator) + // } + //} + + return res +} + +func getFieldsArray(path string, params *ArrayParameters, filterFunc FilterFunc) (res []PathField) { + + switch params := params.Item.Params.(type) { + case *ObjectParameters: + res = append(res, getFieldsObject(path, params, filterFunc, params.Inline)...) + + case *ArrayParameters: + res = append(res, getFieldsArray(path, params, filterFunc)...) + } + + return res +} + +func getFieldsObject(path string, params *ObjectParameters, filterFunc FilterFunc, ignoreInline bool) (res []PathField) { + for k, v := range params.Fields { + if v == nil { + continue + } + + var newPath string + lastIdx := strings.LastIndex(path, ".") + + if path == "" || !ignoreInline && params.Inline && lastIdx < 0 { + newPath = k + } else { + if !params.Inline || ignoreInline { + newPath = strings.Join([]string{path, k}, FieldSeparator) + } else { + newPath = strings.Join([]string{path[:lastIdx], k}, FieldSeparator) + } + } + + if flds := v.GetFields(filterFunc, newPath); len(flds) > 0 { + res = append(res, flds...) + } + } + + return res +} + +func (f *Field) GetNestedFields() []*Field { + switch params := f.Params.(type) { + case *ObjectParameters: + flds := make([]*Field, 0, len(params.Fields)) + for _, v := range params.Fields { + if v == nil { + continue + } + flds = append(flds, v) + } + return flds + case *ArrayParameters: + return []*Field{params.Item} + } + + return nil +} + +// Clone Ñоздает копию Ð¿Ð¾Ð»Ñ +// Параметр reset указывает необходимо ли отвÑзать параметры Ð¿Ð¾Ð»Ñ Ð¾Ñ‚ вложенных полей +func (f Field) Clone(reset bool) *Field { + if f.UI != nil { + ui := *f.UI + f.UI = &ui + } + + if len(f.Translations) > 0 { + f.Translations = append(make([]Translation, 0, len(f.Translations)), f.Translations...) + } + + if f.Options != nil { + opts := make(Options) + for k, v := range f.Options { + opts[k] = v + } + f.Options = opts + } + + if f.Params != nil { + f.Params = f.Params.Clone(reset) + } + + return &f +} + +func (f *Field) mergeField(fld *Field) error { + if f.Title == "" { + f.Title = fld.Title + } + + if f.Description == "" { + f.Description = fld.Description + } + + if len(f.Translations) == 0 { + f.Translations = fld.Translations + } + + if f.UI == nil { + f.UI = fld.UI + } + + if len(f.Includes) > 0 { + f.Includes = fld.Includes + } + + if f.Params == nil { + f.Params = fld.Params + } else if fld.Params != nil { + type Merger interface { + Merge(parameters Parameters) error + } + + if merger, ok := f.Params.(Merger); ok { + if err := merger.Merge(fld.Params); err != nil { + return err + } + } + } + + if f.Options == nil { + f.Options = fld.Options + } + + if f.Condition == "" { + f.Condition = fld.Condition + } + + return nil +} + +func (f *Field) Merge(fields ...*Field) error { + for _, fld := range fields { + f.mergeField(fld) + } + return nil +} + +func (f *Field) loadIncludes(ctx context.Context, loader Loader, depth int) error { + if depth > IncludeLimit { + return errors.New("limit for included fields exceeded") + } + + for _, i := range f.Includes { + if loader == nil { + panic("schema loader not set") + } + importedField, err := loader.Load(ctx, i.Ref) + if err != nil { + if i.Optional { + continue + } + return err + } + + for _, fld := range importedField { + depth += 1 + if err := fld.loadIncludes(ctx, loader, depth); err != nil { + return err + } + } + + if err = f.Merge(importedField...); err != nil { + return err + } + } + for _, i := range f.GetNestedFields() { + if err := i.loadIncludes(ctx, loader, depth); err != nil { + return err + } + } + return nil +} + +func (f *Field) LoadIncludes(ctx context.Context, loader Loader) error { + return f.loadIncludes(ctx, loader, 0) +} + +func (f *Field) LoadRef(ctx context.Context, ref string, loader Loader) error { + f.SetIncludes(ref) + return f.LoadIncludes(ctx, loader) +} diff --git a/pkg/schema/field/field_json.go b/pkg/schema/field/field_json.go new file mode 100644 index 0000000000000000000000000000000000000000..dfb212044838d66da90436b127594427d3f842da --- /dev/null +++ b/pkg/schema/field/field_json.go @@ -0,0 +1,86 @@ +package field + +import ( + "encoding/json" + + jsoniter "github.com/json-iterator/go" + "github.com/pkg/errors" +) + +type FieldData Field + +type jsonField struct { + FieldData + Type string `json:"type"` + Params json.RawMessage `json:"params,omitempty"` +} + +type ParametersConverter interface { + ConvertParameters(p Parameters) (Parameters, error) +} + +type ParametersValidator interface { + ValidateParameters(p Parameters) error +} + +func (f *Field) UnmarshalJSON(b []byte) error { + var j jsonField + if err := jsoniter.Unmarshal(b, &j); err != nil { + return errors.Wrapf(err, "error unmarshal json into field") + } + + typ, ok := GetType(j.Type) + + jsonParams := j.Params + + // Unknown type, Ñоздаем Ð¿Ð¾Ð»Ñ Unknown которое будет хранить оригинальные параметры + if !ok { + *f = Field(j.FieldData) + f.Params = &UnknownParameters{Typ: j.Type, Params: jsonParams} + return nil + } + + params := typ.NewParameters() + if len(j.Params) > 0 { + if err := jsoniter.Unmarshal(jsonParams, params); err != nil { + return errors.Wrapf(err, "error unmarshal json into field type %s", typ.Name()) + } + } + + if converter, ok := typ.(ParametersConverter); ok { + var err error + if params, err = converter.ConvertParameters(params); err != nil { + return errors.Wrap(err, "error unmarshal json") + } + } + + if validator, ok := typ.(ParametersValidator); ok { + var err error + if err = validator.ValidateParameters(params); err != nil { + return errors.Wrap(err, "error validate json") + } + } + + *f = Field(j.FieldData) + f.Params = params + f.Prepare() + return nil +} + +func (f *Field) MarshalJSON() ([]byte, error) { + j := jsonField{ + FieldData: FieldData(*f), + } + + j.Type = f.GetType().Name() + + if f.Params != nil { + b, err := jsoniter.Marshal(f.Params) + if err != nil { + return nil, err + } + j.Params = b + } + + return jsoniter.Marshal(&j) +} diff --git a/pkg/schema/field/init.go b/pkg/schema/field/init.go new file mode 100644 index 0000000000000000000000000000000000000000..45c4d11c1ecdc51927109cb3202704e093c2d53d --- /dev/null +++ b/pkg/schema/field/init.go @@ -0,0 +1,13 @@ +package field + +func init() { + Register(boolType) + Register(stringType) + Register(numberType) + Register(arrayType) + Register(timeType) + Register(objectType) + Register(unknownType) + Register(locationType) + Register(primaryKeyType) +} diff --git a/pkg/schema/field/loader.go b/pkg/schema/field/loader.go new file mode 100644 index 0000000000000000000000000000000000000000..583ecc2c7c6bdc0d278819d2327280ec87858117 --- /dev/null +++ b/pkg/schema/field/loader.go @@ -0,0 +1,34 @@ +package field + +import ( + "context" + "fmt" +) + +// Loader Ð¸Ð½Ñ‚ÐµÑ€Ñ„ÐµÐ¹Ñ Ð·Ð°Ð³Ñ€ÑƒÐ·Ñ‡Ð¸ÐºÐ° Ñхем +type Loader interface { + Load(ctx context.Context, ref string) (fs []*Field, err error) +} + +type LoaderFunc func(ref string) (fs []*Field, err error) + +func (f LoaderFunc) Load(ctx context.Context, ref string) (fs []*Field, err error) { + return f(ref) +} + +type multiLoader struct { + loaders []Loader +} + +func (c *multiLoader) Load(ctx context.Context, ref string) (fs []*Field, err error) { + for _, l := range c.loaders { + if f, err := l.Load(nil, ref); err == nil { + return f, nil + } + } + return nil, fmt.Errorf("invalid schema reference: %s", ref) +} + +func MultiLoader(loaders ...Loader) Loader { + return &multiLoader{loaders: loaders} +} diff --git a/pkg/schema/field/location.go b/pkg/schema/field/location.go new file mode 100644 index 0000000000000000000000000000000000000000..eb6e175552a2504b667658c8b227bc3b7ff661b8 --- /dev/null +++ b/pkg/schema/field/location.go @@ -0,0 +1,137 @@ +package field + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/errors" + "github.com/mitchellh/mapstructure" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "go.mongodb.org/mongo-driver/x/bsonx" +) + +var locationType = &LocationType{} + +type LocationParameters struct{} + +func (p LocationParameters) Type() Type { return locationType } +func (p LocationParameters) Clone(reset bool) Parameters { return &LocationParameters{} } + +func (p LocationParameters) GetMongoIndexes(path string, f *Field) []mongo.IndexModel { + var add, geo mongo.IndexModel + a := path + ".address" + g := path + ".geometry" + add.Options = options.Index().SetName(a) + //add.Options.SetSparse(true) + add.Options.SetPartialFilterExpression(bson.M{a: bson.M{"$exists": true}}) + geo.Options = options.Index().SetName(g) + if f.Unique { + add.Options.SetUnique(true) + geo.Options.SetUnique(true) + } + + if f.Indexed { + add.Keys = bsonx.Doc{{Key: a, Value: bsonx.Int32(1)}} + geo.Keys = bsonx.Doc{{Key: g, Value: bsonx.String("2dsphere")}} + } + return []mongo.IndexModel{add, geo} +} + +type LocationType struct{} + +type GeoJSON struct { + Type string `json:"type" bson:"type" mapstructure:"type,omitempty"` + Coordinates []float64 `json:"coordinates" bson:"coordinates" mapstructure:"coordinates"` +} + +type GeoObject struct { + Address string `json:"address,omitempty" bson:"address" mapstructure:"address,omitempty"` + Geometry *GeoJSON `json:"geometry,omitempty" bson:"geometry" mapstructure:"geometry,omitempty"` +} + +func (LocationType) Name() string { + return "location" +} + +func (LocationType) NewParameters() Parameters { + return &LocationParameters{} +} + +func (LocationType) IsEmpty(v interface{}) bool { + loc, _ := v.(*GeoObject) + return loc == nil || loc.Address != "" && loc.Geometry != nil +} + +func (LocationType) Decode(_ context.Context, _ *Field, v interface{}) (interface{}, error) { + + if v == nil { + return nil, nil + } + + var g GeoObject + if err := mapstructure.Decode(v, &g); err != nil { + return nil, err + } + + if g.Address == "" && g.Geometry == nil { + return nil, errors.New("address or coordinates required") + } + + if g.Geometry != nil { + if len(g.Geometry.Coordinates) != 2 { + return nil, errors.New("latitude and longitude required") + } + + lat := g.Geometry.Coordinates[0] + lon := g.Geometry.Coordinates[1] + + if lat < -180 || lat > 180 { + return nil, errors.New("invalid longitude values, valid are between -180 and 180") + } + + if lon < -90 || lon > 90 { + return nil, errors.New("invalid latitude values, valid are between -90 and 90") + } + + if g.Geometry.Type != "Point" { + g.Geometry.Type = "Point" + } + } + + return &g, nil +} + +func (LocationType) Encode(_ context.Context, _ *Field, v interface{}) (interface{}, error) { + + if v == nil { + return nil, nil + } + + g, ok := v.(*GeoObject) + if !ok { + return nil, errors.New("couldn't encode GeoObject") + } + + res := make(map[string]interface{}) + if g.Address != "" { + res["address"] = g.Address + } + + if g.Geometry != nil { + if len(g.Geometry.Coordinates) != 2 { + return nil, errors.New("latitude and longitude required") + } + + lat := g.Geometry.Coordinates[0] + lon := g.Geometry.Coordinates[1] + + res["geometry"] = map[string]interface{}{"type": g.Geometry.Type, "coordinates": []interface{}{lat, lon}} + } + + return res, nil +} + +func Location(o ...interface{}) *Field { + return NewField(&LocationParameters{}, o...) +} diff --git a/pkg/schema/field/location_test.go b/pkg/schema/field/location_test.go new file mode 100644 index 0000000000000000000000000000000000000000..5b8206b6128ae7f4bf8f4042cedc0540bf357992 --- /dev/null +++ b/pkg/schema/field/location_test.go @@ -0,0 +1,209 @@ +package field + +import ( + "reflect" + "testing" +) + +func TestLocationField_Decode(t *testing.T) { + tests := []struct { + name string + field *Field + data interface{} + want interface{} + wantErr bool + }{ + {"Correct", + Location(), + map[string]interface{}{ + "address": "msk", + "geometry": map[string]interface{}{"type": "Point", "coordinates": []interface{}{55.7042351, 37.6152822}}, + }, + &GeoObject{"msk", &GeoJSON{"Point", []float64{55.7042351, 37.6152822}}}, + false}, + {"Correct", + Location(), + map[string]interface{}{ + "geometry": map[string]interface{}{"type": "Point", "coordinates": []interface{}{55.7042351, 37.6152822}}, + }, + &GeoObject{Geometry: &GeoJSON{"Point", []float64{55.7042351, 37.6152822}}}, + false}, + {"Correct", + Location(), + map[string]interface{}{ + "geometry": map[string]interface{}{"coordinates": []interface{}{55.7042351, 37.6152822}}, + }, + &GeoObject{Geometry: &GeoJSON{"Point", []float64{55.7042351, 37.6152822}}}, + false}, + {"Correct", + Location(), + map[string]interface{}{ + "geometry": map[string]interface{}{"coordinates": []float64{55.7042351, 37.6152822}}, + }, + &GeoObject{Geometry: &GeoJSON{"Point", []float64{55.7042351, 37.6152822}}}, + false}, + {"Correct", + Location(), + map[string]interface{}{ + "geometry": map[string]interface{}{"coordinates": []interface{}{55, 37}}, + }, + &GeoObject{Geometry: &GeoJSON{"Point", []float64{55, 37}}}, + false}, + {"Correct", + Location(), + map[string]interface{}{ + "geometry": map[string]interface{}{"coordinates": []interface{}{180, 90}}, + }, + &GeoObject{Geometry: &GeoJSON{"Point", []float64{180, 90}}}, + false}, + {"Correct", + Location(), + map[string]interface{}{ + "geometry": map[string]interface{}{"coordinates": []interface{}{-180, -90}}, + }, + &GeoObject{Geometry: &GeoJSON{"Point", []float64{-180, -90}}}, + false}, + {"Correct", + Location(), + map[string]interface{}{ + "geometry": map[string]interface{}{"coordinates": []int{55, 37}}, + }, + &GeoObject{Geometry: &GeoJSON{"Point", []float64{55, 37}}}, + false}, + {"Correct", + Location(), + map[string]interface{}{ + "address": "msk", + }, + &GeoObject{Address: "msk"}, + false}, + {"Correct", Location(), nil, nil, false}, + + {"Wrong data", Location(), "", nil, true}, + {"Wrong data", Location(), []interface{}{"55.7042351", "37.6152822"}, nil, true}, + {"Wrong data", Location(), map[string]interface{}{"type": "Point", "coordinates": [][]interface{}{{55.7042351, 37.6152822}}}, nil, true}, + {"Wrong data", Location(), []interface{}{55.7042351, 37.6152822, 1.0}, nil, true}, + {"Wrong data", + Location(), + map[string]interface{}{ + "geometry": map[string]interface{}{"coordinates": []int{55, 37, 67}}, + }, + nil, + true}, + {"Wrong data", + Location(), + map[string]interface{}{ + "geometry": map[string]interface{}{"coordinates": []interface{}{180}}, + }, + nil, + true}, + {"Wrong data", + Location(), + map[string]interface{}{ + "geometry": map[string]interface{}{"coordinates": []interface{}{-180, -90.1}}, + }, + nil, + true}, + {"Wrong data", + Location(), + map[string]interface{}{ + "geometry": map[string]interface{}{"coordinates": []interface{}{180.1, 90.1}}, + }, + nil, + true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Decode(nil, tt.field, tt.data) + if (err != nil) != tt.wantErr { + t.Errorf("Decode() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Decode() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestLocationField_Encode(t *testing.T) { + tests := []struct { + name string + field *Field + data interface{} + want interface{} + wantErr bool + }{ + {"Correct", Location(), + &GeoObject{Address: "msk", Geometry: &GeoJSON{"Point", []float64{55.7042351, 37.6152822}}}, + map[string]interface{}{ + "address": "msk", + "geometry": map[string]interface{}{"type": "Point", "coordinates": []interface{}{55.7042351, 37.6152822}}}, + false}, + {"Correct", Location(), + &GeoObject{Geometry: &GeoJSON{"Point", []float64{55.7042351, 37.6152822}}}, + map[string]interface{}{ + "geometry": map[string]interface{}{"type": "Point", "coordinates": []interface{}{55.7042351, 37.6152822}}}, + false}, + {"Correct", Location(), + &GeoObject{Address: "msk"}, + map[string]interface{}{ + "address": "msk"}, + false}, + {"Correct", Location(), + &GeoObject{Geometry: &GeoJSON{"Point", []float64{55, 37}}}, + map[string]interface{}{ + "geometry": map[string]interface{}{"type": "Point", "coordinates": []interface{}{55.0, 37.0}}}, + false}, + {"Correct", Location(), + &GeoObject{Geometry: &GeoJSON{"Point", []float64{180, 90}}}, + map[string]interface{}{ + "geometry": map[string]interface{}{"type": "Point", "coordinates": []interface{}{180.0, 90.0}}}, + false}, + {"Correct", Location(), + &GeoObject{Geometry: &GeoJSON{"Point", []float64{-180, -90}}}, + map[string]interface{}{ + "geometry": map[string]interface{}{"type": "Point", "coordinates": []interface{}{-180.0, -90.0}}}, + false}, + {"Correct", Location(), nil, nil, false}, + {"Correct", Location(), + &GeoObject{}, + map[string]interface{}{}, + false}, + + {"Wrong data", Location(), "", nil, true}, + {"Wrong data", Location(), []interface{}{55.7042351, 37.6152822}, nil, true}, + {"Wrong data", Location(), + map[string]interface{}{ + "address": "msk", + "geometry": map[string]interface{}{"type": "Point", "coordinates": []interface{}{55.7042351, 37.6152822}}}, + nil, + true}, + {"Wrong data", Location(), + map[string]interface{}{ + "geometry": map[string]interface{}{"type": "Point", "coordinates": []interface{}{55.7042351, 37.6152822}}}, + nil, + true}, + {"Wrong data", Location(), + map[string]interface{}{ + "address": "msk"}, + nil, + true}, + {"Wrong data", Location(), &GeoJSON{}, nil, true}, + {"Wrong data", Location(), &GeoJSON{Coordinates: []float64{55.7042351, 37.6152822}}, nil, true}, + {"Wrong data", Location(), &GeoObject{Geometry: &GeoJSON{"Point", []float64{-180, -90, 50}}}, nil, true}, + {"Wrong data", Location(), &GeoObject{Geometry: &GeoJSON{"Point", []float64{-180}}}, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Encode(nil, tt.field, tt.data) + if (err != nil) != tt.wantErr { + t.Errorf("Encode() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Encode() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/schema/field/number.go b/pkg/schema/field/number.go new file mode 100644 index 0000000000000000000000000000000000000000..502a2fef7c406b4b4dc96aad34efce2e518a1e28 --- /dev/null +++ b/pkg/schema/field/number.go @@ -0,0 +1,112 @@ +package field + +import ( + "context" + "math" + + "github.com/pkg/errors" +) + +const ( + NumberFormatInt = "int" + NumberFormatFloat = "float" +) + +var numberType = &NumberType{} + +type NumberParameters struct { + Format string `json:"format,omitempty"` +} + +func (NumberParameters) Type() Type { return numberType } +func (p NumberParameters) Clone(reset bool) Parameters { return &p } + +type NumberType struct{} + +func (NumberType) Name() string { + return "number" +} + +func (NumberType) NewParameters() Parameters { + return &NumberParameters{} +} + +func (NumberType) IsEmpty(v interface{}) bool { + return v == nil +} + +func ToNumber(i interface{}) (interface{}, error) { + switch v := i.(type) { + case int64: + return v, nil + case int: + return int64(v), nil + case int8: + return int64(v), nil + case int32: + return int64(v), nil + case uint64: + return v, nil + case uint: + return uint64(v), nil + case uint8: + return uint64(v), nil + case uint32: + return uint64(v), nil + case float32: + return float64(v), nil + case float64: + return v, nil + } + return 0, errors.Errorf("error convert \"%s\" to number", i) +} + +func (n NumberType) Decode(ctx context.Context, field *Field, v interface{}) (interface{}, error) { + return n.decode(ctx, field, v) +} + +func (NumberType) decode(_ context.Context, field *Field, v interface{}) (interface{}, error) { + params, ok := field.Params.(*NumberParameters) + if !ok { + return nil, errors.New("field parameters required") + } + + if v == nil { + return v, nil + } + + n, err := ToNumber(v) + if err != nil { + return nil, err + } + + switch params.Format { + case NumberFormatInt: + switch i := n.(type) { + case int64: + return i, nil + case uint64: + return i, nil + case float64: + return int64(math.Round(i)), nil + } + case NumberFormatFloat: + switch i := n.(type) { + case float64: + return i, nil + case int64: + return float64(i), nil + case uint64: + return float64(i), nil + } + } + return n, nil +} + +func (n NumberType) Encode(ctx context.Context, field *Field, v interface{}) (interface{}, error) { + return n.decode(ctx, field, v) +} + +func Number(format string, o ...interface{}) *Field { + return NewField(&NumberParameters{Format: format}, o...) +} diff --git a/pkg/schema/field/number_test.go b/pkg/schema/field/number_test.go new file mode 100644 index 0000000000000000000000000000000000000000..e731793d12135127dbfa67d949572f52708075a6 --- /dev/null +++ b/pkg/schema/field/number_test.go @@ -0,0 +1,88 @@ +package field + +import ( + "reflect" + "testing" +) + +func TestNumberField_Decode(t *testing.T) { + tests := []struct { + name string + field *Field + data interface{} + want interface{} + wantErr bool + }{ + {"Correct", Number("int"), int64(2), int64(2), false}, // #0 + {"Correct", Number("int"), 2.2, int64(2), false}, // #1 + {"Correct", Number("int"), 2, int64(2), false}, // #2 + {"Correct", Number("int"), float32(2.2), int64(2), false}, // #3 + {"Correct", Number("int"), float64(2.6), int64(3), false}, // #4 + {"Correct", Number("int"), 2.6, int64(3), false}, // #5 + + {"Correct", Number("float"), int8(2), 2.0, false}, // #6 + {"Correct", Number("float"), 2.2, 2.2, false}, // #7 + {"Correct", Number("float"), 2, 2.0, false}, // #8 + {"Correct", Number("float"), float32(2.2), 2.200000047683716, false}, // #9 + {"Correct", Number("float"), int64(2), 2.0, false}, // #10 + + {"Wrong data", Number("int"), "", nil, true}, // #0 + {"Wrong data", Number("int"), []byte(""), nil, true}, // #1 + + {"Wrong data", Number("float"), "", nil, true}, // #2 + {"Wrong data", Number("float"), []byte(""), nil, true}, // #3 + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Decode(nil, tt.field, tt.data) + if (err != nil) != tt.wantErr { + t.Errorf("Decode() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Decode() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestNumberField_Encode(t *testing.T) { + tests := []struct { + name string + field *Field + data interface{} + want interface{} + wantErr bool + }{ + {"Correct", Number("int"), int64(2), int64(2), false}, // #0 + {"Correct", Number("int"), 2.2, int64(2), false}, // #1 + {"Correct", Number("int"), 2, int64(2), false}, // #2 + {"Correct", Number("int"), float32(2.2), int64(2), false}, // #3 + {"Correct", Number("int"), float32(2.6), int64(3), false}, // #4 + {"Correct", Number("int"), 2.6, int64(3), false}, // #5 + + {"Correct", Number("float"), int8(2), 2.0, false}, // #6 + {"Correct", Number("float"), 2.2, 2.2, false}, // #7 + {"Correct", Number("float"), 2, 2.0, false}, // #8 + {"Correct", Number("float"), float32(2.2), 2.200000047683716, false}, // #9 + {"Correct", Number("float"), int64(2), 2.0, false}, // #10 + + {"Wrong data", Number("int"), "", nil, true}, // #0 + {"Wrong data", Number("int"), []byte(""), nil, true}, // #1 + + {"Wrong data", Number("float"), "", nil, true}, // #2 + {"Wrong data", Number("float"), []byte(""), nil, true}, // #3 + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Encode(nil, tt.field, tt.data) + if (err != nil) != tt.wantErr { + t.Errorf("Decode() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Decode() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/schema/field/object.go b/pkg/schema/field/object.go new file mode 100644 index 0000000000000000000000000000000000000000..d86aa2352544f68ea837152ec6799677f4624d1a --- /dev/null +++ b/pkg/schema/field/object.go @@ -0,0 +1,297 @@ +package field + +import ( + "context" + "fmt" + "reflect" + "regexp" + + "git.perx.ru/perxis/perxis-go/pkg/errors" + "git.perx.ru/perxis/perxis-go/pkg/expr" + "github.com/hashicorp/go-multierror" +) + +var objectType = &ObjectType{} +var isValidName = regexp.MustCompile(`^[a-zA-Z][\w]*$`).MatchString + +type ObjectParameters struct { + Inline bool `json:"inline"` + Fields map[string]*Field `json:"fields"` +} + +func (ObjectParameters) Type() Type { return objectType } + +func (p ObjectParameters) Clone(reset bool) Parameters { + if reset { + p.Fields = nil + return &p + } + + flds := make(map[string]*Field) + for k, v := range p.Fields { + flds[k] = v.Clone(reset) + } + + p.Fields = flds + return &p +} + +// IsInlineObject определÑет ÑвлÑтьÑÑ Ð»Ð¸ поле name инлайн объектом +func (p ObjectParameters) IsInlineObject(name string) bool { + fld, ok := p.Fields[name] + if !ok { + return false + } + + if fldParams, ok := fld.Params.(*ObjectParameters); ok && fldParams.Inline { + return true + } + + return false +} + +// GetFields возвращает Ð¿Ð¾Ð»Ñ Ð¾Ð±ÑŠÐµÐºÑ‚Ð°. +// Указание withInline позволÑет так же включить Ð¿Ð¾Ð»Ñ ÑƒÐºÐ°Ð·Ð°Ð½Ð½Ñ‹Ðµ во вложенных inline объектам, и получитьÑÑ Ð¿Ð¾Ð»Ñ Ð´Ð»Ñ +// вÑех данных отноÑÑщихÑÑ Ðº текущему объекту. +func (p ObjectParameters) GetFields(withInline bool) map[string]*Field { + fields := make(map[string]*Field) + p.getFields(withInline, fields) + return fields +} + +func (p ObjectParameters) getFields(withInline bool, fields map[string]*Field) { + for k, f := range p.Fields { + if obj, ok := f.Params.(*ObjectParameters); ok && obj.Inline { + obj.getFields(withInline, fields) + continue + } + fields[k] = f + } +} + +func (p *ObjectParameters) Merge(parameters Parameters) error { + op, ok := parameters.(*ObjectParameters) + if !ok { + return errors.New("invalid object parameters") + } + for k, fld := range op.Fields { + if f, ok := p.Fields[k]; ok { + if err := f.Merge(fld); err != nil { + return err + } + } else { + p.Fields[k] = fld + } + } + return nil +} + +type ObjectType struct{} + +func (ObjectType) Name() string { + return "object" +} + +func (ObjectType) NewParameters() Parameters { + return &ObjectParameters{} +} + +func (ObjectType) IsEmpty(v interface{}) bool { + m := reflect.ValueOf(v) + return m.IsNil() || m.Len() == 0 +} + +type fieldNameCtx struct{} + +var FieldName = fieldNameCtx{} + +func (ObjectType) Walk(ctx context.Context, field *Field, v interface{}, fn WalkFunc, opts *WalkOptions) (interface{}, bool, error) { + params, ok := field.Params.(*ObjectParameters) + if !ok { + return nil, false, errors.New("field parameters required") + } + + // Объекта нет в данных, ÑпуÑкатьÑÑ Ðº полÑм мы не будем + // ЕÑли необходимо что бы выполнÑлÑÑ Walk по полÑм необходимо передать пуÑтой объект + // ЕÑли нужно что бы вÑегда объект был, Ñто можно Ñделать через Default + if !opts.WalkSchema && v == nil { + return nil, false, nil + } + + m := reflect.ValueOf(v) + + if m.IsValid() { + if m.Kind() != reflect.Map { + return nil, false, errors.Errorf("incorrect type: \"%s\", expected \"map\"", m.Kind()) + } + } + + if !opts.WalkSchema && m.IsNil() { + return nil, false, nil + } + + // ДобавлÑем к переменным уровень объекта + ctx = expr.WithEnvKV(ctx, "_", v) + + mapNew := make(map[string]interface{}) + + var merr *multierror.Error + var changed bool + for name, fld := range params.Fields { + ctxField := context.WithValue(ctx, FieldName, name) + + // ЕÑли поле ÑвлÑетÑÑ Inline-объектом, то передаютÑÑ Ð´Ð°Ð½Ð½Ñ‹Ðµ текущего объекта + if p, ok := fld.Params.(*ObjectParameters); ok && p.Inline { + valueNew, valueChanged, err := fld.Walk(ctxField, v, fn, WalkOpts(opts)) + + if err != nil { + merr = multierror.Append(merr, errors.WithField(err, name)) + } + + // Значение было изменено и оно не пуÑтое (Inline объект не активен) + if valueChanged && valueNew != nil { + changed = true + } + + if valueNew != nil { + for n, v := range valueNew.(map[string]interface{}) { + mapNew[n] = v + } + } + } else { + // ЕÑли значение нет, мы иÑпользуем nil + var value interface{} + if m.IsValid() && !m.IsZero() && !m.IsNil() { + fieldValue := m.MapIndex(reflect.ValueOf(name)) + if fieldValue.IsValid() { + value = fieldValue.Interface() + } + } + + valueNew, valueChanged, err := fld.Walk(ctxField, value, fn, WalkOpts(opts)) + + if err != nil { + merr = multierror.Append(merr, errors.WithField(err, name)) + } + + // ЕÑли значение было изменено мы заменÑем его на новое + if valueChanged { + changed = true + value = valueNew + } + + // ЕÑли значение не пуÑтое, мы запиÑываем поле в результат + if value != nil { + mapNew[name] = value + } + + } + } + + if merr != nil { + //merr.ErrorFormat = func(i []error) string { + // return fmt.Sprintf("%d error(s)", len(i)) + //} + return nil, false, merr + } + + if v == nil || !m.IsValid() || m.IsZero() || m.IsNil() { + return nil, false, nil + } + + // ПроверÑем изменилоÑÑŒ ли количеÑтво полей объекта. + // Inline-объект игнорирует изменение количеÑтва полей, так как получает так же Ð¿Ð¾Ð»Ñ Ñ€Ð¾Ð´Ð¸Ñ‚ÐµÐ»ÑŒÑкого объекта. + if !changed && !params.Inline { + changed = m.Len() != len(mapNew) + } + + // Объект вÑегда возвращает новый модифицированный результат + return mapNew, changed, nil +} + +func (ObjectType) ValidateParameters(p Parameters) error { + params, ok := p.(*ObjectParameters) + if !ok { + return nil + } + + if len(params.Fields) > 0 { + for k := range params.Fields { + if !isValidName(k) { + return fmt.Errorf("field name '%s' must be in Latin, must not start with a number, "+ + "must not contain spaces - only characters '_' can be used", k) + } + } + } + return nil +} + +func Object(kv ...interface{}) *Field { + params := &ObjectParameters{Fields: make(map[string]*Field)} + + if len(kv) > 0 { + inline, ok := kv[0].(bool) + if ok { + params.Inline = inline + kv = kv[1:] + } + } + + var order []interface{} + i := 0 + for { + if i+2 > len(kv) { + break + } + k, v := kv[i], kv[i+1] + name, kOk := k.(string) + field, vOk := v.(*Field) + if !kOk || !vOk { + break + } + + params.Fields[name] = field + order = append(order, name) + + err := objectType.ValidateParameters(params) + if err != nil { + panic(err.Error()) + } + + i += 2 + } + + fld := NewField(params, kv[i:]...) + if len(order) > 0 { + fld = fld.WithUI(&UI{ + Options: map[string]interface{}{"fields": order}, + }) + } + + return fld +} + +func AddField(field *Field, name string, fld *Field) error { + switch params := field.Params.(type) { + case *ObjectParameters: + if params.Fields == nil { + params.Fields = make(map[string]*Field) + } + params.Fields[name] = fld + case *ArrayParameters: + params.Item = fld + default: + return errors.New("AddField not supported") + } + + return nil +} + +func RemoveAllFields(obj *Field) error { + params, ok := obj.Params.(*ObjectParameters) + if !ok { + return errors.New("obj is not an object") + } + params.Fields = make(map[string]*Field) + return nil +} diff --git a/pkg/schema/field/object_test.go b/pkg/schema/field/object_test.go new file mode 100644 index 0000000000000000000000000000000000000000..2ada594bcdeba09fab5dfcc95eb947e2028766d9 --- /dev/null +++ b/pkg/schema/field/object_test.go @@ -0,0 +1,232 @@ +package field + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestObjectField_Decode(t *testing.T) { + w, _ := time.Parse(time.RFC3339, "2012-11-01T22:08:41Z") + tests := []struct { + name string + field *Field + data interface{} + want interface{} + wantErr bool + }{ + { + "Correct", + Object("name", String(), "date", Time(), "bool", Bool()), + map[string]interface{}{"name": "string", "date": "2012-11-01T22:08:41Z", "bool": true}, + map[string]interface{}{"name": "string", "date": w, "bool": true}, + false, + }, + { + "Remove undefined fields", + Object("name", String(), "date", Time(), "bool", Bool()), + map[string]interface{}{"name": "string", "date": "2012-11-01T22:08:41Z", "bool": true, "extra": "string"}, + map[string]interface{}{"name": "string", "date": w, "bool": true}, + false, + }, + { + "Empty data", + Object("name", String(), "date", Time(), "bool", Bool()), + map[string]interface{}{}, + map[string]interface{}{}, + false, + }, + { + "Nil data", + Object("name", String(), "date", Time(), "bool", Bool()), + nil, + nil, + false, + }, + { + "Incorrect field", + Object("name", String(), "date", Time(), "bool", Bool()), + map[string]interface{}{"name": "string", "date": "2012-11-01"}, + "decode error: 1 error occurred:\n\t* field 'date': TimeType: decode error parsing time \"2012-11-01\" as \"2006-01-02T15:04:05Z07:00\": cannot parse \"\" as \"T\"\n\n", + true, + }, + { + "Incorrect type#1", + Object("name", String(), "date", Time(), "bool", Bool()), + []interface{}{"name", "string", "date", "2012-11-01"}, + "decode error: incorrect type: \"slice\", expected \"map\"", + true, + }, + { + "Incorrect type#2", + Object("name", String(), "date", Time(), "bool", Bool()), + "", + "decode error: incorrect type: \"string\", expected \"map\"", + true, + }, + { + "Incorrect type#3", + Object("name", String(), "date", Time(), "bool", Bool()), + "some", + "decode error: incorrect type: \"string\", expected \"map\"", + true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Decode(nil, tt.field, tt.data) + if tt.wantErr { + require.Error(t, err) + assert.EqualError(t, err, tt.want.(string), fmt.Sprintf("Decode() error = %v, want %v", err, tt.want.(string))) + } + if !tt.wantErr { + require.NoError(t, err) + assert.Equal(t, got, tt.want, fmt.Sprintf("Decode() got = %v, want %v", got, tt.want)) + } + }) + } +} + +func TestObjectField_Encode(t *testing.T) { + w, _ := time.Parse(time.RFC3339, "2012-11-01T22:08:41Z") + tests := []struct { + name string + field *Field + data interface{} + want interface{} + wantErr bool + }{ + { + "Correct", + Object("name", String(), "date", Time(), "bool", Bool()), + + map[string]interface{}{"name": "string", "date": w, "bool": true}, + map[string]interface{}{"bool": true, "name": "string", "date": "2012-11-01T22:08:41Z"}, + false, + }, + { + "Additional properties", + Object("name", String(), "date", Time(), "bool", Bool()), + map[string]interface{}{"name": "string", "date": w, "extra": "string", "bool": true}, + map[string]interface{}{"bool": true, "name": "string", "date": "2012-11-01T22:08:41Z"}, + false, + }, + { + "Empty data", + Object("name", String(), "date", Time(), "bool", Bool()), + map[string]interface{}{}, + map[string]interface{}{}, + false, + }, + { + "Nil data", + Object("name", String(), "date", Time(), "bool", Bool()), + nil, + nil, + false, + }, + { + "Incorrect type#1", + Object("name", String(), "date", Time(), "bool", Bool()), + []interface{}{}, + "encode error: incorrect type: \"slice\", expected \"map\"", + true, + }, + { + "Incorrect type#2", + Object("name", String(), "date", Time(), "bool", Bool()), + "", + "encode error: incorrect type: \"string\", expected \"map\"", + true, + }, + { + "Incorrect type#3", + Object("name", String(), "date", Time(), "bool", Bool()), + "some", + "encode error: incorrect type: \"string\", expected \"map\"", + true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Encode(nil, tt.field, tt.data) + if tt.wantErr { + require.Error(t, err) + assert.EqualError(t, err, tt.want.(string), fmt.Sprintf("Encode() error = %v, want %v", err, tt.want.(string))) + } + if !tt.wantErr { + require.NoError(t, err) + assert.Equal(t, got, tt.want, fmt.Sprintf("Encode() got = %v, want %v", got, tt.want)) + } + }) + } +} + +func TestFieldNameValidate(t *testing.T) { + tests := []struct { + testName string + fieldName string + wantErr bool + }{ + { + "Correct field name", + "name", + false, + }, + { + "Not Latin", + "название", + true, + }, + { + "Start with a number", + "1name", + true, + }, + { + "Contains space", + "field name", + true, + }, + { + "Contains symbols", + "name!", + true, + }, + { + "Contains hyphen", + "field-name", + true, + }, + { + "Contains underscore (success)", + "field_name", + false, + }, + { + "Start with a capital letter (success)", + "Name", + false, + }, + { + "Contain a capital letter (success)", + "fieldName", + false, + }, + } + + for _, tt := range tests { + t.Run(tt.testName, func(t *testing.T) { + params := &ObjectParameters{Fields: map[string]*Field{tt.fieldName: String()}} + err := objectType.ValidateParameters(params) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/pkg/schema/field/options.go b/pkg/schema/field/options.go new file mode 100644 index 0000000000000000000000000000000000000000..c0c6e4e14e4a0a9073dffe91031dbe3d3ceb259f --- /dev/null +++ b/pkg/schema/field/options.go @@ -0,0 +1,175 @@ +package field + +import ( + "encoding/json" + "errors" + "fmt" + "reflect" + "sync" +) + +type Option interface { + Transform(f *Field, v interface{}) (interface{}, error) +} + +type PriorityOption interface { + GetPriority() int +} + +type NamedOption interface { + GetName() string +} + +type OptionValidator interface { + ValidateOption() error +} + +//type jsonTransform struct { +// Name string `json:"name"` +// Options json.RawMessage `json:"options,omitempty"` +//} +// +//func (t *Option) MarshalJSON() ([]byte, error) { +// b, err := json.Marshal(t.Transformation) +// if err != nil { +// return nil, err +// } +// +// j := jsonTransform{Name: GetOptionName(t.Transformation), Options: b} +// +// return json.Marshal(&j) +//} +// +//func (t *Option) UnmarshalJSON(b []byte) error { +// var j jsonTransform +// if err := json.Unmarshal(b, &j); err != nil { +// return err +// } +// +// i, ok := nameToOption.Load(j.Name) +// if !ok { +// return fmt.Errorf("unknown transformer name \"%s\"", j.Name) +// } +// typ := i.(reflect.Type) +// val := reflect.New(typ) +// v := val.Interface() +// +// if len(j.Options) > 0 { +// if err := json.Unmarshal(j.Options, v); err != nil { +// return err +// } +// } +// +// tr, _ := v.(Transformation) +// *t = Option{Transformation: tr} +// return nil +//} + +var ( + nameToOption sync.Map + optionToName sync.Map +) + +func GetOptionName(o interface{}) string { + typ := reflect.TypeOf(o) + if typ.Kind() == reflect.Ptr { + typ = typ.Elem() + } + if val, ok := optionToName.Load(typ); ok { + v := val.(string) + return v + } + return "" +} + +func RegisterOption(o interface{}) { + var name string + typ := reflect.TypeOf(o) + if typ.Kind() == reflect.Ptr { + typ = typ.Elem() + } + + if namer, ok := o.(NamedOption); ok { + name = namer.GetName() + } else { + name = typ.Name() + } + + nameToOption.Store(name, typ) + optionToName.Store(typ, name) +} + +type Options map[string]interface{} + +func (options *Options) Add(opts ...interface{}) { + if len(opts) == 0 { + return + } + if *options == nil { + *options = make(Options) + } + for _, o := range opts { + name := GetOptionName(o) + (*options)[name] = o + } +} + +//func (options Options) MarshalJSON() ([]byte, error) { +// m := make(map[string]json.RawMessage) +// +// for k,v := range options { +// name := GetOptionName(t) +// b, err := json.Marshal(t) +// if err != nil { +// return nil, err +// } +// m[name] = b +// } +// return json.Marshal(&m) +//} + +func (options *Options) UnmarshalJSON(b []byte) error { + m := make(map[string]json.RawMessage) + *options = make(Options) + if err := json.Unmarshal(b, &m); err != nil { + return err + } + + for name, opts := range m { + i, ok := nameToOption.Load(name) + if !ok { + return fmt.Errorf("unknown option name \"%s\"", name) + } + typ := i.(reflect.Type) + val := reflect.New(typ) + v := val.Interface() + if len(opts) > 0 { + if err := json.Unmarshal(opts, v); err != nil { + return err + } + } + if validator, ok := v.(OptionValidator); ok { + err := validator.ValidateOption() + if errors.Is(err, ErrSkipOption) { + continue + } + if err != nil { + return err + } + } + options.Add(v) + } + return nil +} + +func (options Options) Transform(field *Field, v interface{}) (interface{}, error) { + var err error + for _, t := range options { + o := t.(Option) + v, err = o.Transform(field, v) + if err != nil { + return nil, err + } + } + return v, nil +} diff --git a/pkg/schema/field/primary_key.go b/pkg/schema/field/primary_key.go new file mode 100644 index 0000000000000000000000000000000000000000..b0b26e16a91cd506231ba46307485290a138bfb8 --- /dev/null +++ b/pkg/schema/field/primary_key.go @@ -0,0 +1,55 @@ +package field + +import ( + "context" + "fmt" + "reflect" +) + +//TODO readonly + +var primaryKeyType = &PrimaryKeyType{} + +type PrimaryKeyParameters struct{} + +func (p PrimaryKeyParameters) Type() Type { return primaryKeyType } +func (p *PrimaryKeyParameters) Clone(reset bool) Parameters { return p } + +type PrimaryKeyType struct{} + +func (p PrimaryKeyType) Name() string { + return "PrimaryKey" +} + +func (PrimaryKeyType) NewParameters() Parameters { + return &PrimaryKeyParameters{} +} + +func (PrimaryKeyType) IsEmpty(v interface{}) bool { + s, _ := v.(string) + return s == "" +} + +func (PrimaryKeyType) Decode(_ context.Context, _ *Field, v interface{}) (interface{}, error) { + if v == nil { + return nil, nil + } + if _, ok := v.(string); ok { + return v, nil + } + return nil, fmt.Errorf("PrimaryKeyField decode error: unsupported value type : \"%s\"", reflect.ValueOf(v).Kind()) +} + +func (PrimaryKeyType) Encode(_ context.Context, _ *Field, v interface{}) (interface{}, error) { + if v == nil { + return nil, nil + } + if _, ok := v.(string); ok { + return v, nil + } + return nil, fmt.Errorf("PrimaryKeyField encode error: unsupported value type : \"%s\"", reflect.ValueOf(v).Kind()) +} + +func PrimaryKey(o ...interface{}) *Field { + return NewField(&PrimaryKeyParameters{}, o...) +} diff --git a/pkg/schema/field/primary_key_test.go b/pkg/schema/field/primary_key_test.go new file mode 100644 index 0000000000000000000000000000000000000000..f74f32cafc72612d9b1575561718e6c625334d53 --- /dev/null +++ b/pkg/schema/field/primary_key_test.go @@ -0,0 +1,56 @@ +package field + +import ( + "reflect" + "testing" +) + +func TestPrimaryKeyField_Decode(t *testing.T) { + tests := []struct { + name string + field *Field + data interface{} + want interface{} + wantErr bool + }{ + {"Correct", PrimaryKey(), "custom_id", "custom_id", false}, + {"Wrong data", PrimaryKey(), 2, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Decode(nil, tt.field, tt.data) + if (err != nil) != tt.wantErr { + t.Errorf("Decode() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Decode() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestPrimaryKeyField_Encode(t *testing.T) { + tests := []struct { + name string + field *Field + data interface{} + want interface{} + wantErr bool + }{ + {"Correct", PrimaryKey(), "primary key", "primary key", false}, + {"Wrong data", PrimaryKey(), 2, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Encode(nil, tt.field, tt.data) + if (err != nil) != tt.wantErr { + t.Errorf("Decode() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Decode() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/schema/field/string.go b/pkg/schema/field/string.go new file mode 100644 index 0000000000000000000000000000000000000000..b7e548b65f5c1572cc515cad19192899843ab62f --- /dev/null +++ b/pkg/schema/field/string.go @@ -0,0 +1,53 @@ +package field + +import ( + "context" + "fmt" + "reflect" +) + +var stringType = &StringType{} + +type StringParameters struct{} + +func (s StringParameters) Type() Type { return stringType } +func (s *StringParameters) Clone(reset bool) Parameters { return s } + +type StringType struct{} + +func (s StringType) Name() string { + return "string" +} + +func (StringType) NewParameters() Parameters { + return &StringParameters{} +} + +func (StringType) IsEmpty(v interface{}) bool { + s, _ := v.(string) + return s == "" +} + +func (StringType) Decode(_ context.Context, _ *Field, v interface{}) (interface{}, error) { + if v == nil { + return nil, nil + } + if _, ok := v.(string); ok { + return v, nil + } + return nil, fmt.Errorf("StringField decode error: unsupported value type : \"%s\"", reflect.ValueOf(v).Kind()) +} + +func (StringType) Encode(_ context.Context, _ *Field, v interface{}) (interface{}, error) { + if v == nil { + return nil, nil + } + if _, ok := v.(string); ok { + return v, nil + } + return nil, fmt.Errorf("StringField encode error: unsupported value type : \"%s\"", reflect.ValueOf(v).Kind()) +} + +func String(o ...interface{}) *Field { + return NewField(&StringParameters{}, o...) +} diff --git a/pkg/schema/field/string_test.go b/pkg/schema/field/string_test.go new file mode 100644 index 0000000000000000000000000000000000000000..d0fca29b8ed3f0c4ec987bf6705b7ea435116e40 --- /dev/null +++ b/pkg/schema/field/string_test.go @@ -0,0 +1,56 @@ +package field + +import ( + "reflect" + "testing" +) + +func TestStringField_Decode(t *testing.T) { + tests := []struct { + name string + field *Field + data interface{} + want interface{} + wantErr bool + }{ + {"Correct", String(), "string", "string", false}, + {"Wrong data", String(), 2, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Decode(nil, tt.field, tt.data) + if (err != nil) != tt.wantErr { + t.Errorf("Decode() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Decode() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestStringField_Encode(t *testing.T) { + tests := []struct { + name string + field *Field + data interface{} + want interface{} + wantErr bool + }{ + {"Correct", String(), "string", "string", false}, + {"Wrong data", String(), 2, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Encode(nil, tt.field, tt.data) + if (err != nil) != tt.wantErr { + t.Errorf("Decode() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Decode() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/schema/field/time.go b/pkg/schema/field/time.go new file mode 100644 index 0000000000000000000000000000000000000000..064906f236371d2914a0544c3f6abef83fd77f65 --- /dev/null +++ b/pkg/schema/field/time.go @@ -0,0 +1,85 @@ +package field + +import ( + "context" + "fmt" + "reflect" + "time" + + "github.com/pkg/errors" +) + +const DefaultTimeLayout = time.RFC3339 + +var timeType = &TimeType{} + +type TimeParameters struct { + Layout string `json:"layout,omitempty"` +} + +func (p TimeParameters) Type() Type { return timeType } +func (p TimeParameters) Clone(reset bool) Parameters { return &p } + +func (p TimeParameters) GetLayout() string { + if p.Layout != "" { + return p.Layout + } + return DefaultTimeLayout +} + +type TimeType struct{} + +func (TimeType) Name() string { + return "time" +} + +func (TimeType) NewParameters() Parameters { + return &TimeParameters{} +} + +func (TimeType) IsEmpty(v interface{}) bool { + t, _ := v.(time.Time) + return t.IsZero() +} + +func (TimeType) Decode(_ context.Context, field *Field, v interface{}) (interface{}, error) { + params, ok := field.Params.(*TimeParameters) + if !ok { + return nil, errors.New("TimeType: field type parameters required") + } + + if v == nil { + return v, nil + } + switch val := v.(type) { + + case string: + if t, err := time.Parse(params.GetLayout(), val); err != nil { + return nil, fmt.Errorf("TimeType: decode error %w", err) + } else { + return t, nil + } + case time.Time: + return v, nil + } + return nil, fmt.Errorf("TimeType: decode: unsupported value type : \"%s\"", reflect.ValueOf(v).Kind()) +} + +func (TimeType) Encode(_ context.Context, field *Field, v interface{}) (interface{}, error) { + params, ok := field.Params.(*TimeParameters) + if !ok { + return nil, errors.New("TimeType: field type parameters required") + } + + if v == nil { + return v, nil + } + if t, ok := v.(time.Time); ok { + return t.Format(params.GetLayout()), nil + } + return nil, fmt.Errorf("TimeType: encode: unsupported value type : \"%s\"", reflect.ValueOf(v).Kind()) +} + +func Time(o ...interface{}) *Field { + return NewField(&TimeParameters{}, o...) +} diff --git a/pkg/schema/field/time_test.go b/pkg/schema/field/time_test.go new file mode 100644 index 0000000000000000000000000000000000000000..f9a8fb44552a0e0a704f7baae7ab10019c889bec --- /dev/null +++ b/pkg/schema/field/time_test.go @@ -0,0 +1,61 @@ +package field + +import ( + "reflect" + "testing" + "time" +) + +func TestTimeField_Decode(t *testing.T) { + w, _ := time.Parse(time.RFC3339, "2012-11-01T22:08:41Z") + tests := []struct { + name string + field *Field + data interface{} + want interface{} + wantErr bool + }{ + {"Correct", Time(), "2012-11-01T22:08:41Z", w, false}, + {"Incorrect format", Time(), "2012-11-01", nil, true}, + {"Incorrect type", Time(), 2, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Decode(nil, tt.field, tt.data) + if (err != nil) != tt.wantErr { + t.Errorf("Decode() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Decode() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestTimeField_Encode(t *testing.T) { + w, _ := time.Parse(time.RFC3339, "2012-11-01T22:08:41Z") + tests := []struct { + name string + field *Field + data interface{} + want interface{} + wantErr bool + }{ + {"Correct", Time(), w, "2012-11-01T22:08:41Z", false}, + {"Incorrect type string", Time(), "2012-11-01T22:08:41Z", nil, true}, + {"Incorrect type int", Time(), 2, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Encode(nil, tt.field, tt.data) + if (err != nil) != tt.wantErr { + t.Errorf("Encode() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Encode() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/schema/field/type.go b/pkg/schema/field/type.go new file mode 100644 index 0000000000000000000000000000000000000000..edae76734d562d2546d3b68e89e125c7a87744a6 --- /dev/null +++ b/pkg/schema/field/type.go @@ -0,0 +1,47 @@ +package field + +import ( + "fmt" + "reflect" + "sync" +) + +var ( + registry sync.Map +) + +// Parameters - Ð¸Ð½Ñ‚ÐµÑ€Ñ„ÐµÐ¹Ñ ÐºÐ¾Ñ‚Ð¾Ñ€Ñ‹Ð¹ должен реализовывать параметр конкретного типа +type Parameters interface { + Type() Type + Clone(reset bool) Parameters +} + +// Type - тип полÑ, отвечает за получение, кодирование и декодирование параметров Ð´Ð»Ñ Ð´Ð°Ð½Ð½Ð¾Ð³Ð¾ типа +type Type interface { + Name() string + NewParameters() Parameters + //Encode(ctx *Context.Context, field *Field, v interface{}) (interface{}, error) + //Decode(ctx *Context.Context, field *Field, v interface{}) (interface{}, error) +} + +func Register(typ Type) { + registry.Store(typ.Name(), typ) +} + +func Unregister(typ interface{}) { + switch t := typ.(type) { + case Type: + registry.Delete(t.Name()) + case string: + registry.Delete(t) + default: + panic(fmt.Sprintf("unknown type: \"%s\"", reflect.ValueOf(t).Kind())) + } +} + +func GetType(name string) (Type, bool) { + if i, ok := registry.Load(name); ok { + return i.(Type), true + } + return nil, false +} diff --git a/pkg/schema/field/unknown.go b/pkg/schema/field/unknown.go new file mode 100644 index 0000000000000000000000000000000000000000..f28a51cd1fbdc20a6b026d4504872149c939e727 --- /dev/null +++ b/pkg/schema/field/unknown.go @@ -0,0 +1,57 @@ +package field + +import ( + "encoding/json" + + jsoniter "github.com/json-iterator/go" + "github.com/pkg/errors" +) + +var unknownType = &UnknownType{} + +type UnknownParameters struct { + Typ string `json:"type,omitempty"` + Params json.RawMessage `json:"params,omitempty"` +} + +func (UnknownParameters) Type() Type { return unknownType } +func (p UnknownParameters) Clone(reset bool) Parameters { return &p } + +type UnknownType struct{} + +func (UnknownType) Name() string { + return "unknown" +} + +func (UnknownType) NewParameters() Parameters { + return &UnknownParameters{} +} + +func (UnknownType) ConvertParameters(p Parameters) (Parameters, error) { + unknownParams, ok := p.(*UnknownParameters) + if !ok { + return p, nil + } + + // ПроверÑем возможноÑть воÑÑÑ‚Ð°Ð½Ð¾Ð²Ð»ÐµÐ½Ð¸Ñ Ð¸Ñходного типа Ð¿Ð¾Ð»Ñ Ð¸ возвращаем его еÑли возможно + typ, ok := GetType(unknownParams.Typ) + if !ok { + return p, nil + } + + params := typ.NewParameters() + if len(unknownParams.Params) > 0 { + if err := jsoniter.Unmarshal(unknownParams.Params, params); err != nil { + return p, errors.Wrapf(err, "error recover field type %s", typ.Name()) + } + } + + return params, nil +} + +func Unknown(typ string, params json.RawMessage, o ...interface{}) *Field { + var pc ParametersConverter + pc = unknownType + _ = pc + return NewField(&UnknownParameters{Typ: typ, Params: params}, o...) +} diff --git a/pkg/schema/field/walk.go b/pkg/schema/field/walk.go new file mode 100644 index 0000000000000000000000000000000000000000..d7b24a10098944b6c742421027075a0e2febe54e --- /dev/null +++ b/pkg/schema/field/walk.go @@ -0,0 +1,57 @@ +package field + +import "context" + +type WalkFuncResult struct { + Context context.Context + Value interface{} + Changed bool + Stop bool +} + +// WalkFunc - Ñ„ÑƒÐ½ÐºÑ†Ð¸Ñ ÐºÐ¾Ñ‚Ð¾Ñ€Ð°Ñ Ð±ÑƒÐ´ÐµÑ‚ применена к значению каждого Ð¿Ð¾Ð»Ñ Ð² результате обход данных функцией `Walk` +// Возвращает новое значение еÑли таковое имеетÑÑ, признак Ð¸Ð·Ð¼ÐµÐ½ÐµÐ½Ð¸Ñ Ð´Ð°Ð½Ð½Ñ‹Ñ… и ошибку в Ñлучае Ð²Ð¾Ð·Ð½Ð¸ÐºÐ½Ð¾Ð²ÐµÐ½Ð¸Ñ +type WalkFunc func(ctx context.Context, fld *Field, v interface{}) (result WalkFuncResult, err error) + +// FieldWalker - Ð¸Ð½Ñ‚ÐµÑ€Ñ„ÐµÐ¹Ñ Ð¿Ð¾Ð·Ð²Ð¾Ð»Ñющий выполнить обход предоÑтавленного Ð¿Ð¾Ð»Ñ +// РеализуетÑÑ Ð² типах +type FieldWalker interface { + // Walk выполнÑет обход данных в ÑоответÑтвии Ñ Ð¿Ð¾Ð»ÐµÐ¼. Ð¤ÑƒÐ½ÐºÑ†Ð¸Ñ Ð²Ð¾Ð·Ð²Ñ€Ð°Ñ‰Ð°ÐµÑ‚ результат: + // результат обхода, флаг что данные изменены, ошибку + Walk(ctx context.Context, fld *Field, v interface{}, fn WalkFunc, opts *WalkOptions) (interface{}, bool, error) +} + +type WalkOptions struct { + WalkSchema bool +} + +type WalkOption func(opts *WalkOptions) + +// WalkSchema указывает что необходимо выполнÑть обход по Ñхеме, вмеÑто обхода по данным. +// При обходе по данным в Ñлучае отÑутÑÑ‚Ð²Ð¸Ñ Ð´Ð°Ð½Ð½Ñ‹Ñ… Ð´Ð»Ñ Ð¿Ð¾Ð»Ñ Ð´Ð°Ð»ÑŒÐ½ÐµÐ¹ÑˆÐ°Ñ Ð¾Ð±Ñ€Ð°Ð±Ð¾Ñ‚ÐºÐ° данного Ð¿Ð¾Ð»Ñ Ð½Ðµ выполнÑетÑÑ. +// При обходе по Ñхемы будут обработаны вÑе Ð¿Ð¾Ð»Ñ Ð¿Ñ€Ð¸ÑутÑтвующие в Ñхеме вне завиÑимоÑти от Ð½Ð°Ð»Ð¸Ñ‡Ð¸Ñ Ð´Ð°Ð½Ð½Ñ‹Ñ…. +func WalkSchema() WalkOption { + return func(opts *WalkOptions) { + opts.WalkSchema = true + } +} + +func WalkOpts(o *WalkOptions) WalkOption { + return func(opts *WalkOptions) { + *opts = *o + } +} + +func NewWalkOptions(opt ...WalkOption) *WalkOptions { + opts := &WalkOptions{} + for _, o := range opt { + o(opts) + } + return opts +} + +// Walker - Ð¸Ð½Ñ‚ÐµÑ€Ñ„ÐµÐ¹Ñ Ð¿Ð¾Ð·Ð²Ð¾Ð»Ñющий выполнить обход данных и Ñодержит Ñхему внутри +// Реализовано в Field, Schema +type Walker interface { + Walk(ctx context.Context, v interface{}, fn WalkFunc, opt ...WalkOption) (interface{}, bool, error) +} diff --git a/pkg/schema/loader.go b/pkg/schema/loader.go new file mode 100644 index 0000000000000000000000000000000000000000..5a646e2398eec49bdd1b8be135f9b9a54952a113 --- /dev/null +++ b/pkg/schema/loader.go @@ -0,0 +1,25 @@ +package schema + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/schema/field" +) + +var defaultLoader field.Loader + +func SetDefaultLoader(l field.Loader) { + defaultLoader = l +} + +func GetLoader() field.Loader { + return defaultLoader +} + +func Load(ctx context.Context, ref string) (*Schema, error) { + s := New() + if err := s.Field.LoadRef(ctx, ref, GetLoader()); err != nil { + return nil, err + } + return s, nil +} diff --git a/pkg/schema/modify/default.go b/pkg/schema/modify/default.go new file mode 100644 index 0000000000000000000000000000000000000000..66ce6b89c0619ceb232050fa5ab057e4330fdfcb --- /dev/null +++ b/pkg/schema/modify/default.go @@ -0,0 +1,47 @@ +package modify + +import ( + "context" + "encoding/json" + + "git.perx.ru/perxis/perxis-go/pkg/schema/field" +) + +type defaultValue struct { + Value interface{} + ready bool +} + +func (d *defaultValue) Prepare(f *field.Field) error { + var err error + d.Value, err = field.Decode(nil, f, d.Value) + return err +} + +func (d *defaultValue) UnmarshalJSON(bytes []byte) error { + return json.Unmarshal(bytes, &d.Value) +} + +func (d defaultValue) MarshalJSON() ([]byte, error) { + return json.Marshal(d.Value) +} + +func Default(v interface{}) Modifier { + return &defaultValue{Value: v} +} + +func (defaultValue) GetName() string { return "default" } + +func (d defaultValue) Modify(ctx context.Context, f *field.Field, v interface{}) (interface{}, bool, error) { + if !d.ready { + var err error + if d.Value, err = field.Decode(ctx, f, d.Value); err != nil { + return nil, false, err + } + } + + if v == nil && d.Value != nil { + return d.Value, true, nil + } + return v, false, nil +} diff --git a/pkg/schema/modify/default_test.go b/pkg/schema/modify/default_test.go new file mode 100644 index 0000000000000000000000000000000000000000..b99630dc65a9249d04bc1b29ac9e9fc902c745cf --- /dev/null +++ b/pkg/schema/modify/default_test.go @@ -0,0 +1,58 @@ +package modify + +import ( + "testing" + "time" + + "git.perx.ru/perxis/perxis-go/pkg/schema/field" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDefault(t *testing.T) { + now := time.Now() + _ = now + w, _ := time.Parse(time.RFC3339, "2012-11-01T22:08:41Z") + + tests := []struct { + name string + field *field.Field + data interface{} + want interface{} + wantErr bool + }{ + {"String value", field.String(Default("some")), "same", "same", false}, + {"String", field.String(Default("some")), nil, "some", false}, + {"Int", field.Number(field.NumberFormatInt, Default(2)), nil, int64(2), false}, + {"Time", field.Time(Default(now)), nil, now, false}, + {"Bool", field.Bool(Default(true)), nil, true, false}, + {"Object: nil with field default", field.Object("name", field.String(Default("test"))), nil, nil, false}, + {"Object: nil with object default", field.Object("name", field.String(Default("test"))).AddOptions(Default(map[string]interface{}{"name": "a"})), nil, map[string]interface{}{"name": "a"}, false}, + {"Object: empty", field.Object( + "a", field.String(Default("a")), + "b", field.Number(field.NumberFormatInt, Default(1)), + "c", field.String(), + ), + map[string]interface{}{}, + map[string]interface{}{"a": "a", "b": int64(1)}, + false}, + {"Array of time", + field.Object("array", field.Array(field.Time())), + map[string]interface{}{"array": []interface{}{"2012-11-01T22:08:41Z", "2012-11-01T22:08:41Z"}}, + map[string]interface{}{"array": []interface{}{w, w}}, + false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := field.Decode(nil, tt.field, tt.data) + require.NoError(t, err) + got, _, err = Modify(nil, tt.field, got) + if (err != nil) != tt.wantErr { + t.Errorf("Modify() error = %v, wantErr %v", err, tt.wantErr) + return + } + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/schema/modify/modify.go b/pkg/schema/modify/modify.go new file mode 100644 index 0000000000000000000000000000000000000000..191a428c4efac916511dd37481d7e1bac00a87bc --- /dev/null +++ b/pkg/schema/modify/modify.go @@ -0,0 +1,106 @@ +package modify + +import ( + "context" + "sort" + + "git.perx.ru/perxis/perxis-go/pkg/errors" + "git.perx.ru/perxis/perxis-go/pkg/expr" + "git.perx.ru/perxis/perxis-go/pkg/schema/field" +) + +const ModifierPriority = 1000 + +type Modifier interface { + Modify(ctx context.Context, f *field.Field, v interface{}) (interface{}, bool, error) +} + +type Modifiers []Modifier + +func (l Modifiers) Len() int { return len(l) } +func (l Modifiers) Less(i, j int) bool { + pi, pj := ModifierPriority, ModifierPriority + if o, ok := l[i].(field.PriorityOption); ok { + pi = o.GetPriority() + } + if o, ok := l[j].(field.PriorityOption); ok { + pj = o.GetPriority() + } + if pi == pj { + return field.GetOptionName(l[i]) < field.GetOptionName(l[j]) + } + return pi < pj +} +func (l Modifiers) Swap(i, j int) { l[i], l[j] = l[j], l[i] } + +func getModifiers(f *field.Field) Modifiers { + var ms Modifiers + for _, o := range f.Options { + if m, ok := o.(Modifier); ok { + ms = append(ms, m) + } + } + sort.Sort(ms) + return ms +} + +func applyModifiers(ctx context.Context, f *field.Field, v interface{}) (interface{}, bool, error) { + var err error + var ok, modified bool + modifiers := getModifiers(f) + for _, i := range modifiers { + v, ok, err = i.Modify(ctx, f, v) + if err != nil { + return nil, false, err + } + modified = modified || ok + } + return v, modified, nil +} + +func Modify(ctx context.Context, w field.Walker, v interface{}) (interface{}, bool, error) { + if m, ok := v.(map[string]interface{}); ok { + ctx = expr.WithEnv(ctx, m) + } + + v, c, err := w.Walk(ctx, v, func(ctx context.Context, fld *field.Field, v interface{}) (res field.WalkFuncResult, err error) { + var vv interface{} + var changed bool + + if vv, changed, err = applyModifiers(ctx, fld, v); err != nil { + return + } + + if changed { + v = vv + } + + if modifier, ok := fld.GetType().(Modifier); ok { + vv, ch, err := modifier.Modify(ctx, fld, v) + + if err != nil { + return res, err + } + + if ch { + v = vv + } + } + + res.Value = v + res.Changed = changed + return + }) + + if err != nil { + return nil, false, errors.Wrap(err, "modification error") + } + + return v, c, nil +} + +func init() { + field.RegisterOption(trimSpace(true)) + field.RegisterOption(defaultValue{}) + field.RegisterOption(value{}) +} diff --git a/pkg/schema/modify/string.go b/pkg/schema/modify/string.go new file mode 100644 index 0000000000000000000000000000000000000000..2f0ecc6f24e114f89d97b5e0d54251da68c4aebf --- /dev/null +++ b/pkg/schema/modify/string.go @@ -0,0 +1,33 @@ +package modify + +import ( + "context" + "fmt" + "reflect" + "strings" + + "git.perx.ru/perxis/perxis-go/pkg/schema/field" +) + +type trimSpace bool + +func TrimSpace() Modifier { + t := trimSpace(true) + return &t +} + +func (c trimSpace) Modify(ctx context.Context, field *field.Field, v interface{}) (interface{}, bool, error) { + if !c { + return v, false, nil + } + + if v == nil { + return nil, false, nil + } + + if s, ok := v.(string); ok { + s = strings.TrimSpace(s) + return s, true, nil + } + return nil, false, fmt.Errorf("incorrect type: \"%s\", expected \"string\"", reflect.ValueOf(v).Kind()) +} diff --git a/pkg/schema/modify/string_test.go b/pkg/schema/modify/string_test.go new file mode 100644 index 0000000000000000000000000000000000000000..ef90f7f6f4d6a43ff13b3ac10bda68602f09c5f4 --- /dev/null +++ b/pkg/schema/modify/string_test.go @@ -0,0 +1,78 @@ +package modify + +import ( + "reflect" + "testing" + + "git.perx.ru/perxis/perxis-go/pkg/schema/field" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestTrimSpace(t *testing.T) { + tests := []struct { + name string + field *field.Field + data interface{} + want interface{} + wantErr bool + }{ + {"String with spaces", field.String().AddOptions(TrimSpace()), " string string ", "string string", false}, + {"Nil", field.String().AddOptions(TrimSpace()), nil, nil, false}, + {"Spaces", field.String().AddOptions(TrimSpace()), " ", "", false}, + {"Empty string", field.String().AddOptions(TrimSpace()), "", "", false}, + {"Not a string", field.String().AddOptions(TrimSpace()), 2, nil, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, _, err := Modify(nil, tt.field, tt.data) + if (err != nil) != tt.wantErr { + t.Errorf("Modify() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("Modify() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestModify(t *testing.T) { + tests := []struct { + name string + field *field.Field + data interface{} + wantErr bool + error string + }{ + {"String Length Max", field.String().AddOptions(TrimSpace()), " ", false, ""}, + {"String Length Min", field.String().AddOptions(TrimSpace()), " ", false, ""}, + {"Nil Length Max", field.String().AddOptions(TrimSpace()), nil, false, ""}, + {"Nil Length Min", field.String().AddOptions(TrimSpace()), nil, false, ""}, + {"Int Length Max", field.String().AddOptions(TrimSpace()), 1, true, "modification error: incorrect type: \"int\", expected \"string\""}, + {"Int Length Min", field.String().AddOptions(TrimSpace()), 1, true, "modification error: incorrect type: \"int\", expected \"string\""}, + {"Float Length Max", field.String().AddOptions(TrimSpace()), 1.0, true, "modification error: incorrect type: \"float64\", expected \"string\""}, + {"Float Length Min", field.String().AddOptions(TrimSpace()), 1.0, true, "modification error: incorrect type: \"float64\", expected \"string\""}, + {"Bool Length Max", field.String().AddOptions(TrimSpace()), true, true, "modification error: incorrect type: \"bool\", expected \"string\""}, + {"Bool Length Min", field.String().AddOptions(TrimSpace()), true, true, "modification error: incorrect type: \"bool\", expected \"string\""}, + {"Array Length Max", field.String().AddOptions(TrimSpace()), [1]string{""}, true, "modification error: incorrect type: \"array\", expected \"string\""}, + {"Array Length Min", field.String().AddOptions(TrimSpace()), [1]string{""}, true, "modification error: incorrect type: \"array\", expected \"string\""}, + {"Slice Length Max", field.String().AddOptions(TrimSpace()), []string{""}, true, "modification error: incorrect type: \"slice\", expected \"string\""}, + {"Slice Length Min", field.String().AddOptions(TrimSpace()), []string{""}, true, "modification error: incorrect type: \"slice\", expected \"string\""}, + {"Map Length Max", field.String().AddOptions(TrimSpace()), map[string]string{"": ""}, true, "modification error: incorrect type: \"map\", expected \"string\""}, + {"Map Length Min", field.String().AddOptions(TrimSpace()), map[string]string{"": ""}, true, "modification error: incorrect type: \"map\", expected \"string\""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, _, err := Modify(nil, tt.field, tt.data) + if tt.wantErr { + require.Error(t, err) + assert.EqualError(t, err, tt.error) + } + if !tt.wantErr { + require.NoError(t, err) + } + }) + } +} diff --git a/pkg/schema/modify/value.go b/pkg/schema/modify/value.go new file mode 100644 index 0000000000000000000000000000000000000000..1f87c80387c802eaf7fb7e3fec221ea263092050 --- /dev/null +++ b/pkg/schema/modify/value.go @@ -0,0 +1,39 @@ +package modify + +import ( + "context" + "encoding/json" + "reflect" + + "git.perx.ru/perxis/perxis-go/pkg/expr" + "git.perx.ru/perxis/perxis-go/pkg/schema/field" + "github.com/pkg/errors" +) + +type value struct { + Expression string +} + +func (d *value) UnmarshalJSON(bytes []byte) error { + return json.Unmarshal(bytes, &d.Expression) +} + +func (d value) MarshalJSON() ([]byte, error) { + return json.Marshal(d.Expression) +} + +func Value(exp string) Modifier { + return &value{Expression: exp} +} + +func (d value) Modify(ctx context.Context, f *field.Field, v interface{}) (interface{}, bool, error) { + val, err := expr.EvalKV(ctx, d.Expression, "_value", v) + if err != nil { + return nil, false, errors.Wrap(err, "value evaluation error") + } + // иÑпользование reflect.DeepEqual Ð´Ð»Ñ ÑƒÑ‡ÐµÑ‚Ð° Ñлучаев, когда в val маÑÑив или объект + if !reflect.DeepEqual(val, v) { + return val, true, nil + } + return v, false, nil +} diff --git a/pkg/schema/modify/value_test.go b/pkg/schema/modify/value_test.go new file mode 100644 index 0000000000000000000000000000000000000000..9878fc70663005f7650ac567899634a94446e8b9 --- /dev/null +++ b/pkg/schema/modify/value_test.go @@ -0,0 +1,63 @@ +package modify + +import ( + "reflect" + "testing" + + "git.perx.ru/perxis/perxis-go/pkg/schema/field" +) + +func TestValue(t *testing.T) { + tests := []struct { + name string + field *field.Field + data interface{} + want interface{} + wantErr bool + }{ + {"Condition with _value", field.String().AddOptions(Value("_value == 'ab' ? _value : _value + 'b'")), "a", "ab", false}, + {"Condition with _value", field.String().AddOptions(Value("_value == 'ab' ? _value : _value + 'b'")), "ab", "ab", false}, + {"Default with _value", field.String().AddOptions(Value("_value == nil ? 'abc' : _value")), nil, "abc", false}, + {"Field global", field.Object( + "fld1", field.String(), + "fld2", field.String().AddOptions(Value("fld1 + 'b'")), + ), + map[string]interface{}{"fld1": "a"}, + map[string]interface{}{"fld1": "a", "fld2": "ab"}, false}, + {"Field local", field.Object( + "fld1", field.String(), + "fld2", field.String().AddOptions(Value("_.fld1 + 'b'")), + ), + map[string]interface{}{"fld1": "a"}, + map[string]interface{}{"fld1": "a", "fld2": "ab"}, false}, + {"Multiple fields ", field.Object( + "fld1", field.String(), + "fld2", field.String(), + "fld3", field.String(), + "fld4", field.String(), + "fld5", field.String().AddOptions(Value("_.fld1 + fld2 + _.fld3+fld4")), + ), + map[string]interface{}{"fld1": "a", "fld2": "b", "fld3": "c", "fld4": "d"}, + map[string]interface{}{"fld1": "a", "fld2": "b", "fld3": "c", "fld4": "d", "fld5": "abcd"}, + false}, + {"Nil field error", field.Object( + "fld1", field.String(), + "fld2", field.String().AddOptions(Value("_.fld1 + 'b'")), + ), + map[string]interface{}{}, + map[string]interface{}{}, + true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, _, err := Modify(nil, tt.field, tt.data) + if (err != nil) != tt.wantErr { + t.Errorf("Modify() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && !reflect.DeepEqual(got, tt.want) { + t.Errorf("Modify() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/schema/schema.go b/pkg/schema/schema.go new file mode 100644 index 0000000000000000000000000000000000000000..e0eca6b6ffc02511acbe3712b2f2bf83fc5bc05e --- /dev/null +++ b/pkg/schema/schema.go @@ -0,0 +1,215 @@ +package schema + +import ( + "context" + + "git.perx.ru/perxis/perxis-go/pkg/errors" + "git.perx.ru/perxis/perxis-go/pkg/expr" + "git.perx.ru/perxis/perxis-go/pkg/schema/field" + "git.perx.ru/perxis/perxis-go/pkg/schema/modify" + "git.perx.ru/perxis/perxis-go/pkg/schema/validate" +) + +type Schema struct { + field.Field + Loaded bool `json:"loaded"` +} + +func New(kv ...interface{}) *Schema { + return &Schema{Field: *field.Object(kv...)} +} + +func NewFromField(f *field.Field) *Schema { + return &Schema{Field: *f} +} + +var ( + Encode = field.Encode + Decode = field.Decode + Modify = modify.Modify + Validate = validate.Validate + Evaluate = field.Evaluate +) + +func (s *Schema) Clone(reset bool) *Schema { + return &Schema{ + Field: *s.Field.Clone(reset), + Loaded: s.Loaded, + } +} + +func (s Schema) WithIncludes(includes ...interface{}) *Schema { + s.Field.SetIncludes(includes...) + return &s +} + +func (s *Schema) Load(ctx context.Context) error { + if s.Loaded { + return nil + } + return s.LoadIncludes(ctx, nil) +} + +func (s *Schema) LoadIncludes(ctx context.Context, loader field.Loader) (err error) { + if loader == nil { + loader = GetLoader() + } + err = s.Field.LoadIncludes(ctx, loader) + if err == nil { + s.Loaded = true + } + return +} + +func (s *Schema) Modify(ctx context.Context, data map[string]interface{}) (res map[string]interface{}, err error) { + if err = s.Load(ctx); err != nil { + return nil, err + } + + v, _, err := Modify(ctx, s, data) + if err != nil || v == nil { + return + } + + res, _ = v.(map[string]interface{}) + return +} + +func (s *Schema) Validate(ctx context.Context, data map[string]interface{}) (err error) { + if err = s.Load(ctx); err != nil { + return err + } + + return Validate(ctx, s, data) +} + +func (s *Schema) Evaluate(ctx context.Context, data map[string]interface{}) (res map[string]interface{}, err error) { + if err = s.Load(ctx); err != nil { + return nil, err + } + + v, err := Evaluate(ctx, s, data) + if err != nil || v == nil { + return + } + res, _ = v.(map[string]interface{}) + return +} + +func (s *Schema) Decode(ctx context.Context, v interface{}) (res map[string]interface{}, err error) { + if err = s.Load(ctx); err != nil { + return nil, err + } + + if v, err = Decode(ctx, s, v); err != nil { + return nil, err + } + res, _ = v.(map[string]interface{}) + return +} + +func (s *Schema) Encode(ctx context.Context, v interface{}) (interface{}, error) { + if err := s.Load(ctx); err != nil { + return nil, err + } + + var res interface{} + var err error + + if res, err = Encode(ctx, s, v); err != nil { + return nil, err + } + + return res, nil +} + +func (s *Schema) ToValue(ctx context.Context, data map[string]interface{}) (res map[string]interface{}, err error) { + if err = s.Load(ctx); err != nil { + return nil, err + } + + if data, err = s.Decode(ctx, data); err != nil { + return nil, err + } + if data, err = s.Modify(ctx, data); err != nil { + return nil, err + } + if data, err = s.Evaluate(ctx, data); err != nil { + return nil, err + } + if err = s.Validate(ctx, data); err != nil { + return nil, err + } + return data, err +} + +type parentFieldCtxKey struct{} + +func (s *Schema) Introspect(ctx context.Context, data map[string]interface{}) (map[string]interface{}, *Schema, error) { + if err := s.Load(ctx); err != nil { + return nil, nil, err + } + + var err error + + chg := true + val := data + i := 0 + + var mutatedSchema *Schema + + for chg { + mutatedSchema = nil + + var res interface{} + res, chg, err = s.Walk(expr.WithEnv(ctx, val), val, func(ctx context.Context, f *field.Field, v interface{}) (res field.WalkFuncResult, err error) { + parent, _ := ctx.Value(parentFieldCtxKey{}).(*field.Field) + name, _ := ctx.Value(field.FieldName).(string) + enabled, err := f.IsEnabled(ctx) + if err != nil { + return + } + + if !enabled { + res.Stop = true + if v != nil { + res.Changed = true + } + return + } + + fld := f.Clone(true) + if mutatedSchema == nil { + mutatedSchema = &Schema{Field: *fld} + fld = &mutatedSchema.Field + } + + if parent != nil && name != "" { + field.AddField(parent, name, fld) + } + + ctx = context.WithValue(ctx, parentFieldCtxKey{}, fld) + res.Context = ctx + + return + }, field.WalkSchema()) + + if err != nil { + return nil, nil, errors.Wrap(err, "evaluation error") + } + + if res != nil { + val = res.(map[string]interface{}) + } else { + val = nil + } + + i += 1 + + if i > field.EvaluatePassesLimit { + return nil, nil, errors.New("fail to evaluate data conditions") + } + } + + return val, mutatedSchema, nil +} diff --git a/pkg/schema/schema_json.go b/pkg/schema/schema_json.go new file mode 100644 index 0000000000000000000000000000000000000000..906acb5d0d361611d01f43d10bd0f80377ccb4a7 --- /dev/null +++ b/pkg/schema/schema_json.go @@ -0,0 +1,54 @@ +package schema + +import ( + "git.perx.ru/perxis/perxis-go/pkg/errors" + jsoniter "github.com/json-iterator/go" +) + +type jsonSchema struct { + //Field json.RawMessage `json:"field,inline"` + Loaded bool `json:"loaded"` +} + +func (s *Schema) UnmarshalJSON(b []byte) error { + + var j *jsonSchema + if err := jsoniter.Unmarshal(b, &j); err != nil { + return errors.Wrapf(err, "error unmarshal json into field") + } + s.Loaded = j.Loaded + + if err := s.Field.UnmarshalJSON(b); err != nil { + return err + } + + //if len(j.Field) > 0 { + // if err := s.Field.UnmarshalJSON(j.Field); err != nil { + // return err + // } + // //if err := jsoniter.Unmarshal(j.Field, &s.Field); err != nil { + // // return err + // //} + //} + + return nil +} + +func (s *Schema) MarshalJSON() ([]byte, error) { + + jsonField, err := s.Field.MarshalJSON() + if err != nil { + return nil, err + } + + jsonSch, err := jsoniter.Marshal(jsonSchema{ + //Field: b, + Loaded: s.Loaded, + }) + if err != nil { + return nil, err + } + jsonSch[0] = ',' // вмеÑто такого можно Ñначала jsonField размаршалить в map[string]interface{}, добавить поле и замаршалить еще раз + + return append(jsonField[:len(jsonField)-1], jsonSch...), nil +} diff --git a/pkg/schema/test/object_test.go b/pkg/schema/test/object_test.go new file mode 100644 index 0000000000000000000000000000000000000000..e5af975cdb9ed8e86044e5d357530dd2420afee9 --- /dev/null +++ b/pkg/schema/test/object_test.go @@ -0,0 +1,1490 @@ +package test + +import ( + "context" + "encoding/json" + "fmt" + "testing" + "time" + + "git.perx.ru/perxis/perxis-go/pkg/data" + "git.perx.ru/perxis/perxis-go/pkg/schema" + "git.perx.ru/perxis/perxis-go/pkg/schema/field" + "git.perx.ru/perxis/perxis-go/pkg/schema/modify" + "git.perx.ru/perxis/perxis-go/pkg/schema/validate" + "github.com/hashicorp/go-multierror" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDefaultTimeField_JSON(t *testing.T) { + w, _ := time.Parse(time.RFC3339, "2012-11-01T22:08:41Z") + fld := field.Object( + "timeField", field.Time(modify.Default(w)), + ) + + b, err := json.MarshalIndent(fld, "", " ") + require.NoError(t, err) + + res := &field.Field{} + err = json.Unmarshal(b, res) + require.NoError(t, err) + + assert.Equal(t, fld, res) +} + +func TestStringField_JSON(t *testing.T) { + fld := field.String().AddOptions(validate.MaxLength(200), modify.TrimSpace()) + + b, err := json.MarshalIndent(fld, "", " ") + require.NoError(t, err) + + res := field.NewField(nil) + err = json.Unmarshal(b, res) + require.NoError(t, err) + + assert.Equal(t, fld, res) +} + +func TestNumberField_JSON(t *testing.T) { + fld := field.Number(field.NumberFormatInt).AddOptions( + validate.Min(0), + validate.Max(10), + validate.MultipleOf(2), + validate.Enum( + validate.EnumOpt{ + Name: "N 1", + Value: 1.0, + }, + validate.EnumOpt{ + Name: "N 2", + Value: 2.0, + }, + ), + ) + + b, err := json.MarshalIndent(fld, "", " ") + require.NoError(t, err) + //fmt.Println(string(b)) + + res := field.NewField(nil) + err = json.Unmarshal(b, res) + require.NoError(t, err) + + assert.Equal(t, fld, res) +} + +func TestSchema_JSON(t *testing.T) { + enumStr := field.String().AddOptions( + validate.Enum( + validate.EnumOpt{ + Name: "N 1", + Value: "n1", + }, validate.EnumOpt{ + Name: "N 2", + Value: "n2", + }, + ), + ).SetAdditionalValues() + enumInt := field.Number(field.NumberFormatFloat).AddOptions( + validate.Enum( + validate.EnumOpt{ + Name: "N 1", + Value: 1.1, + }, validate.EnumOpt{ + Name: "N 2", + Value: 2.5, + }, + ), + ) + sch := schema.New( + "stringField", field.String().WithUI(&field.UI{Placeholder: "Test name"}).AddOptions(modify.TrimSpace()).AddOptions(validate.MinLength(2), validate.MaxLength(10)), + "stringField2", field.String(modify.Default("default")), + "intField", field.Number("int", validate.Required()), + "floatField", field.Number("float").SetIndexed(true), + "enumStringField", enumStr, + "enumIntField", enumInt, + "timeField", field.Time().SetSingleLocale(true), + "arrayField", field.Array(field.String(modify.Default("default"))), + "objectField", field.Object("innerStringField", field.String()).WithIncludes("ref1", field.Include{Ref: "ref2", Optional: true}), + "evaluatedField", field.String(modify.Value("stringField2 + '_' ")), + ) + sch.Loaded = true + + b, err := json.MarshalIndent(sch, "", " ") + require.NoError(t, err) + //fmt.Println(string(b)) + + res := schema.New() + err = json.Unmarshal(b, res) + require.NoError(t, err) + + assert.Equal(t, sch, res) +} + +func TestSchemaUI_UnmarshalJSON(t *testing.T) { + vw := &field.View{ + Widget: "Widget", + Options: map[string]interface{}{"title": "name", "key": "name"}, + } + ui := &field.UI{ + Widget: "Widget", + Placeholder: "Placeholder", + Options: map[string]interface{}{"title": "name", "key": "name"}, + ListView: vw, + ReadView: vw, + EditView: vw, + } + schm := schema.New( + "name", field.String().WithUI(ui), + ) + schm.UI = ui + + j := `{ + "ui": { + "widget": "Widget", + "placeholder": "Placeholder", + "options": { + "title": "name", + "key": "name" + }, + "read_view": { + "widget": "Widget", + "options": { + "title": "name", + "key": "name" + } + }, + "edit_view": { + "widget": "Widget", + "options": { + "title": "name", + "key": "name" + } + }, + "list_view": { + "widget": "Widget", + "options": { + "title": "name", + "key": "name" + } + } + }, + "type": "object", + "params": { + "inline": false, + "fields": { + "name": { + "ui": { + "widget": "Widget", + "placeholder": "Placeholder", + "options": { + "title": "name", + "key": "name" + }, + "read_view": { + "widget": "Widget", + "options": { + "title": "name", + "key": "name" + } + }, + "edit_view": { + "widget": "Widget", + "options": { + "title": "name", + "key": "name" + } + }, + "list_view": { + "widget": "Widget", + "options": { + "title": "name", + "key": "name" + } + } + }, + "type": "string", + "params": {} + } + } + }, + "loaded": false +}` + + sch := schema.New() + err := sch.UnmarshalJSON([]byte(j)) + require.NoError(t, err) + assert.Equal(t, sch, schm) +} + +func TestSchema_GetField(t *testing.T) { + + sch := schema.New( + "str", field.String(), + "num", field.Number(field.NumberFormatInt), + "obj", field.Object( + "bool", field.Bool(), + "arr", field.Array(field.Time()), + "list", field.Array( + field.Object( + "num1", field.Number(field.NumberFormatFloat), + "str1", field.String(), + "obj1", field.Object( + "str2", field.String(), + ), + ), + ), + "geo", field.Location(), + ), + ) + + data := []struct { + fld string + exists bool + typ string + }{ + {"str", true, "string"}, + {"obj.bool", true, "bool"}, + {"obj.list.num1", true, "number"}, + {"obj.list.obj1.str2", true, "string"}, + {"obj_list", false, ""}, + {"zzz", false, ""}, + {"obj.geo", true, "location"}, + } + + for _, d := range data { + t.Run(d.fld, func(t *testing.T) { + f := sch.GetField(d.fld) + if d.exists { + require.NotNil(t, f, fmt.Sprintf("not found '%s'", d.fld)) + assert.Equal(t, d.typ, f.GetType().Name(), fmt.Sprintf("field '%s'", d.fld)) + } else { + require.Nil(t, f) + } + }) + } +} + +func TestSchema_GetField_WithInline(t *testing.T) { + + sch := schema.New( + "str", field.String(), + "obj1", field.Object( + true, + "obj11", field.Object( + true, + "obj111", field.Object( + true, + "str1", field.String(), + "str2", field.String(), + ), + "arr1", field.Array(field.Object( + "str3", field.String(), + )), + "arr2", field.Array(field.String()), + ), + ), + "obj2", field.Object( + true, + "a", field.String(), + "b", field.String(), + ), + "zz", field.Object( + true, + "zz", field.Array(field.Object( + "str3", field.String(), + )), + ), + ) + + data := []struct { + fld string + exists bool + typ string + }{ + {"str", true, "string"}, + {"a", true, "string"}, + {"b", true, "string"}, + {"str1", true, "string"}, + {"str2", true, "string"}, + {"arr1", true, "array"}, + {"arr2", true, "array"}, + {"arr1.str3", true, "string"}, + {"zz.str3", true, "string"}, + } + + for _, d := range data { + t.Run(d.fld, func(t *testing.T) { + f := sch.GetField(d.fld) + if d.exists { + require.NotNil(t, f, fmt.Sprintf("not found '%s'", d.fld)) + assert.Equal(t, d.typ, f.GetType().Name(), fmt.Sprintf("field '%s'", d.fld)) + } else { + require.Nil(t, f) + } + }) + } +} + +func TestSchema_GetFields(t *testing.T) { + sch := schema.New( + "str", field.String().SetTitle("Str"), + "num", field.Number(field.NumberFormatInt).SetIndexed(true).SetTitle("Num"), + "obj", field.Object( + "arr", field.Array( + field.Array(field.Time()).SetIndexed(true).SetTitle("NestedArr"), + ).SetTitle("Arr"), + "list", field.Array( + field.Object( + "obj3", field.Object( + "str", field.String().SetIndexed(true).SetTitle("Obj2.List.Str"), + ).SetTitle("Obj3"), + ).SetTitle("Obj2"), + ).SetTitle("List"), + "geo", field.Location().SetTitle("Geo"), + ).SetTitle("Obj"), + ) + + flds := sch.GetFields(func(f *field.Field, path string) bool { return true }) + assert.Len(t, flds, 8) + + paths := make([]string, 0, len(flds)) + + for _, fld := range flds { + switch fld.Path { + case "str": + assert.IsType(t, &field.StringParameters{}, fld.Params) + case "num": + assert.IsType(t, &field.NumberParameters{}, fld.Params) + case "obj": + assert.IsType(t, &field.ObjectParameters{}, fld.Params) + case "obj.arr": + assert.IsType(t, &field.ArrayParameters{}, fld.Params) + case "obj.list": + assert.IsType(t, &field.ArrayParameters{}, fld.Params) + case "obj.list.obj3": + assert.IsType(t, &field.ObjectParameters{}, fld.Params) + case "obj.list.obj3.str": + assert.IsType(t, &field.StringParameters{}, fld.Params) + case "obj.geo": + assert.IsType(t, &field.LocationParameters{}, fld.Params) + + } + + paths = append(paths, fld.Path) + } + + assert.ElementsMatch( + t, + []string{"str", "num", "obj", "obj.arr", "obj.list", "obj.list.obj3", "obj.list.obj3.str", "obj.geo"}, + paths, + ) + +} + +func TestSchema_GetFieldByPath(t *testing.T) { + sch := schema.New( + "str", field.String().SetTitle("Str"), + "num", field.Number(field.NumberFormatInt).SetIndexed(true).SetTitle("Num"), + "obj", field.Object( + "arr", field.Array( + field.Array(field.Time()).SetIndexed(true).SetTitle("NestedArr"), + ).SetTitle("Arr"), + "list", field.Array( + field.Object( + "obj3", field.Object( + "str", field.String().SetIndexed(true).SetTitle("Obj2.List.Str"), + ).SetTitle("Obj3"), + ).SetTitle("Obj2"), + ).SetTitle("List"), + "geo", field.Location().SetTitle("Geo"), + ).SetTitle("Obj"), + ) + + dt := []struct { + name string + paths []string + want []string + }{ + { + "all", + []string{"*"}, + []string{"str", "num", "obj", "obj.arr", "obj.list", "obj.list.obj3", "obj.list.obj3.str", "obj.geo"}, + }, + { + "full match", + []string{"str", "obj.list.obj3", "some"}, + []string{"str", "obj.list.obj3"}, + }, + { + "glob", + []string{"str*", "obj.list*", "*geo"}, + []string{"str", "obj.list", "obj.list.obj3", "obj.list.obj3.str", "obj.geo"}, + }, + } + + for _, d := range dt { + t.Run(d.name, func(t *testing.T) { + got := field.GetFieldsPath(sch.GetFields(func(f *field.Field, path string) bool { + return data.GlobMatch(path, d.paths...) + })) + assert.ElementsMatch(t, d.want, got) + }) + } +} + +func TestSchema_GetFieldsInline(t *testing.T) { + t.Run("Basic", func(t *testing.T) { + sch := schema.New( + "str_1", field.String(), + "num", field.Number(field.NumberFormatInt).SetIndexed(true), + "obj_1", field.Object( + "arr", field.Array(field.Time()).SetIndexed(true), + "list", field.Array( + field.Object( + "obj_2", field.Object( + "str_2", field.String().SetIndexed(true), + ), + ), + ), + "geo", field.Location(), + ), + ) + + flds := field.GetFieldsPath(sch.GetFields(func(f *field.Field, p string) bool { + return true + }, "data")) + assert.ElementsMatch( + t, + []string{ + "data", + "data.str_1", + "data.num", + "data.obj_1", + "data.obj_1.arr", + "data.obj_1.list", + "data.obj_1.list.obj_2", + "data.obj_1.list.obj_2.str_2", + "data.obj_1.geo", + }, + flds, + ) + }) + t.Run("Inline fields in schema in a row", func(t *testing.T) { + sch := schema.New( + "obj_inline_1", field.Object( + true, + "inline_field1", field.String().SetUnique(true), + "obj_inline_2", field.Object(true, + "inline_field2", field.String(), + "arr", field.Array(field.Object(true, + "inline_field3", field.String(), + )), + ), + ), + ) + + flds := field.GetFieldsPath(sch.GetFields(func(f *field.Field, p string) bool { + return true + })) + assert.ElementsMatch( + t, + []string{ + "obj_inline_1", + "inline_field1", + "obj_inline_2", + "inline_field2", + "arr", + "arr.inline_field3", + }, + flds, + ) + }) + t.Run("Inline fields in schema in a row with prefix", func(t *testing.T) { + sch := schema.New( + "obj_inline_1", field.Object(true, + "inline_field1", field.String().SetUnique(true), + "obj_inline_2", field.Object(true, + "inline_field2", field.String(), + "obj_inline_3", field.Object(true, + "inline_field3", field.String(), + ), + ), + ), + ) + + flds := field.GetFieldsPath(sch.GetFields(func(f *field.Field, p string) bool { + return true + }, "data")) + assert.ElementsMatch( + t, + []string{ + "data", + "data.obj_inline_1", + "data.inline_field1", + "data.obj_inline_2", + "data.inline_field2", + "data.obj_inline_3", + "data.inline_field3", + }, + flds, + ) + }) + t.Run("Mixed fields in schema in a row", func(t *testing.T) { + sch := schema.New( + "obj_not_inline_1", field.Object( + "not_inline_field_1", field.String().SetUnique(true), + "obj_inline_1", field.Object(true, + "inline_field1", field.String(), + "obj_not_inline_2", field.Object( + "not_inline_field_2", field.String(), + "obj_inline_2", field.Object(true, + "inline_field2", field.String(), + ), + ), + ), + ), + ) + + flds := field.GetFieldsPath(sch.GetFields(func(f *field.Field, p string) bool { + return true + }, "data")) + assert.ElementsMatch( + t, + []string{ + "data", + "data.obj_not_inline_1", + "data.obj_not_inline_1.not_inline_field_1", + "data.obj_not_inline_1.obj_inline_1", + "data.obj_not_inline_1.inline_field1", + "data.obj_not_inline_1.obj_not_inline_2", + "data.obj_not_inline_1.obj_not_inline_2.not_inline_field_2", + "data.obj_not_inline_1.obj_not_inline_2.obj_inline_2", + "data.obj_not_inline_1.obj_not_inline_2.inline_field2", + }, + flds, + ) + }) +} + +func TestSchema_Clone(t *testing.T) { + sch := schema.New( + "f", field.String().WithUI(&field.UI{Placeholder: "Test name"}).AddOptions(modify.TrimSpace()).AddTranslation("ru", "Ñ„", "Поле Ф"), + "obj", field.Object( + "list", field.Array( + field.Object( + "obj", field.Object( + "field", field.String(), + ), + ), + ), + ), + ) + + t.Run("Simple", func(t *testing.T) { + f := sch.GetField("f") + fld := f.Clone(false) + + assert.Equal(t, f.UI, fld.UI) + assert.Equal(t, f.Options, fld.Options) + assert.Equal(t, f.Translations, fld.Translations) + assert.Equal(t, f.Params, fld.Params) + }) + + t.Run("Reset", func(t *testing.T) { + f := sch.GetField("obj") + fld := f.Clone(true) + + assert.Equal(t, f.UI, fld.UI) + assert.Equal(t, f.Options, fld.Options) + assert.Equal(t, f.Translations, fld.Translations) + assert.NotEqual(t, f.Params, fld.Params) + + f = sch.GetField("obj.list") + fld = f.Clone(true) + + assert.Equal(t, f.UI, fld.UI) + assert.Equal(t, f.Options, fld.Options) + assert.Equal(t, f.Translations, fld.Translations) + assert.NotEqual(t, f.Params, fld.Params) + + f = sch.GetField("obj.list.obj") + fld = f.Clone(true) + + assert.Equal(t, f.UI, fld.UI) + assert.Equal(t, f.Options, fld.Options) + assert.Equal(t, f.Translations, fld.Translations) + assert.NotEqual(t, f.Params, fld.Params) + }) +} + +func TestSchema_Modify(t *testing.T) { + sch := schema.New( + "name", field.String(validate.Required()), + "last_name", field.String(validate.Required()), + "got_nobel", field.Bool(), + "times", field.Number("int"), + "dates", field.Array(field.Time()), + ) + + in := map[string]interface{}{"last_name": "Curie", "name": "Marie"} + _, _, err := modify.Modify(nil, sch, in) + require.NoError(t, err) +} + +func TestSchema_Validate(t *testing.T) { + sch := schema.New( + "name", field.String(validate.Required()), + "last_name", field.String(), + "info", field.Object( + "time", field.Time(), + "numbers", field.Number( + field.NumberFormatInt, + validate.Enum( + validate.EnumOpt{Name: "first", Value: 1}, + validate.EnumOpt{Name: "second", Value: 2}, + ), + ), + ), + ) + + in := map[string]interface{}{"info": map[string]interface{}{"time": time.Now()}, "name": "Name"} + err := validate.Validate(nil, sch, in) + require.NoError(t, err) +} + +func TestSchema_DecodeErrors(t *testing.T) { + sch := schema.New( + "name", field.String(validate.Required()), + "last_name", field.String(), + "a", field.Object( + "time", field.Time(), + "num1", field.Number(field.NumberFormatInt), + "num2", field.Number(field.NumberFormatInt), + "num3", field.Number(field.NumberFormatInt), + "b", field.Object( + "num1", field.Number(field.NumberFormatInt), + "num2", field.Number(field.NumberFormatInt), + "num3", field.Number(field.NumberFormatInt), + ), + "c", field.Array(field.Number(field.NumberFormatInt)), + "d", field.Number(field.NumberFormatInt, validate.Max(10)), + ), + ) + + in := map[string]interface{}{"a": map[string]interface{}{"time": time.Now(), "num1": "a", "num2": "b", "num3": "c", "d": 20, + "b": map[string]interface{}{"time": time.Now(), "num1": "a", "num2": "b", "num3": "c", "str": "s"}, "c": []interface{}{"a", "b", "c"}}, + "name": "Name"} + _, err := schema.Decode(nil, sch, in) + assert.Error(t, err) + assert.Contains(t, err.Error(), "decode error") +} + +func TestSchema_ValidateErrors(t *testing.T) { + sch := schema.New( + "a", field.Object( + "num1", field.Number(field.NumberFormatInt, validate.Required()), + "num2", field.Number(field.NumberFormatInt, validate.Max(10)), + "num3", field.Number(field.NumberFormatInt, validate.Min(10)), + "str1", field.String(validate.MaxLength(5)), + "str2", field.String(validate.MinLength(5)), + "str3", field.String(validate.MinLength(5), validate.Enum(validate.EnumOpt{Value: "somesome"}, validate.EnumOpt{Value: "romoromo"})), + ), + ) + + in := map[string]interface{}{"a": map[string]interface{}{"num2": 20, "num3": 5, "str1": "123456", "str2": "123", "str3": "some"}} + decoded, err := schema.Decode(nil, sch, in) + require.NoError(t, err) + err = validate.Validate(nil, sch, decoded) + require.Error(t, err) + require.Contains(t, err.Error(), "validation error") + var merr *multierror.Error + require.ErrorAs(t, err, &merr) + assert.Len(t, merr.Errors, 6) +} + +func TestSchema_ValidateEmptyObject(t *testing.T) { + { + sch := schema.New( + "num1", field.Number(field.NumberFormatInt, validate.Required()), + ) + + res, err := schema.Decode(nil, sch, nil) + require.NoError(t, err) + res, _, err = modify.Modify(nil, sch, res) + require.NoError(t, err) + err = validate.Validate(nil, sch, res) + require.NoError(t, err, "Ð¿Ð¾Ð»Ñ Ð¾Ð±ÑŠÐµÐºÑ‚Ð° nil не проверÑÑŽÑ‚ÑÑ") + } + { + sch := schema.New( + "num1", field.Number(field.NumberFormatInt, validate.Required()), + ) + + res, err := schema.Decode(nil, sch, map[string]interface{}{}) + require.NoError(t, err) + res, _, err = modify.Modify(nil, sch, res) + require.NoError(t, err) + err = validate.Validate(nil, sch, res) + require.Error(t, err, "Ð¿Ð¾Ð»Ñ Ð¿ÑƒÑтого объекта проверÑÑŽÑ‚ÑÑ") + } + { + sch := schema.New( + "num1", field.Number(field.NumberFormatInt, validate.Required()), + ) + + res, err := schema.Decode(nil, sch, map[string]interface{}{"a": "sss"}) + require.NoError(t, err) + res, _, err = modify.Modify(nil, sch, res) + require.NoError(t, err) + err = validate.Validate(nil, sch, res) + require.Error(t, err, "Ð¿Ð¾Ð»Ñ Ð¾Ð±ÑŠÐµÐºÑ‚Ð° Ñ Ð½ÐµÐºÐ¾Ñ€Ñ€ÐµÐºÑ‚Ð½Ñ‹Ð¼Ð¸ данными проверÑÑŽÑ‚ÑÑ") + } + + { + sch := schema.New( + "num1", field.Number(field.NumberFormatInt, validate.Required()), + ).AddOptions(modify.Default(map[string]interface{}{})) + + res, err := schema.Decode(nil, sch, nil) + require.NoError(t, err) + res, _, err = modify.Modify(nil, sch, res) + require.NoError(t, err) + err = validate.Validate(nil, sch, res) + require.Error(t, err, "Ð¿Ð¾Ð»Ñ nil объекта Default данными проверÑÑŽÑ‚ÑÑ") + } +} + +func TestSchema_ModificationErrors(t *testing.T) { + sch := schema.New( + "a", field.Object( + "num1", field.Number(field.NumberFormatInt, modify.TrimSpace()), + "str1", field.String(modify.TrimSpace()), + ), + ) + + in := map[string]interface{}{"a": map[string]interface{}{"num1": 20, "num3": 5, "str1": "123456", "str2": "123", "str3": "some"}} + decoded, err := schema.Decode(nil, sch, in) + require.NoError(t, err) + _, _, err = modify.Modify(nil, sch, decoded) + require.Error(t, err) + require.Contains(t, err.Error(), "modification error") + var merr *multierror.Error + require.ErrorAs(t, err, &merr) + assert.Len(t, merr.Errors, 1) +} + +func TestSchema_UnknownJSON(t *testing.T) { + sch := schema.New( + "name", field.String(validate.Required()), + "last_name", field.String(validate.Required()), + "got_nobel", field.Bool(), + "times", field.Number("int"), + "dates", field.Array(field.Time()), + ) + + b, err := json.Marshal(sch) + require.NoError(t, err) + field.Unregister("object") + + s1 := schema.New() + err = json.Unmarshal(b, s1) + require.NoError(t, err) + assert.Equal(t, "unknown", s1.GetType().Name(), "Схема неизвеÑтного типа должна определÑтьÑÑ ÐºÐ°Ðº unknown") + + in := map[string]interface{}{"info": map[string]interface{}{"time": time.Now()}, "name": "Name"} + out, err := field.Decode(nil, s1, in) + require.NoError(t, err) + assert.Equal(t, in, out, "Данные неизвеÑтного типа не изменÑÑŽÑ‚ÑÑ Ð¿Ñ€Ð¸ декодировании") + err = validate.Validate(nil, s1, in) + require.NoError(t, err, "Данные неизвеÑтного типа не валидируютÑÑ Ð²Ð³Ð»ÑƒÐ±ÑŒ") + + b, err = json.Marshal(s1) + require.NoError(t, err) + s2 := schema.New() + err = json.Unmarshal(b, s2) + require.NoError(t, err) + b, err = json.Marshal(s2) + require.NoError(t, err) + assert.Equal(t, "unknown", s2.GetType().Name(), "Схема неизвеÑтного типа должна определÑтьÑÑ ÐºÐ°Ðº unknown") + assert.Equal(t, s1, s2, "Схема не должна менÑтьÑÑ Ð¿Ñ€Ð¸ повторном маршалинге") + + field.Register(&field.ObjectType{}) + s3 := schema.New() + err = json.Unmarshal(b, s3) + require.NoError(t, err) + assert.Equal(t, "object", s3.GetType().Name(), "Схема должна воÑÑтановить тип object при воÑÑтановление региÑтрации типа") + assert.Equal(t, sch, s3, "Схема должна воÑÑтановитьÑÑ Ð¿Ñ€Ð¸ воÑÑтановление региÑтрации типа") +} + +func TestSchema_ValidOptions(t *testing.T) { + t.Run("Valid Options", func(t *testing.T) { + schm := `{ + "type": "object", + "params": { + "fields": { + "required": { + "options": { + "required": true + }, + "type": "string" + }, + "readonly": { + "options": { + "readonly": true + }, + "type": "string" + }, + "enum": { + "options": { + "enum": [{ + "name": "One", + "value": "one" + }, + { + "name": "Two", + "value": "two" + } + ] + }, + "type": "string" + } + } + } +}` + + s := schema.New() + err := json.Unmarshal([]byte(schm), s) + require.NoError(t, err) + + required := s.GetField("required") + readonly := s.GetField("readonly") + enum := s.GetField("enum") + + require.NotEmpty(t, required.Options) + require.NotEmpty(t, readonly.Options) + require.NotEmpty(t, enum.Options) + }) + + t.Run("Invalid Options", func(t *testing.T) { + schm := `{ + "type": "object", + "params": { + "fields": { + "required": { + "options": { + "required": false + }, + "type": "string" + }, + "readonly": { + "options": { + "readonly": false + }, + "type": "string" + } + } + } +}` + + s := schema.New() + err := json.Unmarshal([]byte(schm), s) + require.NoError(t, err) + + required := s.GetField("required") + readonly := s.GetField("readonly") + + require.Empty(t, required.Options) + require.Empty(t, readonly.Options) + }) + + t.Run("Required Enum Name", func(t *testing.T) { + schm := `{ + "type": "object", + "params": { + "fields": { + "enum": { + "options": { + "enum": [{ + "value": "one" + }, + { + "value": "two" + } + ] + }, + "type": "string" + } + } + } +}` + s := schema.New() + err := json.Unmarshal([]byte(schm), s) + require.Error(t, err) + assert.Contains(t, err.Error(), "enum name is required") + }) +} + +func TestSchema_Condition(t *testing.T) { + sch := schema.New( + "type", field.String(modify.TrimSpace()), + "a", field.Number(field.NumberFormatInt).SetCondition("type contains 'a'"), + "b", field.Number(field.NumberFormatInt, validate.Required()).SetCondition("type contains 'b'"), + "c", field.Number(field.NumberFormatInt).SetCondition("a==10"), + "obj", field.Object( + "a", field.Number(field.NumberFormatInt).SetCondition("type contains 'a'"), + "b", field.Number(field.NumberFormatInt).SetCondition("type contains 'b'"), + "c", field.Number(field.NumberFormatInt).SetCondition("_.a < 10"), + "d", field.Number(field.NumberFormatInt, modify.Default(11)).SetCondition("_.a < 10"), + ), + "obj3", field.Object( + "fld1", field.Number(field.NumberFormatInt), + ).SetCondition("obj.d > 10"), + ) + + tests := []struct { + name string + data map[string]interface{} + want map[string]interface{} + wantErr bool + }{ + {"type a", + map[string]interface{}{ + "type": "a", + "a": int64(10), + "b": int64(10), + "c": int64(1), + "obj": map[string]interface{}{"a": int64(1), "b": int64(20), "c": int64(11), "d": int64(11)}, + "obj3": map[string]interface{}{"fld1": int64(6)}, + }, + map[string]interface{}{ + "type": "a", + "a": int64(10), + "c": int64(1), + "obj": map[string]interface{}{"a": int64(1), "c": int64(11), "d": int64(11)}, + "obj3": map[string]interface{}{"fld1": int64(6)}, + }, + false}, + {"type b", + map[string]interface{}{ + "type": "b", + "a": int64(10), + "b": int64(10), + "c": int64(1), + "obj": map[string]interface{}{"a": int64(1), "b": int64(20), "c": int64(11), "d": int64(11)}, + "obj3": map[string]interface{}{"fld1": int64(6)}, + }, + map[string]interface{}{ + "type": "b", + "b": int64(10), + "obj": map[string]interface{}{"b": int64(20)}, + }, + false}, + {"type ab + default", + map[string]interface{}{ + "type": " ab ", + "a": int64(1), + "b": int64(10), + "c": int64(1), + "obj": map[string]interface{}{"a": int64(1), "b": int64(20), "c": int64(11)}, + "obj3": map[string]interface{}{"fld1": int64(6)}, + }, + map[string]interface{}{ + "type": "ab", + "a": int64(1), + "b": int64(10), + "obj": map[string]interface{}{"a": int64(1), "b": int64(20), "c": int64(11), "d": int64(11)}, + "obj3": map[string]interface{}{"fld1": int64(6)}, + }, + false}, + } + + ctx := context.Background() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := sch.ToValue(ctx, tt.data) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + assert.Equal(t, tt.want, got) + }) + } + +} + +func TestSchema_Inline(t *testing.T) { + sch := schema.New( + "a", field.String(), + "b", field.String(), + "obj", field.Object( + true, + "c", field.String(), + "d", field.Number(field.NumberFormatInt), + "inner_obj", field.Object( + true, + "f", field.String(), + ), + ).SetCondition("a == 'universe'"), + "overlap", field.Object( + "obj1", field.Object( + true, + "f1", field.Number(field.NumberFormatInt), + "f2", field.String(), + ), + "obj2", field.Object( + true, + "f1", field.Number(field.NumberFormatInt), + "f2", field.Number(field.NumberFormatInt), + ), + ), + "arr", field.Array( + field.Object( + true, + "x", field.String(), + ), + ), + ) + + tests := []struct { + name string + data map[string]interface{} + want map[string]interface{} + wantErr bool + }{ + {"Condition success", + map[string]interface{}{"a": "universe", "b": "life", "c": "everything", "d": int64(42)}, + map[string]interface{}{"a": "universe", "b": "life", "c": "everything", "d": int64(42)}, + false, + }, + {"Condition fail", + map[string]interface{}{"a": "life", "b": "universe", "c": "everything", "d": int64(42)}, + map[string]interface{}{"a": "life", "b": "universe"}, + false, + }, + {"Condition success, level 2 inline", + map[string]interface{}{"a": "universe", "b": "life", "c": "everything", "d": int64(42), "f": "some"}, + map[string]interface{}{"a": "universe", "b": "life", "c": "everything", "d": int64(42), "f": "some"}, + false, + }, + {"Condition fail, level 2 inline", + map[string]interface{}{"a": "life", "b": "universe", "c": "everything", "d": int64(42), "f": "some"}, + map[string]interface{}{"a": "life", "b": "universe"}, + false, + }, + {"Overlapped", + map[string]interface{}{"overlap": map[string]interface{}{"f1": 42}}, + map[string]interface{}{"overlap": map[string]interface{}{"f1": int64(42)}}, + false, + }, + {"Overlapped, type conflict", + map[string]interface{}{"overlap": map[string]interface{}{"f1": 42, "f2": "everything"}}, + nil, + true, + }, + {"Array, ignore inline", + map[string]interface{}{"a": "life", "b": "universe", "c": "everything", "d": int64(42), "x": "some", "arr": []interface{}{map[string]interface{}{"x": "some"}}}, + map[string]interface{}{"a": "life", "b": "universe", "arr": []interface{}{map[string]interface{}{"x": "some"}}}, + false, + }, + } + + ctx := context.Background() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := sch.ToValue(ctx, tt.data) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + assert.Equal(t, tt.want, got) + }) + } +} + +func TestSchema_Introspect(t *testing.T) { + tests := []struct { + name string + data map[string]interface{} + schema *schema.Schema + want map[string]interface{} + wantFields []string + dontWantFields []string + wantErr bool + }{ + {"single true condition", + map[string]interface{}{ + "a": "b", + "b": "b", + }, + schema.New( + "a", field.String(), + "b", field.String().SetCondition("a == 'b'"), + ), + map[string]interface{}{ + "a": "b", + "b": "b", + }, + []string{"a", "b"}, + []string{}, + false}, + {"single false condition", + map[string]interface{}{ + "a": "a", + "b": "b", + }, + schema.New( + "a", field.String(), + "b", field.String().SetCondition("a == 'b'"), + ), + map[string]interface{}{ + "a": "a", + }, + []string{"a"}, + []string{"b"}, + false}, + {"multiple true conditions", + map[string]interface{}{ + "a": "a", + "b": "b", + "c": "c", + "d": "d", + }, + schema.New( + "a", field.String(), + "b", field.String().SetCondition("a == 'a'"), + "c", field.String().SetCondition("b == 'b'"), + "d", field.String().SetCondition("c == 'c'"), + ), + map[string]interface{}{ + "a": "a", + "b": "b", + "c": "c", + "d": "d", + }, + []string{"a", "b", "c", "d"}, + []string{}, + false}, + {"multiple conditions some true", + map[string]interface{}{ + "a": "a", + "b": "bb", + "c": "c", + "d": "d", + }, + schema.New( + "a", field.String(), + "b", field.String().SetCondition("a == 'a'"), + "c", field.String().SetCondition("b == 'b'"), + "d", field.String().SetCondition("c == 'c'"), + ), + map[string]interface{}{ + "a": "a", + "b": "bb", + }, + []string{"a", "b"}, + []string{"c", "d"}, + false}, + {"nil data", + nil, + schema.New( + "a", field.String(), + "b", field.String(), + ), + nil, + []string{"a", "b"}, + nil, + false}, + {"empty data", + map[string]interface{}{}, + schema.New( + "a", field.String(), + "b", field.String(), + ), + map[string]interface{}{}, + []string{"a", "b"}, + nil, + false}, + {"data with other fields", + map[string]interface{}{"some": "some"}, + schema.New( + "a", field.String(), + "b", field.String(), + ), + map[string]interface{}{}, + []string{"a", "b"}, + nil, + false}, + {"nil object", + map[string]interface{}{"a": "aa"}, + schema.New( + "a", field.String(), + "j", field.Object( + "aa", field.String(), + "bb", field.Number(field.NumberFormatInt), + ), + ), + map[string]interface{}{"a": "aa"}, + []string{"a", "j", "j.aa", "j.bb"}, + nil, + false}, + { + "object condition", + map[string]interface{}{"key": "a", "object_b": map[string]interface{}{"field1": "a", "field2": "a"}}, + schema.New( + "key", field.String(modify.Default("default")), + "object_b", field.Object( + "field1", field.String(), + "field2", field.String(), + ), + "object_a", field.Object( + "field1", field.String(), + "field2", field.String(), + ).SetCondition("key=='a'"), + ), + map[string]interface{}{"key": "a", "object_b": map[string]interface{}{"field1": "a", "field2": "a"}}, + []string{"key", "object_b", "object_a", "object_b.field1", "object_b.field2"}, + []string{"field1", "field2"}, + false, + }, + { + "object condition with nil data", + nil, + schema.New( + "key", field.String(modify.Default("default")), + "object_b", field.Object( + "field1", field.String(), + "field2", field.String(), + ), + "object_a", field.Object( + "field1", field.String(), + "field2", field.String(), + ).SetCondition("key=='a'"), + ), + nil, + []string{"key", "object_b", "object_b.field1", "object_b.field2"}, + []string{"object_a", "field1", "field2"}, + false, + }, + } + + ctx := context.Background() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotValue, gotSchema, err := tt.schema.Introspect(ctx, tt.data) + require.NoError(t, err) + + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + for _, f := range tt.wantFields { + fld := gotSchema.GetField(f) + assert.NotNil(t, fld, fmt.Sprintf("поле '%s' должно приÑутÑтвовать в Ñхеме", f)) + } + + for _, f := range tt.dontWantFields { + fld := gotSchema.GetField(f) + assert.Nil(t, fld, fmt.Sprintf("поле '%s' должно отÑутÑтвовать в Ñхеме", f)) + } + + //b, err := json.MarshalIndent(got.Schema, "", " ") + //require.NoError(t, err) + //fmt.Printf("---\n%s\n---\n", b) + assert.Equal(t, tt.want, gotValue) + }) + } + +} + +func TestSchema_IntrospectObjectArray(t *testing.T) { + tests := []struct { + name string + data map[string]interface{} + schema *schema.Schema + want map[string]interface{} + wantParams []string + wantErr bool + }{ + { + "simple", + map[string]interface{}{"array": []interface{}{map[string]interface{}{"field1": "a", "field2": "a"}}}, + schema.New( + "array", field.Array( + field.Object( + "field1", field.String(), + "field2", field.String(), + ), + )), + map[string]interface{}{"array": []interface{}{map[string]interface{}{"field1": "a", "field2": "a"}}}, + []string{"field1", "field2"}, + false, + }, + { + "empty data", + nil, + schema.New( + "array", field.Array( + field.Object( + "field1", field.String(), + "field2", field.String(), + ), + )), + nil, + []string{"field1", "field2"}, + false, + }, + } + + ctx := context.Background() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotValue, gotSchema, err := tt.schema.Introspect(ctx, tt.data) + require.NoError(t, err) + + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + for _, f := range tt.wantParams { + fld := gotSchema.GetField("array") + p, ok := fld.Params.(*field.ArrayParameters).Item.Params.(*field.ObjectParameters) + assert.True(t, ok) + assert.Contains(t, p.Fields, f, fmt.Sprintf("поле '%s' должно приÑутÑтвовать в параметрах Item", f)) + } + assert.Equal(t, tt.want, gotValue) + }) + } + +} + +func TestSchema_Load(t *testing.T) { + f1 := schema.New( + "f", field.String(), + "s3", field.Object("a1", field.Number(field.NumberFormatInt), "f", field.String()).WithIncludes("f2"), + ) + f2 := schema.New("a", field.String()) + + sch := schema.NewFromField(field.Object( + "s1", field.String(), + "s2", field.String(), + "s3", field.Object("a1", field.String(), "a2", field.String()), + "s4", field.Array(field.Object().WithIncludes("f2")), + ).WithIncludes("f1", "f2"), + ) + + loader := field.MultiLoader( + field.LoaderFunc(func(ref string) (fs []*field.Field, err error) { + if ref == "f1" { + f := f1.Field // copy + return []*field.Field{&f}, nil + } + return nil, fmt.Errorf("invalid schema reference: %s", ref) + }), + field.LoaderFunc(func(ref string) (fs []*field.Field, err error) { + if ref == "f2" { + f := f2.Field // copy + return []*field.Field{&f}, nil + } + return nil, fmt.Errorf("invalid schema reference: %s", ref) + }), + ) + schema.SetDefaultLoader(loader) + + err := sch.Load(nil) + require.NoError(t, err) + + //b, _ := json.MarshalIndent(sch, "", " ") + //fmt.Println(string(b)) + + assert.NotNil(t, sch.GetField("s1")) + assert.NotNil(t, sch.GetField("s2")) + assert.NotNil(t, sch.GetField("f")) + assert.NotNil(t, sch.GetField("a")) + assert.NotNil(t, sch.GetField("s3")) + assert.NotNil(t, sch.GetField("s3.f")) + assert.NotNil(t, sch.GetField("s3.a")) + { + f := sch.GetField("s3.a1") + require.NotNil(t, f) + assert.Equal(t, f.GetType(), &field.StringType{}) + } + assert.NotNil(t, sch.GetField("s4.a")) + +} + +func TestSchema_WithIncludesCircle(t *testing.T) { + f1 := schema.New("f2", field.Object().WithIncludes("f2")) + f2 := schema.New("f3", field.Object().WithIncludes("f3")) + f3 := schema.New("f1", field.Object().WithIncludes("f1")) + + loader := field.MultiLoader( + field.LoaderFunc(func(ref string) (fs []*field.Field, err error) { + if ref == "f1" { + f := f1.Field // copy + return []*field.Field{&f}, nil + } + return nil, fmt.Errorf("invalid schema reference: %s", ref) + }), + field.LoaderFunc(func(ref string) (fs []*field.Field, err error) { + if ref == "f2" { + f := f2.Field // copy + return []*field.Field{&f}, nil + } + return nil, fmt.Errorf("invalid schema reference: %s", ref) + }), + field.LoaderFunc(func(ref string) (fs []*field.Field, err error) { + if ref == "f3" { + f := f3.Field // copy + return []*field.Field{&f}, nil + } + return nil, fmt.Errorf("invalid schema reference: %s", ref) + }), + ) + schema.SetDefaultLoader(loader) + sch := schema.NewFromField(field.Object().WithIncludes("f1")) + + err := sch.Load(nil) + require.Error(t, err) + assert.EqualError(t, err, "limit for included fields exceeded") +} + +func TestSchema_EnumUIOptions(t *testing.T) { + schm := `{ + "type": "object", + "params": { + "fields": { + "enum": { + "options": { + "enum": [{ + "name": "1", + "value": "one", + "ui" : { + "color": "color", + "icon": "icon", + "spin": true, + "blink": false + } + }, + { + "name": "2", + "value": "two" + } + ] + }, + "type": "string" + } + } + } +}` + s := schema.New() + err := json.Unmarshal([]byte(schm), s) + require.NoError(t, err) +} diff --git a/pkg/schema/validate/array.go b/pkg/schema/validate/array.go new file mode 100644 index 0000000000000000000000000000000000000000..09e17e289cdfeca4cbe743271b77300a4cb7a0d8 --- /dev/null +++ b/pkg/schema/validate/array.go @@ -0,0 +1,51 @@ +package validate + +import ( + "context" + "fmt" + "reflect" + + "git.perx.ru/perxis/perxis-go/pkg/schema/field" +) + +type maxItems int + +func MaxItems(max int) Validator { + v := maxItems(max) + return &v +} + +func (t maxItems) Validate(_ context.Context, _ *field.Field, value interface{}) error { + if value == nil { + return nil + } + v := reflect.ValueOf(value) + if !v.IsValid() || v.Kind() != reflect.Array && v.Kind() != reflect.Slice { + return fmt.Errorf("incorrect type: \"%s\", expected \"array\"", v.Kind()) + } + if t > 0 && v.Len() > int(t) { + return fmt.Errorf("maximum elements number is %d", t) + } + return nil +} + +type minItems int + +func MinItems(min int) Validator { + v := minItems(min) + return &v +} + +func (t minItems) Validate(_ context.Context, _ *field.Field, value interface{}) error { + if value == nil { + return nil + } + v := reflect.ValueOf(value) + if !v.IsValid() || v.Kind() != reflect.Array && v.Kind() != reflect.Slice { + return fmt.Errorf("incorrect type: \"%s\", expected \"array\"", v.Kind()) + } + if t > 0 && v.Len() < int(t) { + return fmt.Errorf("minimum elements number is %d", t) + } + return nil +} diff --git a/pkg/schema/validate/array_test.go b/pkg/schema/validate/array_test.go new file mode 100644 index 0000000000000000000000000000000000000000..57b99445d4681b3994cb9cc5e3c35822a38a4900 --- /dev/null +++ b/pkg/schema/validate/array_test.go @@ -0,0 +1,80 @@ +package validate + +import ( + "testing" + + "git.perx.ru/perxis/perxis-go/pkg/schema/field" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestArray(t *testing.T) { + tests := []struct { + name string + field *field.Field + data interface{} + wantErr bool + }{ + {"Max Array", field.Array(field.String()).AddOptions(MaxItems(2)), []interface{}{"a", "b"}, false}, + {"Max Array (invalid)", field.Array(field.String()).AddOptions(MaxItems(2)), []interface{}{"a", "b", "c"}, true}, + {"Max Array (negative max number)", field.Array(field.String()).AddOptions(MaxItems(-2)), []interface{}{"a", "b", "c"}, false}, + {"Max Array (zero max number)", field.Array(field.String()).AddOptions(MaxItems(0)), []interface{}{"a", "b", "c"}, false}, + + {"Min Array", field.Array(field.String()).AddOptions(MinItems(2)), []interface{}{"a", "b"}, false}, + {"Min Array (invalid)", field.Array(field.String()).AddOptions(MinItems(2)), []interface{}{"a"}, true}, + {"Min Array (negative max number)", field.Array(field.String()).AddOptions(MinItems(-2)), []interface{}{"a", "b", "c"}, false}, + {"Min Array (zero max number)", field.Array(field.String()).AddOptions(MinItems(0)), []interface{}{"a", "b", "c"}, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := field.Decode(nil, tt.field, tt.data) + require.NoError(t, err) + err = Validate(nil, tt.field, got) + if (err != nil) != tt.wantErr { + t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } +} + +func TestArrayValidate(t *testing.T) { + tests := []struct { + name string + field *field.Field + data interface{} + wantErr bool + error string + }{ + {"Nil Max Items", field.Array(field.String()).AddOptions(MaxItems(1)), nil, false, ""}, + {"Nil Min Items", field.Array(field.String()).AddOptions(MinItems(1)), nil, false, ""}, + {"Array Max Items", field.Array(field.String()).AddOptions(MaxItems(1)), [1]interface{}{1}, true, "validation error: incorrect type: \"array\", expected \"[]interface{}\""}, + {"Array Min Items", field.Array(field.String()).AddOptions(MinItems(1)), [1]interface{}{1}, true, "validation error: incorrect type: \"array\", expected \"[]interface{}\""}, + {"Slice Max Items", field.Array(field.String()).AddOptions(MaxItems(0)), []interface{}{}, false, ""}, + {"Slice Min Items", field.Array(field.String()).AddOptions(MinItems(0)), []interface{}{}, false, ""}, + {"Bool Max Items", field.Array(field.String()).AddOptions(MaxItems(1)), true, true, "validation error: incorrect type: \"bool\", expected \"array\""}, + {"Bool Min Items", field.Array(field.String()).AddOptions(MinItems(1)), true, true, "validation error: incorrect type: \"bool\", expected \"array\""}, + {"Int Max Items", field.Array(field.String()).AddOptions(MaxItems(1)), 1, true, "validation error: incorrect type: \"int\", expected \"array\""}, + {"Int Min Items", field.Array(field.String()).AddOptions(MinItems(1)), 1, true, "validation error: incorrect type: \"int\", expected \"array\""}, + {"Float Max Items", field.Array(field.String()).AddOptions(MaxItems(1)), 1.0, true, "validation error: incorrect type: \"float64\", expected \"array\""}, + {"Float Min Items", field.Array(field.String()).AddOptions(MinItems(1)), 1.0, true, "validation error: incorrect type: \"float64\", expected \"array\""}, + {"String Max Items", field.Array(field.String()).AddOptions(MaxItems(1)), "1", true, "validation error: incorrect type: \"string\", expected \"array\""}, + {"String Min Items", field.Array(field.String()).AddOptions(MinItems(1)), "1", true, "validation error: incorrect type: \"string\", expected \"array\""}, + {"Map Max Items", field.Array(field.String()).AddOptions(MaxItems(1)), map[int]string{1: "1"}, true, "validation error: incorrect type: \"map\", expected \"array\""}, + {"Map Min Items", field.Array(field.String()).AddOptions(MinItems(1)), map[int]string{1: "1"}, true, "validation error: incorrect type: \"map\", expected \"array\""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := Validate(nil, tt.field, tt.data) + if tt.wantErr { + require.Error(t, err) + assert.EqualError(t, err, tt.error) + } + if !tt.wantErr { + require.NoError(t, err) + } + }) + } +} diff --git a/pkg/schema/validate/enum.go b/pkg/schema/validate/enum.go new file mode 100644 index 0000000000000000000000000000000000000000..76d2ecc698792346e1f9dd60940e47d050593b96 --- /dev/null +++ b/pkg/schema/validate/enum.go @@ -0,0 +1,68 @@ +package validate + +import ( + "context" + "fmt" + + "git.perx.ru/perxis/perxis-go/pkg/schema/field" + "github.com/pkg/errors" +) + +type EnumUI struct { + Color string `json:"color,omitempty"` + Icon string `json:"icon,omitempty"` + Spin bool `json:"spin,omitempty"` + Blink bool `json:"blink,omitempty"` +} + +type EnumOpt struct { + Name string `json:"name"` + Value interface{} `json:"value"` + UI *EnumUI `json:"ui,omitempty"` +} + +func (o EnumOpt) String() string { + return fmt.Sprintf("%s", o.Value) +} + +type enum []EnumOpt + +func Enum(opts ...EnumOpt) Validator { + var e enum + for _, i := range opts { + e = append(e, i) + } + return &e +} + +func (t enum) Validate(ctx context.Context, f *field.Field, value interface{}) error { + // Значение может отÑутÑтвовать, что не ÑвлÑетÑÑ Ð¾ÑˆÐ¸Ð±ÐºÐ¾Ð¹ + if value == nil { + return nil + } + + if f.AdditionalValues { + return nil + } + + for _, i := range t { + enumValue, err := field.Decode(ctx, f, i.Value) + if err != nil { + return fmt.Errorf("error decode option value: %w", err) + } + if value == enumValue { + return nil + } + } + + return errors.Errorf("value required to be one of %s", t) +} + +func (t enum) ValidateOption() error { + for _, i := range t { + if i.Name == "" { + return errors.Errorf("enum name is required") + } + } + return nil +} diff --git a/pkg/schema/validate/enum_test.go b/pkg/schema/validate/enum_test.go new file mode 100644 index 0000000000000000000000000000000000000000..a473dc015f80e92a2b0845228b02023f3f92b63a --- /dev/null +++ b/pkg/schema/validate/enum_test.go @@ -0,0 +1,69 @@ +package validate + +import ( + "testing" + + "git.perx.ru/perxis/perxis-go/pkg/schema/field" + "github.com/stretchr/testify/require" +) + +func TestEnum(t *testing.T) { + stringEnum := Enum( + EnumOpt{ + Name: "N 1", + Value: "n1", + }, + EnumOpt{ + Name: "N 2", + Value: "n2", + }, + ) + intEnum := Enum( + EnumOpt{ + Name: "N 1", + Value: 1, + }, + EnumOpt{ + Name: "N 2", + Value: 2, + }, + ) + floatEnum := Enum( + EnumOpt{ + Name: "N 1", + Value: 1.1, + }, + EnumOpt{ + Name: "N 2", + Value: int(2), + }, + ) + tests := []struct { + name string + field *field.Field + data interface{} + wantErr bool + }{ + {"Correct string", field.String(stringEnum), "n1", false}, + {"Correct int", field.Number(field.NumberFormatInt, intEnum), 1, false}, + {"Correct float", field.Number(field.NumberFormatFloat, floatEnum), 1.1, false}, + {"Correct float", field.Number(field.NumberFormatFloat, floatEnum), int(2), false}, + {"Incorrect string", field.String(stringEnum), "n3", true}, + {"Incorrect int", field.Number(field.NumberFormatInt, intEnum), 3, true}, + {"Incorrect float", field.Number(field.NumberFormatFloat, floatEnum), 1.0, true}, + {"Incorrect enum value", field.String(intEnum), "n1", true}, + {"Incorrect enum value", field.String(stringEnum).SetAdditionalValues(), "non enum value", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := field.Decode(nil, tt.field, tt.data) + require.NoError(t, err) + err = Validate(nil, tt.field, got) + if (err != nil) != tt.wantErr { + t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } +} diff --git a/pkg/schema/validate/number.go b/pkg/schema/validate/number.go new file mode 100644 index 0000000000000000000000000000000000000000..81ab150b21d0af3a5e6cc91dac664c3a479a9f11 --- /dev/null +++ b/pkg/schema/validate/number.go @@ -0,0 +1,136 @@ +package validate + +import ( + "context" + "fmt" + "math" + "reflect" + "strconv" + + "git.perx.ru/perxis/perxis-go/pkg/schema/field" +) + +type max float64 + +func Max(m float64) Validator { + v := max(m) + return &v +} + +func (t max) Validate(_ context.Context, field *field.Field, value interface{}) error { + var num float64 + switch v := value.(type) { + case int: + num = float64(v) + case int8: + num = float64(v) + case int32: + num = float64(v) + case int64: + num = float64(v) + case uint: + num = float64(v) + case uint8: + num = float64(v) + case uint32: + num = float64(v) + case uint64: + num = float64(v) + case float32: + num = float64(v) + case float64: + num = v + case nil: + return nil + default: + return fmt.Errorf("incorrect type: \"%s\", expected \"number\"", reflect.ValueOf(v).Kind()) + } + if num > float64(t) { + return fmt.Errorf("maximum number is %s, got %s", strconv.FormatFloat(float64(t), 'f', -1, 64), strconv.FormatFloat(float64(num), 'f', -1, 64)) + } + return nil + +} + +type min float64 + +func Min(m float64) Validator { + v := min(m) + return &v +} + +func (t min) Validate(_ context.Context, fld *field.Field, value interface{}) error { + var num float64 + switch v := value.(type) { + case int: + num = float64(v) + case int8: + num = float64(v) + case int32: + num = float64(v) + case int64: + num = float64(v) + case uint: + num = float64(v) + case uint8: + num = float64(v) + case uint32: + num = float64(v) + case uint64: + num = float64(v) + case float32: + num = float64(v) + case float64: + num = v + case nil: + return nil + default: + return fmt.Errorf("incorrect type: \"%s\", expected \"number\"", reflect.ValueOf(v).Kind()) + } + if num < float64(t) { + return fmt.Errorf("minimum number is %s, got %s", strconv.FormatFloat(float64(t), 'f', -1, 64), strconv.FormatFloat(float64(num), 'f', -1, 64)) + } + return nil +} + +type multipleOf float64 + +func MultipleOf(d float64) Validator { + v := multipleOf(d) + return &v +} + +func (t multipleOf) Validate(_ context.Context, field *field.Field, value interface{}) error { + var num float64 + switch v := value.(type) { + case int: + num = float64(v) + case int8: + num = float64(v) + case int32: + num = float64(v) + case int64: + num = float64(v) + case uint: + num = float64(v) + case uint8: + num = float64(v) + case uint32: + num = float64(v) + case uint64: + num = float64(v) + case float32: + num = float64(v) + case float64: + num = v + case nil: + return nil + default: + return fmt.Errorf("incorrect type: \"%s\", expected \"number\"", reflect.ValueOf(v).Kind()) + } + if math.Mod(num, float64(t)) != 0 { + return fmt.Errorf("number must be a multiple of %f", t) + + } + return nil +} diff --git a/pkg/schema/validate/number_test.go b/pkg/schema/validate/number_test.go new file mode 100644 index 0000000000000000000000000000000000000000..edbd218cf3b19ecd5b412e5a7e74015a8dfaecb5 --- /dev/null +++ b/pkg/schema/validate/number_test.go @@ -0,0 +1,99 @@ +package validate + +import ( + "testing" + + "git.perx.ru/perxis/perxis-go/pkg/schema/field" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNumber(t *testing.T) { + tests := []struct { + name string + field *field.Field + data interface{} + wantErr bool + }{ + {"Max Int", field.Number("int").AddOptions(Max(5.0)), 6, true}, + {"Min Int", field.Number("float").AddOptions(Min(10)), 9.0, true}, + + {"Max Float", field.Number("float").AddOptions(Max(5.5)), 5.6, true}, + {"Min Float", field.Number("float").AddOptions(Min(10.1)), 9.9, true}, + + {"MultipleOf Int", field.Number("float").AddOptions(MultipleOf(5)), 1.0, true}, + {"MultipleOf Int", field.Number("float").AddOptions(MultipleOf(5)), 10.0, false}, + + {"MultipleOf Float", field.Number("float").AddOptions(MultipleOf(0.01)), 0.2, true}, + {"MultipleOf Float", field.Number("float").AddOptions(MultipleOf(0.05)), 0.1, false}, + + {"MinMaxMultipleOf Int", field.Number("int").AddOptions(Max(5), Min(2), MultipleOf(5)), 6, true}, + {"MinMaxMultipleOf Int", field.Number("int").AddOptions(Max(5), Min(2), MultipleOf(2)), 4, false}, + {"MinMaxMultipleOf Int", field.Number("int").AddOptions(Max(5), Min(1), MultipleOf(2)), -2, true}, + {"MinMaxMultipleOf Int", field.Number("int").AddOptions(Max(5), Min(-1), MultipleOf(2)), 0, false}, + {"MinMaxMultipleOf Int", field.Number("int").AddOptions(Max(10), Min(7), MultipleOf(5)), 6, true}, + {"MinMaxMultipleOf Float", field.Number("int").AddOptions(Max(5.5), Min(2.3), MultipleOf(5)), 6, true}, + {"MinMaxMultipleOf Float", field.Number("float").AddOptions(Max(10.1), Min(7.7), MultipleOf(0.1)), 6.1, true}, + + {"Enum miss", field.Number("int").AddOptions(Enum(EnumOpt{Name: "N 1", Value: 1}, EnumOpt{Name: "N 2", Value: 2})), 3, true}, + {"Enum match", field.Number("int").AddOptions(Enum(EnumOpt{Name: "N 1", Value: 1}, EnumOpt{Name: "N 2", Value: 2})), 2, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := field.Decode(nil, tt.field, tt.data) + require.NoError(t, err) + err = Validate(nil, tt.field, got) + if (err != nil) != tt.wantErr { + t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } +} + +func TestNumberValidate(t *testing.T) { + tests := []struct { + name string + field *field.Field + data interface{} + wantErr bool + error string + }{ + {"Nil Max", field.Number("int").AddOptions(Max(1.0)), nil, false, ""}, + {"Nil Min", field.Number("int").AddOptions(Min(1.0)), nil, false, ""}, + {"Nil MultipleOf", field.Number("int").AddOptions(MultipleOf(1)), nil, false, ""}, + {"Int Max", field.Number("int").AddOptions(Max(1.0)), 1, false, ""}, + {"Int Min", field.Number("int").AddOptions(Min(1.0)), 1, false, ""}, + {"Int MultipleOf", field.Number("int").AddOptions(Min(1.0)), 1, false, ""}, + {"Flоat Max", field.Number("float").AddOptions(Max(1.0)), 1.0, false, ""}, + {"Flоat Min", field.Number("float").AddOptions(Min(1.0)), 1.0, false, ""}, + {"Flоat MultipleOf", field.Number("float").AddOptions(Min(1.0)), 1.0, false, ""}, + {"Bool Max Num", field.Number("int").AddOptions(Max(1.0)), true, true, "validation error: incorrect type: \"bool\", expected \"number\""}, + {"Bool Min Num", field.Number("int").AddOptions(Min(1.0)), true, true, "validation error: incorrect type: \"bool\", expected \"number\""}, + {"Bool MultipleOf Num", field.Number("int").AddOptions(MultipleOf(1)), true, true, "validation error: incorrect type: \"bool\", expected \"number\""}, + {"String Max Num", field.Number("int").AddOptions(Max(1.0)), "1", true, "validation error: incorrect type: \"string\", expected \"number\""}, + {"String Min Num", field.Number("int").AddOptions(Min(1.0)), "1", true, "validation error: incorrect type: \"string\", expected \"number\""}, + {"String MultipleOf Num", field.Number("int").AddOptions(MultipleOf(1)), "1", true, "validation error: incorrect type: \"string\", expected \"number\""}, + {"Array Max Num", field.Number("int").AddOptions(Max(1.0)), [1]int{1}, true, "validation error: incorrect type: \"array\", expected \"number\""}, + {"Array Min Num", field.Number("int").AddOptions(Min(1.0)), [1]int{1}, true, "validation error: incorrect type: \"array\", expected \"number\""}, + {"Slice Max Num", field.Number("int").AddOptions(Max(1.0)), []int{1}, true, "validation error: incorrect type: \"slice\", expected \"number\""}, + {"Slice Min Num", field.Number("int").AddOptions(Min(1.0)), []int{1}, true, "validation error: incorrect type: \"slice\", expected \"number\""}, + {"Slice MultipleOf Num", field.Number("int").AddOptions(MultipleOf(1)), []int{}, true, "validation error: incorrect type: \"slice\", expected \"number\""}, + {"Map Max Num", field.Number("int").AddOptions(Max(1.0)), map[int]int{}, true, "validation error: incorrect type: \"map\", expected \"number\""}, + {"Map Min Num", field.Number("int").AddOptions(Min(1.0)), map[int]int{}, true, "validation error: incorrect type: \"map\", expected \"number\""}, + {"Map MultipleOf Num", field.Number("int").AddOptions(MultipleOf(1)), map[int]int{}, true, "validation error: incorrect type: \"map\", expected \"number\""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := Validate(nil, tt.field, tt.data) + if tt.wantErr { + require.Error(t, err) + assert.EqualError(t, err, tt.error) + } + if !tt.wantErr { + require.NoError(t, err) + } + }) + } +} diff --git a/pkg/schema/validate/readonly.go b/pkg/schema/validate/readonly.go new file mode 100644 index 0000000000000000000000000000000000000000..8738d747dd1d86fad7203f20b9df83cb5d2334a4 --- /dev/null +++ b/pkg/schema/validate/readonly.go @@ -0,0 +1,30 @@ +package validate + +import ( + "context" + "errors" + + "git.perx.ru/perxis/perxis-go/pkg/schema/field" +) + +type readonly bool + +func ReadOnly() Validator { + r := readonly(true) + return &r +} + +func (r readonly) Validate(_ context.Context, f *field.Field, value interface{}) error { + if value != nil { + return errors.New("value is readonly") + } + return nil + +} + +func (r readonly) ValidateOption() error { + if !r { + return field.ErrSkipOption + } + return nil +} diff --git a/pkg/schema/validate/readonly_test.go b/pkg/schema/validate/readonly_test.go new file mode 100644 index 0000000000000000000000000000000000000000..ee8ca9ba89a16a02375556d365884bb35d378f83 --- /dev/null +++ b/pkg/schema/validate/readonly_test.go @@ -0,0 +1,32 @@ +package validate + +import ( + "testing" + + "git.perx.ru/perxis/perxis-go/pkg/schema/field" + "github.com/stretchr/testify/require" +) + +func TestReadonly(t *testing.T) { + tests := []struct { + name string + field *field.Field + data interface{} + wantErr bool + }{ + {"ReadOnly", field.String(ReadOnly()), nil, false}, + {"ReadOnly error", field.String(ReadOnly()), "1234567", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := field.Decode(nil, tt.field, tt.data) + require.NoError(t, err) + err = Validate(nil, tt.field, got) + if (err != nil) != tt.wantErr { + t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } +} diff --git a/pkg/schema/validate/required.go b/pkg/schema/validate/required.go new file mode 100644 index 0000000000000000000000000000000000000000..96591e6eaf4134f07523ab87f29e932b8fcee60e --- /dev/null +++ b/pkg/schema/validate/required.go @@ -0,0 +1,48 @@ +package validate + +import ( + "context" + "errors" + + "git.perx.ru/perxis/perxis-go/pkg/schema/field" +) + +type required bool + +func Required() Validator { + r := required(true) + return &r +} + +func (r required) Validate(_ context.Context, f *field.Field, value interface{}) error { + + if value != nil { + t, ok := f.GetType().(interface{ IsEmpty(interface{}) bool }) + if ok && !t.IsEmpty(value) { + return nil + } + + /* + tt, ok := f.GetType().(interface { + IsEmpty(interface{}) (bool, error) + }) + empty, err := tt.IsEmpty(value) + if err != nil { + return err + } + if ok && !empty { + return nil + } + */ + } + + return errors.New("value is required") + +} + +func (r required) ValidateOption() error { + if !r { + return field.ErrSkipOption + } + return nil +} diff --git a/pkg/schema/validate/required_test.go b/pkg/schema/validate/required_test.go new file mode 100644 index 0000000000000000000000000000000000000000..41a03ab50d6fab906e7f78229b560e4298475dde --- /dev/null +++ b/pkg/schema/validate/required_test.go @@ -0,0 +1,53 @@ +package validate + +import ( + "testing" + "time" + + "git.perx.ru/perxis/perxis-go/pkg/schema/field" + "github.com/stretchr/testify/require" +) + +func TestRequired(t *testing.T) { + tests := []struct { + name string + field *field.Field + data interface{} + wantErr bool + }{ + {"Array no value", field.Array(field.String(), Required()), nil, true}, + {"Array empty", field.Array(field.String(), Required()), []interface{}{}, true}, + {"Array not empty", field.Array(field.String(), Required()), []interface{}{"a", "b"}, false}, + {"Boolean no value", field.Bool(Required()), nil, true}, + {"Boolean", field.Bool(Required()), false, false}, + {"Location: no value", field.Location(Required()), nil, true}, + //{"Location: empty", field.Location(Required()), map[string]interface{}{}, true}, // не имеет ÑмыÑла, при Decode вернетÑÑ Ð¾ÑˆÐ¸Ð±ÐºÐ° еÑли объект пуÑтой + {"Location: not empty", field.Location(Required()), &field.GeoObject{Address: "addr"}, false}, + {"Number (int) no value", field.Number(field.NumberFormatInt, Required()), nil, true}, + {"Number (int) empty", field.Number(field.NumberFormatInt, Required()), 0, false}, + {"Number (int) not empty", field.Number(field.NumberFormatInt, Required()), 42, false}, + {"Number (float) no value", field.Number(field.NumberFormatFloat, Required()), nil, true}, + {"Number (float) empty", field.Number(field.NumberFormatFloat, Required()), 0.0, false}, + {"Number (float) not empty", field.Number(field.NumberFormatFloat, Required()), 42.2, false}, + {"Object no value", field.Object("a", field.String(), "b", field.Bool(), Required()), nil, true}, + {"Object no value", field.Object("a", field.String(), "b", field.Bool(), Required()), map[string]interface{}{}, true}, + {"Object not empty", field.Object("a", field.String(), "b", field.Bool(), Required()), map[string]interface{}{"b": true}, false}, + {"String no value", field.String(Required()), nil, true}, + {"String empty", field.String(Required()), "", true}, + {"String not empty", field.String(Required()), "1234567", false}, + {"Time no value", field.Time(Required()), nil, true}, + {"Time not empty", field.Time(Required()), time.Now().Format(time.RFC3339), false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := field.Decode(nil, tt.field, tt.data) + require.NoError(t, err) + err = Validate(nil, tt.field, got) + if (err != nil) != tt.wantErr { + t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } +} diff --git a/pkg/schema/validate/string.go b/pkg/schema/validate/string.go new file mode 100644 index 0000000000000000000000000000000000000000..f05ca540034e3cfab7e57c1aa324bcb343b97431 --- /dev/null +++ b/pkg/schema/validate/string.go @@ -0,0 +1,71 @@ +package validate + +import ( + "context" + "fmt" + "reflect" + "unicode/utf8" + + "git.perx.ru/perxis/perxis-go/pkg/schema/field" + "github.com/pkg/errors" +) + +type maxLength int + +func MaxLength(max int) Validator { + v := maxLength(max) + return &v +} + +func (t maxLength) Validate(_ context.Context, field *field.Field, value interface{}) error { + if s, ok := value.(string); ok { + n := utf8.RuneCountInString(s) + if t > 0 && n > int(t) { + return fmt.Errorf("maximum string length is %d, got \"%s\" (length=%d)", t, s, len(s)) + } + return nil + } + return fmt.Errorf("incorrect type: \"%s\", expected \"string\"", reflect.ValueOf(value).Kind()) +} + +type minLength int + +func MinLength(max int) Validator { + v := minLength(max) + return &v +} + +func (t minLength) Validate(_ context.Context, field *field.Field, value interface{}) error { + if s, ok := value.(string); ok { + n := utf8.RuneCountInString(s) + if n < int(t) { + return fmt.Errorf("minimum string length is %d, got \"%s\" (length=%d)", t, s, len(s)) + } + return nil + } + return fmt.Errorf("incorrect type: \"%s\", expected \"string\"", reflect.ValueOf(value).Kind().String()) +} + +type schema bool + +func Schema() Validator { + v := schema(true) + return &v +} + +func (t schema) Validate(_ context.Context, _ *field.Field, value interface{}) error { + if value == nil { + return nil + } + if s, ok := value.(string); ok { + if s == "" { + return nil + } + sch := field.Object() + if err := sch.UnmarshalJSON([]byte(s)); err != nil { + return errors.New("value is not valid schema") + } + return nil + } + return errors.Errorf("incorrect type: \"%s\", expected string", reflect.ValueOf(value).Kind()) +} diff --git a/pkg/schema/validate/string_test.go b/pkg/schema/validate/string_test.go new file mode 100644 index 0000000000000000000000000000000000000000..b67e8c8d4f5056c61d5483a536701777d716bead --- /dev/null +++ b/pkg/schema/validate/string_test.go @@ -0,0 +1,160 @@ +package validate + +import ( + "testing" + + "git.perx.ru/perxis/perxis-go/pkg/schema/field" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestString(t *testing.T) { + invalidOptionsSchema := `{ + "type": "object", + "params": { + "fields": { + "required": { + "options": { + "err": true + }, + "type": "string" + } + } + } +}` + requiredOptionsSchema := `{ + "type": "object", + "params": { + "fields": { + "enum": { + "options": { + "enum": [{ + "value": "one" + }, + { + "value": "two" + } + ] + }, + "type": "string" + } + } + } +}` + + validSchema := `{ + "type": "object", + "params": { + "fields": { + "required": { + "options": { + "required": true + }, + "type": "string" + }, + "readonly": { + "options": { + "readonly": true + }, + "type": "string" + }, + "enum": { + "options": { + "enum": [{ + "name": "One", + "value": "one" + }, + { + "name": "Two", + "value": "two" + } + ] + }, + "type": "string" + } + } + } +}` + + unknownFieldSchema := `{ + "type": "object", + "params": { + "fields": { + "string": {} + } + } +}` + + tests := []struct { + name string + field *field.Field + data interface{} + wantErr bool + }{ + {"Length Max", field.String().AddOptions(MaxLength(5)), "1234567", true}, + {"Length Min", field.String().AddOptions(MinLength(10)), "1234", true}, + {"Length MinMax", field.String().AddOptions(MaxLength(6), MinLength(2)), "1234567", true}, + {"Length MinMax", field.String().AddOptions(MaxLength(10), MinLength(7)), "123456", true}, + {"Enum miss", field.String().AddOptions(Enum(EnumOpt{Name: "N 1", Value: "n1"}, EnumOpt{Name: "N 2", Value: "n2"})), "n3", true}, + {"Enum match", field.String().AddOptions(Enum(EnumOpt{Name: "N 1", Value: "n1"}, EnumOpt{Name: "N 2", Value: "n2"})), "n2", false}, + {"Invalid Schema Options", field.String().AddOptions(Schema()), invalidOptionsSchema, true}, + {"Required Schema Options", field.String().AddOptions(Schema()), requiredOptionsSchema, true}, + {"Valid Schema", field.String().AddOptions(Schema()), validSchema, false}, + {"Invalid Schema#1", field.String().AddOptions(Schema()), "test", true}, + {"Unknown Field", field.String().AddOptions(Schema()), unknownFieldSchema, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := field.Decode(nil, tt.field, tt.data) + require.NoError(t, err) + err = Validate(nil, tt.field, got) + if tt.wantErr { + require.Error(t, err) + } + if !tt.wantErr { + require.NoError(t, err) + } + }) + } +} + +func TestStringValidate(t *testing.T) { + tests := []struct { + name string + field *field.Field + data interface{} + wantErr bool + error string + }{ + {"String Length Max", field.String().AddOptions(MaxLength(1)), "1", false, ""}, + {"String Length Min", field.String().AddOptions(MinLength(1)), "1", false, ""}, + {"Nil Length Max", field.String().AddOptions(MaxLength(1)), nil, true, "validation error: incorrect type: \"invalid\", expected \"string\""}, + {"Nil Length Min", field.String().AddOptions(MinLength(1)), nil, true, "validation error: incorrect type: \"invalid\", expected \"string\""}, + {"Int Length Max", field.String().AddOptions(MaxLength(1)), 1, true, "validation error: incorrect type: \"int\", expected \"string\""}, + {"Int Length Min", field.String().AddOptions(MinLength(1)), 1, true, "validation error: incorrect type: \"int\", expected \"string\""}, + {"Float Length Max", field.String().AddOptions(MaxLength(1)), 1.0, true, "validation error: incorrect type: \"float64\", expected \"string\""}, + {"Float Length Min", field.String().AddOptions(MinLength(1)), 1.0, true, "validation error: incorrect type: \"float64\", expected \"string\""}, + {"Bool Length Max", field.String().AddOptions(MaxLength(1)), true, true, "validation error: incorrect type: \"bool\", expected \"string\""}, + {"Bool Length Min", field.String().AddOptions(MinLength(1)), true, true, "validation error: incorrect type: \"bool\", expected \"string\""}, + {"Array Length Max", field.String().AddOptions(MaxLength(1)), [1]string{""}, true, "validation error: incorrect type: \"array\", expected \"string\""}, + {"Array Length Min", field.String().AddOptions(MinLength(1)), [1]string{""}, true, "validation error: incorrect type: \"array\", expected \"string\""}, + {"Slice Length Max", field.String().AddOptions(MaxLength(1)), []string{""}, true, "validation error: incorrect type: \"slice\", expected \"string\""}, + {"Slice Length Min", field.String().AddOptions(MinLength(1)), []string{""}, true, "validation error: incorrect type: \"slice\", expected \"string\""}, + {"Map Length Max", field.String().AddOptions(MaxLength(1)), map[string]string{"": ""}, true, "validation error: incorrect type: \"map\", expected \"string\""}, + {"Map Length Min", field.String().AddOptions(MinLength(1)), map[string]string{"": ""}, true, "validation error: incorrect type: \"map\", expected \"string\""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := Validate(nil, tt.field, tt.data) + if tt.wantErr { + require.Error(t, err) + assert.EqualError(t, err, tt.error) + } + if !tt.wantErr { + require.NoError(t, err) + } + }) + } +} diff --git a/pkg/schema/validate/validate.go b/pkg/schema/validate/validate.go new file mode 100644 index 0000000000000000000000000000000000000000..65ac4c400d19055deae67ddfcc5678775d0e3376 --- /dev/null +++ b/pkg/schema/validate/validate.go @@ -0,0 +1,99 @@ +package validate + +import ( + "context" + "sort" + + "git.perx.ru/perxis/perxis-go/pkg/errors" + "git.perx.ru/perxis/perxis-go/pkg/expr" + "git.perx.ru/perxis/perxis-go/pkg/schema/field" +) + +const ValidatorPriority = 2000 + +type Validator interface { + Validate(ctx context.Context, f *field.Field, v interface{}) error +} + +type Validators []Validator + +func (l Validators) Len() int { return len(l) } +func (l Validators) Less(i, j int) bool { + pi, pj := ValidatorPriority, ValidatorPriority + if o, ok := l[i].(field.PriorityOption); ok { + pi = o.GetPriority() + } + if o, ok := l[j].(field.PriorityOption); ok { + pj = o.GetPriority() + } + if pi == pj { + return field.GetOptionName(l[i]) < field.GetOptionName(l[j]) + } + return pi < pj +} +func (l Validators) Swap(i, j int) { l[i], l[j] = l[j], l[i] } + +func getValidators(f *field.Field) Validators { + var vs Validators + for _, o := range f.Options { + if v, ok := o.(Validator); ok { + vs = append(vs, v) + } + } + sort.Sort(vs) + return vs +} + +func validateOptions(ctx context.Context, f *field.Field, v interface{}) error { + var err error + validators := getValidators(f) + for _, i := range validators { + err = i.Validate(ctx, f, v) + if err != nil { + return err + } + } + return nil +} + +func Validate(ctx context.Context, w field.Walker, v interface{}) error { + if m, ok := v.(map[string]interface{}); ok { + ctx = expr.WithEnv(ctx, m) + } + + _, _, err := w.Walk(ctx, v, func(ctx context.Context, fld *field.Field, v interface{}) (res field.WalkFuncResult, err error) { + enabled, _ := fld.IsEnabled(ctx) + + res.Value = v // Значение не менÑетÑÑ + + if !enabled { + res.Stop = true + return + } + + if err = validateOptions(ctx, fld, v); err != nil { + return + } + + if validator, ok := fld.GetType().(Validator); ok { + err = validator.Validate(ctx, fld, v) + } + + return + }) + + return errors.Wrap(err, "validation error") +} + +func init() { + field.RegisterOption(minLength(0)) + field.RegisterOption(maxLength(0)) + field.RegisterOption(min(0)) + field.RegisterOption(max(0)) + field.RegisterOption(multipleOf(1)) + field.RegisterOption(enum{}) + field.RegisterOption(readonly(true)) + field.RegisterOption(required(true)) + field.RegisterOption(maxItems(0)) + field.RegisterOption(schema(true)) +} diff --git a/pkg/schema/walk/fn.go b/pkg/schema/walk/fn.go new file mode 100644 index 0000000000000000000000000000000000000000..c3eaf35c569c18ba8ead7c7ff48c08d03f2a9d90 --- /dev/null +++ b/pkg/schema/walk/fn.go @@ -0,0 +1,11 @@ +package walk + +func GenericMerge(c *WalkContext) (err error) { + return +} + +func KeepSrc(c *WalkContext) (err error) { + c.Dst = c.Src + c.Changed = true + return +} diff --git a/pkg/schema/walk/walk.go b/pkg/schema/walk/walk.go new file mode 100644 index 0000000000000000000000000000000000000000..4757742f82efcaffd5faba47eb076ed727783c75 --- /dev/null +++ b/pkg/schema/walk/walk.go @@ -0,0 +1,182 @@ +package walk + +import ( + "context" + "fmt" + "strings" + + "git.perx.ru/perxis/perxis-go/pkg/schema" + "git.perx.ru/perxis/perxis-go/pkg/schema/field" +) + +// DataFunc тип Ð´Ð»Ñ Ñ„ÑƒÐ½ÐºÑ†Ð¸Ð¸ обработки данных +type DataFunc func(c *WalkContext) error + +// FieldConfig опиÑывает какие дейÑÑ‚Ð²Ð¸Ñ Ñ Ð¿Ð¾Ð»ÐµÐ¼ нужно предпринÑть при обходе данных +type FieldConfig struct { + Fn DataFunc // ПользовательÑÐºÐ°Ñ Ñ„ÑƒÐ½ÐºÑ†Ð¸Ñ Ð¾Ð±Ñ€Ð°Ð±Ð¾Ñ‚ÐºÐ¸ данных Ð¿Ð¾Ð»Ñ +} + +// WalkConfig наÑтройки обхода данных +type WalkConfig struct { + // ÐаÑтройки Ð´Ð»Ñ Ð¿Ð¾Ð»ÐµÐ¹, в качеÑтве ключа указываетÑÑ Ð°Ð±Ñолютный путь Ð¿Ð¾Ð»Ñ + // Ðапример: "a.b.c.1" (чиÑла Ð´Ð»Ñ slice) + Fields map[string]FieldConfig +} + +// Walker позволÑет выполнÑть обход данных Ð´Ð»Ñ ÑоответÑтвующей Ñхемы +type Walker struct { + schema *schema.Schema + config *WalkConfig + DefaultFn DataFunc // Ð¤ÑƒÐ½ÐºÑ†Ð¸Ñ Ð¾Ð±Ñ€Ð°Ð±Ð¾Ñ‚ÐºÐ¸ данных применÑÐµÐ¼Ð°Ñ Ð¿Ð¾ умолчанию +} + +// NewWalker Ñоздает ÑкземплÑÑ€ +func NewWalker(schema *schema.Schema, config *WalkConfig) *Walker { + return &Walker{ + schema: schema, + config: config, + DefaultFn: GenericMerge, + } +} + +// WalkContext контекÑÑ‚ обхода данных +type WalkContext struct { + Ctx context.Context + Path string // Путь к родительÑким данным + Key interface{} // Ключ или Ð¸Ð½Ð´ÐµÐºÑ Ñ‚ÐµÐºÑƒÑ‰Ð¸Ñ… данных + Field *field.Field // ÐŸÐ¾Ð»Ñ Ñхемы ÑоответÑвующее текущим данным + Dst interface{} // Данные приемника + Src interface{} // Данные иÑточника + Changed bool // Флаг показывающий, что данные приемника изменилиÑÑŒ +} + +// GetPath возвращает путь ÑоответÑвующий текущему контекÑту +func (w WalkContext) GetPath(keys ...interface{}) string { + p := make([]string, 0, 10) + + if w.Path != "" { + p = append(p, w.Path) + } + + if w.Key != nil { + p = append(p, fmt.Sprintf("%v", w.Key)) + } + + for _, k := range keys { + p = append(p, fmt.Sprintf("%v", k)) + } + + return strings.Join(p, ".") +} + +// Clone Ñоздает копию контекÑта +func (w WalkContext) Clone() *WalkContext { + return &w +} + +// DataWalk выполнÑет обход данных и возвращает измененные данные +func (m *Walker) DataWalk(ctx context.Context, dst, src interface{}) (res interface{}, changed bool, err error) { + wc := &WalkContext{ + Ctx: ctx, + Field: &m.schema.Field, + Dst: dst, + Src: src, + } + + err = m.datawalk(wc) + + return wc.Dst, wc.Changed, err +} + +func (m *Walker) datawalk(w *WalkContext) (err error) { + path := w.GetPath() + fn := m.DefaultFn + + fieldCfg, _ := m.config.Fields[path] + + if fieldCfg.Fn != nil { + fn = fieldCfg.Fn + } + + if err = fn(w); err != nil { + return + } + + switch p := w.Field.Params.(type) { + + case *field.ObjectParameters: + d, _ := w.Dst.(map[string]interface{}) + s, _ := w.Src.(map[string]interface{}) + res := make(map[string]interface{}) + + keys := make(map[string]struct{}) + for k := range d { + keys[k] = struct{}{} + } + for k := range s { + keys[k] = struct{}{} + } + + fields := p.GetFields(true) + + for k := range keys { + f, ok := fields[k] + if !ok { + continue + } + + wc := WalkContext{ + Ctx: w.Ctx, + Path: w.GetPath(), + Key: k, + Field: f, + Dst: d[k], + Src: s[k], + } + + if err = m.datawalk(&wc); err != nil { + return + } + + if wc.Dst != nil { + res[k] = wc.Dst + } + + if wc.Changed { + w.Changed = true + } + } + if len(res) > 0 { + w.Dst = res + } + + case *field.ArrayParameters: + d, _ := w.Dst.([]interface{}) + s, _ := w.Src.([]interface{}) + for i, v := range d { + var src_v interface{} + if i < len(s) { + src_v = s[i] + } + wc := WalkContext{ + Ctx: w.Ctx, + Path: w.GetPath(), + Key: i, + Field: p.Item, + Dst: v, + Src: src_v, + } + if err = m.datawalk(&wc); err != nil { + return + } + if wc.Changed { + d[i] = wc.Dst + w.Changed = true + } + } + + } + + return +} diff --git a/pkg/schema/walk/walk_test.go b/pkg/schema/walk/walk_test.go new file mode 100644 index 0000000000000000000000000000000000000000..e20adb462735e83c92c8b04f5c159e336e49f2b0 --- /dev/null +++ b/pkg/schema/walk/walk_test.go @@ -0,0 +1,115 @@ +package walk + +import ( + "context" + "testing" + + "git.perx.ru/perxis/perxis-go/pkg/schema" + "git.perx.ru/perxis/perxis-go/pkg/schema/field" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestWalker_DataWalk(t *testing.T) { + s := schema.New( + "a", field.String(), + "b", field.String(), + "obj1", field.Object( + "a", field.String(), + "b", field.String(), + "obj2", field.Object( + "a", field.String(), + "b", field.String(), + ), + "obj3", field.Object( + "e", field.String(), + ), + ), + "slice", field.Array(field.String()), + "inline_outer_str", field.Object( + true, + "inline_str_1", field.String(), + "inline_obj", field.Object( + true, + "inline_str_2", field.String(), + ), + ), + ) + + tests := []struct { + name string + config *WalkConfig + src map[string]interface{} + dst map[string]interface{} + res map[string]interface{} + wantChanged bool + wantErr bool + }{ + {"generic", + &WalkConfig{ + Fields: map[string]FieldConfig{ + "obj1.a": {Fn: KeepSrc}, + "slice.1": {Fn: KeepSrc}, + "inline_str_1": {Fn: KeepSrc}, + "inline_str_2": {Fn: KeepSrc}, + }, + }, + map[string]interface{}{ + "a": "src_a", + "b": "src_b", + "obj1": map[string]interface{}{ + "a": "src_obj1_a", + "b": "src_obj1_b", + "obj2": map[string]interface{}{ + "a": "dst_obj1_obj2_a", + }, + "obj3": map[string]interface{}{ + "e": "dst_obj1_obj3_e", + }, + }, + "inline_str_1": "src_inline_1", + "inline_str_2": "src_inline_2", + "slice": []interface{}{"src_s1", "src_s2"}, + }, + map[string]interface{}{ + "a": "dst_a", + "field_not_extists": "remove", + "obj1": map[string]interface{}{ + "a": "dst_obj1_a", + "obj2": map[string]interface{}{ + "a": "dst_obj1_obj2_a", + }, + }, + "inline_str_1": "dst_inline_1", + "inline_str_2": "dst_inline_2", + "slice": []interface{}{"dst_s1", "dst_s2", "dst_s3"}, + }, + map[string]interface{}{ + "a": "dst_a", + "obj1": map[string]interface{}{ + "a": "src_obj1_a", + "obj2": map[string]interface{}{ + "a": "dst_obj1_obj2_a", + }, + }, + "inline_str_1": "src_inline_1", + "inline_str_2": "src_inline_2", + "slice": []interface{}{"dst_s1", "src_s2", "dst_s3"}, + }, + false, false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := NewWalker(s, tt.config) + dst := tt.dst + res, _, err := m.DataWalk(context.Background(), dst, tt.src) + assert.Equal(t, tt.res, res) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +}