From 2a6642c8512a067c96050fed94afd75a6b38bb71 Mon Sep 17 00:00:00 2001
From: Pavel Antonov <antonov@perx.ru>
Date: Mon, 9 Oct 2023 17:09:20 +0400
Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B1=D1=80=D0=B0=D0=B1=D0=BE=D1=82?=
 =?UTF-8?q?=D0=BA=D0=B0=20Action=20=D0=B2=20=D1=80=D0=B0=D1=81=D1=88=D0=B8?=
 =?UTF-8?q?=D1=80=D0=B5=D0=BD=D0=B8=D1=8F=D1=85?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 pkg/extension/action_handler.go               | 39 +++++++++++++++
 .../action.go => extension/action_url.go}     | 14 +++---
 .../action_url_test.go}                       | 20 ++++----
 pkg/extension/server.go                       | 26 ++++++----
 pkg/extension/server_test.go                  |  8 ++--
 pkg/extension/service/extension.go            | 48 ++++++++++++-------
 6 files changed, 109 insertions(+), 46 deletions(-)
 create mode 100644 pkg/extension/action_handler.go
 rename pkg/{action/action.go => extension/action_url.go} (68%)
 rename pkg/{action/action_test.go => extension/action_url_test.go} (90%)

diff --git a/pkg/extension/action_handler.go b/pkg/extension/action_handler.go
new file mode 100644
index 00000000..3079b885
--- /dev/null
+++ b/pkg/extension/action_handler.go
@@ -0,0 +1,39 @@
+package extension
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+)
+
+var (
+	ErrInvalidAction = errors.New("invalid action")
+)
+
+// ActionHandler выполняет действие в url
+type ActionHandler func(ctx context.Context, url *ActionURL, req *ActionRequest) (*ActionResponse, error)
+
+// ActionRouter возвращает ActionHandler для указанного действия
+type ActionRouter func(ctx context.Context, url *ActionURL, req *ActionRequest) (ActionHandler, error)
+
+// Chain возвращает ActionRouter, который последовательно вызывает переданные ActionRouter
+func Chain(chain ...ActionRouter) ActionRouter {
+	return func(ctx context.Context, url *ActionURL, req *ActionRequest) (ActionHandler, error) {
+		for _, f := range chain {
+			if h, err := f(ctx, url, req); err == nil && h != nil {
+				return h, nil
+			}
+		}
+		return nil, ErrInvalidAction
+	}
+}
+
+// Named возвращает ActionRouter, который возвращает ActionHandler для указанного действия по имени
+func Named(name string, handler ActionHandler) ActionRouter {
+	return func(ctx context.Context, url *ActionURL, req *ActionRequest) (ActionHandler, error) {
+		if url.Action() == name {
+			return handler, nil
+		}
+		return nil, ErrInvalidAction
+	}
+}
diff --git a/pkg/action/action.go b/pkg/extension/action_url.go
similarity index 68%
rename from pkg/action/action.go
rename to pkg/extension/action_url.go
index 4233d986..463ac68f 100644
--- a/pkg/action/action.go
+++ b/pkg/extension/action_url.go
@@ -1,4 +1,4 @@
-package action
+package extension
 
 import (
 	"net/url"
@@ -6,20 +6,20 @@ import (
 )
 
 // URL структура для хранения данных о переданном действии.
-type URL struct {
+type ActionURL struct {
 	*url.URL
 }
 
 // NewURL возвращает структуру ActionURL
-func NewURL(action string) (*URL, error) {
+func NewActionURL(action string) (*ActionURL, error) {
 	u, err := url.Parse(action)
 	if err != nil {
 		return nil, err
 	}
-	return &URL{URL: u}, nil
+	return &ActionURL{URL: u}, nil
 }
 
-func (u *URL) actionParts() (string, string) {
+func (u *ActionURL) actionParts() (string, string) {
 	if u.URL != nil && u.URL.Scheme == "grpc" {
 		splitPath := strings.Split(strings.TrimLeft(u.Path, "/"), "/")
 		if len(splitPath) >= 2 {
@@ -29,12 +29,12 @@ func (u *URL) actionParts() (string, string) {
 	return "", ""
 }
 
-func (u *URL) Action() string {
+func (u *ActionURL) Action() string {
 	_, action := u.actionParts()
 	return action
 }
 
-func (u *URL) Extension() string {
+func (u *ActionURL) Extension() string {
 	ext, _ := u.actionParts()
 	return ext
 }
diff --git a/pkg/action/action_test.go b/pkg/extension/action_url_test.go
similarity index 90%
rename from pkg/action/action_test.go
rename to pkg/extension/action_url_test.go
index 2988744f..be9a924b 100644
--- a/pkg/action/action_test.go
+++ b/pkg/extension/action_url_test.go
@@ -1,4 +1,4 @@
-package action
+package extension
 
 import (
 	"fmt"
@@ -12,13 +12,13 @@ func TestActionURL_New(t *testing.T) {
 	tests := []struct {
 		name    string
 		action  string
-		want    *URL
+		want    *ActionURL
 		url     string
 		wantErr assert.ErrorAssertionFunc
 	}{
 		{
 			name: "Without action",
-			want: &URL{
+			want: &ActionURL{
 				URL: &url.URL{},
 			},
 			wantErr: assert.NoError,
@@ -26,7 +26,7 @@ func TestActionURL_New(t *testing.T) {
 		{
 			name:   "Without deprecated action call",
 			action: "build-site",
-			want: &URL{
+			want: &ActionURL{
 				URL: &url.URL{
 					Path: "build-site",
 				},
@@ -37,7 +37,7 @@ func TestActionURL_New(t *testing.T) {
 		{
 			name:   "With grpc action",
 			action: "grpc:///perxisweb/build-site",
-			want: &URL{
+			want: &ActionURL{
 				URL: &url.URL{
 					Scheme: "grpc",
 					Path:   "/perxisweb/build-site",
@@ -49,7 +49,7 @@ func TestActionURL_New(t *testing.T) {
 		{
 			name:   "With ui action",
 			action: "ui:///space/env/coll",
-			want: &URL{
+			want: &ActionURL{
 				URL: &url.URL{
 					Scheme: "ui",
 					Path:   "/space/env/coll",
@@ -61,7 +61,7 @@ func TestActionURL_New(t *testing.T) {
 		{
 			name:   "With http action",
 			action: "https://perx.ru",
-			want: &URL{
+			want: &ActionURL{
 				URL: &url.URL{
 					Scheme: "https",
 					Host:   "perx.ru",
@@ -79,7 +79,7 @@ func TestActionURL_New(t *testing.T) {
 		{
 			name:   "With no action id",
 			action: "grpc:///perxisweb",
-			want: &URL{
+			want: &ActionURL{
 				URL: &url.URL{
 					Scheme: "grpc",
 					Path:   "/perxisweb",
@@ -90,7 +90,7 @@ func TestActionURL_New(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			got, err := NewURL(tt.action)
+			got, err := NewActionURL(tt.action)
 			if !tt.wantErr(t, err, fmt.Sprintf("NewURL(%v)", tt.action)) {
 				return
 			}
@@ -128,7 +128,7 @@ func TestActionURL_String(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			p, _ := NewURL(tt.url)
+			p, _ := NewActionURL(tt.url)
 			assert.Equalf(t, tt.url, p.String(), "String()")
 		})
 	}
diff --git a/pkg/extension/server.go b/pkg/extension/server.go
index f9b603f5..467b4396 100644
--- a/pkg/extension/server.go
+++ b/pkg/extension/server.go
@@ -3,7 +3,6 @@ package extension
 import (
 	"context"
 
-	"git.perx.ru/perxis/perxis-go/pkg/action"
 	"git.perx.ru/perxis/perxis-go/pkg/errors"
 	pb "git.perx.ru/perxis/perxis-go/proto/extensions"
 )
@@ -80,25 +79,36 @@ func (srv *Server) Update(ctx context.Context, request *UpdateRequest) (*UpdateR
 	return &UpdateResponse{Results: res}, nil
 }
 
-func (srv *Server) Action(ctx context.Context, in *pb.ActionRequest) (*pb.ActionResponse, error) {
-	actionURL, err := action.NewURL(in.Action)
+func (srv *Server) getExtensionService(ctx context.Context, in *pb.ActionRequest) (Extension, error) {
+	actionURL, err := NewActionURL(in.Action)
+
 	if err != nil {
-		return nil, err
+		return nil, ErrInvalidAction
 	}
+
 	ext := actionURL.Extension()
 	if ext == "" {
 		ext = in.Extension
 	}
 	if ext == "" {
-		return nil, errors.New("extension ID required")
+		return nil, ErrInvalidAction
 	}
 
 	svc, ok := srv.services[ext]
-	if !ok {
-		return nil, ErrUnknownExtension
+	if ok {
+		return svc, nil
+	}
+
+	return nil, ErrInvalidAction
+}
+
+func (srv *Server) Action(ctx context.Context, in *pb.ActionRequest) (out *pb.ActionResponse, err error) {
+	svc, err := srv.getExtensionService(ctx, in)
+	if err != nil {
+		return nil, err
 	}
 
-	out, err := svc.Action(ctx, in)
+	out, err = svc.Action(ctx, in)
 
 	if out == nil {
 		out = &ActionResponse{}
diff --git a/pkg/extension/server_test.go b/pkg/extension/server_test.go
index bee68b1a..64f10e98 100644
--- a/pkg/extension/server_test.go
+++ b/pkg/extension/server_test.go
@@ -142,7 +142,7 @@ func TestServer_Action(t *testing.T) {
 				EnvId:   "env",
 			},
 			want:    &ActionResponse{State: ResponseDone},
-			wantErr: "extension ID required",
+			wantErr: "invalid action",
 		},
 		{
 			name:     "Deprecated call",
@@ -164,7 +164,7 @@ func TestServer_Action(t *testing.T) {
 				EnvId:   "env",
 			},
 			want:    nil,
-			wantErr: ErrUnknownExtension.Error(),
+			wantErr: "invalid action",
 		},
 		{
 			name:     "Deprecated call, without extension",
@@ -175,7 +175,7 @@ func TestServer_Action(t *testing.T) {
 				EnvId:   "env",
 			},
 			want:    nil,
-			wantErr: "extension ID required",
+			wantErr: "invalid action",
 		},
 		{
 			name:     "Deprecated call, without action and extension)",
@@ -185,7 +185,7 @@ func TestServer_Action(t *testing.T) {
 				EnvId:   "env",
 			},
 			want:    nil,
-			wantErr: "extension ID required",
+			wantErr: "invalid action",
 		},
 	}
 	for _, tt := range tests {
diff --git a/pkg/extension/service/extension.go b/pkg/extension/service/extension.go
index 6d9608ab..a92d1623 100644
--- a/pkg/extension/service/extension.go
+++ b/pkg/extension/service/extension.go
@@ -4,10 +4,8 @@ import (
 	"context"
 	"fmt"
 
-	"git.perx.ru/perxis/perxis-go/pkg/action"
 	"git.perx.ru/perxis/perxis-go/pkg/clients"
 	"git.perx.ru/perxis/perxis-go/pkg/content"
-	"git.perx.ru/perxis/perxis-go/pkg/errors"
 	"git.perx.ru/perxis/perxis-go/pkg/extension"
 	"git.perx.ru/perxis/perxis-go/pkg/roles"
 	"git.perx.ru/perxis/perxis-go/pkg/setup"
@@ -32,6 +30,7 @@ type Extension struct {
 	Content   *content.Content
 	Logger    *zap.Logger
 	manager   extension.Manager
+	router    extension.ActionRouter
 
 	withClient bool
 	role       *roles.Role
@@ -155,25 +154,40 @@ func (s *Extension) Uninstall(ctx context.Context, in *extension.UninstallReques
 	return s.GetSetup(in.SpaceId, in.EnvId).WithForce(in.Force).WithRemove(in.Remove).Uninstall(ctx)
 }
 
-func (s *Extension) Action(ctx context.Context, in *extension.ActionRequest) (*extension.ActionResponse, error) {
-	actionURL, err := action.NewURL(in.Action)
-	if err != nil {
-		return nil, err
-	}
-	ext := actionURL.Extension()
+// isCorrectExtension проверяет что расширение в url совпадает с расширением расширения
+func (s *Extension) isCorrectExtension(ctx context.Context, url *extension.ActionURL, in *extension.ActionRequest) bool {
+	ext := url.Extension()
 	if ext == "" {
 		ext = in.Extension
 	}
-	if ext == "" {
-		return nil, errors.New("extension ID required")
-	}
-	ok, err := extension.CheckInstalled(ctx, s.Content, in.SpaceId, in.EnvId, ext)
-	if err != nil {
-		return nil, errors.Wrap(err, "check extension installed")
+
+	if in.Extension == s.desc.Extension {
+		return true
 	}
-	if !ok {
-		return nil, errors.New("extension not installed")
+	return false
+}
+
+func (s *Extension) Action(ctx context.Context, in *extension.ActionRequest) (*extension.ActionResponse, error) {
+	// TBD: нужно ли проверять что действие установлено в пространство
+	// мы так и не договорились, но проверка появилась
+	// пусть решает каждое расширение само
+	//
+	//ok, err := extension.CheckInstalled(ctx, s.Content, in.SpaceId, in.EnvId, ext)
+	//if err != nil {
+	//	return nil, errors.Wrap(err, "check extension installed")
+	//}
+	//if !ok {
+	//	return nil, errors.New("extension not installed")
+	//}
+
+	if s.router == nil {
+		url, err := extension.NewActionURL(in.Action)
+		if err == nil && url != nil && s.isCorrectExtension(ctx, url, in) {
+			if h, err := s.router(ctx, url, in); err == nil && h != nil {
+				return h(ctx, url, in)
+			}
+		}
 	}
 
-	return &extension.ActionResponse{}, nil
+	return nil, extension.ErrInvalidAction
 }
-- 
GitLab