diff --git a/go.mod b/go.mod index 8cd52d3b3533422183a392bd755566f2090087d5..c1c371718e1900db44be42b040fd4d19424ce424 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,9 @@ module git.perx.ru/perxis/perxis-go go 1.21 require ( - github.com/antonmedv/expr v1.9.0 github.com/avast/retry-go/v4 v4.5.1 github.com/bep/gowebp v0.2.0 + github.com/expr-lang/expr v1.15.8 github.com/go-kit/kit v0.13.0 github.com/gosimple/slug v1.13.1 github.com/hashicorp/go-multierror v1.1.1 diff --git a/go.sum b/go.sum index fcb538d7446ef905cc38573b2c88d2496b342ac9..4fa995ca097f0a961d0c4fc9d10cbe640e972c00 100644 --- a/go.sum +++ b/go.sum @@ -2,9 +2,6 @@ cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiV cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= -github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= -github.com/antonmedv/expr v1.9.0 h1:j4HI3NHEdgDnN9p6oI6Ndr0G5QryMY0FNxT4ONrFDGU= -github.com/antonmedv/expr v1.9.0/go.mod h1:5qsM3oLGDND7sDmQGDXHkYfkjYMUX14qsgqmHhwGEk8= github.com/avast/retry-go/v4 v4.5.1 h1:AxIx0HGi4VZ3I02jr78j5lZ3M6x1E0Ivxa6b0pUUh7o= github.com/avast/retry-go/v4 v4.5.1/go.mod h1:/sipNsvNB3RRuT5iNcb6h73nw3IBmXJ/H3XrCQYSOpc= github.com/bep/gowebp v0.2.0 h1:ZVfK8i9PpZqKHEmthQSt3qCnnHycbLzBPEsVtk2ch2Q= @@ -12,12 +9,11 @@ github.com/bep/gowebp v0.2.0/go.mod h1:ZhFodwdiFp8ehGJpF4LdPl6unxZm9lLFjxD3z2h2A github.com/brianvoe/gofakeit/v6 v6.26.3 h1:3ljYrjPwsUNAUFdUIr2jVg5EhKdcke/ZLop7uVg1Er8= github.com/brianvoe/gofakeit/v6 v6.26.3/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= -github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM= +github.com/expr-lang/expr v1.15.8 h1:FL8+d3rSSP4tmK9o+vKfSMqqpGL8n15pEPiHcnBpxoI= +github.com/expr-lang/expr v1.15.8/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ= github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU= github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg= github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= @@ -64,10 +60,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s= -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.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -87,25 +79,19 @@ github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OS github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rivo/tview v0.0.0-20200219210816-cd38d7432498/go.mod h1:6lkG1x+13OShEf0EaOCaTQYyB7d5nSbb181KtjlS+84= -github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/sanity-io/litter v1.2.0/go.mod h1:JF6pZUFgu2Q0sBZ+HSV35P8TVPI1TTzEwyu9FXAw2W4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0= github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0= -github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= @@ -160,8 +146,6 @@ golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -172,7 +156,6 @@ golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -198,7 +181,6 @@ google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/id/bson.go b/id/bson.go new file mode 100644 index 0000000000000000000000000000000000000000..9d44c5a8074361b381524dd7c99ad77906cfc68c --- /dev/null +++ b/id/bson.go @@ -0,0 +1,32 @@ +package id + +import ( + "git.perx.ru/perxis/perxis-go/pkg/errors" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/bsonrw" + "go.mongodb.org/mongo-driver/bson/bsontype" +) + +func (id *ID) MarshalBSONValue() (bsontype.Type, []byte, error) { + return bson.MarshalValue(id.String()) +} + +func (id *ID) UnmarshalBSONValue(btype bsontype.Type, data []byte) error { + if btype != bson.TypeString { + return errors.New("cannot unmarshal non-string bson value to ID") + } + dec, err := bson.NewDecoder(bsonrw.NewBSONValueReader(btype, data)) + if err != nil { + return err + } + var str string + if err = dec.Decode(&str); err != nil { + return err + } + t, err := Parse(str) + if err != nil { + return err + } + *id = *t + return nil +} diff --git a/id/bson_test.go b/id/bson_test.go new file mode 100644 index 0000000000000000000000000000000000000000..c8080b5d7ae12181343a8f6502e76484c414e578 --- /dev/null +++ b/id/bson_test.go @@ -0,0 +1,107 @@ +package id + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/bson" +) + +func TestID_MarshalUnmarshalBSON(t *testing.T) { + tests := []struct { + name string + id *ID + }{ + { + name: "OrganizationID", + id: &ID{Descriptor: &OrganizationID{OrganizationID: "1"}}, + }, + { + name: "UserID", + id: &ID{Descriptor: &UserID{UserID: "1"}}, + }, + { + name: "ServiceID", + id: &ID{Descriptor: &ServiceID{ServiceID: "1"}}, + }, + { + name: "SpaceID", + id: &ID{Descriptor: &SpaceID{SpaceID: "1"}}, + }, + { + name: "EnvironmentID", + id: &ID{Descriptor: &EnvironmentID{EnvironmentID: "1", SpaceID: SpaceID{SpaceID: "1"}}}, + }, + { + name: "ClientID", + id: &ID{Descriptor: &ClientID{ClientID: "1", SpaceID: SpaceID{SpaceID: "1"}}}, + }, + { + name: "RoleID", + id: &ID{Descriptor: &RoleID{RoleID: "1", SpaceID: SpaceID{SpaceID: "1"}}}, + }, + { + name: "CollectionID", + id: &ID{Descriptor: &CollectionID{CollectionID: "1", EnvironmentID: EnvironmentID{EnvironmentID: "1", SpaceID: SpaceID{SpaceID: "1"}}}}, + }, + { + name: "SchemaID", + id: &ID{Descriptor: &SchemaID{CollectionID: "1", EnvironmentID: EnvironmentID{EnvironmentID: "1", SpaceID: SpaceID{SpaceID: "1"}}}}, + }, + { + name: "ItemID", + id: &ID{Descriptor: &ItemID{ItemID: "1", CollectionID: CollectionID{CollectionID: "1", EnvironmentID: EnvironmentID{EnvironmentID: "1", SpaceID: SpaceID{SpaceID: "1"}}}}}, + }, + { + name: "RevisionID", + id: &ID{Descriptor: &RevisionID{RevisionID: "1", ItemID: ItemID{ItemID: "1", CollectionID: CollectionID{CollectionID: "1", EnvironmentID: EnvironmentID{EnvironmentID: "1", SpaceID: SpaceID{SpaceID: "1"}}}}}}, + }, + { + name: "FieldID", + id: &ID{Descriptor: &FieldID{FieldName: "1", ItemID: ItemID{ItemID: "1", CollectionID: CollectionID{CollectionID: "1", EnvironmentID: EnvironmentID{EnvironmentID: "1", SpaceID: SpaceID{SpaceID: "1"}}}}}}, + }, + { + name: "SystemID", + id: &ID{Descriptor: &SystemID{}}, + }, + } + type test struct { + Text string + ID *ID + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &test{Text: tt.name, ID: tt.id} + + b, err := bson.Marshal(s) + require.NoError(t, err) + + var v *test + require.NoError(t, bson.Unmarshal(b, &v)) + assert.Equal(t, s, v, "после Unmarshal объект должен быть идентичен исходному") + }) + } +} + +func TestID_ExampleBSON(t *testing.T) { + type data struct { + ID *ID + Text string + Number int + } + + test := &data{ + ID: &ID{Descriptor: &SpaceID{SpaceID: Space}}, + Text: "text", + Number: 1, + } + + b, err := bson.Marshal(test) + require.NoError(t, err) + + buf := new(data) + err = bson.Unmarshal(b, &buf) + require.NoError(t, err) + assert.Equal(t, test, buf, "после Unmarshal объект должен совпадать с исходным") +} diff --git a/id/client.go b/id/client.go new file mode 100644 index 0000000000000000000000000000000000000000..db941c5ad54b539551193004674ac15f15d314be --- /dev/null +++ b/id/client.go @@ -0,0 +1,61 @@ +package id + +const ( + Client = "client" + ClientsPrefix = "clients" +) + +type ClientID struct { + SpaceID + ClientID string `json:"client_id,omitempty" bson:"client_id,omitempty"` +} + +func (t *ClientID) Type() string { return Client } + +func (t *ClientID) String() string { + return Join(t.SpaceID.String(), ClientsPrefix, t.ClientID) + +} + +func (t *ClientID) ToMap() map[string]any { + m := t.SpaceID.ToMap() + m["client_id"] = t.ClientID + m["type"] = Client + return m +} + +func (t *ClientID) FromMap(m map[string]any) error { + if err := t.SpaceID.FromMap(m); err != nil { + return err + } + t.ClientID = m["client_id"].(string) + return nil +} + +func (t *ClientID) Validate() error { + if t.ClientID == "" { + return ErrInvalidID + } + + return t.SpaceID.Validate() +} + +func parseClientID(parts []string) (*ClientID, error) { + if len(parts) != 4 || parts[2] != ClientsPrefix { + return nil, ErrInvalidID + } + + spaceID, err := parseSpaceID(parts[:2]) + if err != nil { + return nil, err + } + + var id ClientID + id.SpaceID = *spaceID + id.ClientID = parts[3] + return &id, nil +} + +func NewClientID(spaceID, id string) *ID { + return &ID{Descriptor: &ClientID{SpaceID: SpaceID{SpaceID: spaceID}, ClientID: id}} +} diff --git a/id/collection.go b/id/collection.go new file mode 100644 index 0000000000000000000000000000000000000000..e0f37585b41e77b58530cc50f249283beb6d8eca --- /dev/null +++ b/id/collection.go @@ -0,0 +1,60 @@ +package id + +const ( + Collection = "collection" + CollectionsPrefix = "cols" +) + +type CollectionID struct { + EnvironmentID + CollectionID string `json:"col_id,omitempty" bson:"col_id, omitempty"` +} + +func (t *CollectionID) Type() string { return Collection } + +func (t *CollectionID) String() string { + return Join(t.EnvironmentID.String(), CollectionsPrefix, t.CollectionID) +} + +func (t *CollectionID) ToMap() map[string]any { + m := t.EnvironmentID.ToMap() + m["col_id"] = t.CollectionID + m["type"] = Collection + return m +} + +func (t *CollectionID) FromMap(m map[string]any) error { + if err := t.EnvironmentID.FromMap(m); err != nil { + return err + } + t.CollectionID = m["col_id"].(string) + return nil +} + +func (t *CollectionID) Validate() error { + if t.CollectionID == "" { + return ErrInvalidID + } + + return t.EnvironmentID.Validate() +} + +func parseCollectionID(parts []string) (*CollectionID, error) { + if len(parts) != 6 || parts[4] != CollectionsPrefix { + return nil, ErrInvalidID + } + + envID, err := parseEnvironmentID(parts[:4]) + if err != nil { + return nil, err + } + + var id CollectionID + id.CollectionID = parts[5] + id.EnvironmentID = *envID + return &id, nil +} + +func NewCollectionID(spaceID, envID, id string) *ID { + return &ID{Descriptor: &CollectionID{EnvironmentID: EnvironmentID{SpaceID: SpaceID{SpaceID: spaceID}, EnvironmentID: envID}, CollectionID: id}} +} diff --git a/id/environment.go b/id/environment.go new file mode 100644 index 0000000000000000000000000000000000000000..d42df3e658bc04ae70989d199257a7836bd30f00 --- /dev/null +++ b/id/environment.go @@ -0,0 +1,61 @@ +package id + +const ( + Environment = "environment" + EnvironmentsPrefix = "envs" +) + +type EnvironmentID struct { + SpaceID + EnvironmentID string `json:"env_id,omitempty" bson:"env_id,omitempty"` +} + +func (t *EnvironmentID) Type() string { return Environment } + +func (t *EnvironmentID) String() string { + return Join(t.SpaceID.String(), EnvironmentsPrefix, t.EnvironmentID) + +} + +func (t *EnvironmentID) ToMap() map[string]any { + m := t.SpaceID.ToMap() + m["env_id"] = t.EnvironmentID + m["type"] = Environment + return m +} + +func (t *EnvironmentID) FromMap(m map[string]any) error { + if err := t.SpaceID.FromMap(m); err != nil { + return err + } + t.EnvironmentID = m["env_id"].(string) + return nil +} + +func (t *EnvironmentID) Validate() error { + if t.EnvironmentID == "" { + return ErrInvalidID + } + + return t.SpaceID.Validate() +} + +func parseEnvironmentID(parts []string) (*EnvironmentID, error) { + if len(parts) != 4 || parts[2] != EnvironmentsPrefix { + return nil, ErrInvalidID + } + + spaceID, err := parseSpaceID(parts[:2]) + if err != nil { + return nil, err + } + + var id EnvironmentID + id.EnvironmentID = parts[3] + id.SpaceID = *spaceID + return &id, nil +} + +func NewEnvironmentID(spaceID, id string) *ID { + return &ID{Descriptor: &EnvironmentID{SpaceID: SpaceID{SpaceID: spaceID}, EnvironmentID: id}} +} diff --git a/id/field.go b/id/field.go new file mode 100644 index 0000000000000000000000000000000000000000..ca1577552dc7df38d555549ddfc53a51e70e63f1 --- /dev/null +++ b/id/field.go @@ -0,0 +1,60 @@ +package id + +const ( + Field = "field" + FieldsPrefix = "fields" +) + +type FieldID struct { + ItemID + FieldName string `json:"field_name,omitempty" bson:"field_name,omitempty"` +} + +func (t *FieldID) Type() string { return Field } + +func (t *FieldID) String() string { + return Join(t.ItemID.String(), FieldsPrefix, t.FieldName) + +} + +func (t *FieldID) ToMap() map[string]any { + m := t.ItemID.ToMap() + m["field_name"] = t.FieldName + m["type"] = Field + return m +} + +func (t *FieldID) FromMap(m map[string]any) error { + if err := t.ItemID.FromMap(m); err != nil { + return err + } + t.FieldName = m["field_name"].(string) + return nil +} + +func (t *FieldID) Validate() error { + if t.FieldName == "" { + return ErrInvalidID + } + + return t.ItemID.Validate() +} + +func parseFieldID(parts []string) (*FieldID, error) { + if len(parts) != 10 || parts[8] != FieldsPrefix { + return nil, ErrInvalidID + } + + itemID, err := parseItemID(parts[:8]) + if err != nil { + return nil, err + } + + var id FieldID + id.ItemID = *itemID + id.FieldName = parts[9] + return &id, nil +} +func NewFieldID(spaceID, envID, collID, itemID, id string) *ID { + return &ID{Descriptor: &FieldID{ItemID: ItemID{CollectionID: CollectionID{EnvironmentID: EnvironmentID{SpaceID: SpaceID{SpaceID: spaceID}, EnvironmentID: envID}, CollectionID: collID}, ItemID: itemID}, FieldName: id}} +} diff --git a/id/id.go b/id/id.go new file mode 100644 index 0000000000000000000000000000000000000000..ca75c29f94b6fe9100b435e8f546835991bc9fa3 --- /dev/null +++ b/id/id.go @@ -0,0 +1,143 @@ +package id + +import ( + "strings" + + "git.perx.ru/perxis/perxis-go/pkg/errors" +) + +const Separator = '/' + +var ( + ErrInvalidID = errors.New("invalid id") +) + +type Descriptor interface { + String() string + Type() string + ToMap() map[string]any + FromMap(map[string]any) error + Validate() error +} + +type ID struct { + Descriptor +} + +func Parse(s string) (*ID, error) { + parts := Split(s) + + if id, _ := parseServiceID(parts); id != nil { + return &ID{Descriptor: id}, nil + } + + if id, _ := parseUserID(parts); id != nil { + return &ID{Descriptor: id}, nil + } + + if id, _ := parseOrganizationID(parts); id != nil { + return &ID{Descriptor: id}, nil + } + + if id, _ := parseSpaceID(parts); id != nil { + return &ID{Descriptor: id}, nil + } + + if id, _ := parseEnvironmentID(parts); id != nil { + return &ID{Descriptor: id}, nil + } + + if id, _ := parseClientID(parts); id != nil { + return &ID{Descriptor: id}, nil + } + + if id, _ := parseRoleID(parts); id != nil { + return &ID{Descriptor: id}, nil + } + + if id, _ := parseCollectionID(parts); id != nil { + return &ID{Descriptor: id}, nil + } + + if id, _ := parseSchemaID(parts); id != nil { + return &ID{Descriptor: id}, nil + } + + if id, _ := parseItemID(parts); id != nil { + return &ID{Descriptor: id}, nil + } + + if id, _ := parseRevisionID(parts); id != nil { + return &ID{Descriptor: id}, nil + } + + if id, _ := parseFieldID(parts); id != nil { + return &ID{Descriptor: id}, nil + } + + if id, _ := parseSystemID(parts); id != nil { + return &ID{Descriptor: id}, nil + } + + return nil, ErrInvalidID +} + +func Split(id string) []string { + if id[0] != Separator { + return nil + } + return strings.Split(id[1:], string(Separator)) +} + +func Join(parts ...string) string { + s := strings.Join(parts, string(Separator)) + if s[0] != Separator { + s = string(Separator) + s + } + return s +} + +func FromMap(m map[string]any) (*ID, error) { + if m == nil { + return nil, errors.New("nil map") + } + + v := new(ID) + + switch m["type"] { + case Organization: + v.Descriptor = new(OrganizationID) + case Service: + v.Descriptor = new(ServiceID) + case User: + v.Descriptor = new(UserID) + case Space: + v.Descriptor = new(SpaceID) + case Environment: + v.Descriptor = new(EnvironmentID) + case Client: + v.Descriptor = new(ClientID) + case Role: + v.Descriptor = new(RoleID) + case Collection: + v.Descriptor = new(CollectionID) + case Schema: + v.Descriptor = new(SchemaID) + case Item: + v.Descriptor = new(ItemID) + case Revision: + v.Descriptor = new(RevisionID) + case Field: + v.Descriptor = new(FieldID) + case System: + v.Descriptor = new(SystemID) + default: + return nil, errors.New("unknown type") + } + + if err := v.Descriptor.FromMap(m); err != nil { + return nil, err + } + + return v, nil +} diff --git a/id/id_test.go b/id/id_test.go new file mode 100644 index 0000000000000000000000000000000000000000..041f1cdc6018e6c84c0c4c6a4be4b3944ef34082 --- /dev/null +++ b/id/id_test.go @@ -0,0 +1,301 @@ +package id + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_ParseID(t *testing.T) { + tests := []struct { + name string + id string + result *ID + wantError bool + }{ + { + name: "ServiceID", + id: "/services/<service_id>", + result: &ID{Descriptor: &ServiceID{ServiceID: "<service_id>"}}, + }, + { + name: "UserID", + id: "/users/<user_id>", + result: &ID{Descriptor: &UserID{UserID: "<user_id>"}}, + }, + { + name: "OrganizationID", + id: "/orgs/<org_id>", + result: &ID{Descriptor: &OrganizationID{OrganizationID: "<org_id>"}}, + }, + { + name: "SpaceID", + id: "/spaces/<space_id>", + result: &ID{Descriptor: &SpaceID{SpaceID: "<space_id>"}}, + }, + { + name: "ClientID", + id: "/spaces/<space_id>/clients/<client_id>", + result: &ID{Descriptor: &ClientID{ + SpaceID: SpaceID{SpaceID: "<space_id>"}, + ClientID: "<client_id>", + }}, + }, + { + name: "RoleID", + id: "/spaces/<space_id>/roles/<role_id>", + result: &ID{Descriptor: &RoleID{ + SpaceID: SpaceID{SpaceID: "<space_id>"}, + RoleID: "<role_id>", + }}, + }, + { + name: "EnvironmentID", + id: "/spaces/<space_id>/envs/<env_id>", + result: &ID{Descriptor: &EnvironmentID{ + SpaceID: SpaceID{SpaceID: "<space_id>"}, + EnvironmentID: "<env_id>", + }}, + }, + { + name: "CollectionID", + id: "/spaces/<space_id>/envs/<env_id>/cols/<collection_id>", + result: &ID{Descriptor: &CollectionID{ + EnvironmentID: EnvironmentID{ + SpaceID: SpaceID{SpaceID: "<space_id>"}, + EnvironmentID: "<env_id>", + }, + CollectionID: "<collection_id>", + }}, + }, + { + name: "SchemaID", + id: "/spaces/<space_id>/envs/<env_id>/schema/<collection_id>", + result: &ID{Descriptor: &SchemaID{ + EnvironmentID: EnvironmentID{ + SpaceID: SpaceID{SpaceID: "<space_id>"}, + EnvironmentID: "<env_id>", + }, + CollectionID: "<collection_id>", + }}, + }, + { + name: "ItemID", + id: "/spaces/<space_id>/envs/<env_id>/cols/<collection_id>/items/<item_id>", + result: &ID{Descriptor: &ItemID{ + CollectionID: CollectionID{ + EnvironmentID: EnvironmentID{ + SpaceID: SpaceID{SpaceID: "<space_id>"}, + EnvironmentID: "<env_id>", + }, + CollectionID: "<collection_id>", + }, + ItemID: "<item_id>", + }}, + }, + { + name: "RevisionID", + id: "/spaces/<space_id>/envs/<env_id>/cols/<collection_id>/items/<item_id>/revs/<rev_id>", + result: &ID{Descriptor: &RevisionID{ + ItemID: ItemID{ + CollectionID: CollectionID{ + EnvironmentID: EnvironmentID{ + SpaceID: SpaceID{SpaceID: "<space_id>"}, + EnvironmentID: "<env_id>", + }, + CollectionID: "<collection_id>", + }, + ItemID: "<item_id>", + }, + RevisionID: "<rev_id>", + }}, + }, + { + name: "FieldID", + id: "/spaces/<space_id>/envs/<env_id>/cols/<collection_id>/items/<item_id>/fields/<field_name>", + result: &ID{Descriptor: &FieldID{ + ItemID: ItemID{ + CollectionID: CollectionID{ + EnvironmentID: EnvironmentID{ + SpaceID: SpaceID{SpaceID: "<space_id>"}, + EnvironmentID: "<env_id>", + }, + CollectionID: "<collection_id>", + }, + ItemID: "<item_id>", + }, + FieldName: "<field_name>", + }}, + }, + { + name: "SystemID", + id: "/system", + result: &ID{Descriptor: &SystemID{}}, + }, + { + name: "With error #1: no backslash in the beginning of id", + id: "spaces/<space_id>", + result: nil, + wantError: true, + }, + { + name: "With error #2: backslash in the end of id", + id: "/spaces/<space_id>/", + result: nil, + wantError: true, + }, + { + name: "With error #3: typo in 'spaces'", + id: "/space/<space_id>", + result: nil, + wantError: true, + }, + { + name: "With error #4: no space_id in id", + id: "/spaces", + result: nil, + wantError: true, + }, + { + name: "With error #5: multiple backslashes in the end of id", + id: "/spaces/<space_id>///", + result: nil, + wantError: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + id, err := Parse(tt.id) + if tt.wantError { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, tt.result, id) + require.Equal(t, tt.id, id.String(), "проверяем корректность работы метода String, полученное ID должно совпадать с исходным") + }) + } +} + +func Test_Map(t *testing.T) { + tests := []struct { + name string + id *ID + }{ + { + name: "ServiceID", + id: &ID{Descriptor: &ServiceID{ServiceID: "<service_id>"}}, + }, + { + name: "UserID", + id: &ID{Descriptor: &UserID{UserID: "<user_id>"}}, + }, + { + name: "OrganizationID", + id: &ID{Descriptor: &OrganizationID{OrganizationID: "<org_id>"}}, + }, + { + name: "SpaceID", + id: &ID{Descriptor: &SpaceID{SpaceID: "<space_id>"}}, + }, + { + name: "ClientID", + id: &ID{Descriptor: &ClientID{ + SpaceID: SpaceID{SpaceID: "<space_id>"}, + ClientID: "<client_id>", + }}, + }, + { + name: "RoleID", + id: &ID{Descriptor: &RoleID{ + SpaceID: SpaceID{SpaceID: "<space_id>"}, + RoleID: "<role_id>", + }}, + }, + { + name: "EnvironmentID", + id: &ID{Descriptor: &EnvironmentID{ + SpaceID: SpaceID{SpaceID: "<space_id>"}, + EnvironmentID: "<env_id>", + }}, + }, + { + name: "CollectionID", + id: &ID{Descriptor: &CollectionID{ + EnvironmentID: EnvironmentID{ + SpaceID: SpaceID{SpaceID: "<space_id>"}, + EnvironmentID: "<env_id>", + }, + CollectionID: "<collection_id>", + }}, + }, + { + name: "Schema ID", + id: &ID{Descriptor: &SchemaID{ + EnvironmentID: EnvironmentID{ + SpaceID: SpaceID{SpaceID: "<space_id>"}, + EnvironmentID: "<env_id>", + }, + CollectionID: "<collection_id>", + }}, + }, + { + name: "ItemID", + id: &ID{Descriptor: &ItemID{ + CollectionID: CollectionID{ + EnvironmentID: EnvironmentID{ + SpaceID: SpaceID{SpaceID: "<space_id>"}, + EnvironmentID: "<env_id>", + }, + CollectionID: "<collection_id>", + }, + ItemID: "<item_id>", + }}, + }, + { + name: "RevisionID", + id: &ID{Descriptor: &RevisionID{ + ItemID: ItemID{ + CollectionID: CollectionID{ + EnvironmentID: EnvironmentID{ + SpaceID: SpaceID{SpaceID: "<space_id>"}, + EnvironmentID: "<env_id>", + }, + CollectionID: "<collection_id>", + }, + ItemID: "<item_id>", + }, + RevisionID: "<rev_id>", + }}, + }, + { + name: "FieldID", + id: &ID{Descriptor: &FieldID{ + ItemID: ItemID{ + CollectionID: CollectionID{ + EnvironmentID: EnvironmentID{ + SpaceID: SpaceID{SpaceID: "<space_id>"}, + EnvironmentID: "<env_id>", + }, + CollectionID: "<collection_id>", + }, + ItemID: "<item_id>", + }, + FieldName: "<field_name>", + }}, + }, + { + name: "SystemID", + id: &ID{Descriptor: &SystemID{}}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v, err := FromMap(tt.id.ToMap()) + require.NoError(t, err) + assert.Equal(t, tt.id, v, "проверка FromMap для типа ID, должен быть равен исходному значению") + assert.Equal(t, v.ToMap(), tt.id.ToMap()) + }) + } +} diff --git a/id/item.go b/id/item.go new file mode 100644 index 0000000000000000000000000000000000000000..70c3e7be95b517a85e07e487eb00f94f9f73e14f --- /dev/null +++ b/id/item.go @@ -0,0 +1,61 @@ +package id + +const ( + Item = "item" + ItemsPrefix = "items" +) + +type ItemID struct { + CollectionID + ItemID string `json:"item_id,omitempty" bson:"item_id,omitempty"` +} + +func (t *ItemID) Type() string { return Item } + +func (t *ItemID) String() string { + return Join(t.CollectionID.String(), ItemsPrefix, t.ItemID) + +} + +func (t *ItemID) ToMap() map[string]any { + m := t.CollectionID.ToMap() + m["item_id"] = t.ItemID + m["type"] = Item + return m +} + +func (t *ItemID) FromMap(m map[string]any) error { + if err := t.CollectionID.FromMap(m); err != nil { + return err + } + t.ItemID = m["item_id"].(string) + return nil +} + +func (t *ItemID) Validate() error { + if t.ItemID == "" { + return ErrInvalidID + } + + return t.CollectionID.Validate() +} + +func parseItemID(parts []string) (*ItemID, error) { + if len(parts) != 8 || parts[6] != ItemsPrefix { + return nil, ErrInvalidID + } + + collID, err := parseCollectionID(parts[:6]) + if err != nil { + return nil, err + } + + var id ItemID + id.CollectionID = *collID + id.ItemID = parts[7] + return &id, nil +} + +func NewItemID(spaceID, envID, collID, id string) *ID { + return &ID{Descriptor: &ItemID{CollectionID: CollectionID{EnvironmentID: EnvironmentID{SpaceID: SpaceID{SpaceID: spaceID}, EnvironmentID: envID}, CollectionID: collID}, ItemID: id}} +} diff --git a/id/json.go b/id/json.go new file mode 100644 index 0000000000000000000000000000000000000000..34b87589dfd81e6b793c5812fb2b90f43fb724f1 --- /dev/null +++ b/id/json.go @@ -0,0 +1,23 @@ +package id + +import ( + jsoniter "github.com/json-iterator/go" +) + +func (id *ID) MarshalJSON() ([]byte, error) { + return jsoniter.Marshal(id.String()) +} + +func (id *ID) UnmarshalJSON(b []byte) error { + var s string + var err error + if err = jsoniter.Unmarshal(b, &s); err != nil { + return err + } + t, err := Parse(s) + if err != nil { + return err + } + *id = *t + return nil +} diff --git a/id/json_test.go b/id/json_test.go new file mode 100644 index 0000000000000000000000000000000000000000..afe831d4ef6038eaa6bc8a565b0623334cdf6bc8 --- /dev/null +++ b/id/json_test.go @@ -0,0 +1,101 @@ +package id + +import ( + "testing" + + jsoniter "github.com/json-iterator/go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestID_MarshalUnmarshalJSON(t *testing.T) { + tests := []struct { + name string + id *ID + }{ + { + name: "OrganizationID", + id: &ID{Descriptor: &OrganizationID{OrganizationID: "1"}}, + }, + { + name: "UserID", + id: &ID{Descriptor: &UserID{UserID: "1"}}, + }, + { + name: "ServiceID", + id: &ID{Descriptor: &ServiceID{ServiceID: "1"}}, + }, + { + name: "SpaceID", + id: &ID{Descriptor: &SpaceID{SpaceID: "1"}}, + }, + { + name: "EnvironmentID", + id: &ID{Descriptor: &EnvironmentID{EnvironmentID: "1", SpaceID: SpaceID{SpaceID: "1"}}}, + }, + { + name: "ClientID", + id: &ID{Descriptor: &ClientID{ClientID: "1", SpaceID: SpaceID{SpaceID: "1"}}}, + }, + { + name: "RoleID", + id: &ID{Descriptor: &RoleID{RoleID: "1", SpaceID: SpaceID{SpaceID: "1"}}}, + }, + { + name: "CollectionID", + id: &ID{Descriptor: &CollectionID{CollectionID: "1", EnvironmentID: EnvironmentID{EnvironmentID: "1", SpaceID: SpaceID{SpaceID: "1"}}}}, + }, + { + name: "SchemaID", + id: &ID{Descriptor: &SchemaID{CollectionID: "1", EnvironmentID: EnvironmentID{EnvironmentID: "1", SpaceID: SpaceID{SpaceID: "1"}}}}, + }, + { + name: "ItemID", + id: &ID{Descriptor: &ItemID{ItemID: "1", CollectionID: CollectionID{CollectionID: "1", EnvironmentID: EnvironmentID{EnvironmentID: "1", SpaceID: SpaceID{SpaceID: "1"}}}}}, + }, + { + name: "RevisionID", + id: &ID{Descriptor: &RevisionID{RevisionID: "1", ItemID: ItemID{ItemID: "1", CollectionID: CollectionID{CollectionID: "1", EnvironmentID: EnvironmentID{EnvironmentID: "1", SpaceID: SpaceID{SpaceID: "1"}}}}}}, + }, + { + name: "FieldID", + id: &ID{Descriptor: &FieldID{FieldName: "1", ItemID: ItemID{ItemID: "1", CollectionID: CollectionID{CollectionID: "1", EnvironmentID: EnvironmentID{EnvironmentID: "1", SpaceID: SpaceID{SpaceID: "1"}}}}}}, + }, + { + name: "SystemID", + id: &ID{Descriptor: &SystemID{}}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b, err := jsoniter.Marshal(&tt.id) + require.NoError(t, err) + + var i ID + require.NoError(t, jsoniter.Unmarshal(b, &i)) + assert.Equal(t, tt.id, &i, "после Unmarshal объект должен быть идентичен исходному") + }) + } +} + +func TestID_ExampleJSON(t *testing.T) { + type data struct { + ID *ID + Text string + Number int + } + + test := &data{ + ID: &ID{Descriptor: &SpaceID{SpaceID: Space}}, + Text: "text", + Number: 1, + } + + b, err := jsoniter.Marshal(test) + require.NoError(t, err) + + buf := new(data) + err = jsoniter.Unmarshal(b, &buf) + require.NoError(t, err) + assert.Equal(t, test, buf, "после Unmarshal объект должен совпадать с исходным") +} diff --git a/id/organization.go b/id/organization.go new file mode 100644 index 0000000000000000000000000000000000000000..fe9d22837977b486e78689ebee5f426bdee80fcf --- /dev/null +++ b/id/organization.go @@ -0,0 +1,49 @@ +package id + +const ( + Organization = "organization" + OrganizationsPrefix = "orgs" +) + +type OrganizationID struct { + OrganizationID string `json:"organization_id,omitempty" bson:"organization_id,omitempty"` +} + +func (t *OrganizationID) Type() string { return Organization } + +func (t *OrganizationID) String() string { + return Join(OrganizationsPrefix, t.OrganizationID) +} + +func (t *OrganizationID) ToMap() map[string]any { + return map[string]any{ + "organization_id": t.OrganizationID, + "type": Organization, + } +} + +func (t *OrganizationID) FromMap(m map[string]any) error { + t.OrganizationID = m["organization_id"].(string) + return nil +} + +func (t *OrganizationID) Validate() error { + if t.OrganizationID == "" { + return ErrInvalidID + } + return nil +} + +func parseOrganizationID(parts []string) (*OrganizationID, error) { + var id OrganizationID + if len(parts) != 2 || parts[0] != OrganizationsPrefix { + return nil, ErrInvalidID + } + + id.OrganizationID = parts[1] + return &id, nil +} + +func NewOrganizationID(id string) *ID { + return &ID{Descriptor: &OrganizationID{OrganizationID: id}} +} diff --git a/id/revision.go b/id/revision.go new file mode 100644 index 0000000000000000000000000000000000000000..0cb417e132fe1683f1e53a081e7c6244820478df --- /dev/null +++ b/id/revision.go @@ -0,0 +1,61 @@ +package id + +const ( + Revision = "revision" + RevisionsPrefix = "revs" +) + +type RevisionID struct { + ItemID + RevisionID string `json:"rev_id" bson:"rev_id,omitempty"` +} + +func (t *RevisionID) Type() string { return Revision } + +func (t *RevisionID) String() string { + return Join(t.ItemID.String(), RevisionsPrefix, t.RevisionID) + +} + +func (t *RevisionID) ToMap() map[string]any { + m := t.ItemID.ToMap() + m["rev_id"] = t.RevisionID + m["type"] = Revision + return m +} + +func (t *RevisionID) FromMap(m map[string]any) error { + if err := t.ItemID.FromMap(m); err != nil { + return err + } + t.RevisionID = m["rev_id"].(string) + return nil +} + +func (t *RevisionID) Validate() error { + if t.RevisionID == "" { + return ErrInvalidID + } + + return t.ItemID.Validate() +} + +func parseRevisionID(parts []string) (*RevisionID, error) { + if len(parts) != 10 || parts[8] != RevisionsPrefix { + return nil, ErrInvalidID + } + + itemID, err := parseItemID(parts[:8]) + if err != nil { + return nil, err + } + + var id RevisionID + id.ItemID = *itemID + id.RevisionID = parts[9] + return &id, nil +} + +func NewRevisionID(spaceID, envID, collID, itemID, id string) *ID { + return &ID{Descriptor: &RevisionID{ItemID: ItemID{CollectionID: CollectionID{EnvironmentID: EnvironmentID{SpaceID: SpaceID{SpaceID: spaceID}, EnvironmentID: envID}, CollectionID: collID}, ItemID: itemID}, RevisionID: id}} +} diff --git a/id/role.go b/id/role.go new file mode 100644 index 0000000000000000000000000000000000000000..abb6537fc605d3f01f644a67a724cc81e3a6b54b --- /dev/null +++ b/id/role.go @@ -0,0 +1,61 @@ +package id + +const ( + Role = "role" + RolesPrefix = "roles" +) + +type RoleID struct { + SpaceID + RoleID string `json:"role_id,omitempty" bson:"role_id,omitempty"` +} + +func (t *RoleID) Type() string { return Role } + +func (t *RoleID) String() string { + return Join(t.SpaceID.String(), RolesPrefix, t.RoleID) + +} + +func (t *RoleID) ToMap() map[string]any { + m := t.SpaceID.ToMap() + m["role_id"] = t.RoleID + m["type"] = Role + return m +} + +func (t *RoleID) FromMap(m map[string]any) error { + if err := t.SpaceID.FromMap(m); err != nil { + return err + } + t.RoleID = m["role_id"].(string) + return nil +} + +func (t *RoleID) Validate() error { + if t.RoleID == "" { + return ErrInvalidID + } + + return t.SpaceID.Validate() +} + +func parseRoleID(parts []string) (*RoleID, error) { + if len(parts) != 4 || parts[2] != RolesPrefix { + return nil, ErrInvalidID + } + + spaceID, err := parseSpaceID(parts[:2]) + if err != nil { + return nil, err + } + + var id RoleID + id.SpaceID = *spaceID + id.RoleID = parts[3] + return &id, nil +} + +func NewRoleID(spaceID, id string) *ID { + return &ID{Descriptor: &RoleID{SpaceID: SpaceID{SpaceID: spaceID}, RoleID: id}} +} diff --git a/id/schema.go b/id/schema.go new file mode 100644 index 0000000000000000000000000000000000000000..e3afee8c6cf29bca6cae1f1eedd72253c47362a5 --- /dev/null +++ b/id/schema.go @@ -0,0 +1,60 @@ +package id + +const ( + Schema = "schema" + SchemaPrefix = "schema" +) + +type SchemaID struct { + EnvironmentID + CollectionID string `json:"col_id" bson:"col_id,omitempty"` +} + +func (t *SchemaID) Type() string { return Schema } + +func (t *SchemaID) String() string { + return Join(t.EnvironmentID.String(), SchemaPrefix, t.CollectionID) +} + +func (t *SchemaID) ToMap() map[string]any { + m := t.EnvironmentID.ToMap() + m["col_id"] = t.CollectionID + m["type"] = Schema + return m +} + +func (t *SchemaID) FromMap(m map[string]any) error { + if err := t.EnvironmentID.FromMap(m); err != nil { + return err + } + t.CollectionID = m["col_id"].(string) + return nil +} + +func (t *SchemaID) Validate() error { + if t.CollectionID == "" { + return ErrInvalidID + } + + return t.EnvironmentID.Validate() +} + +func parseSchemaID(parts []string) (*SchemaID, error) { + if len(parts) != 6 || parts[4] != SchemaPrefix { + return nil, ErrInvalidID + } + + envID, err := parseEnvironmentID(parts[:4]) + if err != nil { + return nil, err + } + + var id SchemaID + id.EnvironmentID = *envID + id.CollectionID = parts[5] + return &id, nil +} + +func NewSchemaID(spaceID, envID, id string) *ID { + return &ID{Descriptor: &SchemaID{EnvironmentID: EnvironmentID{SpaceID: SpaceID{SpaceID: spaceID}, EnvironmentID: envID}, CollectionID: id}} +} diff --git a/id/service.go b/id/service.go new file mode 100644 index 0000000000000000000000000000000000000000..23bb23aa500fbba082e1aae578eae6da5624a5b9 --- /dev/null +++ b/id/service.go @@ -0,0 +1,49 @@ +package id + +const ( + Service = "service" + ServicesPrefix = "services" +) + +type ServiceID struct { + ServiceID string `json:"service_id,omitempty" bson:"service_id,omitempty"` +} + +func (t *ServiceID) Type() string { return Service } + +func (t *ServiceID) String() string { + return Join(ServicesPrefix, t.ServiceID) +} + +func (t *ServiceID) ToMap() map[string]any { + return map[string]any{ + "service_id": t.ServiceID, + "type": Service, + } +} + +func (t *ServiceID) FromMap(m map[string]any) error { + t.ServiceID = m["service_id"].(string) + return nil +} + +func (t *ServiceID) Validate() error { + if t.ServiceID == "" { + return ErrInvalidID + } + return nil +} + +func parseServiceID(parts []string) (*ServiceID, error) { + var id ServiceID + if len(parts) != 2 || parts[0] != ServicesPrefix { + return nil, ErrInvalidID + } + + id.ServiceID = parts[1] + return &id, nil +} + +func NewServiceID(id string) *ID { + return &ID{Descriptor: &ServiceID{ServiceID: id}} +} diff --git a/id/space.go b/id/space.go new file mode 100644 index 0000000000000000000000000000000000000000..39096673456d74462ef203b10814396fe48b2bcf --- /dev/null +++ b/id/space.go @@ -0,0 +1,48 @@ +package id + +const ( + Space = "space" + SpacesPrefix = "spaces" +) + +type SpaceID struct { + SpaceID string `json:"space_id,omitempty" bson:"space_id,omitempty"` +} + +func (t *SpaceID) Type() string { return Space } + +func (t *SpaceID) String() string { + return Join(SpacesPrefix, t.SpaceID) +} + +func (t *SpaceID) ToMap() map[string]any { + return map[string]any{ + "space_id": t.SpaceID, + "type": Space, + } +} + +func (t *SpaceID) FromMap(m map[string]any) error { + t.SpaceID = m["space_id"].(string) + return nil +} + +func (t *SpaceID) Validate() error { + if t.SpaceID == "" { + return ErrInvalidID + } + return nil +} + +func parseSpaceID(parts []string) (*SpaceID, error) { + var id SpaceID + if len(parts) != 2 || parts[0] != SpacesPrefix { + return nil, ErrInvalidID + } + + id.SpaceID = parts[1] + return &id, nil +} +func NewSpaceID(id string) *ID { + return &ID{Descriptor: &SpaceID{SpaceID: id}} +} diff --git a/id/system.go b/id/system.go new file mode 100644 index 0000000000000000000000000000000000000000..de2f3c2571896e448f8aad17be32df58b73a6ea4 --- /dev/null +++ b/id/system.go @@ -0,0 +1,22 @@ +package id + +const System = "system" + +type SystemID struct{} + +func (t *SystemID) Type() string { return Space } +func (t *SystemID) String() string { return string(Separator) + System } +func (t *SystemID) ToMap() map[string]any { return map[string]any{"type": System} } +func (t *SystemID) FromMap(m map[string]any) error { return nil } +func (t *SystemID) Validate() error { return nil } + +func parseSystemID(parts []string) (*SystemID, error) { + var id SystemID + if len(parts) != 1 || parts[0] != System { + return nil, ErrInvalidID + } + return &id, nil +} +func NewSystemID() *ID { + return &ID{Descriptor: &SystemID{}} +} diff --git a/id/user.go b/id/user.go new file mode 100644 index 0000000000000000000000000000000000000000..c76f6c9fa0e8ce440a1ef15e36bbb82a73301be5 --- /dev/null +++ b/id/user.go @@ -0,0 +1,49 @@ +package id + +const ( + User = "user" + UsersPrefix = "users" +) + +type UserID struct { + UserID string `json:"user_id,omitempty" bson:"user_id,omitempty"` +} + +func (t *UserID) Type() string { return User } + +func (t *UserID) String() string { + return Join(UsersPrefix, t.UserID) +} + +func (t *UserID) ToMap() map[string]any { + return map[string]any{ + "user_id": t.UserID, + "type": User, + } +} + +func (t *UserID) FromMap(m map[string]any) error { + t.UserID = m["user_id"].(string) + return nil +} + +func (t *UserID) Validate() error { + if t.UserID == "" { + return ErrInvalidID + } + return nil +} + +func parseUserID(parts []string) (*UserID, error) { + var id UserID + if len(parts) != 2 || parts[0] != UsersPrefix { + return nil, ErrInvalidID + } + + id.UserID = parts[1] + return &id, nil +} + +func NewUserID(id string) *ID { + return &ID{Descriptor: &UserID{UserID: id}} +} diff --git a/pkg/environments/environment.go b/pkg/environments/environment.go index cbd468dc361951e048a0840f4158a42c962e7af1..e9d2b96e8337ac2ad94315b671aeea42e54c47cb 100644 --- a/pkg/environments/environment.go +++ b/pkg/environments/environment.go @@ -91,24 +91,4 @@ func (e Environment) Clone() *Environment { } 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") - } -} +} \ No newline at end of file diff --git a/pkg/expr/config.go b/pkg/expr/config.go index 628111173d92f81ccdcc5e4c40b1aecc2ff27626..e6ba9d90b4f4e04c56c17b8591d4a92e30984e6c 100644 --- a/pkg/expr/config.go +++ b/pkg/expr/config.go @@ -1,8 +1,8 @@ package expr import ( - "github.com/antonmedv/expr" - "github.com/antonmedv/expr/conf" + "github.com/expr-lang/expr" + "github.com/expr-lang/expr/conf" ) type ExprConfig struct { diff --git a/pkg/expr/expr.go b/pkg/expr/expr.go index 93d6ee612968ac0a7e1c963a05cda4955532f110..1969b58c6bec892b05f8eff38eec86f3d4099e01 100644 --- a/pkg/expr/expr.go +++ b/pkg/expr/expr.go @@ -5,9 +5,9 @@ import ( "strings" "git.perx.ru/perxis/perxis-go/pkg/data" - compiler2 "github.com/antonmedv/expr/compiler" - "github.com/antonmedv/expr/parser" - "github.com/antonmedv/expr/vm" + exprcompiler "github.com/expr-lang/expr/compiler" + "github.com/expr-lang/expr/parser" + "github.com/expr-lang/expr/vm" "golang.org/x/net/context" ) @@ -39,7 +39,7 @@ func Eval(ctx context.Context, input string, env map[string]interface{}) (interf env, _ = cfg.Env.(map[string]interface{}) - program, err := compiler2.Compile(tree, nil) + program, err := exprcompiler.Compile(tree, cfg) if err != nil { return nil, err } @@ -78,4 +78,4 @@ func IsExpression(input string) bool { } return false -} +} \ No newline at end of file diff --git a/pkg/expr/expr_test.go b/pkg/expr/expr_test.go index 5eafc368bed934e0bd0805f4818442a75256931c..35153da271daace9f9acb6c7a02411f948f47054 100644 --- a/pkg/expr/expr_test.go +++ b/pkg/expr/expr_test.go @@ -1,11 +1,13 @@ package expr import ( + "context" "fmt" "testing" "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestIsExpression(t *testing.T) { @@ -49,3 +51,75 @@ func TestIsExpression(t *testing.T) { }) } } + +type testEnvStruct struct { + ID string `expr:"id"` + Size int `expr:"size"` + Data interface{} `expr:"data"` +} + +func (s *testEnvStruct) Equal(other *testEnvStruct) bool { + return s.ID == other.ID +} + +func TestExpr_Example(t *testing.T) { + ctx := context.Background() + + tests := []struct { + name string + exp string + env map[string]interface{} + wantErr bool + wantResult interface{} + }{ + { + name: "get field by expr tag", + exp: "s.id", + env: map[string]interface{}{"s": &testEnvStruct{ID: "id1"}}, + wantResult: "id1", + }, + { + name: "get field by field name", + exp: "s.ID", + env: map[string]interface{}{"s": &testEnvStruct{ID: "id1"}}, + wantResult: "id1", + }, + { + name: "get nested field", + exp: "m.s.size", + env: map[string]interface{}{"m": map[string]interface{}{"s": &testEnvStruct{Size: 1}}}, + wantResult: 1, + }, + { + name: "check field", + exp: "s.data.size < 100", + env: map[string]interface{}{"s": &testEnvStruct{Data: &testEnvStruct{Size: 0}}}, + wantResult: true, + }, + { + name: "use method", + exp: "s1.Equal(s2)", + env: map[string]interface{}{"s1": &testEnvStruct{ID: "id1"}, "s2": &testEnvStruct{ID: "id2"}}, + wantResult: false, + }, + { + name: "field not exists", + exp: "s.not_exists", + env: map[string]interface{}{"s": &testEnvStruct{}}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := Eval(ctx, tt.exp, tt.env) + if tt.wantErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + require.Equal(t, tt.wantResult, result) + }) + } +} diff --git a/pkg/expr/mongo.go b/pkg/expr/mongo.go index 989454178f640144791a9e6fd84d39317fd1e283..597180a671e80c2a5bc79bb8efd9bbee940120e4 100644 --- a/pkg/expr/mongo.go +++ b/pkg/expr/mongo.go @@ -4,13 +4,14 @@ import ( "context" "fmt" "regexp" + "strconv" "strings" - "github.com/antonmedv/expr" - "github.com/antonmedv/expr/ast" - compiler2 "github.com/antonmedv/expr/compiler" - "github.com/antonmedv/expr/conf" - "github.com/antonmedv/expr/parser" + "github.com/expr-lang/expr" + "github.com/expr-lang/expr/ast" + exprcompiler "github.com/expr-lang/expr/compiler" + "github.com/expr-lang/expr/conf" + "github.com/expr-lang/expr/parser" "go.mongodb.org/mongo-driver/bson" ) @@ -76,7 +77,7 @@ func (c *compiler) eval(node ast.Node) interface{} { Node: node, Source: c.tree.Source, } - prg, err := compiler2.Compile(t, c.config) + prg, err := exprcompiler.Compile(t, c.config) if err != nil { panic(fmt.Sprintf("compile error %s", err.Error())) } @@ -107,18 +108,14 @@ func (c *compiler) compile(node ast.Node) interface{} { return c.UnaryNode(n) case *ast.BinaryNode: return c.BinaryNode(n) - case *ast.MatchesNode: - return c.MatchesNode(n) - case *ast.PropertyNode: - return c.PropertyNode(n) - case *ast.IndexNode: - return c.IndexNode(n) + case *ast.MemberNode: + return c.MemberNode(n) + case *ast.ChainNode: + return c.ChainNode(n) case *ast.SliceNode: return c.SliceNode(n) - case *ast.MethodNode: - return c.MethodNode(n) - case *ast.FunctionNode: - return c.FunctionNode(n) + case *ast.CallNode: + return c.CallNode(n) case *ast.BuiltinNode: return c.BuiltinNode(n) case *ast.ClosureNode: @@ -127,6 +124,8 @@ func (c *compiler) compile(node ast.Node) interface{} { return c.PointerNode(n) case *ast.ConditionalNode: return c.ConditionalNode(n) + case *ast.VariableDeclaratorNode: + return c.VariableDeclaratorNode(n) case *ast.ArrayNode: return c.ArrayNode(n) case *ast.MapNode: @@ -208,12 +207,14 @@ func (c *compiler) ConstantNode(node *ast.ConstantNode) interface{} { } func (c *compiler) UnaryNode(node *ast.UnaryNode) interface{} { - op := c.compile(node.Node) - switch node.Operator { - case "!", "not": - return bson.M{"$not": op} + nodeIn, ok := node.Node.(*ast.BinaryNode) + if ok && nodeIn.Operator == "in" { + return bson.M{c.identifier(nodeIn.Left): bson.M{"$nin": c.eval(nodeIn.Right)}} + } + + return bson.M{"$nor": bson.A{c.compile(node.Node)}} default: panic(fmt.Sprintf("unknown operator (%v)", node.Operator)) } @@ -221,8 +222,8 @@ func (c *compiler) UnaryNode(node *ast.UnaryNode) interface{} { func (c *compiler) identifier(node ast.Node) string { switch l := node.(type) { - case *ast.PropertyNode: - return c.PropertyNode(l) + case *ast.MemberNode: + return c.MemberNode(l) case *ast.IdentifierNode: return c.IdentifierNode(l) } @@ -230,6 +231,10 @@ func (c *compiler) identifier(node ast.Node) string { } func (c *compiler) BinaryNode(node *ast.BinaryNode) interface{} { + if result := c.handleLenNode(node); result != nil { + return result + } + switch node.Operator { case "==": return bson.M{c.identifier(node.Left): c.eval(node.Right)} @@ -324,45 +329,24 @@ func (c *compiler) BinaryNode(node *ast.BinaryNode) interface{} { } } -func (c *compiler) MatchesNode(node *ast.MatchesNode) interface{} { - panic("unsupported match node") - //if node.Regexp != nil { - // c.compile(node.Left) - // c.emit(OpMatchesConst, c.makeConstant(node.Regexp)...) - // return - //} - //c.compile(node.Left) - //c.compile(node.Right) - //c.emit(OpMatches) +func (c *compiler) ChainNode(node *ast.ChainNode) string { + panic("unsupported chain node") } -func (c *compiler) PropertyNode(node *ast.PropertyNode) string { +func (c *compiler) MemberNode(node *ast.MemberNode) string { v := c.compile(node.Node) if val, ok := v.(string); ok { - return fmt.Sprintf("%s.%s", val, node.Property) + return fmt.Sprintf("%s.%s", val, c.compile(node.Property)) } panic(fmt.Sprintf("unsupported property for %v", ast.Dump(node.Node))) } -func (c *compiler) IndexNode(node *ast.IndexNode) string { - return fmt.Sprintf("{index-%v}", c.compile(node.Index)) -} - func (c *compiler) SliceNode(node *ast.SliceNode) interface{} { panic("unsupported slice node") } -func (c *compiler) MethodNode(node *ast.MethodNode) interface{} { - panic("unsupported method node") - //c.compile(node.Node) - //for _, arg := range node.Arguments { - // c.compile(arg) - //} - //c.emit(OpMethod, c.makeConstant(Call{Name: node.Method, Size: len(node.Arguments)})...) -} - -func (c *compiler) FunctionNode(node *ast.FunctionNode) interface{} { - switch node.Name { +func (c *compiler) CallNode(node *ast.CallNode) interface{} { + switch node.Callee.String() { case "search", "q": val := c.compile(node.Arguments[0]) return bson.M{"$text": bson.M{"$search": val}} @@ -425,6 +409,12 @@ func (c *compiler) FunctionNode(node *ast.FunctionNode) interface{} { } return bson.M{fields: bson.M{"$in": array}} + case "exists": + if len(node.Arguments) != 1 { + panic("exists() expects exactly 1 argument") + } + field := c.identifier(node.Arguments[0]) + return bson.M{field: bson.M{"$exists": true}} case "icontains": v := c.identifier(node.Arguments[0]) @@ -633,6 +623,10 @@ func (c *compiler) ConditionalNode(node *ast.ConditionalNode) interface{} { //c.patchJump(end) } +func (c *compiler) VariableDeclaratorNode(node *ast.VariableDeclaratorNode) int { + panic("unsupported variable declarator node ") +} + func (c *compiler) ArrayNode(node *ast.ArrayNode) interface{} { panic("unsupported array node") //for _, node := range node.Nodes { @@ -658,3 +652,50 @@ func (c *compiler) PairNode(node *ast.PairNode) interface{} { //c.compile(node.Key) //c.compile(node.Value) } + +// handleLenNode получает узел AST и возвращает запрос для mongo, +// если узел представляет вызов функции len, и nil в противном случае. +func (c *compiler) handleLenNode(node *ast.BinaryNode) bson.M { + lenNode, ok := node.Left.(*ast.BuiltinNode) + if !ok || lenNode.Name != "len" { + return nil + } + + if len(lenNode.Arguments) != 1 { + panic("len() expects exactly 1 argument") + } + + length, ok := c.eval(node.Right).(int) + if !ok { + panic("len() can only be compared with number value") + } + if length < 0 { + panic("len() can only be compared with non-negative number") + } + + field := c.identifier(lenNode.Arguments[0]) + switch op := node.Operator; { + case (op == "==" || op == "<=") && length == 0: + return bson.M{field: bson.M{"$eq": bson.A{}}} + case (op == "!=" || op == ">") && length == 0: + return bson.M{field: bson.M{"$exists": true, "$type": "array", "$ne": bson.A{}}} + case op == ">=" && length == 0: + return bson.M{field: bson.M{"$exists": true, "$type": "array"}} + case op == "<" && length == 0: + panic("invalid comparison: len() cannot be less than 0") + case op == "==": + return bson.M{field: bson.M{"$size": length}} + case op == "!=": + return bson.M{field: bson.M{"$not": bson.M{"$size": length}, "$type": "array"}} + case op == ">": + return bson.M{field + "." + strconv.Itoa(length): bson.M{"$exists": true}} + case op == ">=": + return bson.M{field + "." + strconv.Itoa(length-1): bson.M{"$exists": true}} + case op == "<": + return bson.M{field + "." + strconv.Itoa(length-1): bson.M{"$exists": false}, field: bson.M{"$type": "array"}} + case op == "<=": + return bson.M{field + "." + strconv.Itoa(length): bson.M{"$exists": false}, field: bson.M{"$type": "array"}} + default: + panic("invalid comparison operator with len()") + } +} diff --git a/pkg/expr/mongo_test.go b/pkg/expr/mongo_test.go index 75ec627c124cc0a24768d3cec30a4eac0ae34c15..dee6e668c490a6d375a77c532862f5389b1bd5f4 100644 --- a/pkg/expr/mongo_test.go +++ b/pkg/expr/mongo_test.go @@ -6,8 +6,8 @@ import ( "time" "git.perx.ru/perxis/perxis-go/pkg/id" - "github.com/antonmedv/expr" - "github.com/antonmedv/expr/ast" + "github.com/expr-lang/expr" + "github.com/expr-lang/expr/ast" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.mongodb.org/mongo-driver/bson" @@ -29,6 +29,29 @@ func TestConvertToMongo(t *testing.T) { }{ {"equal", "s == 3", nil, bson.M{"s": 3}, false}, {"in array", "s in [1,2,3]", nil, bson.M{"s": bson.M{"$in": []interface{}{1, 2, 3}}}, false}, + {"not in array", "s not in [1,2,3]", nil, bson.M{"s": bson.M{"$nin": []interface{}{1, 2, 3}}}, false}, + {"exists#1", "exists(s)", nil, bson.M{"s": bson.M{"$exists": true}}, false}, + {"exists#2", "exists(s, s)", nil, nil, true}, + {"len#1", "len(s)", nil, nil, true}, + {"len#2", "len(s) <> 1", nil, nil, true}, + {"len#3", "len(s) == -1", nil, nil, true}, + {"len#4", "len(s, s) == -1", nil, nil, true}, + {"len#5", "len(s) == s", nil, nil, true}, + {"len eq", "len(s) == 1", nil, bson.M{"s": bson.M{"$size": 1}}, false}, + {"len eq zero", "len(s) == 0", nil, bson.M{"s": bson.M{"$eq": bson.A{}}}, false}, + {"len ne", "len(s) != 1", nil, bson.M{"s": bson.M{"$not": bson.M{"$size": 1}, "$type": "array"}}, false}, + {"len ne zero", "len(s) != 0", nil, bson.M{"s": bson.M{"$exists": true, "$ne": bson.A{}, "$type": "array"}}, false}, + {"len gt", "len(s) > 1", nil, bson.M{"s.1": bson.M{"$exists": true}}, false}, + {"len gt zero", "len(s) > 0", nil, bson.M{"s": bson.M{"$exists": true, "$type": "array", "$ne": bson.A{}}}, false}, + {"len gte", "len(s) >= 1", nil, bson.M{"s.0": bson.M{"$exists": true}}, false}, + {"len gte zero", "len(s) >= 0", nil, bson.M{"s": bson.M{"$exists": true, "$type": "array"}}, false}, + {"len lt", "len(s) < 1", nil, bson.M{"s.0": bson.M{"$exists": false}, "s": bson.M{"$type": "array"}}, false}, + {"len lt zero", "len(s) < 0", nil, nil, true}, + {"len lte", "len(s) <= 1", nil, bson.M{"s.1": bson.M{"$exists": false}, "s": bson.M{"$type": "array"}}, false}, + {"len lte zero", "len(s) <= 0", nil, bson.M{"s": bson.M{"$eq": bson.A{}}}, false}, + {"field#1", "s.test > 3", nil, bson.M{"s.test": bson.M{"$gt": 3}}, false}, + {"field#2", "s['test'] > 3", nil, bson.M{"s.test": bson.M{"$gt": 3}}, false}, + {"field#3", "s[test] > 3", nil, bson.M{"s.test": bson.M{"$gt": 3}}, false}, {"contains", "s contains 'some'", nil, bson.M{"s": bson.M{"$regex": "some"}}, false}, {"contains with . + () $ {} ^", "value contains 'something with . + () $ {} ^'", nil, bson.M{"value": bson.M{"$regex": "something with \\. \\+ \\(\\) \\$ \\{\\} \\^"}}, false}, {"startsWith", "s startsWith 'some'", nil, bson.M{"s": bson.M{"$regex": "^some.*"}}, false}, @@ -42,6 +65,8 @@ func TestConvertToMongo(t *testing.T) { {"iendsWith", "iendsWith(s, 'some')", nil, bson.M{"s": bson.M{"$regex": ".*some$", "$options": "i"}}, false}, {"iendsWith . + () $ {} ^", "iendsWith(s,'. + () $ {} ^')", nil, bson.M{"s": bson.M{"$regex": ".*\\. \\+ \\(\\) \\$ \\{\\} \\^$", "$options": "i"}}, false}, {"or", "s==2 || s > 10", nil, bson.M{"$or": bson.A{bson.M{"s": 2}, bson.M{"s": bson.M{"$gt": 10}}}}, false}, + {"not#1", "not icontains(s, 'some')", nil, bson.M{"$nor": bson.A{bson.M{"s": bson.M{"$options": "i", "$regex": "some"}}}}, false}, + {"not#2", "not (s.test > 3)", nil, bson.M{"$nor": bson.A{bson.M{"s.test": bson.M{"$gt": 3}}}}, false}, {"search", "search('some') || s > 10", nil, bson.M{"$or": bson.A{bson.M{"$text": bson.M{"$search": "some"}}, bson.M{"s": bson.M{"$gt": 10}}}}, false}, {"vars:or", "s== a + 2 || s > a + 10", map[string]interface{}{"a": 100}, bson.M{"$or": bson.A{bson.M{"s": 102}, bson.M{"s": bson.M{"$gt": 110}}}}, false}, {"near", "near(a, [55.5, 37.5], 1000)", map[string]interface{}{"a": []interface{}{55, 37}}, bson.M{"a.geometry": bson.M{"$near": bson.D{{Key: "$geometry", Value: map[string]interface{}{"coordinates": []interface{}{55.5, 37.5}, "type": "Point"}}, {Key: "$maxDistance", Value: 1000}}}}, false}, @@ -51,6 +76,7 @@ func TestConvertToMongo(t *testing.T) { {"in", "In(s, [1,2,3])", nil, bson.M{"s": bson.M{"$in": []interface{}{1, 2, 3}}}, false}, {"in", "In(s, 1)", nil, bson.M{"s": bson.M{"$in": []interface{}{1}}}, false}, {"text search or id", "id", nil, nil, true}, + {"struct env", "db_item.id == env_item.id", map[string]interface{}{"env_item": &testEnvStruct{ID: "id1"}}, bson.M{"db_item.id": "id1"}, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -83,8 +109,7 @@ func BenchmarkConvertToMongo(b *testing.B) { type testVisitor struct{} -func (v *testVisitor) Enter(node *ast.Node) {} -func (v *testVisitor) Exit(node *ast.Node) { +func (v *testVisitor) Visit(node *ast.Node) { if n, ok := (*node).(*ast.IdentifierNode); ok { n.Value = "some" + "." + n.Value } diff --git a/pkg/expr/time.go b/pkg/expr/time.go index 740da301a70b65a15f83ac70b1f512c47c171b35..b72292c0c9f4d85bbe42d282f52dcde060cdf8e4 100644 --- a/pkg/expr/time.go +++ b/pkg/expr/time.go @@ -3,7 +3,7 @@ package expr import ( "time" - "github.com/antonmedv/expr" + "github.com/expr-lang/expr" ) const DefaultTimeLayout = time.RFC3339 @@ -18,7 +18,6 @@ func init() { expr.Operator("<", "Time.Before"), expr.Operator(">", "Time.After"), expr.Operator("<=", "Time.BeforeOrEqual"), - expr.Operator(">", "Time.After"), expr.Operator(">=", "Time.AfterOrEqual"), // Time and duration manipulation. diff --git a/pkg/extension/schema.go b/pkg/extension/schema.go index 501de9a247c3ba4240dc50ba641d7e2c56a1d2fc..521ebad6428dbd308228090469f369367e65068d 100644 --- a/pkg/extension/schema.go +++ b/pkg/extension/schema.go @@ -72,7 +72,7 @@ func NewActionsCollection(spaceID, envID string) *collections.Collection { // UI sch.Field.UI.ListView = &field.View{Options: map[string]interface{}{ - "fields": []interface{}{"name", "action", "kind", "updated_at", "updated_by", "state"}, + "fields": []interface{}{"icon", "name", "action", "kind", "updated_at", "updated_by", "state"}, "sort": []interface{}{"name"}, "page_size": float64(50), }} diff --git a/pkg/files/file.go b/pkg/files/file.go index 0e4d89f7004413b2c2a0ae9eff663cdcfc0ef3a1..d2236b83368e3d5efc763fd6f7e53664e15cc827 100644 --- a/pkg/files/file.go +++ b/pkg/files/file.go @@ -16,13 +16,13 @@ const ( // File - описание файла в системе хранения perxis type File struct { - ID string `mapstructure:"id,omitempty" json:"id"` // Уникальный идентификатор файла в хранилище - Name string `mapstructure:"name,omitempty" json:"name" bson:"name,omitempty"` // Имя файла - Size int `mapstructure:"size,omitempty" json:"size" bson:"size,omitempty"` // Размер файла - MimeType string `mapstructure:"mimeType,omitempty" json:"mimeType" bson:"mimeType,omitempty"` // Mime-type файла - URL string `mapstructure:"url,omitempty" json:"url" bson:"url,omitempty"` // Адрес для загрузки файла - Key string `mapstructure:"key,omitempty" json:"key" bson:"key,omitempty"` // Ключ для хранения файла в хранилище - File fs.File `mapstructure:"-" json:"-" bson:"-"` // Файл для загрузки(из файловой системы) + ID string `mapstructure:"id,omitempty" json:"id" expr:"id"` // Уникальный идентификатор файла в хранилище + Name string `mapstructure:"name,omitempty" json:"name" bson:"name,omitempty" expr:"name"` // Имя файла + Size int `mapstructure:"size,omitempty" json:"size" bson:"size,omitempty" expr:"size"` // Размер файла + MimeType string `mapstructure:"mimeType,omitempty" json:"mimeType" bson:"mimeType,omitempty" expr:"mime_type"` // Mime-type файла + URL string `mapstructure:"url,omitempty" json:"url" bson:"url,omitempty" expr:"url"` // Адрес для загрузки файла + Key string `mapstructure:"key,omitempty" json:"key" bson:"key,omitempty" expr:"key"` // Ключ для хранения файла в хранилище + File fs.File `mapstructure:"-" json:"-" bson:"-"` // Файл для загрузки(из файловой системы) } func (f File) Clone() *File { @@ -47,26 +47,6 @@ func (f *File) SetURLWithTemplate(t *template.Template) error { return nil } -func (f File) Fetch(i interface{}) interface{} { - p, _ := i.(string) - switch p { - case "id": - return f.ID - case "name": - return f.Name - case "size": - return f.Size - case "mime_type": - return f.MimeType - case "url": - return f.URL - case "key": - return f.Key - default: - panic("unknown parameter") - } -} - func NewFile(name, mimeType string, size int, temp bool) *File { i := id.GenerateNewID() if temp { diff --git a/pkg/files/file_test.go b/pkg/files/file_test.go index 14fb89ce8c054d57ea708a10eb27318b474ff99c..617bed4c09ef95ab58874d856a36c5584abd4589 100644 --- a/pkg/files/file_test.go +++ b/pkg/files/file_test.go @@ -1,9 +1,11 @@ package files import ( + "context" "testing" "text/template" + "git.perx.ru/perxis/perxis-go/pkg/expr" "github.com/stretchr/testify/require" ) @@ -55,3 +57,35 @@ func TestFile_SetURLWithTemplate(t *testing.T) { }) } } + +func TestFile_InExpr(t *testing.T) { + ctx := context.Background() + + tests := []struct { + exp string + env map[string]interface{} + wantResult interface{} + wantErr bool + }{ + {"f.id", map[string]interface{}{"f": &File{ID: "some_id"}}, "some_id", false}, + {"f.name", map[string]interface{}{"f": &File{Name: "some_name"}}, "some_name", false}, + {"f.size", map[string]interface{}{"f": &File{Size: 1}}, 1, false}, + {"f.mime_type", map[string]interface{}{"f": &File{MimeType: "some_mime_type"}}, "some_mime_type", false}, + {"f.url", map[string]interface{}{"f": &File{URL: "some_url"}}, "some_url", false}, + {"f.key", map[string]interface{}{"f": &File{Key: "some_key"}}, "some_key", false}, + {"f.not_exists", map[string]interface{}{"f": &File{}}, "", true}, + } + + for _, tt := range tests { + t.Run(tt.exp, func(t *testing.T) { + result, err := expr.Eval(ctx, tt.exp, tt.env) + if tt.wantErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + require.Equal(t, tt.wantResult, result) + }) + } +} diff --git a/pkg/items/dummy.go b/pkg/items/dummy.go new file mode 100644 index 0000000000000000000000000000000000000000..fc1f725abc3e5f9c16164daca30f3334c16f9f86 --- /dev/null +++ b/pkg/items/dummy.go @@ -0,0 +1,17 @@ +package items + +import "context" + +type FindResultDummy struct { + Items []*Item + Total int + Error error +} +type Dummy struct { + Items + FindResult *FindResultDummy +} + +func (d *Dummy) Find(_ context.Context, _, _, _ string, _ *Filter, _ ...*FindOptions) ([]*Item, int, error) { + return d.FindResult.Items, d.FindResult.Total, d.FindResult.Error +} diff --git a/pkg/items/item.go b/pkg/items/item.go index 7c34d2bb3563b338d6c3cb0baf97c4e49f58b535..2e76469c7b0a63d4cddf4ad3cd74961f1932a458 100644 --- a/pkg/items/item.go +++ b/pkg/items/item.go @@ -398,6 +398,16 @@ func (i *Item) Get(field string) (any, error) { return i.getItemData(field) } +// Delete удаляет значение поля Data +func (i *Item) Delete(field string) error { + // Если data == nil, то нет необходимости выполнять удаление + if i.Data == nil { + return nil + } + + return data.Delete(field, i.Data) +} + // GetSystemField возвращает описание поля для системных аттрибутов Item func GetSystemField(fld string) (*field.Field, error) { switch fld { diff --git a/pkg/items/item_test.go b/pkg/items/item_test.go index fb54fc501f45281bbafd37983de3b8638d5692d4..dfcc16ee1c7b40441b5df54acc48a12b276669ca 100644 --- a/pkg/items/item_test.go +++ b/pkg/items/item_test.go @@ -25,6 +25,42 @@ func TestItem_Set(t *testing.T) { } +func TestItem_DeleteItemData(t *testing.T) { + tests := []struct { + name string + item *Item + field string + want map[string]any + wantErr assert.ErrorAssertionFunc + }{ + { + name: "Simple", + item: &Item{Data: map[string]any{"a": "b", "c": "d"}}, + field: "a", + want: map[string]any{"c": "d"}, + wantErr: assert.NoError, + }, + { + name: "Item data is nil", + item: &Item{Data: nil}, + field: "a", + want: nil, + wantErr: assert.NoError, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := tc.item.Delete(tc.field) + assert.NoError(t, err) + if !tc.wantErr(t, err) { + return + } + assert.Equal(t, tc.want, tc.item.Data) + }) + } +} + func TestGetField(t *testing.T) { sch := schema.New( "a", field.String(), diff --git a/pkg/items/pagination.go b/pkg/items/pagination.go index 0c07c415ccfd2ad2d005c2c02fae92ad8e01036d..1910a34ba67c21cb1ebf54923ebe750af0fd10d2 100644 --- a/pkg/items/pagination.go +++ b/pkg/items/pagination.go @@ -130,6 +130,11 @@ func (b *BatchProcessor) Do(ctx context.Context, f func(batch []*Item) error) (i return 0, err } + // на случай, когда первый запрос вернул 0 элементов + if len(batch) == 0 { + break + } + if err = f(batch); err != nil { return 0, err } diff --git a/pkg/items/pagination_test.go b/pkg/items/pagination_test.go new file mode 100644 index 0000000000000000000000000000000000000000..bf13af39e00bd19550ddd49bae3e165b5cfa9fff --- /dev/null +++ b/pkg/items/pagination_test.go @@ -0,0 +1,35 @@ +package items + +import ( + "context" + "testing" + + "git.perx.ru/perxis/perxis-go/pkg/environments" + "git.perx.ru/perxis/perxis-go/pkg/options" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBatchProcessor(t *testing.T) { + + itemssvc := &Dummy{FindResult: &FindResultDummy{Items: nil, Total: 0, Error: nil}} + + b := &BatchProcessor{ + Items: itemssvc, + SpaceID: "sp", + EnvID: environments.DefaultEnvironment, + CollectionID: "col", + FindOptions: &FindOptions{ + Regular: true, + Hidden: true, + Templates: true, + FindOptions: *options.NewFindOptions(0, 10), + }, + Filter: NewFilter("a > 5"), + } + + var counter int + _, err := b.Do(context.Background(), func(batch []*Item) error { counter++; return nil }) + require.NoError(t, err) + assert.Equal(t, 0, counter) +} diff --git a/pkg/references/reference.go b/pkg/references/reference.go index 5740c929bfde2c0c11abcd96d75ce3122448f50e..171ded420c3fd5601cac9ad9f74ad69883f964ea 100644 --- a/pkg/references/reference.go +++ b/pkg/references/reference.go @@ -7,9 +7,9 @@ import ( ) type Reference struct { - ID string `json:"id" bson:"id" mapstructure:"id"` - CollectionID string `json:"collection_id" bson:"collection_id" mapstructure:"collection_id"` - Disabled bool `json:"disabled,omitempty" bson:"disabled,omitempty" mapstructure:"disabled"` + ID string `json:"id" bson:"id" mapstructure:"id" expr:"id"` + CollectionID string `json:"collection_id" bson:"collection_id" mapstructure:"collection_id" expr:"collection_id"` + Disabled bool `json:"disabled,omitempty" bson:"disabled,omitempty" mapstructure:"disabled" expr:"disabled"` } func (r *Reference) MarshalBSON() ([]byte, error) { @@ -108,17 +108,3 @@ func EqualArrays(sr1, sr2 []*Reference) bool { func (r *Reference) IsValid() bool { return r != nil && r.ID != "" && r.CollectionID != "" && !r.Disabled } - -func (r *Reference) Fetch(i interface{}) interface{} { - p, _ := i.(string) - switch p { - case "id": - return r.ID - case "collection_id": - return r.CollectionID - case "disabled": - return r.Disabled - default: - panic("unknown parameter") - } -} diff --git a/pkg/references/reference_test.go b/pkg/references/reference_test.go new file mode 100644 index 0000000000000000000000000000000000000000..5e79de47ab96cc9e48b8ba0eb644f9519e9ee914 --- /dev/null +++ b/pkg/references/reference_test.go @@ -0,0 +1,41 @@ +package references + +import ( + "context" + "testing" + + "git.perx.ru/perxis/perxis-go/pkg/expr" + "github.com/stretchr/testify/require" +) + +func TestReference_InExpr(t *testing.T) { + ctx := context.Background() + + tests := []struct { + exp string + env map[string]interface{} + wantResult interface{} + wantErr bool + }{ + {"r.id", map[string]interface{}{"r": &Reference{ID: "some_id"}}, "some_id", false}, + {"r.collection_id", map[string]interface{}{"r": &Reference{CollectionID: "some_coll_id"}}, "some_coll_id", false}, + {"r.disabled", map[string]interface{}{"r": &Reference{Disabled: true}}, true, false}, + {"r.String()", map[string]interface{}{"r": &Reference{ID: "id", CollectionID: "collID"}}, "collID.id", false}, + {"r1.Equal(r2)", map[string]interface{}{"r1": &Reference{"id", "collID", false}, "r2": &Reference{"id", "collID", false}}, true, false}, + {"r.IsValid()", map[string]interface{}{"r": &Reference{}}, false, false}, + {"r.not_exists", map[string]interface{}{"r": &Reference{}}, false, true}, + } + + for _, tt := range tests { + t.Run(tt.exp, func(t *testing.T) { + result, err := expr.Eval(ctx, tt.exp, tt.env) + if tt.wantErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + require.Equal(t, tt.wantResult, result) + }) + } +} diff --git a/pkg/spaces/space.go b/pkg/spaces/space.go index aa039c46f95e6c1ec3b0e49b65e766dc7451cd9f..83edb3f14706793aed6077adcee856598358b044 100644 --- a/pkg/spaces/space.go +++ b/pkg/spaces/space.go @@ -42,24 +42,4 @@ type StateInfo struct { func (s Space) Clone() *Space { return &s -} - -func (s Space) Fetch(i interface{}) interface{} { - p, _ := i.(string) - switch p { - case "ID": - return s.ID - case "OrgID": - return s.OrgID - case "Name": - return s.Name - case "Description": - return s.Description - case "Config": - return s.Config - case "StateInfo": - return s.StateInfo - default: - panic("unknown parameter") - } -} +} \ No newline at end of file